Make civsim and sites deterministic.

For anything in worldgen where you use a HashMap, *please* think
carefully about which hasher you are going to use!  This is
especially true if (for some reason) you are depending on hashmap
iteration order remaining stable for some aspect of worldgen.
This commit is contained in:
Joshua Yanovski 2020-05-21 21:20:01 +02:00
parent cdee191dd6
commit 34427373ef
14 changed files with 269 additions and 148 deletions

View File

@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed
- Improved camera aiming
- Made civsim, sites, etc. deterministic from the same seed.
### Removed

2
Cargo.lock generated
View File

@ -4993,6 +4993,7 @@ dependencies = [
"crossbeam",
"dot_vox",
"find_folder",
"fxhash",
"hashbrown",
"image",
"indexmap",
@ -5115,6 +5116,7 @@ dependencies = [
"arr_macro",
"bincode",
"bitvec",
"fxhash",
"hashbrown",
"image",
"itertools",

View File

@ -13,6 +13,7 @@ specs-idvs = { git = "https://gitlab.com/veloren/specs-idvs.git" }
specs = { version = "0.15.1", features = ["serde", "nightly", "storage-event-control"] }
vek = { version = "0.10.0", features = ["serde"] }
dot_vox = "4.0.0"
fxhash = "0.2.1"
image = "0.22.3"
mio = "0.6.19"
mio-extras = "2.0.5"

View File

@ -1,7 +1,11 @@
use crate::path::Path;
use core::cmp::Ordering::Equal;
use core::{
cmp::Ordering::{self, Equal},
f32, fmt,
hash::{BuildHasher, Hash},
};
use hashbrown::{HashMap, HashSet};
use std::{cmp::Ordering, collections::BinaryHeap, f32, hash::Hash};
use std::collections::BinaryHeap;
#[derive(Copy, Clone, Debug)]
pub struct PathEntry<S> {
@ -43,33 +47,62 @@ impl<T> PathResult<T> {
}
}
#[derive(Clone, Debug)]
pub struct Astar<S: Clone + Eq + Hash> {
#[derive(Clone)]
pub struct Astar<S, Hasher> {
iter: usize,
max_iters: usize,
potential_nodes: BinaryHeap<PathEntry<S>>,
came_from: HashMap<S, S>,
cheapest_scores: HashMap<S, f32>,
final_scores: HashMap<S, f32>,
visited: HashSet<S>,
came_from: HashMap<S, S, Hasher>,
cheapest_scores: HashMap<S, f32, Hasher>,
final_scores: HashMap<S, f32, Hasher>,
visited: HashSet<S, Hasher>,
cheapest_node: Option<S>,
cheapest_cost: Option<f32>,
}
impl<S: Clone + Eq + Hash> Astar<S> {
pub fn new(max_iters: usize, start: S, heuristic: impl FnOnce(&S) -> f32) -> Self {
/// NOTE: Must manually derive since Hasher doesn't implement it.
impl<S: Clone + Eq + Hash + fmt::Debug, H: BuildHasher> fmt::Debug for Astar<S, H> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Astar")
.field("iter", &self.iter)
.field("max_iters", &self.max_iters)
.field("potential_nodes", &self.potential_nodes)
.field("came_from", &self.came_from)
.field("cheapest_scores", &self.cheapest_scores)
.field("final_scores", &self.final_scores)
.field("visited", &self.visited)
.field("cheapest_node", &self.cheapest_node)
.field("cheapest_cost", &self.cheapest_cost)
.finish()
}
}
impl<S: Clone + Eq + Hash, H: BuildHasher + Clone> Astar<S, H> {
pub fn new(max_iters: usize, start: S, heuristic: impl FnOnce(&S) -> f32, hasher: H) -> Self {
Self {
max_iters,
iter: 0,
potential_nodes: std::iter::once(PathEntry {
potential_nodes: core::iter::once(PathEntry {
cost: 0.0,
node: start.clone(),
})
.collect(),
came_from: HashMap::default(),
cheapest_scores: std::iter::once((start.clone(), 0.0)).collect(),
final_scores: std::iter::once((start.clone(), heuristic(&start))).collect(),
visited: std::iter::once(start).collect(),
came_from: HashMap::with_hasher(hasher.clone()),
cheapest_scores: {
let mut h = HashMap::with_capacity_and_hasher(1, hasher.clone());
h.extend(core::iter::once((start.clone(), 0.0)));
h
},
final_scores: {
let mut h = HashMap::with_capacity_and_hasher(1, hasher.clone());
h.extend(core::iter::once((start.clone(), heuristic(&start))));
h
},
visited: {
let mut s = HashSet::with_capacity_and_hasher(1, hasher);
s.extend(core::iter::once(start));
s
},
cheapest_node: None,
cheapest_cost: None,
}

View File

@ -3,6 +3,7 @@ use crate::{
terrain::Block,
vol::{BaseVol, ReadVol},
};
use hashbrown::hash_map::DefaultHashBuilder;
use rand::{thread_rng, Rng};
use std::iter::FromIterator;
use vek::*;
@ -92,7 +93,11 @@ impl Route {
pub struct Chaser {
last_search_tgt: Option<Vec3<f32>>,
route: Route,
astar: Option<Astar<Vec3<i32>>>,
/// We use this hasher (AAHasher) because:
/// (1) we care about DDOS attacks (ruling out FxHash);
/// (2) we don't care about determinism across computers (we can use
/// AAHash).
astar: Option<Astar<Vec3<i32>, DefaultHashBuilder>>,
}
impl Chaser {
@ -147,7 +152,7 @@ impl Chaser {
}
fn find_path<V>(
astar: &mut Option<Astar<Vec3<i32>>>,
astar: &mut Option<Astar<Vec3<i32>, DefaultHashBuilder>>,
vol: &V,
startf: Vec3<f32>,
endf: Vec3<f32>,
@ -263,7 +268,12 @@ where
let satisfied = |pos: &Vec3<i32>| pos == &end;
let mut new_astar = match astar.take() {
None => Astar::new(20_000, start, heuristic.clone()),
None => Astar::new(
20_000,
start,
heuristic.clone(),
DefaultHashBuilder::default(),
),
Some(astar) => astar,
};

View File

@ -5,10 +5,12 @@ use std::{
ops::{Index, IndexMut},
};
pub struct Id<T>(usize, PhantomData<T>);
// NOTE: We use u64 to make sure we are consistent across all machines. We
// assume that usize fits into 8 bytes.
pub struct Id<T>(u64, PhantomData<T>);
impl<T> Id<T> {
pub fn id(&self) -> usize { self.0 }
pub fn id(&self) -> u64 { self.0 }
}
impl<T> Copy for Id<T> {}
@ -37,12 +39,19 @@ impl<T> Default for Store<T> {
}
impl<T> Store<T> {
pub fn get(&self, id: Id<T>) -> &T { self.items.get(id.0).unwrap() }
pub fn get(&self, id: Id<T>) -> &T {
// NOTE: Safe conversion, because it came from usize.
self.items.get(id.0 as usize).unwrap()
}
pub fn get_mut(&mut self, id: Id<T>) -> &mut T { self.items.get_mut(id.0).unwrap() }
pub fn get_mut(&mut self, id: Id<T>) -> &mut T {
// NOTE: Safe conversion, because it came from usize.
self.items.get_mut(id.0 as usize).unwrap()
}
pub fn ids(&self) -> impl Iterator<Item = Id<T>> {
(0..self.items.len()).map(|i| Id(i, PhantomData))
// NOTE: Assumes usize fits into 8 bytes.
(0..self.items.len() as u64).map(|i| Id(i, PhantomData))
}
pub fn iter(&self) -> impl Iterator<Item = &T> { self.items.iter() }
@ -53,11 +62,13 @@ impl<T> Store<T> {
self.items
.iter()
.enumerate()
.map(|(i, item)| (Id(i, PhantomData), item))
// NOTE: Assumes usize fits into 8 bytes.
.map(|(i, item)| (Id(i as u64, PhantomData), item))
}
pub fn insert(&mut self, item: T) -> Id<T> {
let id = Id(self.items.len(), PhantomData);
// NOTE: Assumes usize fits into 8 bytes.
let id = Id(self.items.len() as u64, PhantomData);
self.items.push(item);
id
}

View File

@ -8,6 +8,7 @@ edition = "2018"
bincode = "1.2.0"
common = { package = "veloren-common", path = "../common" }
bitvec = "0.17.4"
fxhash = "0.2.1"
image = "0.22.3"
itertools = "0.8.2"
vek = "0.10.0"

View File

@ -2,6 +2,7 @@
mod econ;
use self::{Occupation::*, Stock::*};
use crate::{
sim::WorldSim,
site::{Dungeon, Settlement, Site as WorldSite},
@ -15,10 +16,15 @@ use common::{
terrain::TerrainChunkSize,
vol::RectVolSize,
};
use core::{
fmt,
hash::{BuildHasherDefault, Hash},
ops::Range,
};
use fxhash::{FxHasher32, FxHasher64};
use hashbrown::{HashMap, HashSet};
use rand::prelude::*;
use rand_chacha::ChaChaRng;
use std::{fmt, hash::Hash, ops::Range};
use vek::*;
const INITIAL_CIV_COUNT: usize = 64;
@ -29,7 +35,15 @@ pub struct Civs {
places: Store<Place>,
tracks: Store<Track>,
track_map: HashMap<Id<Site>, HashMap<Id<Site>, Id<Track>>>,
/// We use this hasher (FxHasher64) because
/// (1) we don't care about DDOS attacks (ruling out SipHash);
/// (2) we care about determinism across computers (ruling out AAHash);
/// (3) we have 8-byte keys (for which FxHash is fastest).
track_map: HashMap<
Id<Site>,
HashMap<Id<Site>, Id<Track>, BuildHasherDefault<FxHasher64>>,
BuildHasherDefault<FxHasher64>,
>,
sites: Store<Site>,
}
@ -235,7 +249,16 @@ impl Civs {
let transition =
|a: &Id<Site>, b: &Id<Site>| self.tracks.get(self.track_between(*a, *b).unwrap()).cost;
let satisfied = |p: &Id<Site>| *p == b;
let mut astar = Astar::new(100, a, heuristic);
// We use this hasher (FxHasher64) because
// (1) we don't care about DDOS attacks (ruling out SipHash);
// (2) we care about determinism across computers (ruling out AAHash);
// (3) we have 8-byte keys (for which FxHash is fastest).
let mut astar = Astar::new(
100,
a,
heuristic,
BuildHasherDefault::<FxHasher64>::default(),
);
astar
.poll(100, heuristic, neighbors, transition, satisfied)
.into_path()
@ -281,8 +304,12 @@ impl Civs {
loc: Vec2<i32>,
area: Range<usize>,
) -> Option<Id<Place>> {
let mut dead = HashSet::new();
let mut alive = HashSet::new();
// We use this hasher (FxHasher64) because
// (1) we don't care about DDOS attacks (ruling out SipHash);
// (2) we care about determinism across computers (ruling out AAHash);
// (3) we have 8-byte keys (for which FxHash is fastest).
let mut dead = HashSet::with_hasher(BuildHasherDefault::<FxHasher64>::default());
let mut alive = HashSet::with_hasher(BuildHasherDefault::<FxHasher64>::default());
alive.insert(loc);
// Fill the surrounding area
@ -495,7 +522,16 @@ fn find_path(
let transition =
|a: &Vec2<i32>, b: &Vec2<i32>| 1.0 + walk_in_dir(sim, *a, *b - *a).unwrap_or(10000.0);
let satisfied = |l: &Vec2<i32>| *l == b;
let mut astar = Astar::new(20000, a, heuristic);
// We use this hasher (FxHasher64) because
// (1) we don't care about DDOS attacks (ruling out SipHash);
// (2) we care about determinism across computers (ruling out AAHash);
// (3) we have 8-byte keys (for which FxHash is fastest).
let mut astar = Astar::new(
20000,
a,
heuristic,
BuildHasherDefault::<FxHasher64>::default(),
);
astar
.poll(20000, heuristic, neighbors, transition, satisfied)
.into_path()
@ -688,13 +724,13 @@ impl fmt::Display for Site {
writeln!(f, "- coin: {}", self.coin.floor() as u32)?;
writeln!(f, "Stocks")?;
for (stock, q) in self.stocks.iter() {
writeln!(f, "- {}: {}", stock, q.floor())?;
writeln!(f, "- {:?}: {}", stock, q.floor())?;
}
writeln!(f, "Values")?;
for stock in TRADE_STOCKS.iter() {
writeln!(
f,
"- {}: {}",
"- {:?}: {}",
stock,
self.values[*stock]
.map(|x| x.to_string())
@ -703,11 +739,16 @@ impl fmt::Display for Site {
}
writeln!(f, "Laborers")?;
for (labor, n) in self.labors.iter() {
writeln!(f, "- {}: {}", labor, (*n * self.population).floor() as u32)?;
writeln!(
f,
"- {:?}: {}",
labor,
(*n * self.population).floor() as u32
)?;
}
writeln!(f, "Export targets")?;
for (stock, n) in self.export_targets.iter() {
writeln!(f, "- {}: {}", stock, n)?;
writeln!(f, "- {:?}: {}", stock, n)?;
}
Ok(())
@ -723,43 +764,50 @@ pub enum SiteKind {
impl Site {
pub fn simulate(&mut self, years: f32, nat_res: &NaturalResources) {
// Insert natural resources into the economy
if self.stocks[FISH] < nat_res.river {
self.stocks[FISH] = nat_res.river;
if self.stocks[Fish] < nat_res.river {
self.stocks[Fish] = nat_res.river;
}
if self.stocks[WHEAT] < nat_res.farmland {
self.stocks[WHEAT] = nat_res.farmland;
if self.stocks[Wheat] < nat_res.farmland {
self.stocks[Wheat] = nat_res.farmland;
}
if self.stocks[LOGS] < nat_res.wood {
self.stocks[LOGS] = nat_res.wood;
if self.stocks[Logs] < nat_res.wood {
self.stocks[Logs] = nat_res.wood;
}
if self.stocks[GAME] < nat_res.wood {
self.stocks[GAME] = nat_res.wood;
if self.stocks[Game] < nat_res.wood {
self.stocks[Game] = nat_res.wood;
}
if self.stocks[ROCK] < nat_res.rock {
self.stocks[ROCK] = nat_res.rock;
if self.stocks[Rock] < nat_res.rock {
self.stocks[Rock] = nat_res.rock;
}
// We use this hasher (FxHasher32) because
// (1) we don't care about DDOS attacks (ruling out SipHash);
// (2) we care about determinism across computers (ruling out AAHash);
// (3) we have 1-byte keys (for which FxHash is supposedly fastest).
let orders = vec![
(None, vec![(FOOD, 0.5)]),
(Some(COOK), vec![(FLOUR, 16.0), (MEAT, 4.0), (WOOD, 3.0)]),
(Some(LUMBERJACK), vec![(LOGS, 4.5)]),
(Some(MINER), vec![(ROCK, 7.5)]),
(Some(FISHER), vec![(FISH, 4.0)]),
(Some(HUNTER), vec![(GAME, 4.0)]),
(Some(FARMER), vec![(WHEAT, 4.0)]),
(None, vec![(Food, 0.5)]),
(Some(Cook), vec![(Flour, 16.0), (Meat, 4.0), (Wood, 3.0)]),
(Some(Lumberjack), vec![(Logs, 4.5)]),
(Some(Miner), vec![(Rock, 7.5)]),
(Some(Fisher), vec![(Fish, 4.0)]),
(Some(Hunter), vec![(Game, 4.0)]),
(Some(Farmer), vec![(Wheat, 4.0)]),
]
.into_iter()
.collect::<HashMap<_, Vec<(Stock, f32)>>>();
.collect::<HashMap<_, Vec<(Stock, f32)>, BuildHasherDefault<FxHasher32>>>();
// Per labourer, per year
let production = Stocks::from_list(&[
(FARMER, (FLOUR, 2.0)),
(LUMBERJACK, (WOOD, 1.5)),
(MINER, (STONE, 0.6)),
(FISHER, (MEAT, 3.0)),
(HUNTER, (MEAT, 0.25)),
(COOK, (FOOD, 20.0)),
]);
let production = MapVec::from_list(
&[
(Farmer, (Flour, 2.0)),
(Lumberjack, (Wood, 1.5)),
(Miner, (Stone, 0.6)),
(Fisher, (Meat, 3.0)),
(Hunter, (Meat, 0.25)),
(Cook, (Food, 20.0)),
],
(Rock, 0.0),
);
let mut demand = Stocks::from_default(0.0);
for (labor, orders) in &orders {
@ -881,7 +929,7 @@ impl Site {
// Births/deaths
const NATURAL_BIRTH_RATE: f32 = 0.15;
const DEATH_RATE: f32 = 0.05;
let birth_rate = if self.surplus[FOOD] > 0.0 {
let birth_rate = if self.surplus[Food] > 0.0 {
NATURAL_BIRTH_RATE
} else {
0.0
@ -890,26 +938,33 @@ impl Site {
}
}
type Occupation = &'static str;
const FARMER: Occupation = "farmer";
const LUMBERJACK: Occupation = "lumberjack";
const MINER: Occupation = "miner";
const FISHER: Occupation = "fisher";
const HUNTER: Occupation = "hunter";
const COOK: Occupation = "cook";
#[repr(u8)]
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
enum Occupation {
Farmer = 0,
Lumberjack = 1,
Miner = 2,
Fisher = 3,
Hunter = 4,
Cook = 5,
}
type Stock = &'static str;
const WHEAT: Stock = "wheat";
const FLOUR: Stock = "flour";
const MEAT: Stock = "meat";
const FISH: Stock = "fish";
const GAME: Stock = "game";
const FOOD: Stock = "food";
const LOGS: Stock = "logs";
const WOOD: Stock = "wood";
const ROCK: Stock = "rock";
const STONE: Stock = "stone";
const TRADE_STOCKS: [Stock; 5] = [FLOUR, MEAT, FOOD, WOOD, STONE];
#[repr(u8)]
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub enum Stock {
Wheat = 0,
Flour = 1,
Meat = 2,
Fish = 3,
Game = 4,
Food = 5,
Logs = 6,
Wood = 7,
Rock = 8,
Stone = 9,
}
const TRADE_STOCKS: [Stock; 5] = [Flour, Meat, Food, Wood, Stone];
#[derive(Debug, Clone)]
struct TradeState {
@ -934,21 +989,35 @@ impl Default for TradeState {
pub type Stocks<T> = MapVec<Stock, T>;
#[derive(Default, Clone, Debug)]
#[derive(Clone, Debug)]
pub struct MapVec<K, T> {
entries: HashMap<K, T>,
/// We use this hasher (FxHasher32) because
/// (1) we don't care about DDOS attacks (ruling out SipHash);
/// (2) we care about determinism across computers (ruling out AAHash);
/// (3) we have 1-byte keys (for which FxHash is supposedly fastest).
entries: HashMap<K, T, BuildHasherDefault<FxHasher32>>,
default: T,
}
impl<K: Copy + Eq + Hash, T: Default + Clone> MapVec<K, T> {
pub fn from_list<'a>(i: impl IntoIterator<Item = &'a (K, T)>) -> Self
/// Need manual implementation of Default since K doesn't need that bound.
impl<K, T: Default> Default for MapVec<K, T> {
fn default() -> Self {
Self {
entries: Default::default(),
default: Default::default(),
}
}
}
impl<K: Copy + Eq + Hash, T: Clone> MapVec<K, T> {
pub fn from_list<'a>(i: impl IntoIterator<Item = &'a (K, T)>, default: T) -> Self
where
K: 'a,
T: 'a,
{
Self {
entries: i.into_iter().cloned().collect(),
default: T::default(),
default,
}
}
@ -986,12 +1055,12 @@ impl<K: Copy + Eq + Hash, T: Default + Clone> MapVec<K, T> {
}
}
impl<K: Copy + Eq + Hash, T: Default + Clone> std::ops::Index<K> for MapVec<K, T> {
impl<K: Copy + Eq + Hash, T: Clone> std::ops::Index<K> for MapVec<K, T> {
type Output = T;
fn index(&self, entry: K) -> &Self::Output { self.get(entry) }
}
impl<K: Copy + Eq + Hash, T: Default + Clone> std::ops::IndexMut<K> for MapVec<K, T> {
impl<K: Copy + Eq + Hash, T: Clone> std::ops::IndexMut<K> for MapVec<K, T> {
fn index_mut(&mut self, entry: K) -> &mut Self::Output { self.get_mut(entry) }
}

View File

@ -1,3 +1,5 @@
use core::hash::BuildHasherDefault;
use fxhash::FxHasher64;
use hashbrown::HashSet;
use rand::{seq::SliceRandom, Rng};
use vek::*;
@ -7,7 +9,11 @@ pub struct Location {
pub(crate) name: String,
pub(crate) center: Vec2<i32>,
pub(crate) kingdom: Option<Kingdom>,
pub(crate) neighbours: HashSet<usize>,
// We use this hasher (FxHasher64) because
// (1) we don't care about DDOS attacks (ruling out SipHash);
// (2) we care about determinism across computers (ruling out AAHash);
// (3) we have 8-byte keys (for which FxHash is fastest).
pub(crate) neighbours: HashSet<u64, BuildHasherDefault<FxHasher64>>,
}
impl Location {

View File

@ -1353,14 +1353,15 @@ impl WorldSim {
.map(|l| l.center)
.enumerate()
.collect::<Vec<_>>();
// NOTE: We assume that usize is 8 or fewer bytes.
(0..locations.len()).for_each(|i| {
let pos = locations[i].center.map(|e| e as i64);
loc_clone.sort_by_key(|(_, l)| l.map(|e| e as i64).distance_squared(pos));
loc_clone.iter().skip(1).take(2).for_each(|(j, _)| {
locations[i].neighbours.insert(*j);
locations[*j].neighbours.insert(i);
locations[i].neighbours.insert(*j as u64);
locations[*j].neighbours.insert(i as u64);
});
});

View File

@ -16,9 +16,11 @@ use common::{
terrain::{Block, BlockKind, Structure, TerrainChunkSize},
vol::{BaseVol, ReadVol, RectSizedVol, RectVolSize, Vox, WriteVol},
};
use core::{f32, hash::BuildHasherDefault};
use fxhash::FxHasher64;
use lazy_static::lazy_static;
use rand::prelude::*;
use std::{f32, sync::Arc};
use std::sync::Arc;
use vek::*;
impl WorldSim {
@ -381,7 +383,16 @@ impl Floor {
_ => 100000.0,
};
let satisfied = |l: &Vec2<i32>| *l == b;
let mut astar = Astar::new(20000, a, heuristic);
// We use this hasher (FxHasher64) because
// (1) we don't care about DDOS attacks (ruling out SipHash);
// (2) we don't care about determinism across computers (we could use AAHash);
// (3) we have 8-byte keys (for which FxHash is fastest).
let mut astar = Astar::new(
20000,
a,
heuristic,
BuildHasherDefault::<FxHasher64>::default(),
);
let path = astar
.poll(
FLOOR_SIZE.product() as usize + 1,

View File

@ -18,9 +18,10 @@ use common::{
terrain::{Block, BlockKind, TerrainChunkSize},
vol::{BaseVol, ReadVol, RectSizedVol, RectVolSize, Vox, WriteVol},
};
use fxhash::FxHasher64;
use hashbrown::{HashMap, HashSet};
use rand::prelude::*;
use std::{collections::VecDeque, f32};
use std::{collections::VecDeque, f32, hash::BuildHasherDefault};
use vek::*;
#[allow(dead_code)]
@ -967,7 +968,11 @@ pub struct Sample<'a> {
}
pub struct Land {
tiles: HashMap<Vec2<i32>, Tile>,
/// We use this hasher (FxHasher64) because
/// (1) we need determinism across computers (ruling out AAHash);
/// (2) we don't care about DDOS attacks (ruling out SipHash);
/// (3) we have 8-byte keys (for which FxHash is fastest).
tiles: HashMap<Vec2<i32>, Tile, BuildHasherDefault<FxHasher64>>,
plots: Store<Plot>,
sampler_warp: StructureGen2d,
hazard: Id<Plot>,
@ -978,7 +983,7 @@ impl Land {
let mut plots = Store::default();
let hazard = plots.insert(Plot::Hazard);
Self {
tiles: HashMap::new(),
tiles: HashMap::default(),
plots,
sampler_warp: StructureGen2d::new(rng.gen(), AREA_SIZE, AREA_SIZE * 2 / 5),
hazard,
@ -1089,21 +1094,38 @@ impl Land {
|from: &Vec2<i32>, to: &Vec2<i32>| path_cost_fn(self.tile_at(*from), self.tile_at(*to));
let satisfied = |pos: &Vec2<i32>| *pos == dest;
Astar::new(250, origin, heuristic)
// We use this hasher (FxHasher64) because
// (1) we don't care about DDOS attacks (ruling out SipHash);
// (2) we don't care about determinism across computers (we could use AAHash);
// (3) we have 8-byte keys (for which FxHash is fastest).
Astar::new(
250,
origin,
heuristic,
BuildHasherDefault::<FxHasher64>::default(),
)
.poll(250, heuristic, neighbors, transition, satisfied)
.into_path()
}
/// We use this hasher (FxHasher64) because
/// (1) we don't care about DDOS attacks (ruling out SipHash);
/// (2) we care about determinism across computers (ruling out AAHash);
/// (3) we have 8-byte keys (for which FxHash is fastest).
fn grow_from(
&self,
start: Vec2<i32>,
max_size: usize,
_rng: &mut impl Rng,
mut match_fn: impl FnMut(Option<&Plot>) -> bool,
) -> HashSet<Vec2<i32>> {
) -> HashSet<Vec2<i32>, BuildHasherDefault<FxHasher64>> {
let mut open = VecDeque::new();
open.push_back(start);
let mut closed = HashSet::new();
// We use this hasher (FxHasher64) because
// (1) we don't care about DDOS attacks (ruling out SipHash);
// (2) we care about determinism across computers (ruling out AAHash);
// (3) we have 8-byte keys (for which FxHash is fastest).
let mut closed = HashSet::with_hasher(BuildHasherDefault::<FxHasher64>::default());
while open.len() + closed.len() < max_size {
let next_pos = if let Some(next_pos) = open.pop_front() {

View File

@ -1,45 +0,0 @@
use hashbrown::HashMap;
use std::hash::Hash;
pub struct HashCache<K: Hash + Eq + Clone, V> {
capacity: usize,
map: HashMap<K, (usize, V)>,
counter: usize,
}
impl<K: Hash + Eq + Clone, V> Default for HashCache<K, V> {
fn default() -> Self { Self::with_capacity(1024) }
}
impl<K: Hash + Eq + Clone, V> HashCache<K, V> {
pub fn with_capacity(capacity: usize) -> Self {
Self {
capacity,
map: HashMap::with_capacity(1024),
counter: 0,
}
}
pub fn maintain(&mut self) {
const CACHE_BLOAT_RATE: usize = 2;
if self.map.len() > self.capacity * CACHE_BLOAT_RATE {
let (capacity, counter) = (self.capacity, self.counter);
self.map.retain(|_, (c, _)| *c + capacity > counter);
}
}
pub fn get<F: FnOnce(K) -> V>(&mut self, key: K, f: F) -> &V {
self.maintain();
let counter = &mut self.counter;
&self
.map
.entry(key.clone())
.or_insert_with(|| {
*counter += 1;
(*counter, f(key))
})
.1
}
}

View File

@ -1,6 +1,5 @@
pub mod fast_noise;
pub mod grid;
pub mod hash_cache;
pub mod random;
pub mod sampler;
pub mod seed_expan;
@ -12,7 +11,6 @@ pub mod unit_chooser;
pub use self::{
fast_noise::FastNoise,
grid::Grid,
hash_cache::HashCache,
random::{RandomField, RandomPerm},
sampler::{Sampler, SamplerMut},
small_cache::SmallCache,