Merge branch 'sharp/deterministic-civsim' into 'master'

Make civsim and sites deterministic.

See merge request veloren/veloren!1009
This commit is contained in:
Joshua Barretto 2020-05-26 19:22:48 +00:00
commit 786665eac4
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 ### Changed
- Improved camera aiming - Improved camera aiming
- Made civsim, sites, etc. deterministic from the same seed.
### Removed ### Removed

2
Cargo.lock generated
View File

@ -4993,6 +4993,7 @@ dependencies = [
"crossbeam", "crossbeam",
"dot_vox", "dot_vox",
"find_folder", "find_folder",
"fxhash",
"hashbrown", "hashbrown",
"image", "image",
"indexmap", "indexmap",
@ -5115,6 +5116,7 @@ dependencies = [
"arr_macro", "arr_macro",
"bincode", "bincode",
"bitvec", "bitvec",
"fxhash",
"hashbrown", "hashbrown",
"image", "image",
"itertools", "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"] } specs = { version = "0.15.1", features = ["serde", "nightly", "storage-event-control"] }
vek = { version = "0.10.0", features = ["serde"] } vek = { version = "0.10.0", features = ["serde"] }
dot_vox = "4.0.0" dot_vox = "4.0.0"
fxhash = "0.2.1"
image = "0.22.3" image = "0.22.3"
mio = "0.6.19" mio = "0.6.19"
mio-extras = "2.0.5" mio-extras = "2.0.5"

View File

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

View File

