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,
|
fmt,
|
||||||
hash::{BuildHasher, Hash},
|
hash::{BuildHasher, Hash},
|
||||||
};
|
};
|
||||||
use hashbrown::{HashMap, HashSet};
|
use hashbrown::HashMap;
|
||||||
use std::collections::BinaryHeap;
|
use std::collections::BinaryHeap;
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug)]
|
#[derive(Copy, Clone, Debug)]
|
||||||
pub struct PathEntry<S> {
|
pub struct PathEntry<S> {
|
||||||
cost: f32,
|
// cost so far + heursitic
|
||||||
|
cost_estimate: f32,
|
||||||
node: S,
|
node: S,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -23,25 +24,50 @@ impl<S: Eq> Ord for PathEntry<S> {
|
|||||||
// This method implements reverse ordering, so that the lowest cost
|
// This method implements reverse ordering, so that the lowest cost
|
||||||
// will be ordered first
|
// will be ordered first
|
||||||
fn cmp(&self, other: &PathEntry<S>) -> Ordering {
|
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> {
|
impl<S: Eq> PartialOrd for PathEntry<S> {
|
||||||
fn partial_cmp(&self, other: &PathEntry<S>) -> Option<Ordering> { Some(self.cmp(other)) }
|
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> {
|
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>),
|
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>),
|
Exhausted(Path<T>),
|
||||||
Path(Path<T>),
|
/// Path succefully found.
|
||||||
|
///
|
||||||
|
/// Second field is cost.
|
||||||
|
Path(Path<T>, f32),
|
||||||
Pending,
|
Pending,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T> PathResult<T> {
|
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 {
|
match self {
|
||||||
PathResult::Path(path) => Some(path),
|
PathResult::Path(path, cost) => Some((path, cost)),
|
||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -50,23 +76,38 @@ impl<T> PathResult<T> {
|
|||||||
match self {
|
match self {
|
||||||
PathResult::None(p) => PathResult::None(f(p)),
|
PathResult::None(p) => PathResult::None(f(p)),
|
||||||
PathResult::Exhausted(p) => PathResult::Exhausted(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,
|
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)]
|
#[derive(Clone)]
|
||||||
pub struct Astar<S, Hasher> {
|
pub struct Astar<S, Hasher> {
|
||||||
iter: usize,
|
iter: usize,
|
||||||
max_iters: usize,
|
max_iters: usize,
|
||||||
potential_nodes: BinaryHeap<PathEntry<S>>,
|
max_cost: f32,
|
||||||
came_from: HashMap<S, S, Hasher>,
|
potential_nodes: BinaryHeap<PathEntry<S>>, // cost, node pairs
|
||||||
cheapest_scores: HashMap<S, f32, Hasher>,
|
visited_nodes: HashMap<S, NodeEntry<S>, Hasher>,
|
||||||
final_scores: HashMap<S, f32, Hasher>,
|
/// Node with the lowest heuristic value so far.
|
||||||
visited: HashSet<S, Hasher>,
|
///
|
||||||
cheapest_node: Option<S>,
|
/// (node, heuristic value)
|
||||||
cheapest_cost: Option<f32>,
|
closest_node: Option<(S, f32)>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// NOTE: Must manually derive since Hasher doesn't implement it.
|
/// 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("iter", &self.iter)
|
||||||
.field("max_iters", &self.max_iters)
|
.field("max_iters", &self.max_iters)
|
||||||
.field("potential_nodes", &self.potential_nodes)
|
.field("potential_nodes", &self.potential_nodes)
|
||||||
.field("came_from", &self.came_from)
|
.field("visited_nodes", &self.visited_nodes)
|
||||||
.field("cheapest_scores", &self.cheapest_scores)
|
.field("closest_node", &self.closest_node)
|
||||||
.field("final_scores", &self.final_scores)
|
|
||||||
.field("visited", &self.visited)
|
|
||||||
.field("cheapest_node", &self.cheapest_node)
|
|
||||||
.field("cheapest_cost", &self.cheapest_cost)
|
|
||||||
.finish()
|
.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 {
|
pub fn new(max_iters: usize, start: S, hasher: H) -> Self {
|
||||||
Self {
|
Self {
|
||||||
max_iters,
|
max_iters,
|
||||||
|
max_cost: f32::MAX,
|
||||||
iter: 0,
|
iter: 0,
|
||||||
potential_nodes: core::iter::once(PathEntry {
|
potential_nodes: core::iter::once(PathEntry {
|
||||||
cost: 0.0,
|
cost_estimate: 0.0,
|
||||||
node: start.clone(),
|
node: start.clone(),
|
||||||
})
|
})
|
||||||
.collect(),
|
.collect(),
|
||||||
came_from: HashMap::with_hasher(hasher.clone()),
|
visited_nodes: {
|
||||||
cheapest_scores: {
|
let mut s = HashMap::with_capacity_and_hasher(1, hasher);
|
||||||
let mut h = HashMap::with_capacity_and_hasher(1, hasher.clone());
|
s.extend(core::iter::once((start.clone(), NodeEntry {
|
||||||
h.extend(core::iter::once((start.clone(), 0.0)));
|
came_from: start,
|
||||||
h
|
cost: 0.0,
|
||||||
},
|
})));
|
||||||
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));
|
|
||||||
s
|
s
|
||||||
},
|
},
|
||||||
cheapest_node: None,
|
closest_node: None,
|
||||||
cheapest_cost: None,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn with_max_cost(mut self, max_cost: f32) -> Self {
|
||||||
|
self.max_cost = max_cost;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
pub fn poll<I>(
|
pub fn poll<I>(
|
||||||
&mut self,
|
&mut self,
|
||||||
iters: usize,
|
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,
|
mut heuristic: impl FnMut(&S, &S) -> f32,
|
||||||
|
// get neighboring nodes
|
||||||
mut neighbors: impl FnMut(&S) -> I,
|
mut neighbors: impl FnMut(&S) -> I,
|
||||||
mut transition: impl FnMut(&S, &S) -> f32,
|
// have we reached target?
|
||||||
mut satisfied: impl FnMut(&S) -> bool,
|
mut satisfied: impl FnMut(&S) -> bool,
|
||||||
) -> PathResult<S>
|
) -> PathResult<S>
|
||||||
where
|
where
|
||||||
I: Iterator<Item = S>,
|
I: Iterator<Item = (S, f32)>, // (node, transition cost)
|
||||||
{
|
{
|
||||||
let iter_limit = self.max_iters.min(self.iter + iters);
|
let iter_limit = self.max_iters.min(self.iter + iters);
|
||||||
while self.iter < iter_limit {
|
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) {
|
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 {
|
} else {
|
||||||
for neighbor in neighbors(&node) {
|
for (neighbor, transition_cost) in neighbors(&node) {
|
||||||
let node_cheapest = self.cheapest_scores.get(&node).unwrap_or(&f32::MAX);
|
if neighbor == came_from {
|
||||||
let neighbor_cheapest =
|
continue;
|
||||||
self.cheapest_scores.get(&neighbor).unwrap_or(&f32::MAX);
|
}
|
||||||
|
let neighbor_cost = self
|
||||||
|
.visited_nodes
|
||||||
|
.get(&neighbor)
|
||||||
|
.map_or(f32::MAX, |n| n.cost);
|
||||||
|
|
||||||
let cost = node_cheapest + transition(&node, &neighbor);
|
// compute cost to traverse to each neighbor
|
||||||
if cost < *neighbor_cheapest {
|
let cost = node_cost + transition_cost;
|
||||||
self.came_from.insert(neighbor.clone(), node.clone());
|
|
||||||
self.cheapest_scores.insert(neighbor.clone(), 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 h = heuristic(&neighbor, &node);
|
||||||
let neighbor_cost = cost + h;
|
// note that `cost` field does not include the heuristic
|
||||||
self.final_scores.insert(neighbor.clone(), neighbor_cost);
|
// priority queue does include heuristic
|
||||||
|
let cost_estimate = cost + h;
|
||||||
|
|
||||||
if self.cheapest_cost.map(|cc| h < cc).unwrap_or(true) {
|
if self
|
||||||
self.cheapest_node = Some(node.clone());
|
.closest_node
|
||||||
self.cheapest_cost = Some(h);
|
.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 {
|
self.potential_nodes.push(PathEntry {
|
||||||
|
cost_estimate,
|
||||||
node: neighbor,
|
node: neighbor,
|
||||||
cost: neighbor_cost,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -163,9 +238,9 @@ impl<S: Clone + Eq + Hash, H: BuildHasher + Clone> Astar<S, H> {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
return PathResult::None(
|
return PathResult::None(
|
||||||
self.cheapest_node
|
self.closest_node
|
||||||
.clone()
|
.clone()
|
||||||
.map(|lc| self.reconstruct_path_to(lc))
|
.map(|(lc, _)| self.reconstruct_path_to(lc))
|
||||||
.unwrap_or_default(),
|
.unwrap_or_default(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -175,9 +250,9 @@ impl<S: Clone + Eq + Hash, H: BuildHasher + Clone> Astar<S, H> {
|
|||||||
|
|
||||||
if self.iter >= self.max_iters {
|
if self.iter >= self.max_iters {
|
||||||
PathResult::Exhausted(
|
PathResult::Exhausted(
|
||||||
self.cheapest_node
|
self.closest_node
|
||||||
.clone()
|
.clone()
|
||||||
.map(|lc| self.reconstruct_path_to(lc))
|
.map(|(lc, _)| self.reconstruct_path_to(lc))
|
||||||
.unwrap_or_default(),
|
.unwrap_or_default(),
|
||||||
)
|
)
|
||||||
} else {
|
} 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> {
|
fn reconstruct_path_to(&mut self, end: S) -> Path<S> {
|
||||||
let mut path = vec![end.clone()];
|
let mut path = vec![end.clone()];
|
||||||
let mut cnode = &end;
|
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());
|
path.push(node.clone());
|
||||||
cnode = node;
|
cnode = node;
|
||||||
}
|
}
|
||||||
|
@ -537,6 +537,17 @@ where
|
|||||||
};
|
};
|
||||||
|
|
||||||
let heuristic = |pos: &Vec3<i32>, _: &Vec3<i32>| (pos.distance_squared(end) as f32).sqrt();
|
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 neighbors = |pos: &Vec3<i32>| {
|
||||||
let pos = *pos;
|
let pos = *pos;
|
||||||
const DIRS: [Vec3<i32>; 17] = [
|
const DIRS: [Vec3<i32>; 17] = [
|
||||||
@ -616,7 +627,10 @@ where
|
|||||||
.map(|b| !b.is_solid())
|
.map(|b| !b.is_solid())
|
||||||
.unwrap_or(true)))
|
.unwrap_or(true)))
|
||||||
})
|
})
|
||||||
.map(move |(pos, dir)| pos + dir)
|
.map(|(pos, dir)| {
|
||||||
|
let destination = pos + dir;
|
||||||
|
(destination, transition(pos, destination))
|
||||||
|
})
|
||||||
// .chain(
|
// .chain(
|
||||||
// DIAGONALS
|
// DIAGONALS
|
||||||
// .iter()
|
// .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 satisfied = |pos: &Vec3<i32>| pos == &end;
|
||||||
|
|
||||||
let mut new_astar = match astar.take() {
|
let mut new_astar = match astar.take() {
|
||||||
@ -645,12 +648,12 @@ where
|
|||||||
Some(astar) => astar,
|
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);
|
*astar = Some(new_astar);
|
||||||
|
|
||||||
match path_result {
|
match path_result {
|
||||||
PathResult::Path(path) => {
|
PathResult::Path(path, _cost) => {
|
||||||
*astar = None;
|
*astar = None;
|
||||||
(Some(path), true)
|
(Some(path), true)
|
||||||
},
|
},
|
||||||
|
@ -942,6 +942,12 @@ pub fn handle_manipulate_loadout(
|
|||||||
BuildHasherDefault::<FxHasher64>::default(),
|
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
|
// Neighbors are all neighboring blocks that are air
|
||||||
let neighbors = |pos: &Vec3<i32>| {
|
let neighbors = |pos: &Vec3<i32>| {
|
||||||
const DIRS: [Vec3<i32>; 6] = [
|
const DIRS: [Vec3<i32>; 6] = [
|
||||||
@ -953,24 +959,23 @@ pub fn handle_manipulate_loadout(
|
|||||||
Vec3::new(0, 0, -1),
|
Vec3::new(0, 0, -1),
|
||||||
];
|
];
|
||||||
let pos = *pos;
|
let pos = *pos;
|
||||||
DIRS.iter().map(move |dir| dir + pos).filter(|pos| {
|
DIRS.iter()
|
||||||
|
.map(move |dir| {
|
||||||
|
let dest = dir + pos;
|
||||||
|
(dest, transition(pos, dest))
|
||||||
|
})
|
||||||
|
.filter(|(pos, _)| {
|
||||||
data.terrain
|
data.terrain
|
||||||
.get(*pos)
|
.get(*pos)
|
||||||
.ok()
|
.ok()
|
||||||
.map_or(false, |block| !block.is_filled())
|
.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()
|
|
||||||
};
|
|
||||||
// Pathing satisfied when it reaches the sprite position
|
// Pathing satisfied when it reaches the sprite position
|
||||||
let satisfied = |pos: &Vec3<i32>| *pos == sprite_pos;
|
let satisfied = |pos: &Vec3<i32>| *pos == sprite_pos;
|
||||||
|
|
||||||
let not_blocked_by_terrain = astar
|
let not_blocked_by_terrain = astar
|
||||||
.poll(iters, heuristic, neighbors, transition, satisfied)
|
.poll(iters, heuristic, neighbors, satisfied)
|
||||||
.into_path()
|
.into_path()
|
||||||
.is_some();
|
.is_some();
|
||||||
|
|
||||||
|
@ -3,6 +3,7 @@ use super::{
|
|||||||
vec2_as_uniform_idx, TerrainChunkSize, NEIGHBOR_DELTA, TERRAIN_CHUNK_BLOCKS_LG,
|
vec2_as_uniform_idx, TerrainChunkSize, NEIGHBOR_DELTA, TERRAIN_CHUNK_BLOCKS_LG,
|
||||||
};
|
};
|
||||||
use crate::vol::RectVolSize;
|
use crate::vol::RectVolSize;
|
||||||
|
use common_base::prof_span;
|
||||||
use core::{f32, f64, iter, ops::RangeInclusive};
|
use core::{f32, f64, iter, ops::RangeInclusive};
|
||||||
use vek::*;
|
use vek::*;
|
||||||
|
|
||||||
@ -451,6 +452,7 @@ impl<'a> MapConfig<'a> {
|
|||||||
sample_wpos: impl Fn(Vec2<i32>) -> f32,
|
sample_wpos: impl Fn(Vec2<i32>) -> f32,
|
||||||
mut write_pixel: impl FnMut(Vec2<usize>, (u8, u8, u8, u8)),
|
mut write_pixel: impl FnMut(Vec2<usize>, (u8, u8, u8, u8)),
|
||||||
) -> MapDebug {
|
) -> MapDebug {
|
||||||
|
prof_span!("MapConfig::generate");
|
||||||
let MapConfig {
|
let MapConfig {
|
||||||
map_size_lg,
|
map_size_lg,
|
||||||
dimensions,
|
dimensions,
|
||||||
|
@ -24,7 +24,7 @@ use common::{
|
|||||||
vol::{ReadVol, WriteVol},
|
vol::{ReadVol, WriteVol},
|
||||||
weather::{Weather, WeatherGrid},
|
weather::{Weather, WeatherGrid},
|
||||||
};
|
};
|
||||||
use common_base::span;
|
use common_base::{prof_span, span};
|
||||||
use common_ecs::{PhysicsMetrics, SysMetrics};
|
use common_ecs::{PhysicsMetrics, SysMetrics};
|
||||||
use common_net::sync::{interpolation as sync_interp, WorldSyncExt};
|
use common_net::sync::{interpolation as sync_interp, WorldSyncExt};
|
||||||
use core::{convert::identity, time::Duration};
|
use core::{convert::identity, time::Duration};
|
||||||
@ -179,6 +179,7 @@ impl State {
|
|||||||
map_size_lg: MapSizeLg,
|
map_size_lg: MapSizeLg,
|
||||||
default_chunk: Arc<TerrainChunk>,
|
default_chunk: Arc<TerrainChunk>,
|
||||||
) -> specs::World {
|
) -> specs::World {
|
||||||
|
prof_span!("State::setup_ecs_world");
|
||||||
let mut ecs = specs::World::new();
|
let mut ecs = specs::World::new();
|
||||||
// Uids for sync
|
// Uids for sync
|
||||||
ecs.register_sync_marker();
|
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 heuristic = |tile: &Vec2<i32>, _: &Vec2<i32>| tile.as_::<f32>().distance(end.as_());
|
||||||
let mut astar = Astar::new(1000, start, BuildHasherDefault::<FxHasher64>::default());
|
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 distance = a.as_::<f32>().distance(b.as_());
|
||||||
let a_tile = site.tiles.get(*a);
|
let a_tile = site.tiles.get(a);
|
||||||
let b_tile = site.tiles.get(*b);
|
let b_tile = site.tiles.get(b);
|
||||||
|
|
||||||
let terrain = match &b_tile.kind {
|
let terrain = match &b_tile.kind {
|
||||||
TileKind::Empty => 3.0,
|
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() {
|
let building = if a_tile.is_building() && b_tile.is_road() {
|
||||||
a_tile
|
a_tile
|
||||||
.plot
|
.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)
|
.unwrap_or(10000.0)
|
||||||
} else if b_tile.is_building() && a_tile.is_road() {
|
} else if b_tile.is_building() && a_tile.is_road() {
|
||||||
b_tile
|
b_tile
|
||||||
.plot
|
.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)
|
.unwrap_or(10000.0)
|
||||||
} else if (a_tile.is_building() || b_tile.is_building()) && a_tile.plot != b_tile.plot {
|
} else if (a_tile.is_building() || b_tile.is_building()) && a_tile.plot != b_tile.plot {
|
||||||
10000.0
|
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 neighbors = |tile: &Vec2<i32>| {
|
||||||
let tile = *tile;
|
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()
|
*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 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
|
world
|
||||||
.civs()
|
.civs()
|
||||||
.track_between(*a, *b)
|
.track_between(a, b)
|
||||||
.map(|(id, _)| world.civs().tracks.get(id).cost)
|
.map(|(id, _)| world.civs().tracks.get(id).cost)
|
||||||
.unwrap_or(f32::INFINITY)
|
.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| {
|
path.map(|path| {
|
||||||
let path = path
|
let path = path
|
||||||
@ -170,7 +178,7 @@ fn path_site(
|
|||||||
let end = site.wpos_tile_pos(end.as_());
|
let end = site.wpos_tile_pos(end.as_());
|
||||||
|
|
||||||
let nodes = match path_in_site(start, end, site) {
|
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::Exhausted(p) => p.nodes,
|
||||||
PathResult::None(_) | PathResult::Pending => return None,
|
PathResult::None(_) | PathResult::Pending => return None,
|
||||||
};
|
};
|
||||||
@ -198,7 +206,7 @@ fn path_towns(
|
|||||||
path: p.nodes.into(),
|
path: p.nodes.into(),
|
||||||
repoll: true,
|
repoll: true,
|
||||||
}),
|
}),
|
||||||
PathResult::Path(p) => Some(PathData {
|
PathResult::Path(p, _c) => Some(PathData {
|
||||||
end,
|
end,
|
||||||
path: p.nodes.into(),
|
path: p.nodes.into(),
|
||||||
repoll: false,
|
repoll: false,
|
||||||
|
@ -83,6 +83,7 @@ use common::{
|
|||||||
terrain::{TerrainChunk, TerrainChunkSize},
|
terrain::{TerrainChunk, TerrainChunkSize},
|
||||||
vol::RectRasterableVol,
|
vol::RectRasterableVol,
|
||||||
};
|
};
|
||||||
|
use common_base::prof_span;
|
||||||
use common_ecs::run_now;
|
use common_ecs::run_now;
|
||||||
use common_net::{
|
use common_net::{
|
||||||
msg::{ClientType, DisconnectReason, ServerGeneral, ServerInfo, ServerMsg},
|
msg::{ClientType, DisconnectReason, ServerGeneral, ServerInfo, ServerMsg},
|
||||||
@ -221,6 +222,7 @@ impl Server {
|
|||||||
data_dir: &std::path::Path,
|
data_dir: &std::path::Path,
|
||||||
runtime: Arc<Runtime>,
|
runtime: Arc<Runtime>,
|
||||||
) -> Result<Self, Error> {
|
) -> Result<Self, Error> {
|
||||||
|
prof_span!("Server::new");
|
||||||
info!("Server data dir is: {}", data_dir.display());
|
info!("Server data dir is: {}", data_dir.display());
|
||||||
if settings.auth_server_address.is_none() {
|
if settings.auth_server_address.is_none() {
|
||||||
info!("Authentication is disabled");
|
info!("Authentication is disabled");
|
||||||
@ -282,6 +284,8 @@ impl Server {
|
|||||||
default_chunk: Arc::new(world.generate_oob_chunk()),
|
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(
|
let mut state = State::server(
|
||||||
pools,
|
pools,
|
||||||
world.sim().map_size_lg(),
|
world.sim().map_size_lg(),
|
||||||
@ -451,9 +455,7 @@ impl Server {
|
|||||||
// Insert the world into the ECS (todo: Maybe not an Arc?)
|
// Insert the world into the ECS (todo: Maybe not an Arc?)
|
||||||
let world = Arc::new(world);
|
let world = Arc::new(world);
|
||||||
state.ecs_mut().insert(Arc::clone(&world));
|
state.ecs_mut().insert(Arc::clone(&world));
|
||||||
state
|
state.ecs_mut().insert(lod);
|
||||||
.ecs_mut()
|
|
||||||
.insert(lod::Lod::from_world(&world, index.as_index_ref()));
|
|
||||||
state.ecs_mut().insert(index.clone());
|
state.ecs_mut().insert(index.clone());
|
||||||
|
|
||||||
// Set starting time for the server.
|
// Set starting time for the server.
|
||||||
|
@ -17,19 +17,23 @@ pub struct Lod {
|
|||||||
|
|
||||||
impl Lod {
|
impl Lod {
|
||||||
#[cfg(feature = "worldgen")]
|
#[cfg(feature = "worldgen")]
|
||||||
pub fn from_world(world: &World, index: IndexRef) -> Self {
|
pub fn from_world(world: &World, index: IndexRef, threadpool: &rayon::ThreadPool) -> Self {
|
||||||
let mut zones = HashMap::new();
|
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;
|
||||||
|
|
||||||
for i in 0..zone_sz.x {
|
use rayon::prelude::*;
|
||||||
for j in 0..zone_sz.y {
|
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);
|
let zone_pos = Vec2::new(i, j).map(|e| e as i32);
|
||||||
zones.insert(zone_pos, world.get_lod_zone(zone_pos, index));
|
(zone_pos, world.get_lod_zone(zone_pos, index))
|
||||||
}
|
})
|
||||||
}
|
.collect();
|
||||||
|
|
||||||
Self { zones }
|
Self { zones }
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(not(feature = "worldgen"))]
|
#[cfg(not(feature = "worldgen"))]
|
||||||
|
@ -72,6 +72,9 @@ name = "tree"
|
|||||||
name = "chunk_compression_benchmarks"
|
name = "chunk_compression_benchmarks"
|
||||||
required-features = ["bin_compression"]
|
required-features = ["bin_compression"]
|
||||||
|
|
||||||
|
[[example]]
|
||||||
|
name = "world_generate_time"
|
||||||
|
|
||||||
[[example]]
|
[[example]]
|
||||||
name = "world_block_statistics"
|
name = "world_block_statistics"
|
||||||
required-features = ["bin_compression"]
|
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,
|
vol::RectVolSize,
|
||||||
};
|
};
|
||||||
|
use common_base::prof_span;
|
||||||
use core::{fmt, hash::BuildHasherDefault, ops::Range};
|
use core::{fmt, hash::BuildHasherDefault, ops::Range};
|
||||||
use fxhash::FxHasher64;
|
use fxhash::FxHasher64;
|
||||||
use rand::prelude::*;
|
use rand::prelude::*;
|
||||||
@ -160,7 +161,7 @@ impl<'a, R: Rng> GenCtx<'a, R> {
|
|||||||
|
|
||||||
impl Civs {
|
impl Civs {
|
||||||
pub fn generate(seed: u32, sim: &mut WorldSim, index: &mut Index) -> Self {
|
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 mut this = Self::default();
|
||||||
let rng = ChaChaRng::from_seed(seed_expan::rng_state(seed));
|
let rng = ChaChaRng::from_seed(seed_expan::rng_state(seed));
|
||||||
let name_rng = rng.clone();
|
let name_rng = rng.clone();
|
||||||
@ -181,14 +182,18 @@ impl Civs {
|
|||||||
// this.generate_caves(&mut ctx);
|
// this.generate_caves(&mut ctx);
|
||||||
|
|
||||||
info!("starting civilisation creation");
|
info!("starting civilisation creation");
|
||||||
|
prof_span!(guard, "create civs");
|
||||||
for _ in 0..initial_civ_count {
|
for _ in 0..initial_civ_count {
|
||||||
|
prof_span!("create civ");
|
||||||
debug!("Creating civilisation...");
|
debug!("Creating civilisation...");
|
||||||
if this.birth_civ(&mut ctx.reseed()).is_none() {
|
if this.birth_civ(&mut ctx.reseed()).is_none() {
|
||||||
warn!("Failed to find starting site for civilisation.");
|
warn!("Failed to find starting site for civilisation.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
drop(guard);
|
||||||
info!(?initial_civ_count, "all civilisations created");
|
info!(?initial_civ_count, "all civilisations created");
|
||||||
|
|
||||||
|
prof_span!(guard, "find locations and establish sites");
|
||||||
for _ in 0..initial_civ_count * 3 {
|
for _ in 0..initial_civ_count * 3 {
|
||||||
attempt(5, || {
|
attempt(5, || {
|
||||||
let (loc, kind) = match ctx.rng.gen_range(0..64) {
|
let (loc, kind) = match ctx.rng.gen_range(0..64) {
|
||||||
@ -260,11 +265,13 @@ impl Civs {
|
|||||||
}))
|
}))
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
drop(guard);
|
||||||
|
|
||||||
// Tick
|
// Tick
|
||||||
//=== old economy is gone
|
//=== old economy is gone
|
||||||
|
|
||||||
// Flatten ground around sites
|
// Flatten ground around sites
|
||||||
|
prof_span!(guard, "Flatten ground around sites");
|
||||||
for site in this.sites.values() {
|
for site in this.sites.values() {
|
||||||
let wpos = site.center * TerrainChunkSize::RECT_SIZE.map(|e: u32| e as i32);
|
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
|
// Place sites in world
|
||||||
|
prof_span!(guard, "Place sites in world");
|
||||||
let mut cnt = 0;
|
let mut cnt = 0;
|
||||||
for sim_site in this.sites.values_mut() {
|
for sim_site in this.sites.values_mut() {
|
||||||
cnt += 1;
|
cnt += 1;
|
||||||
@ -426,6 +435,7 @@ impl Civs {
|
|||||||
}
|
}
|
||||||
debug!(?sim_site.center, "Placed site at location");
|
debug!(?sim_site.center, "Placed site at location");
|
||||||
}
|
}
|
||||||
|
drop(guard);
|
||||||
info!(?cnt, "all sites placed");
|
info!(?cnt, "all sites placed");
|
||||||
|
|
||||||
//this.display_info();
|
//this.display_info();
|
||||||
@ -455,11 +465,12 @@ impl Civs {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: this looks optimizable
|
||||||
|
|
||||||
// collect natural resources
|
// collect natural resources
|
||||||
|
prof_span!(guard, "collect natural resources");
|
||||||
let sites = &mut index.sites;
|
let sites = &mut index.sites;
|
||||||
(0..ctx.sim.map_size_lg().chunks_len())
|
(0..ctx.sim.map_size_lg().chunks_len()).for_each(|posi| {
|
||||||
.into_iter()
|
|
||||||
.for_each(|posi| {
|
|
||||||
let chpos = uniform_idx_as_vec2(ctx.sim.map_size_lg(), 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 wpos = chpos.map(|e| e as i64) * TerrainChunkSize::RECT_SIZE.map(|e| e as i64);
|
||||||
let closest_site = (*sites)
|
let closest_site = (*sites)
|
||||||
@ -472,6 +483,7 @@ impl Civs {
|
|||||||
.add_chunk(ctx.sim.get(chpos).unwrap(), distance_squared);
|
.add_chunk(ctx.sim.get(chpos).unwrap(), distance_squared);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
drop(guard);
|
||||||
sites
|
sites
|
||||||
.iter_mut()
|
.iter_mut()
|
||||||
.for_each(|(_, s)| s.economy.cache_economy());
|
.for_each(|(_, s)| s.economy.cache_economy());
|
||||||
@ -671,9 +683,12 @@ impl Civs {
|
|||||||
.distance_squared(self.sites.get(b).center) as f32)
|
.distance_squared(self.sites.get(b).center) as f32)
|
||||||
.sqrt()
|
.sqrt()
|
||||||
};
|
};
|
||||||
let neighbors = |p: &Id<Site>| self.neighbors(*p);
|
let transition =
|
||||||
let transition = |a: &Id<Site>, b: &Id<Site>| {
|
|a: Id<Site>, b: Id<Site>| self.tracks.get(self.track_between(a, b).unwrap().0).cost;
|
||||||
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;
|
let satisfied = |p: &Id<Site>| *p == b;
|
||||||
// We use this hasher (FxHasher64) because
|
// We use this hasher (FxHasher64) because
|
||||||
@ -681,10 +696,7 @@ impl Civs {
|
|||||||
// (2) we care about determinism across computers (ruling out AAHash);
|
// (2) we care about determinism across computers (ruling out AAHash);
|
||||||
// (3) we have 8-byte keys (for which FxHash is fastest).
|
// (3) we have 8-byte keys (for which FxHash is fastest).
|
||||||
let mut astar = Astar::new(100, a, BuildHasherDefault::<FxHasher64>::default());
|
let mut astar = Astar::new(100, a, BuildHasherDefault::<FxHasher64>::default());
|
||||||
astar
|
astar.poll(100, heuristic, neighbors, satisfied).into_path()
|
||||||
.poll(100, heuristic, neighbors, transition, satisfied)
|
|
||||||
.into_path()
|
|
||||||
.and_then(|path| astar.get_cheapest_cost().map(|cost| (path, cost)))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn birth_civ(&mut self, ctx: &mut GenCtx<impl Rng>) -> Option<Id<Civ>> {
|
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
|
/// Adds lake POIs and names them
|
||||||
fn name_biomes(&mut self, ctx: &mut GenCtx<impl Rng>) {
|
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 map_size_lg = ctx.sim.map_size_lg();
|
||||||
let world_size = map_size_lg.chunks();
|
let world_size = map_size_lg.chunks();
|
||||||
let mut biomes: Vec<(common::terrain::BiomeKind, Vec<usize>)> = Vec::new();
|
let mut biomes: Vec<(common::terrain::BiomeKind, Vec<usize>)> = Vec::new();
|
||||||
@ -769,7 +781,7 @@ impl Civs {
|
|||||||
biomes.push((biome, filled));
|
biomes.push((biome, filled));
|
||||||
}
|
}
|
||||||
|
|
||||||
common_base::prof_span!("after flood fill");
|
prof_span!("after flood fill");
|
||||||
let mut biome_count = 0;
|
let mut biome_count = 0;
|
||||||
for biome in biomes {
|
for biome in biomes {
|
||||||
let name = match biome.0 {
|
let name = match biome.0 {
|
||||||
@ -1013,7 +1025,7 @@ impl Civs {
|
|||||||
|
|
||||||
/// Adds mountain POIs and name them
|
/// Adds mountain POIs and name them
|
||||||
fn name_peaks(&mut self, ctx: &mut GenCtx<impl Rng>) {
|
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();
|
let map_size_lg = ctx.sim.map_size_lg();
|
||||||
const MIN_MOUNTAIN_ALT: f32 = 600.0;
|
const MIN_MOUNTAIN_ALT: f32 = 600.0;
|
||||||
const MIN_MOUNTAIN_CHAOS: f32 = 0.35;
|
const MIN_MOUNTAIN_CHAOS: f32 = 0.35;
|
||||||
@ -1093,6 +1105,7 @@ impl Civs {
|
|||||||
loc: Vec2<i32>,
|
loc: Vec2<i32>,
|
||||||
site_fn: impl FnOnce(Id<Place>) -> Site,
|
site_fn: impl FnOnce(Id<Place>) -> Site,
|
||||||
) -> Id<Site> {
|
) -> Id<Site> {
|
||||||
|
prof_span!("establish_site");
|
||||||
const SITE_AREA: Range<usize> = 1..4; //64..256;
|
const SITE_AREA: Range<usize> = 1..4; //64..256;
|
||||||
|
|
||||||
fn establish_site(
|
fn establish_site(
|
||||||
@ -1112,10 +1125,17 @@ impl Civs {
|
|||||||
let site = establish_site(self, ctx, loc, site_fn);
|
let site = establish_site(self, ctx, loc, site_fn);
|
||||||
|
|
||||||
// Find neighbors
|
// 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
|
let mut nearby = self
|
||||||
.sites
|
.sites
|
||||||
.iter()
|
.iter()
|
||||||
|
.filter(|&(id, _)| id != site)
|
||||||
.filter(|(_, p)| {
|
.filter(|(_, p)| {
|
||||||
matches!(
|
matches!(
|
||||||
p.kind,
|
p.kind,
|
||||||
@ -1139,21 +1159,20 @@ impl Civs {
|
|||||||
| SiteKind::DesertCity
|
| SiteKind::DesertCity
|
||||||
| SiteKind::Castle = self.sites[site].kind
|
| SiteKind::Castle = self.sites[site].kind
|
||||||
{
|
{
|
||||||
for (nearby, _) in nearby.into_iter().take(5) {
|
for (nearby, _) in nearby.into_iter().take(4) {
|
||||||
// Find a novel path
|
prof_span!("for nearby");
|
||||||
if let Some((path, cost)) = find_path(
|
// Find a route using existing paths
|
||||||
ctx,
|
//
|
||||||
|start| self.bridges.get(&start).map(|(end, _)| *end),
|
// If the novel path isn't efficient compared to this, don't use it
|
||||||
loc,
|
let max_novel_cost = self
|
||||||
self.sites.get(nearby).center,
|
|
||||||
) {
|
|
||||||
// Find a path using existing paths
|
|
||||||
if self
|
|
||||||
.route_between(site, nearby)
|
.route_between(site, nearby)
|
||||||
// If the novel path isn't efficient compared to existing routes, don't use it
|
.map_or(f32::MAX, |(_, route_cost)| route_cost / 3.0);
|
||||||
.filter(|(_, route_cost)| *route_cost < cost * 3.0)
|
|
||||||
.is_none()
|
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
|
// Write the track to the world as a path
|
||||||
for locs in path.nodes().windows(3) {
|
for locs in path.nodes().windows(3) {
|
||||||
let mut randomize_offset = false;
|
let mut randomize_offset = false;
|
||||||
@ -1164,8 +1183,7 @@ impl Civs {
|
|||||||
{
|
{
|
||||||
ctx.sim.get_mut(locs[0]).unwrap().path.0.neighbors |=
|
ctx.sim.get_mut(locs[0]).unwrap().path.0.neighbors |=
|
||||||
1 << ((i as u8 + 4) % 8);
|
1 << ((i as u8 + 4) % 8);
|
||||||
ctx.sim.get_mut(locs[1]).unwrap().path.0.neighbors |=
|
ctx.sim.get_mut(locs[1]).unwrap().path.0.neighbors |= 1 << (i as u8);
|
||||||
1 << (i as u8);
|
|
||||||
randomize_offset = true;
|
randomize_offset = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1176,8 +1194,7 @@ impl Civs {
|
|||||||
{
|
{
|
||||||
ctx.sim.get_mut(locs[2]).unwrap().path.0.neighbors |=
|
ctx.sim.get_mut(locs[2]).unwrap().path.0.neighbors |=
|
||||||
1 << ((i as u8 + 4) % 8);
|
1 << ((i as u8 + 4) % 8);
|
||||||
ctx.sim.get_mut(locs[1]).unwrap().path.0.neighbors |=
|
ctx.sim.get_mut(locs[1]).unwrap().path.0.neighbors |= 1 << (i as u8);
|
||||||
1 << (i as u8);
|
|
||||||
randomize_offset = true;
|
randomize_offset = true;
|
||||||
} else if !self.bridges.contains_key(&locs[1]) {
|
} else if !self.bridges.contains_key(&locs[1]) {
|
||||||
let center = (locs[1] + locs[2]) / 2;
|
let center = (locs[1] + locs[2]) / 2;
|
||||||
@ -1218,10 +1235,8 @@ impl Civs {
|
|||||||
*/
|
*/
|
||||||
if randomize_offset {
|
if randomize_offset {
|
||||||
let mut chunk = ctx.sim.get_mut(locs[1]).unwrap();
|
let mut chunk = ctx.sim.get_mut(locs[1]).unwrap();
|
||||||
chunk.path.0.offset = Vec2::new(
|
chunk.path.0.offset =
|
||||||
ctx.rng.gen_range(-16..17),
|
Vec2::new(ctx.rng.gen_range(-16..17), ctx.rng.gen_range(-16..17));
|
||||||
ctx.rng.gen_range(-16..17),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1234,7 +1249,6 @@ impl Civs {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
site
|
site
|
||||||
}
|
}
|
||||||
@ -1302,21 +1316,25 @@ fn find_path(
|
|||||||
get_bridge: impl Fn(Vec2<i32>) -> Option<Vec2<i32>>,
|
get_bridge: impl Fn(Vec2<i32>) -> Option<Vec2<i32>>,
|
||||||
a: Vec2<i32>,
|
a: Vec2<i32>,
|
||||||
b: Vec2<i32>,
|
b: Vec2<i32>,
|
||||||
|
max_path_cost: f32,
|
||||||
) -> Option<(Path<Vec2<i32>>, f32)> {
|
) -> Option<(Path<Vec2<i32>>, f32)> {
|
||||||
|
prof_span!("find_path");
|
||||||
const MAX_PATH_ITERS: usize = 100_000;
|
const MAX_PATH_ITERS: usize = 100_000;
|
||||||
let sim = &ctx.sim;
|
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 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 neighbors = |l: &Vec2<i32>| {
|
||||||
let l = *l;
|
let l = *l;
|
||||||
NEIGHBORS
|
let bridge = get_bridge(l);
|
||||||
.iter()
|
let potential = walk_in_all_dirs(sim, bridge, l);
|
||||||
.filter_map(move |dir| walk_in_dir(sim, get_bridge, l, *dir))
|
potential
|
||||||
.map(move |(p, _)| p)
|
.into_iter()
|
||||||
};
|
.filter_map(|p| p.map(|(node, cost)| (node, cost + 1.0)))
|
||||||
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 satisfied = |l: &Vec2<i32>| *l == b;
|
let satisfied = |l: &Vec2<i32>| *l == b;
|
||||||
// We use this hasher (FxHasher64) because
|
// We use this hasher (FxHasher64) because
|
||||||
@ -1327,73 +1345,103 @@ fn find_path(
|
|||||||
MAX_PATH_ITERS,
|
MAX_PATH_ITERS,
|
||||||
a,
|
a,
|
||||||
BuildHasherDefault::<FxHasher64>::default(),
|
BuildHasherDefault::<FxHasher64>::default(),
|
||||||
);
|
)
|
||||||
|
.with_max_cost(max_path_cost);
|
||||||
astar
|
astar
|
||||||
.poll(MAX_PATH_ITERS, heuristic, neighbors, transition, satisfied)
|
.poll(MAX_PATH_ITERS, heuristic, neighbors, satisfied)
|
||||||
.into_path()
|
.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
|
/// 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
|
/// If permitted, the approximate relative const of traversal is given
|
||||||
// (TODO: by whom?)
|
// (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,
|
sim: &WorldSim,
|
||||||
get_bridge: impl Fn(Vec2<i32>) -> Option<Vec2<i32>>,
|
bridge: Option<Vec2<i32>>,
|
||||||
a: Vec2<i32>,
|
a: Vec2<i32>,
|
||||||
dir: Vec2<i32>,
|
) -> [Option<(Vec2<i32>, f32)>; 8] {
|
||||||
) -> Option<(Vec2<i32>, f32)> {
|
let mut potential = [None; 8];
|
||||||
if let Some(p) = get_bridge(a).filter(|p| (p - a).map(|e| e.signum()) == dir) {
|
|
||||||
// Traversing an existing bridge has no cost.
|
let adjacents = NEIGHBORS.map(|dir| a + dir);
|
||||||
Some((p, 0.0))
|
|
||||||
} else if loc_suitable_for_walking(sim, a + dir) {
|
let Some(a_chunk) = sim.get(a) else { return potential };
|
||||||
let a_chunk = sim.get(a)?;
|
let mut chunks = [None; 8];
|
||||||
let b_chunk = sim.get(a + dir)?;
|
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 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 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() {
|
let wild_cost = if b_chunk.path.0.is_way() {
|
||||||
0.0 // Traversing existing paths has no additional cost!
|
0.0 // Traversing existing paths has no additional cost!
|
||||||
} else {
|
} else {
|
||||||
3.0 // + (1.0 - b_chunk.tree_density) * 20.0 // Prefer going through forests, for aesthetics
|
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 {
|
let cost = 1.0 + hill_cost + water_cost + wild_cost;
|
||||||
(4..=5).find_map(|i| {
|
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)
|
loc_suitable_for_walking(sim, a + dir * i)
|
||||||
.then(|| (a + dir * i, 120.0 + (i - 4) as f32 * 10.0))
|
.then(|| (a + dir * i, 120.0 + (i - 4) as f32 * 10.0))
|
||||||
})
|
});
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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
|
/// Return true if a position is suitable for walking on
|
||||||
fn loc_suitable_for_walking(sim: &WorldSim, loc: Vec2<i32>) -> bool {
|
fn loc_suitable_for_walking(sim: &WorldSim, loc: Vec2<i32>) -> bool {
|
||||||
if sim.get(loc).is_some() {
|
if sim.get(loc).is_some() {
|
||||||
!NEIGHBORS.iter().any(|n| {
|
NEIGHBORS.iter().all(|n| {
|
||||||
sim.get(loc + *n)
|
sim.get(loc + *n)
|
||||||
.map_or(false, |chunk| chunk.river.near_water())
|
.map_or(false, |chunk| !chunk.river.near_water())
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
false
|
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:
|
/// Return true if a position is suitable for site construction (TODO:
|
||||||
/// criteria?)
|
/// 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 {
|
fn check_chunk_occupation(sim: &WorldSim, loc: Vec2<i32>, radius: i32) -> bool {
|
||||||
for x in (-radius)..radius {
|
for x in (-radius)..radius {
|
||||||
for y 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()) {
|
if sim.get(check_loc).map_or(false, |c| !c.sites.is_empty()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@ -1401,8 +1449,9 @@ fn loc_suitable_for_site(sim: &WorldSim, loc: Vec2<i32>, site_kind: SiteKind) ->
|
|||||||
}
|
}
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
let not_occupied = check_chunk_occupation(sim, loc, site_kind.exclusion_radius());
|
let not_occupied = || check_chunk_occupation(sim, loc, site_kind.exclusion_radius());
|
||||||
site_kind.is_suitable_loc(loc, sim) && not_occupied
|
// 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
|
/// Attempt to search for a location that's suitable for site construction
|
||||||
@ -1411,6 +1460,7 @@ fn find_site_loc(
|
|||||||
proximity_reqs: &ProximityRequirements,
|
proximity_reqs: &ProximityRequirements,
|
||||||
site_kind: SiteKind,
|
site_kind: SiteKind,
|
||||||
) -> Option<Vec2<i32>> {
|
) -> Option<Vec2<i32>> {
|
||||||
|
prof_span!("find_site_loc");
|
||||||
const MAX_ATTEMPTS: usize = 10000;
|
const MAX_ATTEMPTS: usize = 10000;
|
||||||
let mut loc = None;
|
let mut loc = None;
|
||||||
for _ in 0..MAX_ATTEMPTS {
|
for _ in 0..MAX_ATTEMPTS {
|
||||||
@ -1421,16 +1471,15 @@ fn find_site_loc(
|
|||||||
)
|
)
|
||||||
});
|
});
|
||||||
|
|
||||||
if proximity_reqs.satisfied_by(test_loc) {
|
let is_suitable_loc = site_kind.is_suitable_loc(test_loc, ctx.sim);
|
||||||
if loc_suitable_for_site(ctx.sim, test_loc, site_kind) {
|
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);
|
return Some(test_loc);
|
||||||
}
|
}
|
||||||
|
|
||||||
loc = ctx.sim.get(test_loc).and_then(|c| {
|
// If the current location is suitable and meets proximity requirements,
|
||||||
site_kind
|
// try nearby spot downhill.
|
||||||
.is_suitable_loc(test_loc, ctx.sim)
|
loc = ctx.sim.get(test_loc).and_then(|c| c.downhill);
|
||||||
.then_some(c.downhill?.wpos_to_cpos())
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -55,11 +55,11 @@ use common::{
|
|||||||
},
|
},
|
||||||
vol::{ReadVol, RectVolSize, WriteVol},
|
vol::{ReadVol, RectVolSize, WriteVol},
|
||||||
};
|
};
|
||||||
|
use common_base::prof_span;
|
||||||
use common_net::msg::{world_msg, WorldMapMsg};
|
use common_net::msg::{world_msg, WorldMapMsg};
|
||||||
use enum_map::EnumMap;
|
use enum_map::EnumMap;
|
||||||
use rand::{prelude::*, Rng};
|
use rand::{prelude::*, Rng};
|
||||||
use rand_chacha::ChaCha8Rng;
|
use rand_chacha::ChaCha8Rng;
|
||||||
use rayon::iter::ParallelIterator;
|
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
use vek::*;
|
use vek::*;
|
||||||
@ -110,6 +110,7 @@ impl World {
|
|||||||
opts: sim::WorldOpts,
|
opts: sim::WorldOpts,
|
||||||
threadpool: &rayon::ThreadPool,
|
threadpool: &rayon::ThreadPool,
|
||||||
) -> (Self, IndexOwned) {
|
) -> (Self, IndexOwned) {
|
||||||
|
prof_span!("World::generate");
|
||||||
// NOTE: Generating index first in order to quickly fail if the color manifest
|
// NOTE: Generating index first in order to quickly fail if the color manifest
|
||||||
// is broken.
|
// is broken.
|
||||||
threadpool.install(|| {
|
threadpool.install(|| {
|
||||||
@ -136,6 +137,7 @@ impl World {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_map_data(&self, index: IndexRef, threadpool: &rayon::ThreadPool) -> WorldMapMsg {
|
pub fn get_map_data(&self, index: IndexRef, threadpool: &rayon::ThreadPool) -> WorldMapMsg {
|
||||||
|
prof_span!("World::get_map_data");
|
||||||
threadpool.install(|| {
|
threadpool.install(|| {
|
||||||
// we need these numbers to create unique ids for cave ends
|
// we need these numbers to create unique ids for cave ends
|
||||||
let num_sites = self.civs().sites().count() as u64;
|
let num_sites = self.civs().sites().count() as u64;
|
||||||
@ -562,6 +564,7 @@ impl World {
|
|||||||
let mut objects = Vec::new();
|
let mut objects = Vec::new();
|
||||||
|
|
||||||
// Add trees
|
// Add trees
|
||||||
|
prof_span!(guard, "add trees");
|
||||||
objects.append(
|
objects.append(
|
||||||
&mut self
|
&mut self
|
||||||
.sim()
|
.sim()
|
||||||
@ -600,6 +603,7 @@ impl World {
|
|||||||
})
|
})
|
||||||
.collect(),
|
.collect(),
|
||||||
);
|
);
|
||||||
|
drop(guard);
|
||||||
|
|
||||||
// Add buildings
|
// Add buildings
|
||||||
objects.extend(
|
objects.extend(
|
||||||
|
@ -51,6 +51,7 @@ use common::{
|
|||||||
},
|
},
|
||||||
vol::RectVolSize,
|
vol::RectVolSize,
|
||||||
};
|
};
|
||||||
|
use common_base::prof_span;
|
||||||
use common_net::msg::WorldMapMsg;
|
use common_net::msg::WorldMapMsg;
|
||||||
use noise::{
|
use noise::{
|
||||||
BasicMulti, Billow, Fbm, HybridMulti, MultiFractal, NoiseFn, RangeFunction, RidgedMulti,
|
BasicMulti, Billow, Fbm, HybridMulti, MultiFractal, NoiseFn, RangeFunction, RidgedMulti,
|
||||||
@ -668,6 +669,7 @@ pub struct WorldSim {
|
|||||||
|
|
||||||
impl WorldSim {
|
impl WorldSim {
|
||||||
pub fn generate(seed: u32, opts: WorldOpts, threadpool: &rayon::ThreadPool) -> Self {
|
pub fn generate(seed: u32, opts: WorldOpts, threadpool: &rayon::ThreadPool) -> Self {
|
||||||
|
prof_span!("WorldSim::generate");
|
||||||
let calendar = opts.calendar; // separate lifetime of elements
|
let calendar = opts.calendar; // separate lifetime of elements
|
||||||
let world_file = opts.world_file;
|
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
|
/// Draw a map of the world based on chunk information. Returns a buffer of
|
||||||
/// u32s.
|
/// u32s.
|
||||||
pub fn get_map(&self, index: IndexRef, calendar: Option<&Calendar>) -> WorldMapMsg {
|
pub fn get_map(&self, index: IndexRef, calendar: Option<&Calendar>) -> WorldMapMsg {
|
||||||
|
prof_span!("WorldSim::get_map");
|
||||||
let mut map_config = MapConfig::orthographic(
|
let mut map_config = MapConfig::orthographic(
|
||||||
self.map_size_lg(),
|
self.map_size_lg(),
|
||||||
core::ops::RangeInclusive::new(CONFIG.sea_level, CONFIG.sea_level + self.max_height),
|
core::ops::RangeInclusive::new(CONFIG.sea_level, CONFIG.sea_level + self.max_height),
|
||||||
@ -1603,6 +1606,7 @@ impl WorldSim {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let samples_data = {
|
let samples_data = {
|
||||||
|
prof_span!("samples data");
|
||||||
let column_sample = ColumnGen::new(self);
|
let column_sample = ColumnGen::new(self);
|
||||||
(0..self.map_size_lg().chunks_len())
|
(0..self.map_size_lg().chunks_len())
|
||||||
.into_par_iter()
|
.into_par_iter()
|
||||||
@ -2293,10 +2297,10 @@ impl WorldSim {
|
|||||||
&self,
|
&self,
|
||||||
wpos_min: Vec2<i32>,
|
wpos_min: Vec2<i32>,
|
||||||
wpos_max: Vec2<i32>,
|
wpos_max: Vec2<i32>,
|
||||||
) -> impl ParallelIterator<Item = TreeAttr> + '_ {
|
) -> impl Iterator<Item = TreeAttr> + '_ {
|
||||||
self.gen_ctx
|
self.gen_ctx
|
||||||
.structure_gen
|
.structure_gen
|
||||||
.par_iter(wpos_min, wpos_max)
|
.iter(wpos_min, wpos_max)
|
||||||
.filter_map(move |(wpos, seed)| {
|
.filter_map(move |(wpos, seed)| {
|
||||||
let lottery = self.make_forest_lottery(wpos);
|
let lottery = self.make_forest_lottery(wpos);
|
||||||
Some(TreeAttr {
|
Some(TreeAttr {
|
||||||
|
@ -3,6 +3,7 @@ use common::{
|
|||||||
terrain::{neighbors, uniform_idx_as_vec2, vec2_as_uniform_idx, MapSizeLg, TerrainChunkSize},
|
terrain::{neighbors, uniform_idx_as_vec2, vec2_as_uniform_idx, MapSizeLg, TerrainChunkSize},
|
||||||
vol::RectVolSize,
|
vol::RectVolSize,
|
||||||
};
|
};
|
||||||
|
use common_base::prof_span;
|
||||||
use noise::{MultiFractal, NoiseFn, Perlin, Seedable};
|
use noise::{MultiFractal, NoiseFn, Perlin, Seedable};
|
||||||
use num::Float;
|
use num::Float;
|
||||||
use rayon::prelude::*;
|
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_angle: impl Fn(F) -> A + Sync,
|
||||||
to_height: impl Fn(F) -> H + Sync,
|
to_height: impl Fn(F) -> H + Sync,
|
||||||
) -> Result<[HorizonMap<A, H>; 2], ()> {
|
) -> Result<[HorizonMap<A, H>; 2], ()> {
|
||||||
|
prof_span!("get_horizon_map");
|
||||||
if maxh < minh {
|
if maxh < minh {
|
||||||
// maxh must be greater than minh
|
// maxh must be greater than minh
|
||||||
return Err(());
|
return Err(());
|
||||||
|
@ -1,3 +1,7 @@
|
|||||||
use crate::{sim::WorldSim, site::economy::simulate_economy, Index};
|
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,
|
&self,
|
||||||
origin: Vec2<i32>,
|
origin: Vec2<i32>,
|
||||||
dest: 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>>> {
|
) -> Option<Path<Vec2<i32>>> {
|
||||||
let heuristic = |pos: &Vec2<i32>, _: &Vec2<i32>| (pos - dest).map(|e| e as f32).magnitude();
|
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 neighbors = |pos: &Vec2<i32>| {
|
||||||
let pos = *pos;
|
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;
|
let satisfied = |pos: &Vec2<i32>| *pos == dest;
|
||||||
|
|
||||||
// We use this hasher (FxHasher64) because
|
// 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);
|
// (2) we don't care about determinism across computers (we could use AAHash);
|
||||||
// (3) we have 8-byte keys (for which FxHash is fastest).
|
// (3) we have 8-byte keys (for which FxHash is fastest).
|
||||||
Astar::new(250, origin, BuildHasherDefault::<FxHasher64>::default())
|
Astar::new(250, origin, BuildHasherDefault::<FxHasher64>::default())
|
||||||
.poll(250, heuristic, neighbors, transition, satisfied)
|
.poll(250, heuristic, neighbors, satisfied)
|
||||||
.into_path()
|
.into_path()
|
||||||
|
.map(|(p, _c)| p)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// We use this hasher (FxHasher64) because
|
/// We use this hasher (FxHasher64) because
|
||||||
|
@ -160,18 +160,23 @@ impl Site {
|
|||||||
}
|
}
|
||||||
max_cost + (dir != old_dir) as i32 as f32 * 35.0
|
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(
|
.poll(
|
||||||
MAX_ITERS,
|
MAX_ITERS,
|
||||||
&heuristic,
|
&heuristic,
|
||||||
|(tile, _)| {
|
|(tile, _)| {
|
||||||
let tile = *tile;
|
let tile = *tile;
|
||||||
CARDINALS.iter().map(move |dir| (tile + *dir, *dir))
|
let this = &self;
|
||||||
},
|
CARDINALS.iter().map(move |dir| {
|
||||||
|(a, _), (b, _)| {
|
let neighbor = (tile + *dir, *dir);
|
||||||
let alt_a = land.get_alt_approx(self.tile_center_wpos(*a));
|
|
||||||
let alt_b = land.get_alt_approx(self.tile_center_wpos(*b));
|
// Transition cost
|
||||||
(alt_a - alt_b).abs() / TILE_SIZE as f32
|
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,
|
|(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>) {
|
fn create_route(&mut self, _ctx: &mut GenCtx<impl Rng>, a: Vec2<i32>, b: Vec2<i32>) {
|
||||||
let heuristic =
|
let heuristic =
|
||||||
move |l: &Vec2<i32>, _: &Vec2<i32>| (l - b).map(|e| e.abs()).reduce_max() as f32;
|
move |l: &Vec2<i32>, _: &Vec2<i32>| (l - b).map(|e| e.abs()).reduce_max() as f32;
|
||||||
let neighbors = |l: &Vec2<i32>| {
|
let transition = |_a: Vec2<i32>, b: Vec2<i32>| match self.tiles.get(b) {
|
||||||
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) {
|
|
||||||
Some(Tile::Room(_)) | Some(Tile::Tunnel) => 1.0,
|
Some(Tile::Room(_)) | Some(Tile::Tunnel) => 1.0,
|
||||||
Some(Tile::Solid) => 25.0,
|
Some(Tile::Solid) => 25.0,
|
||||||
Some(Tile::UpStair(_, _)) | Some(Tile::DownStair(_)) => 0.0,
|
Some(Tile::UpStair(_, _)) | Some(Tile::DownStair(_)) => 0.0,
|
||||||
_ => 100000.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;
|
let satisfied = |l: &Vec2<i32>| *l == b;
|
||||||
// We use this hasher (FxHasher64) because
|
// We use this hasher (FxHasher64) because
|
||||||
// (1) we don't care about DDOS attacks (ruling out SipHash);
|
// (1) we don't care about DDOS attacks (ruling out SipHash);
|
||||||
// (2) we don't care about determinism across computers (we could use AAHash);
|
// (2) we don't care about determinism across computers (we could use AAHash);
|
||||||
// (3) we have 8-byte keys (for which FxHash is fastest).
|
// (3) we have 8-byte keys (for which FxHash is fastest).
|
||||||
let mut astar = Astar::new(20000, a, BuildHasherDefault::<FxHasher64>::default());
|
let mut astar = Astar::new(20000, a, BuildHasherDefault::<FxHasher64>::default());
|
||||||
let path = astar
|
let (path, _cost) = astar
|
||||||
.poll(
|
.poll(
|
||||||
FLOOR_SIZE.product() as usize + 1,
|
FLOOR_SIZE.product() as usize + 1,
|
||||||
heuristic,
|
heuristic,
|
||||||
neighbors,
|
neighbors,
|
||||||
transition,
|
|
||||||
satisfied,
|
satisfied,
|
||||||
)
|
)
|
||||||
.into_path()
|
.into_path()
|
||||||
|
@ -23,13 +23,13 @@ pub use self::{
|
|||||||
|
|
||||||
pub use common::grid::Grid;
|
pub use common::grid::Grid;
|
||||||
|
|
||||||
use fxhash::FxHasher32;
|
use fxhash::{FxHasher32, FxHasher64};
|
||||||
use hashbrown::{HashMap, HashSet};
|
use hashbrown::{HashMap, HashSet};
|
||||||
use std::hash::BuildHasherDefault;
|
use std::hash::BuildHasherDefault;
|
||||||
use vek::*;
|
use vek::*;
|
||||||
|
|
||||||
// Deterministic HashMap and HashSet
|
// 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 type DHashSet<T> = HashSet<T, BuildHasherDefault<FxHasher32>>;
|
||||||
|
|
||||||
pub fn attempt<T>(max_iters: usize, mut f: impl FnMut() -> Option<T>) -> Option<T> {
|
pub fn attempt<T>(max_iters: usize, mut f: impl FnMut() -> Option<T>) -> Option<T> {
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
use super::{RandomField, Sampler};
|
use super::{RandomField, Sampler};
|
||||||
use rayon::prelude::*;
|
|
||||||
use vek::*;
|
use vek::*;
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
@ -69,11 +68,7 @@ impl StructureGen2d {
|
|||||||
|
|
||||||
/// Note: Generates all possible closest samples for elements in the range
|
/// Note: Generates all possible closest samples for elements in the range
|
||||||
/// of min to max, *exclusive.*
|
/// of min to max, *exclusive.*
|
||||||
pub fn par_iter(
|
pub fn iter(&self, min: Vec2<i32>, max: Vec2<i32>) -> impl Iterator<Item = StructureField> {
|
||||||
&self,
|
|
||||||
min: Vec2<i32>,
|
|
||||||
max: Vec2<i32>,
|
|
||||||
) -> impl ParallelIterator<Item = StructureField> {
|
|
||||||
let freq = self.freq;
|
let freq = self.freq;
|
||||||
let spread = self.spread;
|
let spread = self.spread;
|
||||||
let spread_mul = Self::spread_mul(spread);
|
let spread_mul = Self::spread_mul(spread);
|
||||||
@ -102,7 +97,7 @@ impl StructureGen2d {
|
|||||||
let x_field = self.x_field;
|
let x_field = self.x_field;
|
||||||
let y_field = self.y_field;
|
let y_field = self.y_field;
|
||||||
let seed_field = self.seed_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);
|
let index = min_index + Vec2::new((xy % xlen as u64) as i32, (xy / xlen as u64) as i32);
|
||||||
Self::index_to_sample_internal(
|
Self::index_to_sample_internal(
|
||||||
freq,
|
freq,
|
||||||
|
Loading…
Reference in New Issue
Block a user