mirror of
https://gitlab.com/veloren/veloren.git
synced 2024-08-30 18:12:32 +00:00
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:
commit
60dbcf86f9
@ -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;
|
||||
}
|
||||
|
@ -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)
|
||||
},
|
||||
|
@ -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();
|
||||
|
||||
|
@ -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,
|
||||
|
@ -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();
|
||||
|
@ -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,
|
||||
|
@ -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.
|
||||
|
@ -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"))]
|
||||
|
@ -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"]
|
||||
|
23
world/examples/world_generate_time.rs
Normal file
23
world/examples/world_generate_time.rs
Normal 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);
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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(
|
||||
|
@ -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 {
|
||||
|
@ -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(());
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
)
|
||||
|
@ -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()
|
||||
|
@ -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> {
|
||||
|
@ -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,
|
||||
|
Loading…
Reference in New Issue
Block a user