@ -3,6 +3,7 @@ use crate::{
terrain::Block, terrain::Block,
vol::{BaseVol, ReadVol}, vol::{BaseVol, ReadVol},
}; };
use hashbrown::hash_map::DefaultHashBuilder;
use rand::{thread_rng, Rng}; use rand::{thread_rng, Rng};
use std::iter::FromIterator; use std::iter::FromIterator;
use vek::*; use vek::*;
@ -92,7 +93,11 @@ impl Route {
pub struct Chaser { pub struct Chaser {
last_search_tgt: Option<Vec3<f32>>, last_search_tgt: Option<Vec3<f32>>,
route: Route, 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 { impl Chaser {
@ -147,7 +152,7 @@ impl Chaser {
} }
fn find_path<V>( fn find_path<V>(
astar: &mut Option<Astar<Vec3<i32>>>, astar: &mut Option<Astar<Vec3<i32>, DefaultHashBuilder>>,
vol: &V, vol: &V,
startf: Vec3<f32>, startf: Vec3<f32>,
endf: Vec3<f32>, endf: Vec3<f32>,
@ -263,7 +268,12 @@ where
let satisfied = |pos: &Vec3<i32>| pos == &end; let satisfied = |pos: &Vec3<i32>| pos == &end;
let mut new_astar = match astar.take() { 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, Some(astar) => astar,
}; };

View File

@ -5,10 +5,12 @@ use std::{
ops::{Index, IndexMut}, 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> { impl<T> Id<T> {
pub fn id(&self) -> usize { self.0 } pub fn id(&self) -> u64 { self.0 }
} }
impl<T> Copy for Id<T> {} impl<T> Copy for Id<T> {}
@ -37,12 +39,19 @@ impl<T> Default for Store<T> {
} }
impl<T> 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>> { 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() } pub fn iter(&self) -> impl Iterator<Item = &T> { self.items.iter() }
@ -53,11 +62,13 @@ impl<T> Store<T> {
self.items self.items
.iter() .iter()
.enumerate() .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> { 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); self.items.push(item);
id id
} }

View File

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

View File

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

View File

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

View File

@ -16,9 +16,11 @@ use common::{
terrain::{Block, BlockKind, Structure, TerrainChunkSize}, terrain::{Block, BlockKind, Structure, TerrainChunkSize},
vol::{BaseVol, ReadVol, RectSizedVol, RectVolSize, Vox, WriteVol}, vol::{BaseVol, ReadVol, RectSizedVol, RectVolSize, Vox, WriteVol},
}; };
use core::{f32, hash::BuildHasherDefault};
use fxhash::FxHasher64;
use lazy_static::lazy_static; use lazy_static::lazy_static;
use rand::prelude::*; use rand::prelude::*;
use std::{f32, sync::Arc}; use std::sync::Arc;
use vek::*; use vek::*;
impl WorldSim { impl WorldSim {
@ -381,7 +383,16 @@ impl Floor {
_ => 100000.0, _ => 100000.0,
}; };
let satisfied = |l: &Vec2<i32>| *l == b; 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 let path = astar
.poll( .poll(
FLOOR_SIZE.product() as usize + 1, FLOOR_SIZE.product() as usize + 1,

View File

@ -18,9 +18,10 @@ use common::{
terrain::{Block, BlockKind, TerrainChunkSize}, terrain::{Block, BlockKind, TerrainChunkSize},
vol::{BaseVol, ReadVol, RectSizedVol, RectVolSize, Vox, WriteVol}, vol::{BaseVol, ReadVol, RectSizedVol, RectVolSize, Vox, WriteVol},
}; };
use fxhash::FxHasher64;
use hashbrown::{HashMap, HashSet}; use hashbrown::{HashMap, HashSet};
use rand::prelude::*; use rand::prelude::*;
use std::{collections::VecDeque, f32}; use std::{collections::VecDeque, f32, hash::BuildHasherDefault};
use vek::*; use vek::*;
#[allow(dead_code)] #[allow(dead_code)]
@ -967,7 +968,11 @@ pub struct Sample<'a> {
} }
pub struct Land { 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>, plots: Store<Plot>,
sampler_warp: StructureGen2d, sampler_warp: StructureGen2d,
hazard: Id<Plot>, hazard: Id<Plot>,
@ -978,7 +983,7 @@ impl Land {
let mut plots = Store::default(); let mut plots = Store::default();
let hazard = plots.insert(Plot::Hazard); let hazard = plots.insert(Plot::Hazard);
Self { Self {
tiles: HashMap::new(), tiles: HashMap::default(),
plots, plots,
sampler_warp: StructureGen2d::new(rng.gen(), AREA_SIZE, AREA_SIZE * 2 / 5), sampler_warp: StructureGen2d::new(rng.gen(), AREA_SIZE, AREA_SIZE * 2 / 5),
hazard, hazard,
@ -1089,21 +1094,38 @@ impl Land {
|from: &Vec2<i32>, to: &Vec2<i32>| path_cost_fn(self.tile_at(*from), self.tile_at(*to)); |from: &Vec2<i32>, to: &Vec2<i32>| path_cost_fn(self.tile_at(*from), self.tile_at(*to));
let satisfied = |pos: &Vec2<i32>| *pos == dest; let satisfied = |pos: &Vec2<i32>| *pos == dest;
Astar::new(250, origin, heuristic) // We use this hasher (FxHasher64) because
.poll(250, heuristic, neighbors, transition, satisfied) // (1) we don't care about DDOS attacks (ruling out SipHash);
.into_path() // (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( fn grow_from(
&self, &self,
start: Vec2<i32>, start: Vec2<i32>,
max_size: usize, max_size: usize,
_rng: &mut impl Rng, _rng: &mut impl Rng,
mut match_fn: impl FnMut(Option<&Plot>) -> bool, mut match_fn: impl FnMut(Option<&Plot>) -> bool,
) -> HashSet<Vec2<i32>> { ) -> HashSet<Vec2<i32>, BuildHasherDefault<FxHasher64>> {
let mut open = VecDeque::new(); let mut open = VecDeque::new();
open.push_back(start); 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 { while open.len() + closed.len() < max_size {
let next_pos = if let Some(next_pos) = open.pop_front() { 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 fast_noise;
pub mod grid; pub mod grid;
pub mod hash_cache;
pub mod random; pub mod random;
pub mod sampler; pub mod sampler;
pub mod seed_expan; pub mod seed_expan;
@ -12,7 +11,6 @@ pub mod unit_chooser;
pub use self::{ pub use self::{
fast_noise::FastNoise, fast_noise::FastNoise,
grid::Grid, grid::Grid,
hash_cache::HashCache,
random::{RandomField, RandomPerm}, random::{RandomField, RandomPerm},
sampler::{Sampler, SamplerMut}, sampler::{Sampler, SamplerMut},
small_cache::SmallCache, small_cache::SmallCache,