Merge branch 'imbris/server-start-faster' into 'master'

Improve server startup times, in particular, finding paths between sites.

See merge request veloren/veloren!3888
This commit is contained in:
Imbris 2023-05-04 18:06:38 +00:00
commit 60dbcf86f9
20 changed files with 518 additions and 319 deletions

View File

@ -4,12 +4,13 @@ use core::{
fmt,
hash::{BuildHasher, Hash},
};
use hashbrown::{HashMap, HashSet};
use hashbrown::HashMap;
use std::collections::BinaryHeap;
#[derive(Copy, Clone, Debug)]
pub struct PathEntry<S> {
cost: f32,
// cost so far + heursitic
cost_estimate: f32,
node: S,
}
@ -23,25 +24,50 @@ impl<S: Eq> Ord for PathEntry<S> {
// This method implements reverse ordering, so that the lowest cost
// will be ordered first
fn cmp(&self, other: &PathEntry<S>) -> Ordering {
other.cost.partial_cmp(&self.cost).unwrap_or(Equal)
other
.cost_estimate
.partial_cmp(&self.cost_estimate)
.unwrap_or(Equal)
}
}
impl<S: Eq> PartialOrd for PathEntry<S> {
fn partial_cmp(&self, other: &PathEntry<S>) -> Option<Ordering> { Some(self.cmp(other)) }
// This is particularily hot in `BinaryHeap::pop`, so we provide this
// implementation.
//
// NOTE: This probably doesn't handle edge cases like `NaNs` in a consistent
// manner with `Ord`, but I don't think we need to care about that here(?)
//
// See note about reverse ordering above.
fn le(&self, other: &PathEntry<S>) -> bool { other.cost_estimate <= self.cost_estimate }
}
pub enum PathResult<T> {
/// No reachable nodes were satisfactory.
///
/// Contains path to node with the lowest heuristic value (out of the
/// explored nodes).
None(Path<T>),
/// Either max_iters or max_cost was reached.
///
/// Contains path to node with the lowest heuristic value (out of the
/// explored nodes).
Exhausted(Path<T>),
Path(Path<T>),
/// Path succefully found.
///
/// Second field is cost.
Path(Path<T>, f32),
Pending,
}
impl<T> PathResult<T> {
pub fn into_path(self) -> Option<Path<T>> {
/// Returns `Some((path, cost))` if a path reaching the target was
/// successfully found.
pub fn into_path(self) -> Option<(Path<T>, f32)> {
match self {
PathResult::Path(path) => Some(path),
PathResult::Path(path, cost) => Some((path, cost)),
_ => None,
}
}
@ -50,23 +76,38 @@ impl<T> PathResult<T> {
match self {
PathResult::None(p) => PathResult::None(f(p)),
PathResult::Exhausted(p) => PathResult::Exhausted(f(p)),
PathResult::Path(p) => PathResult::Path(f(p)),
PathResult::Path(p, cost) => PathResult::Path(f(p), cost),
PathResult::Pending => PathResult::Pending,
}
}
}
// If node entry exists, this was visited!
#[derive(Clone, Debug)]
struct NodeEntry<S> {
/// Previous node in the cheapest path (known so far) that goes from the
/// start to this node.
///
/// If `came_from == self` this is the start node! (to avoid inflating the
/// size with `Option`)
came_from: S,
/// Cost to reach this node from the start by following the cheapest path
/// known so far. This is the sum of the transition costs between all the
/// nodes on this path.
cost: f32,
}
#[derive(Clone)]
pub struct Astar<S, Hasher> {
iter: usize,
max_iters: usize,
potential_nodes: BinaryHeap<PathEntry<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>,
max_cost: f32,
potential_nodes: BinaryHeap<PathEntry<S>>, // cost, node pairs
visited_nodes: HashMap<S, NodeEntry<S>, Hasher>,
/// Node with the lowest heuristic value so far.
///
/// (node, heuristic value)
closest_node: Option<(S, f32)>,
}
/// NOTE: Must manually derive since Hasher doesn't implement it.
@ -76,12 +117,8 @@ impl<S: Clone + Eq + Hash + fmt::Debug, H: BuildHasher> fmt::Debug for Astar<S,
.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)
.field("visited_nodes", &self.visited_nodes)
.field("closest_node", &self.closest_node)
.finish()
}
}
@ -90,72 +127,110 @@ impl<S: Clone + Eq + Hash, H: BuildHasher + Clone> Astar<S, H> {
pub fn new(max_iters: usize, start: S, hasher: H) -> Self {
Self {
max_iters,
max_cost: f32::MAX,
iter: 0,
potential_nodes: core::iter::once(PathEntry {
cost: 0.0,
cost_estimate: 0.0,
node: start.clone(),
})
.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(), 0.0)));
h
},
visited: {
let mut s = HashSet::with_capacity_and_hasher(1, hasher);
s.extend(core::iter::once(start));
visited_nodes: {
let mut s = HashMap::with_capacity_and_hasher(1, hasher);
s.extend(core::iter::once((start.clone(), NodeEntry {
came_from: start,
cost: 0.0,
})));
s
},
cheapest_node: None,
cheapest_cost: None,
closest_node: None,
}
}
pub fn with_max_cost(mut self, max_cost: f32) -> Self {
self.max_cost = max_cost;
self
}
pub fn poll<I>(
&mut self,
iters: usize,
// Estimate how far we are from the target? but we are given two nodes...
// (current, previous)
mut heuristic: impl FnMut(&S, &S) -> f32,
// get neighboring nodes
mut neighbors: impl FnMut(&S) -> I,
mut transition: impl FnMut(&S, &S) -> f32,
// have we reached target?
mut satisfied: impl FnMut(&S) -> bool,
) -> PathResult<S>
where
I: Iterator<Item = S>,
I: Iterator<Item = (S, f32)>, // (node, transition cost)
{
let iter_limit = self.max_iters.min(self.iter + iters);
while self.iter < iter_limit {
if let Some(PathEntry { node, .. }) = self.potential_nodes.pop() {
if let Some(PathEntry {
node,
cost_estimate,
}) = self.potential_nodes.pop()
{
let (node_cost, came_from) = self
.visited_nodes
.get(&node)
.map(|n| (n.cost, n.came_from.clone()))
.expect("All nodes in the queue should be included in visisted_nodes");
if satisfied(&node) {
return PathResult::Path(self.reconstruct_path_to(node));
return PathResult::Path(self.reconstruct_path_to(node), node_cost);
// Note, we assume that cost_estimate isn't an overestimation
// (i.e. that `heuristic` doesn't overestimate).
} else if cost_estimate > self.max_cost {
return PathResult::Exhausted(
self.closest_node
.clone()
.map(|(lc, _)| self.reconstruct_path_to(lc))
.unwrap_or_default(),
);
} else {
for neighbor in neighbors(&node) {
let node_cheapest = self.cheapest_scores.get(&node).unwrap_or(&f32::MAX);
let neighbor_cheapest =
self.cheapest_scores.get(&neighbor).unwrap_or(&f32::MAX);
for (neighbor, transition_cost) in neighbors(&node) {
if neighbor == came_from {
continue;
}
let neighbor_cost = self
.visited_nodes
.get(&neighbor)
.map_or(f32::MAX, |n| n.cost);
let cost = node_cheapest + transition(&node, &neighbor);
if cost < *neighbor_cheapest {
self.came_from.insert(neighbor.clone(), node.clone());
self.cheapest_scores.insert(neighbor.clone(), cost);
// compute cost to traverse to each neighbor
let cost = node_cost + transition_cost;
if cost < neighbor_cost {
let previously_visited = self
.visited_nodes
.insert(neighbor.clone(), NodeEntry {
came_from: node.clone(),
cost,
})
.is_some();
let h = heuristic(&neighbor, &node);
let neighbor_cost = cost + h;
self.final_scores.insert(neighbor.clone(), neighbor_cost);
// note that `cost` field does not include the heuristic
// priority queue does include heuristic
let cost_estimate = cost + h;
if self.cheapest_cost.map(|cc| h < cc).unwrap_or(true) {
self.cheapest_node = Some(node.clone());
self.cheapest_cost = Some(h);
if self
.closest_node
.as_ref()
.map(|&(_, ch)| h < ch)
.unwrap_or(true)
{
self.closest_node = Some((node.clone(), h));
};
if self.visited.insert(neighbor.clone()) {
// TODO: I think the if here should be removed
// if we hadn't already visited, add this to potential nodes, what about
// its neighbors, wouldn't they need to be revisted???
if !previously_visited {
self.potential_nodes.push(PathEntry {
cost_estimate,
node: neighbor,
cost: neighbor_cost,
});
}
}
@ -163,9 +238,9 @@ impl<S: Clone + Eq + Hash, H: BuildHasher + Clone> Astar<S, H> {
}
} else {
return PathResult::None(
self.cheapest_node
self.closest_node
.clone()
.map(|lc| self.reconstruct_path_to(lc))
.map(|(lc, _)| self.reconstruct_path_to(lc))
.unwrap_or_default(),
);
}
@ -175,9 +250,9 @@ impl<S: Clone + Eq + Hash, H: BuildHasher + Clone> Astar<S, H> {
if self.iter >= self.max_iters {
PathResult::Exhausted(
self.cheapest_node
self.closest_node
.clone()
.map(|lc| self.reconstruct_path_to(lc))
.map(|(lc, _)| self.reconstruct_path_to(lc))
.unwrap_or_default(),
)
} else {
@ -185,12 +260,15 @@ impl<S: Clone + Eq + Hash, H: BuildHasher + Clone> Astar<S, H> {
}
}
pub fn get_cheapest_cost(&self) -> Option<f32> { self.cheapest_cost }
fn reconstruct_path_to(&mut self, end: S) -> Path<S> {
let mut path = vec![end.clone()];
let mut cnode = &end;
while let Some(node) = self.came_from.get(cnode) {
while let Some(node) = self
.visited_nodes
.get(cnode)
.map(|n| &n.came_from)
.filter(|n| *n != cnode)
{
path.push(node.clone());
cnode = node;
}

View File

@ -537,6 +537,17 @@ where
};
let heuristic = |pos: &Vec3<i32>, _: &Vec3<i32>| (pos.distance_squared(end) as f32).sqrt();
let transition = |a: Vec3<i32>, b: Vec3<i32>| {
let crow_line = LineSegment2 {
start: startf.xy(),
end: endf.xy(),
};
// Modify the heuristic a little in order to prefer paths that take us on a
// straight line toward our target. This means we get smoother movement.
1.0 + crow_line.distance_to_point(b.xy().map(|e| e as f32)) * 0.025
+ (b.z - a.z - 1).max(0) as f32 * 10.0
};
let neighbors = |pos: &Vec3<i32>| {
let pos = *pos;
const DIRS: [Vec3<i32>; 17] = [
@ -616,7 +627,10 @@ where
.map(|b| !b.is_solid())
.unwrap_or(true)))
})
.map(move |(pos, dir)| pos + dir)
.map(|(pos, dir)| {
let destination = pos + dir;
(destination, transition(pos, destination))
})
// .chain(
// DIAGONALS
// .iter()
@ -627,17 +641,6 @@ where
// )
};
let transition = |a: &Vec3<i32>, b: &Vec3<i32>| {
let crow_line = LineSegment2 {
start: startf.xy(),
end: endf.xy(),
};
// Modify the heuristic a little in order to prefer paths that take us on a
// straight line toward our target. This means we get smoother movement.
1.0 + crow_line.distance_to_point(b.xy().map(|e| e as f32)) * 0.025
+ (b.z - a.z - 1).max(0) as f32 * 10.0
};
let satisfied = |pos: &Vec3<i32>| pos == &end;
let mut new_astar = match astar.take() {
@ -645,12 +648,12 @@ where
Some(astar) => astar,
};
let path_result = new_astar.poll(100, heuristic, neighbors, transition, satisfied);
let path_result = new_astar.poll(100, heuristic, neighbors, satisfied);
*astar = Some(new_astar);
match path_result {
PathResult::Path(path) => {
PathResult::Path(path, _cost) => {
*astar = None;
(Some(path), true)
},

View File

@ -942,6 +942,12 @@ pub fn handle_manipulate_loadout(
BuildHasherDefault::<FxHasher64>::default(),
);
// Transition uses manhattan distance as the cost, with a slightly lower cost
// for z transitions
let transition = |a: Vec3<i32>, b: Vec3<i32>| {
let (a, b) = (a.map(|x| x as f32), b.map(|x| x as f32));
((a - b) * Vec3::new(1.0, 1.0, 0.9)).map(|e| e.abs()).sum()
};
// Neighbors are all neighboring blocks that are air
let neighbors = |pos: &Vec3<i32>| {
const DIRS: [Vec3<i32>; 6] = [
@ -953,24 +959,23 @@ pub fn handle_manipulate_loadout(
Vec3::new(0, 0, -1),
];
let pos = *pos;
DIRS.iter().map(move |dir| dir + pos).filter(|pos| {
data.terrain
.get(*pos)
.ok()
.map_or(false, |block| !block.is_filled())
})
};
// Transition uses manhattan distance as the cost, with a slightly lower cost
// for z transitions
let transition = |a: &Vec3<i32>, b: &Vec3<i32>| {
let (a, b) = (a.map(|x| x as f32), b.map(|x| x as f32));
((a - b) * Vec3::new(1.0, 1.0, 0.9)).map(|e| e.abs()).sum()
DIRS.iter()
.map(move |dir| {
let dest = dir + pos;
(dest, transition(pos, dest))
})
.filter(|(pos, _)| {
data.terrain
.get(*pos)
.ok()
.map_or(false, |block| !block.is_filled())
})
};
// Pathing satisfied when it reaches the sprite position
let satisfied = |pos: &Vec3<i32>| *pos == sprite_pos;
let not_blocked_by_terrain = astar
.poll(iters, heuristic, neighbors, transition, satisfied)
.poll(iters, heuristic, neighbors, satisfied)
.into_path()
.is_some();

View File

@ -3,6 +3,7 @@ use super::{
vec2_as_uniform_idx, TerrainChunkSize, NEIGHBOR_DELTA, TERRAIN_CHUNK_BLOCKS_LG,
};
use crate::vol::RectVolSize;
use common_base::prof_span;
use core::{f32, f64, iter, ops::RangeInclusive};
use vek::*;
@ -451,6 +452,7 @@ impl<'a> MapConfig<'a> {
sample_wpos: impl Fn(Vec2<i32>) -> f32,
mut write_pixel: impl FnMut(Vec2<usize>, (u8, u8, u8, u8)),
) -> MapDebug {
prof_span!("MapConfig::generate");
let MapConfig {
map_size_lg,
dimensions,

View File

@ -24,7 +24,7 @@ use common::{
vol::{ReadVol, WriteVol},
weather::{Weather, WeatherGrid},
};
use common_base::span;
use common_base::{prof_span, span};
use common_ecs::{PhysicsMetrics, SysMetrics};
use common_net::sync::{interpolation as sync_interp, WorldSyncExt};
use core::{convert::identity, time::Duration};
@ -179,6 +179,7 @@ impl State {
map_size_lg: MapSizeLg,
default_chunk: Arc<TerrainChunk>,
) -> specs::World {
prof_span!("State::setup_ecs_world");
let mut ecs = specs::World::new();
// Uids for sync
ecs.register_sync_marker();

View File

@ -52,10 +52,10 @@ fn path_in_site(start: Vec2<i32>, end: Vec2<i32>, site: &site2::Site) -> PathRes
let heuristic = |tile: &Vec2<i32>, _: &Vec2<i32>| tile.as_::<f32>().distance(end.as_());
let mut astar = Astar::new(1000, start, BuildHasherDefault::<FxHasher64>::default());
let transition = |a: &Vec2<i32>, b: &Vec2<i32>| {
let transition = |a: Vec2<i32>, b: Vec2<i32>| {
let distance = a.as_::<f32>().distance(b.as_());
let a_tile = site.tiles.get(*a);
let b_tile = site.tiles.get(*b);
let a_tile = site.tiles.get(a);
let b_tile = site.tiles.get(b);
let terrain = match &b_tile.kind {
TileKind::Empty => 3.0,
@ -79,12 +79,12 @@ fn path_in_site(start: Vec2<i32>, end: Vec2<i32>, site: &site2::Site) -> PathRes
let building = if a_tile.is_building() && b_tile.is_road() {
a_tile
.plot
.and_then(|plot| is_door_tile(plot, *a).then_some(1.0))
.and_then(|plot| is_door_tile(plot, a).then_some(1.0))
.unwrap_or(10000.0)
} else if b_tile.is_building() && a_tile.is_road() {
b_tile
.plot
.and_then(|plot| is_door_tile(plot, *b).then_some(1.0))
.and_then(|plot| is_door_tile(plot, b).then_some(1.0))
.unwrap_or(10000.0)
} else if (a_tile.is_building() || b_tile.is_building()) && a_tile.plot != b_tile.plot {
10000.0
@ -97,10 +97,13 @@ fn path_in_site(start: Vec2<i32>, end: Vec2<i32>, site: &site2::Site) -> PathRes
let neighbors = |tile: &Vec2<i32>| {
let tile = *tile;
CARDINALS.iter().map(move |c| tile + *c)
CARDINALS.iter().map(move |c| {
let n = tile + *c;
(n, transition(tile, n))
})
};
astar.poll(1000, heuristic, neighbors, transition, |tile| {
astar.poll(1000, heuristic, neighbors, |tile| {
*tile == end || site.tiles.get_known(*tile).is_none()
})
}
@ -135,17 +138,22 @@ fn path_between_sites(
let mut astar = Astar::new(250, start, BuildHasherDefault::<FxHasher64>::default());
let neighbors = |site: &Id<civ::Site>| world.civs().neighbors(*site);
let transition = |a: &Id<civ::Site>, b: &Id<civ::Site>| {
let transition = |a: Id<civ::Site>, b: Id<civ::Site>| {
world
.civs()
.track_between(*a, *b)
.track_between(a, b)
.map(|(id, _)| world.civs().tracks.get(id).cost)
.unwrap_or(f32::INFINITY)
};
let neighbors = |site: &Id<civ::Site>| {
let site = *site;
world
.civs()
.neighbors(site)
.map(move |n| (n, transition(n, site)))
};
let path = astar.poll(250, heuristic, neighbors, transition, |site| *site == end);
let path = astar.poll(250, heuristic, neighbors, |site| *site == end);
path.map(|path| {
let path = path
@ -170,7 +178,7 @@ fn path_site(
let end = site.wpos_tile_pos(end.as_());
let nodes = match path_in_site(start, end, site) {
PathResult::Path(p) => p.nodes,
PathResult::Path(p, _c) => p.nodes,
PathResult::Exhausted(p) => p.nodes,
PathResult::None(_) | PathResult::Pending => return None,
};
@ -198,7 +206,7 @@ fn path_towns(
path: p.nodes.into(),
repoll: true,
}),
PathResult::Path(p) => Some(PathData {
PathResult::Path(p, _c) => Some(PathData {
end,
path: p.nodes.into(),
repoll: false,

View File

@ -83,6 +83,7 @@ use common::{
terrain::{TerrainChunk, TerrainChunkSize},
vol::RectRasterableVol,
};
use common_base::prof_span;
use common_ecs::run_now;
use common_net::{
msg::{ClientType, DisconnectReason, ServerGeneral, ServerInfo, ServerMsg},
@ -221,6 +222,7 @@ impl Server {
data_dir: &std::path::Path,
runtime: Arc<Runtime>,
) -> Result<Self, Error> {
prof_span!("Server::new");
info!("Server data dir is: {}", data_dir.display());
if settings.auth_server_address.is_none() {
info!("Authentication is disabled");
@ -282,6 +284,8 @@ impl Server {
default_chunk: Arc::new(world.generate_oob_chunk()),
};
let lod = lod::Lod::from_world(&world, index.as_index_ref(), &pools);
let mut state = State::server(
pools,
world.sim().map_size_lg(),
@ -451,9 +455,7 @@ impl Server {
// Insert the world into the ECS (todo: Maybe not an Arc?)
let world = Arc::new(world);
state.ecs_mut().insert(Arc::clone(&world));
state
.ecs_mut()
.insert(lod::Lod::from_world(&world, index.as_index_ref()));
state.ecs_mut().insert(lod);
state.ecs_mut().insert(index.clone());
// Set starting time for the server.

View File

@ -17,19 +17,23 @@ pub struct Lod {
impl Lod {
#[cfg(feature = "worldgen")]
pub fn from_world(world: &World, index: IndexRef) -> Self {
let mut zones = HashMap::new();
pub fn from_world(world: &World, index: IndexRef, threadpool: &rayon::ThreadPool) -> Self {
common_base::prof_span!("Lod::from_world");
threadpool.install(|| {
let zone_sz = (world.sim().get_size() + lod::ZONE_SIZE - 1) / lod::ZONE_SIZE;
let zone_sz = (world.sim().get_size() + lod::ZONE_SIZE - 1) / lod::ZONE_SIZE;
use rayon::prelude::*;
let zones = (0..zone_sz.x)
.into_par_iter()
.flat_map(|i| (0..zone_sz.y).into_par_iter().map(move |j| (i, j)))
.map(|(i, j)| {
let zone_pos = Vec2::new(i, j).map(|e| e as i32);
(zone_pos, world.get_lod_zone(zone_pos, index))
})
.collect();
for i in 0..zone_sz.x {
for j in 0..zone_sz.y {
let zone_pos = Vec2::new(i, j).map(|e| e as i32);
zones.insert(zone_pos, world.get_lod_zone(zone_pos, index));
}
}
Self { zones }
Self { zones }
})
}
#[cfg(not(feature = "worldgen"))]

View File

@ -72,6 +72,9 @@ name = "tree"
name = "chunk_compression_benchmarks"
required-features = ["bin_compression"]
[[example]]
name = "world_generate_time"
[[example]]
name = "world_block_statistics"
required-features = ["bin_compression"]

View File

@ -0,0 +1,23 @@
use std::time::Instant;
use veloren_world::{
sim::{FileOpts, WorldOpts, DEFAULT_WORLD_MAP},
World,
};
fn main() {
let threadpool = rayon::ThreadPoolBuilder::new().build().unwrap();
let start = Instant::now();
let (world, index) = World::generate(
0,
WorldOpts {
seed_elements: true,
// Load default map from assets.
world_file: FileOpts::LoadAsset(DEFAULT_WORLD_MAP.into()),
calendar: None,
},
&threadpool,
);
core::hint::black_box((world, index));
println!("{} ms", start.elapsed().as_nanos() / 1_000_000);
}

View File

@ -21,6 +21,7 @@ use common::{
},
vol::RectVolSize,
};
use common_base::prof_span;
use core::{fmt, hash::BuildHasherDefault, ops::Range};
use fxhash::FxHasher64;
use rand::prelude::*;
@ -160,7 +161,7 @@ impl<'a, R: Rng> GenCtx<'a, R> {
impl Civs {
pub fn generate(seed: u32, sim: &mut WorldSim, index: &mut Index) -> Self {
common_base::prof_span!("Civs::generate");
prof_span!("Civs::generate");
let mut this = Self::default();
let rng = ChaChaRng::from_seed(seed_expan::rng_state(seed));
let name_rng = rng.clone();
@ -181,14 +182,18 @@ impl Civs {
// this.generate_caves(&mut ctx);
info!("starting civilisation creation");
prof_span!(guard, "create civs");
for _ in 0..initial_civ_count {
prof_span!("create civ");
debug!("Creating civilisation...");
if this.birth_civ(&mut ctx.reseed()).is_none() {
warn!("Failed to find starting site for civilisation.");
}
}
drop(guard);
info!(?initial_civ_count, "all civilisations created");
prof_span!(guard, "find locations and establish sites");
for _ in 0..initial_civ_count * 3 {
attempt(5, || {
let (loc, kind) = match ctx.rng.gen_range(0..64) {
@ -260,11 +265,13 @@ impl Civs {
}))
});
}
drop(guard);
// Tick
//=== old economy is gone
// Flatten ground around sites
prof_span!(guard, "Flatten ground around sites");
for site in this.sites.values() {
let wpos = site.center * TerrainChunkSize::RECT_SIZE.map(|e: u32| e as i32);
@ -329,8 +336,10 @@ impl Civs {
}
}
}
drop(guard);
// Place sites in world
prof_span!(guard, "Place sites in world");
let mut cnt = 0;
for sim_site in this.sites.values_mut() {
cnt += 1;
@ -426,6 +435,7 @@ impl Civs {
}
debug!(?sim_site.center, "Placed site at location");
}
drop(guard);
info!(?cnt, "all sites placed");
//this.display_info();
@ -455,23 +465,25 @@ impl Civs {
}
}
// TODO: this looks optimizable
// collect natural resources
prof_span!(guard, "collect natural resources");
let sites = &mut index.sites;
(0..ctx.sim.map_size_lg().chunks_len())
.into_iter()
.for_each(|posi| {
let chpos = uniform_idx_as_vec2(ctx.sim.map_size_lg(), posi);
let wpos = chpos.map(|e| e as i64) * TerrainChunkSize::RECT_SIZE.map(|e| e as i64);
let closest_site = (*sites)
.iter_mut()
.filter(|s| !matches!(s.1.kind, crate::site::SiteKind::Dungeon(_)))
.min_by_key(|(_id, s)| s.get_origin().map(|e| e as i64).distance_squared(wpos));
if let Some((_id, s)) = closest_site {
let distance_squared = s.get_origin().map(|e| e as i64).distance_squared(wpos);
s.economy
.add_chunk(ctx.sim.get(chpos).unwrap(), distance_squared);
}
});
(0..ctx.sim.map_size_lg().chunks_len()).for_each(|posi| {
let chpos = uniform_idx_as_vec2(ctx.sim.map_size_lg(), posi);
let wpos = chpos.map(|e| e as i64) * TerrainChunkSize::RECT_SIZE.map(|e| e as i64);
let closest_site = (*sites)
.iter_mut()
.filter(|s| !matches!(s.1.kind, crate::site::SiteKind::Dungeon(_)))
.min_by_key(|(_id, s)| s.get_origin().map(|e| e as i64).distance_squared(wpos));
if let Some((_id, s)) = closest_site {
let distance_squared = s.get_origin().map(|e| e as i64).distance_squared(wpos);
s.economy
.add_chunk(ctx.sim.get(chpos).unwrap(), distance_squared);
}
});
drop(guard);
sites
.iter_mut()
.for_each(|(_, s)| s.economy.cache_economy());
@ -671,9 +683,12 @@ impl Civs {
.distance_squared(self.sites.get(b).center) as f32)
.sqrt()
};
let neighbors = |p: &Id<Site>| self.neighbors(*p);
let transition = |a: &Id<Site>, b: &Id<Site>| {
self.tracks.get(self.track_between(*a, *b).unwrap().0).cost
let transition =
|a: Id<Site>, b: Id<Site>| self.tracks.get(self.track_between(a, b).unwrap().0).cost;
let neighbors = |p: &Id<Site>| {
let p = *p;
self.neighbors(p)
.map(move |neighbor| (neighbor, transition(p, neighbor)))
};
let satisfied = |p: &Id<Site>| *p == b;
// We use this hasher (FxHasher64) because
@ -681,10 +696,7 @@ impl Civs {
// (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, BuildHasherDefault::<FxHasher64>::default());
astar
.poll(100, heuristic, neighbors, transition, satisfied)
.into_path()
.and_then(|path| astar.get_cheapest_cost().map(|cost| (path, cost)))
astar.poll(100, heuristic, neighbors, satisfied).into_path()
}
fn birth_civ(&mut self, ctx: &mut GenCtx<impl Rng>) -> Option<Id<Civ>> {
@ -730,7 +742,7 @@ impl Civs {
/// Adds lake POIs and names them
fn name_biomes(&mut self, ctx: &mut GenCtx<impl Rng>) {
common_base::prof_span!("name_biomes");
prof_span!("name_biomes");
let map_size_lg = ctx.sim.map_size_lg();
let world_size = map_size_lg.chunks();
let mut biomes: Vec<(common::terrain::BiomeKind, Vec<usize>)> = Vec::new();
@ -769,7 +781,7 @@ impl Civs {
biomes.push((biome, filled));
}
common_base::prof_span!("after flood fill");
prof_span!("after flood fill");
let mut biome_count = 0;
for biome in biomes {
let name = match biome.0 {
@ -1013,7 +1025,7 @@ impl Civs {
/// Adds mountain POIs and name them
fn name_peaks(&mut self, ctx: &mut GenCtx<impl Rng>) {
common_base::prof_span!("name_peaks");
prof_span!("name_peaks");
let map_size_lg = ctx.sim.map_size_lg();
const MIN_MOUNTAIN_ALT: f32 = 600.0;
const MIN_MOUNTAIN_CHAOS: f32 = 0.35;
@ -1093,6 +1105,7 @@ impl Civs {
loc: Vec2<i32>,
site_fn: impl FnOnce(Id<Place>) -> Site,
) -> Id<Site> {
prof_span!("establish_site");
const SITE_AREA: Range<usize> = 1..4; //64..256;
fn establish_site(
@ -1112,10 +1125,17 @@ impl Civs {
let site = establish_site(self, ctx, loc, site_fn);
// Find neighbors
const MAX_NEIGHBOR_DISTANCE: f32 = 2000.0;
// Note, the maximum distance that I have so far observed not hitting the
// iteration limit in `find_path` is 364. So I think this is a reasonable
// limit (although the relationship between distance and pathfinding iterations
// can be a bit variable). Note, I have seen paths reach the iteration limit
// with distances as small as 137, so this certainly doesn't catch all
// cases that would fail.
const MAX_NEIGHBOR_DISTANCE: f32 = 400.0;
let mut nearby = self
.sites
.iter()
.filter(|&(id, _)| id != site)
.filter(|(_, p)| {
matches!(
p.kind,
@ -1139,99 +1159,93 @@ impl Civs {
| SiteKind::DesertCity
| SiteKind::Castle = self.sites[site].kind
{
for (nearby, _) in nearby.into_iter().take(5) {
// Find a novel path
if let Some((path, cost)) = find_path(
ctx,
|start| self.bridges.get(&start).map(|(end, _)| *end),
loc,
self.sites.get(nearby).center,
) {
// Find a path using existing paths
if self
.route_between(site, nearby)
// If the novel path isn't efficient compared to existing routes, don't use it
.filter(|(_, route_cost)| *route_cost < cost * 3.0)
.is_none()
{
// Write the track to the world as a path
for locs in path.nodes().windows(3) {
let mut randomize_offset = false;
if let Some((i, _)) = NEIGHBORS
.iter()
.enumerate()
.find(|(_, dir)| **dir == locs[0] - locs[1])
{
ctx.sim.get_mut(locs[0]).unwrap().path.0.neighbors |=
1 << ((i as u8 + 4) % 8);
ctx.sim.get_mut(locs[1]).unwrap().path.0.neighbors |=
1 << (i as u8);
randomize_offset = true;
}
if let Some((i, _)) = NEIGHBORS
.iter()
.enumerate()
.find(|(_, dir)| **dir == locs[2] - locs[1])
{
ctx.sim.get_mut(locs[2]).unwrap().path.0.neighbors |=
1 << ((i as u8 + 4) % 8);
ctx.sim.get_mut(locs[1]).unwrap().path.0.neighbors |=
1 << (i as u8);
randomize_offset = true;
} else if !self.bridges.contains_key(&locs[1]) {
let center = (locs[1] + locs[2]) / 2;
let id =
establish_site(self, &mut ctx.reseed(), center, move |place| {
Site {
kind: SiteKind::Bridge(locs[1], locs[2]),
site_tmp: None,
center,
place,
}
});
self.bridges.insert(locs[1], (locs[2], id));
self.bridges.insert(locs[2], (locs[1], id));
}
/*
let to_prev_idx = NEIGHBORS
.iter()
.enumerate()
.find(|(_, dir)| **dir == (locs[0] - locs[1]).map(|e| e.signum()))
.expect("Track locations must be neighbors")
.0;
let to_next_idx = NEIGHBORS
.iter()
.enumerate()
.find(|(_, dir)| **dir == (locs[2] - locs[1]).map(|e| e.signum()))
.expect("Track locations must be neighbors")
.0;
for (nearby, _) in nearby.into_iter().take(4) {
prof_span!("for nearby");
// Find a route using existing paths
//
// If the novel path isn't efficient compared to this, don't use it
let max_novel_cost = self
.route_between(site, nearby)
.map_or(f32::MAX, |(_, route_cost)| route_cost / 3.0);
let start = loc;
let end = self.sites.get(nearby).center;
// Find a novel path.
let get_bridge = |start| self.bridges.get(&start).map(|(end, _)| *end);
if let Some((path, cost)) = find_path(ctx, get_bridge, start, end, max_novel_cost) {
// Write the track to the world as a path
for locs in path.nodes().windows(3) {
let mut randomize_offset = false;
if let Some((i, _)) = NEIGHBORS
.iter()
.enumerate()
.find(|(_, dir)| **dir == locs[0] - locs[1])
{
ctx.sim.get_mut(locs[0]).unwrap().path.0.neighbors |=
1 << ((to_prev_idx as u8 + 4) % 8);
ctx.sim.get_mut(locs[2]).unwrap().path.0.neighbors |=
1 << ((to_next_idx as u8 + 4) % 8);
let mut chunk = ctx.sim.get_mut(locs[1]).unwrap();
chunk.path.0.neighbors |=
(1 << (to_prev_idx as u8)) | (1 << (to_next_idx as u8));
*/
if randomize_offset {
let mut chunk = ctx.sim.get_mut(locs[1]).unwrap();
chunk.path.0.offset = Vec2::new(
ctx.rng.gen_range(-16..17),
ctx.rng.gen_range(-16..17),
);
}
1 << ((i as u8 + 4) % 8);
ctx.sim.get_mut(locs[1]).unwrap().path.0.neighbors |= 1 << (i as u8);
randomize_offset = true;
}
// Take note of the track
let track = self.tracks.insert(Track { cost, path });
self.track_map
.entry(site)
.or_default()
.insert(nearby, track);
if let Some((i, _)) = NEIGHBORS
.iter()
.enumerate()
.find(|(_, dir)| **dir == locs[2] - locs[1])
{
ctx.sim.get_mut(locs[2]).unwrap().path.0.neighbors |=
1 << ((i as u8 + 4) % 8);
ctx.sim.get_mut(locs[1]).unwrap().path.0.neighbors |= 1 << (i as u8);
randomize_offset = true;
} else if !self.bridges.contains_key(&locs[1]) {
let center = (locs[1] + locs[2]) / 2;
let id =
establish_site(self, &mut ctx.reseed(), center, move |place| {
Site {
kind: SiteKind::Bridge(locs[1], locs[2]),
site_tmp: None,
center,
place,
}
});
self.bridges.insert(locs[1], (locs[2], id));
self.bridges.insert(locs[2], (locs[1], id));
}
/*
let to_prev_idx = NEIGHBORS
.iter()
.enumerate()
.find(|(_, dir)| **dir == (locs[0] - locs[1]).map(|e| e.signum()))
.expect("Track locations must be neighbors")
.0;
let to_next_idx = NEIGHBORS
.iter()
.enumerate()
.find(|(_, dir)| **dir == (locs[2] - locs[1]).map(|e| e.signum()))
.expect("Track locations must be neighbors")
.0;
ctx.sim.get_mut(locs[0]).unwrap().path.0.neighbors |=
1 << ((to_prev_idx as u8 + 4) % 8);
ctx.sim.get_mut(locs[2]).unwrap().path.0.neighbors |=
1 << ((to_next_idx as u8 + 4) % 8);
let mut chunk = ctx.sim.get_mut(locs[1]).unwrap();
chunk.path.0.neighbors |=
(1 << (to_prev_idx as u8)) | (1 << (to_next_idx as u8));
*/
if randomize_offset {
let mut chunk = ctx.sim.get_mut(locs[1]).unwrap();
chunk.path.0.offset =
Vec2::new(ctx.rng.gen_range(-16..17), ctx.rng.gen_range(-16..17));
}
}
// Take note of the track
let track = self.tracks.insert(Track { cost, path });
self.track_map
.entry(site)
.or_default()
.insert(nearby, track);
}
}
}
@ -1302,21 +1316,25 @@ fn find_path(
get_bridge: impl Fn(Vec2<i32>) -> Option<Vec2<i32>>,
a: Vec2<i32>,
b: Vec2<i32>,
max_path_cost: f32,
) -> Option<(Path<Vec2<i32>>, f32)> {
prof_span!("find_path");
const MAX_PATH_ITERS: usize = 100_000;
let sim = &ctx.sim;
// NOTE: If heuristic overestimates the actual cost, then A* is not guaranteed
// to produce the least-cost path (since it will explore partially based on
// the heuristic).
// TODO: heuristic can be larger than actual cost, since existing bridges cost
// 1.0 (after the 1.0 that is added to everthting), but they can cover
// multiple chunks.
let heuristic = move |l: &Vec2<i32>, _: &Vec2<i32>| (l.distance_squared(b) as f32).sqrt();
let get_bridge = &get_bridge;
let neighbors = |l: &Vec2<i32>| {
let l = *l;
NEIGHBORS
.iter()
.filter_map(move |dir| walk_in_dir(sim, get_bridge, l, *dir))
.map(move |(p, _)| p)
};
let transition = |a: &Vec2<i32>, b: &Vec2<i32>| {
1.0 + walk_in_dir(sim, get_bridge, *a, (*b - *a).map(|e| e.signum()))
.map_or(10000.0, |(_, cost)| cost)
let bridge = get_bridge(l);
let potential = walk_in_all_dirs(sim, bridge, l);
potential
.into_iter()
.filter_map(|p| p.map(|(node, cost)| (node, cost + 1.0)))
};
let satisfied = |l: &Vec2<i32>| *l == b;
// We use this hasher (FxHasher64) because
@ -1327,73 +1345,103 @@ fn find_path(
MAX_PATH_ITERS,
a,
BuildHasherDefault::<FxHasher64>::default(),
);
)
.with_max_cost(max_path_cost);
astar
.poll(MAX_PATH_ITERS, heuristic, neighbors, transition, satisfied)
.poll(MAX_PATH_ITERS, heuristic, neighbors, satisfied)
.into_path()
.and_then(|path| astar.get_cheapest_cost().map(|cost| (path, cost)))
}
/// Return Some if travel between a location and a chunk next to it is permitted
/// If permitted, the approximate relative const of traversal is given
// (TODO: by whom?)
fn walk_in_dir(
/// Return tuple: (final location, cost)
///
/// For efficiency, this computes for all 8 directions at once.
fn walk_in_all_dirs(
sim: &WorldSim,
get_bridge: impl Fn(Vec2<i32>) -> Option<Vec2<i32>>,
bridge: Option<Vec2<i32>>,
a: Vec2<i32>,
dir: Vec2<i32>,
) -> Option<(Vec2<i32>, f32)> {
if let Some(p) = get_bridge(a).filter(|p| (p - a).map(|e| e.signum()) == dir) {
// Traversing an existing bridge has no cost.
Some((p, 0.0))
} else if loc_suitable_for_walking(sim, a + dir) {
let a_chunk = sim.get(a)?;
let b_chunk = sim.get(a + dir)?;
) -> [Option<(Vec2<i32>, f32)>; 8] {
let mut potential = [None; 8];
let adjacents = NEIGHBORS.map(|dir| a + dir);
let Some(a_chunk) = sim.get(a) else { return potential };
let mut chunks = [None; 8];
for i in 0..8 {
if loc_suitable_for_walking(sim, adjacents[i]) {
chunks[i] = sim.get(adjacents[i]);
}
}
for i in 0..8 {
let Some(b_chunk) = chunks[i] else { continue };
let hill_cost = ((b_chunk.alt - a_chunk.alt).abs() / 5.0).powi(2);
let water_cost = (b_chunk.water_alt - b_chunk.alt + 8.0).clamped(0.0, 8.0) * 3.0; // Try not to path swamps / tidal areas
let wild_cost = if b_chunk.path.0.is_way() {
0.0 // Traversing existing paths has no additional cost!
} else {
3.0 // + (1.0 - b_chunk.tree_density) * 20.0 // Prefer going through forests, for aesthetics
};
Some((a + dir, 1.0 + hill_cost + water_cost + wild_cost))
} else if dir.x == 0 || dir.y == 0 {
(4..=5).find_map(|i| {
loc_suitable_for_walking(sim, a + dir * i)
.then(|| (a + dir * i, 120.0 + (i - 4) as f32 * 10.0))
})
} else {
None
let cost = 1.0 + hill_cost + water_cost + wild_cost;
potential[i] = Some((adjacents[i], cost));
}
// Look for potential bridge spots in the cardinal directions if
// `loc_suitable_for_wallking` was false for the adjacent chunk.
for (i, &dir) in NEIGHBORS.iter().enumerate() {
let is_cardinal_dir = dir.x == 0 || dir.y == 0;
if is_cardinal_dir && potential[i].is_none() {
// if we can skip over unsuitable area with a bridge
potential[i] = (4..=5).find_map(|i| {
loc_suitable_for_walking(sim, a + dir * i)
.then(|| (a + dir * i, 120.0 + (i - 4) as f32 * 10.0))
});
}
}
// If current position is a bridge, skip to its destination.
if let Some(p) = bridge {
let dir = (p - a).map(|e| e.signum());
if let Some((dir_index, _)) = NEIGHBORS
.iter()
.enumerate()
.find(|(_, n_dir)| **n_dir == dir)
{
potential[dir_index] = Some((p, 0.0));
}
}
potential
}
/// Return true if a position is suitable for walking on
fn loc_suitable_for_walking(sim: &WorldSim, loc: Vec2<i32>) -> bool {
if sim.get(loc).is_some() {
!NEIGHBORS.iter().any(|n| {
NEIGHBORS.iter().all(|n| {
sim.get(loc + *n)
.map_or(false, |chunk| chunk.river.near_water())
.map_or(false, |chunk| !chunk.river.near_water())
})
} else {
false
}
}
/// Return true if a site could be constructed between a location and a chunk
/// next to it is permitted (TODO: by whom?)
fn site_in_dir(sim: &WorldSim, a: Vec2<i32>, dir: Vec2<i32>, site_kind: SiteKind) -> bool {
loc_suitable_for_site(sim, a, site_kind) && loc_suitable_for_site(sim, a + dir, site_kind)
}
/// Return true if a position is suitable for site construction (TODO:
/// criteria?)
fn loc_suitable_for_site(sim: &WorldSim, loc: Vec2<i32>, site_kind: SiteKind) -> bool {
fn loc_suitable_for_site(
sim: &WorldSim,
loc: Vec2<i32>,
site_kind: SiteKind,
is_suitable_loc: bool,
) -> bool {
fn check_chunk_occupation(sim: &WorldSim, loc: Vec2<i32>, radius: i32) -> bool {
for x in (-radius)..radius {
for y in (-radius)..radius {
let check_loc = loc + Vec2::new(x, y).cpos_to_wpos();
let check_loc = loc + Vec2::new(x, y);
if sim.get(check_loc).map_or(false, |c| !c.sites.is_empty()) {
return false;
}
@ -1401,8 +1449,9 @@ fn loc_suitable_for_site(sim: &WorldSim, loc: Vec2<i32>, site_kind: SiteKind) ->
}
true
}
let not_occupied = check_chunk_occupation(sim, loc, site_kind.exclusion_radius());
site_kind.is_suitable_loc(loc, sim) && not_occupied
let not_occupied = || check_chunk_occupation(sim, loc, site_kind.exclusion_radius());
// only check occupation if the location is suitable
is_suitable_loc && not_occupied()
}
/// Attempt to search for a location that's suitable for site construction
@ -1411,6 +1460,7 @@ fn find_site_loc(
proximity_reqs: &ProximityRequirements,
site_kind: SiteKind,
) -> Option<Vec2<i32>> {
prof_span!("find_site_loc");
const MAX_ATTEMPTS: usize = 10000;
let mut loc = None;
for _ in 0..MAX_ATTEMPTS {
@ -1421,16 +1471,15 @@ fn find_site_loc(
)
});
if proximity_reqs.satisfied_by(test_loc) {
if loc_suitable_for_site(ctx.sim, test_loc, site_kind) {
let is_suitable_loc = site_kind.is_suitable_loc(test_loc, ctx.sim);
if is_suitable_loc && proximity_reqs.satisfied_by(test_loc) {
if loc_suitable_for_site(ctx.sim, test_loc, site_kind, is_suitable_loc) {
return Some(test_loc);
}
loc = ctx.sim.get(test_loc).and_then(|c| {
site_kind
.is_suitable_loc(test_loc, ctx.sim)
.then_some(c.downhill?.wpos_to_cpos())
});
// If the current location is suitable and meets proximity requirements,
// try nearby spot downhill.
loc = ctx.sim.get(test_loc).and_then(|c| c.downhill);
}
}

View File

@ -55,11 +55,11 @@ use common::{
},
vol::{ReadVol, RectVolSize, WriteVol},
};
use common_base::prof_span;
use common_net::msg::{world_msg, WorldMapMsg};
use enum_map::EnumMap;
use rand::{prelude::*, Rng};
use rand_chacha::ChaCha8Rng;
use rayon::iter::ParallelIterator;
use serde::Deserialize;
use std::time::Duration;
use vek::*;
@ -110,6 +110,7 @@ impl World {
opts: sim::WorldOpts,
threadpool: &rayon::ThreadPool,
) -> (Self, IndexOwned) {
prof_span!("World::generate");
// NOTE: Generating index first in order to quickly fail if the color manifest
// is broken.
threadpool.install(|| {
@ -136,6 +137,7 @@ impl World {
}
pub fn get_map_data(&self, index: IndexRef, threadpool: &rayon::ThreadPool) -> WorldMapMsg {
prof_span!("World::get_map_data");
threadpool.install(|| {
// we need these numbers to create unique ids for cave ends
let num_sites = self.civs().sites().count() as u64;
@ -562,6 +564,7 @@ impl World {
let mut objects = Vec::new();
// Add trees
prof_span!(guard, "add trees");
objects.append(
&mut self
.sim()
@ -600,6 +603,7 @@ impl World {
})
.collect(),
);
drop(guard);
// Add buildings
objects.extend(

View File

@ -51,6 +51,7 @@ use common::{
},
vol::RectVolSize,
};
use common_base::prof_span;
use common_net::msg::WorldMapMsg;
use noise::{
BasicMulti, Billow, Fbm, HybridMulti, MultiFractal, NoiseFn, RangeFunction, RidgedMulti,
@ -668,6 +669,7 @@ pub struct WorldSim {
impl WorldSim {
pub fn generate(seed: u32, opts: WorldOpts, threadpool: &rayon::ThreadPool) -> Self {
prof_span!("WorldSim::generate");
let calendar = opts.calendar; // separate lifetime of elements
let world_file = opts.world_file;
@ -1586,6 +1588,7 @@ impl WorldSim {
/// Draw a map of the world based on chunk information. Returns a buffer of
/// u32s.
pub fn get_map(&self, index: IndexRef, calendar: Option<&Calendar>) -> WorldMapMsg {
prof_span!("WorldSim::get_map");
let mut map_config = MapConfig::orthographic(
self.map_size_lg(),
core::ops::RangeInclusive::new(CONFIG.sea_level, CONFIG.sea_level + self.max_height),
@ -1603,6 +1606,7 @@ impl WorldSim {
};
let samples_data = {
prof_span!("samples data");
let column_sample = ColumnGen::new(self);
(0..self.map_size_lg().chunks_len())
.into_par_iter()
@ -2293,10 +2297,10 @@ impl WorldSim {
&self,
wpos_min: Vec2<i32>,
wpos_max: Vec2<i32>,
) -> impl ParallelIterator<Item = TreeAttr> + '_ {
) -> impl Iterator<Item = TreeAttr> + '_ {
self.gen_ctx
.structure_gen
.par_iter(wpos_min, wpos_max)
.iter(wpos_min, wpos_max)
.filter_map(move |(wpos, seed)| {
let lottery = self.make_forest_lottery(wpos);
Some(TreeAttr {

View File

@ -3,6 +3,7 @@ use common::{
terrain::{neighbors, uniform_idx_as_vec2, vec2_as_uniform_idx, MapSizeLg, TerrainChunkSize},
vol::RectVolSize,
};
use common_base::prof_span;
use noise::{MultiFractal, NoiseFn, Perlin, Seedable};
use num::Float;
use rayon::prelude::*;
@ -374,6 +375,7 @@ pub fn get_horizon_map<F: Float + Sync, A: Send, H: Send>(
to_angle: impl Fn(F) -> A + Sync,
to_height: impl Fn(F) -> H + Sync,
) -> Result<[HorizonMap<A, H>; 2], ()> {
prof_span!("get_horizon_map");
if maxh < minh {
// maxh must be greater than minh
return Err(());

View File

@ -1,3 +1,7 @@
use crate::{sim::WorldSim, site::economy::simulate_economy, Index};
use common_base::prof_span;
pub fn simulate(index: &mut Index, _world: &mut WorldSim) { simulate_economy(index); }
pub fn simulate(index: &mut Index, _world: &mut WorldSim) {
prof_span!("sim2::simulate");
simulate_economy(index);
}

View File

@ -1347,15 +1347,19 @@ impl Land {
&self,
origin: Vec2<i32>,
dest: Vec2<i32>,
mut path_cost_fn: impl FnMut(Option<&Tile>, Option<&Tile>) -> f32,
path_cost_fn: impl Fn(Option<&Tile>, Option<&Tile>) -> f32,
) -> Option<Path<Vec2<i32>>> {
let heuristic = |pos: &Vec2<i32>, _: &Vec2<i32>| (pos - dest).map(|e| e as f32).magnitude();
let transition =
|from: Vec2<i32>, to: Vec2<i32>| path_cost_fn(self.tile_at(from), self.tile_at(to));
let neighbors = |pos: &Vec2<i32>| {
let pos = *pos;
CARDINALS.iter().map(move |dir| pos + *dir)
let transition = &transition;
CARDINALS.iter().map(move |dir| {
let to = pos + *dir;
(to, transition(pos, to))
})
};
let transition =
|from: &Vec2<i32>, to: &Vec2<i32>| path_cost_fn(self.tile_at(*from), self.tile_at(*to));
let satisfied = |pos: &Vec2<i32>| *pos == dest;
// We use this hasher (FxHasher64) because
@ -1363,8 +1367,9 @@ impl Land {
// (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, BuildHasherDefault::<FxHasher64>::default())
.poll(250, heuristic, neighbors, transition, satisfied)
.poll(250, heuristic, neighbors, satisfied)
.into_path()
.map(|(p, _c)| p)
}
/// We use this hasher (FxHasher64) because

View File

@ -160,18 +160,23 @@ impl Site {
}
max_cost + (dir != old_dir) as i32 as f32 * 35.0
};
let path = Astar::new(MAX_ITERS, (a, Vec2::zero()), DefaultHashBuilder::default())
let (path, _cost) = Astar::new(MAX_ITERS, (a, Vec2::zero()), DefaultHashBuilder::default())
.poll(
MAX_ITERS,
&heuristic,
|(tile, _)| {
let tile = *tile;
CARDINALS.iter().map(move |dir| (tile + *dir, *dir))
},
|(a, _), (b, _)| {
let alt_a = land.get_alt_approx(self.tile_center_wpos(*a));
let alt_b = land.get_alt_approx(self.tile_center_wpos(*b));
(alt_a - alt_b).abs() / TILE_SIZE as f32
let this = &self;
CARDINALS.iter().map(move |dir| {
let neighbor = (tile + *dir, *dir);
// Transition cost
let alt_a = land.get_alt_approx(this.tile_center_wpos(tile));
let alt_b = land.get_alt_approx(this.tile_center_wpos(neighbor.0));
let cost = (alt_a - alt_b).abs() / TILE_SIZE as f32;
(neighbor, cost)
})
},
|(tile, _)| *tile == b,
)

View File

@ -545,31 +545,33 @@ impl Floor {
fn create_route(&mut self, _ctx: &mut GenCtx<impl Rng>, a: Vec2<i32>, b: Vec2<i32>) {
let heuristic =
move |l: &Vec2<i32>, _: &Vec2<i32>| (l - b).map(|e| e.abs()).reduce_max() as f32;
let neighbors = |l: &Vec2<i32>| {
let l = *l;
CARDINALS
.iter()
.map(move |dir| l + dir)
.filter(|pos| self.tiles.get(*pos).is_some())
};
let transition = |_a: &Vec2<i32>, b: &Vec2<i32>| match self.tiles.get(*b) {
let transition = |_a: Vec2<i32>, b: Vec2<i32>| match self.tiles.get(b) {
Some(Tile::Room(_)) | Some(Tile::Tunnel) => 1.0,
Some(Tile::Solid) => 25.0,
Some(Tile::UpStair(_, _)) | Some(Tile::DownStair(_)) => 0.0,
_ => 100000.0,
};
let neighbors = |l: &Vec2<i32>| {
let l = *l;
CARDINALS
.iter()
.map(move |dir| {
let dest = l + dir;
(dest, transition(l, dest))
})
.filter(|&(pos, _)| self.tiles.get(pos).is_some())
};
let satisfied = |l: &Vec2<i32>| *l == b;
// 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, BuildHasherDefault::<FxHasher64>::default());
let path = astar
let (path, _cost) = astar
.poll(
FLOOR_SIZE.product() as usize + 1,
heuristic,
neighbors,
transition,
satisfied,
)
.into_path()

View File

@ -23,13 +23,13 @@ pub use self::{
pub use common::grid::Grid;
use fxhash::FxHasher32;
use fxhash::{FxHasher32, FxHasher64};
use hashbrown::{HashMap, HashSet};
use std::hash::BuildHasherDefault;
use vek::*;
// Deterministic HashMap and HashSet
pub type DHashMap<K, V> = HashMap<K, V, BuildHasherDefault<FxHasher32>>;
pub type DHashMap<K, V> = HashMap<K, V, BuildHasherDefault<FxHasher64>>;
pub type DHashSet<T> = HashSet<T, BuildHasherDefault<FxHasher32>>;
pub fn attempt<T>(max_iters: usize, mut f: impl FnMut() -> Option<T>) -> Option<T> {

View File

@ -1,5 +1,4 @@
use super::{RandomField, Sampler};
use rayon::prelude::*;
use vek::*;
#[derive(Clone)]
@ -69,11 +68,7 @@ impl StructureGen2d {
/// Note: Generates all possible closest samples for elements in the range
/// of min to max, *exclusive.*
pub fn par_iter(
&self,
min: Vec2<i32>,
max: Vec2<i32>,
) -> impl ParallelIterator<Item = StructureField> {
pub fn iter(&self, min: Vec2<i32>, max: Vec2<i32>) -> impl Iterator<Item = StructureField> {
let freq = self.freq;
let spread = self.spread;
let spread_mul = Self::spread_mul(spread);
@ -102,7 +97,7 @@ impl StructureGen2d {
let x_field = self.x_field;
let y_field = self.y_field;
let seed_field = self.seed_field;
(0..len).into_par_iter().map(move |xy| {
(0..len).map(move |xy| {
let index = min_index + Vec2::new((xy % xlen as u64) as i32, (xy / xlen as u64) as i32);
Self::index_to_sample_internal(
freq,