Additional optimizations (such as avoiding calling find_path when we know we won't use the result or that it will fail), cleanup of excess notes and commented code, probably other misc optimizations

This commit is contained in:
Imbris 2023-04-18 03:20:39 -04:00
parent 1f5ebbd100
commit 81885fe8e5
3 changed files with 173 additions and 316 deletions

View File

@ -11,9 +11,8 @@ use std::collections::BinaryHeap;
#[derive(Copy, Clone, Debug)]
pub struct PathEntry<S> {
// cost so far + heursitic
priority: f32,
cost_estimate: f32,
node: S,
//cost: f32,
}
impl<S: Eq> PartialEq for PathEntry<S> {
@ -26,7 +25,10 @@ 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.priority.partial_cmp(&self.priority).unwrap_or(Equal)
other
.cost_estimate
.partial_cmp(&self.cost_estimate)
.unwrap_or(Equal)
}
}
@ -40,7 +42,7 @@ impl<S: Eq> PartialOrd for PathEntry<S> {
// 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.priority <= self.priority }
fn le(&self, other: &PathEntry<S>) -> bool { other.cost_estimate <= self.cost_estimate }
}
pub enum PathResult<T> {
@ -78,32 +80,29 @@ struct NodeEntry<S> {
#[derive(Clone, Debug)]
struct Cluster<S> {
// idea: if we store pointers to adjacent clusters we could avoid the hashmap entirely when
// accessing neighboring nodes, actually I'm not even sure what case we would need a hashmap
// for in this scenario if we store cluster pointer in the priority queue (or index to cluster
// pointer if saving space here is worth it)!
// TODO: we could use `(S, u8)` here?
// idea: if we bake in the gridness we could just store a direction
// idea: if we bake in the gridness we could just store a direction (note: actually some things
// like bridges are not adjacent)
// idea: if we can gain something by making them smaller, we could allocate clusters in a Vec,
// this amoritizes allocation costs some, if we point to neighbors we would need to use
// indices although we could take advantage of that to make them smaller than pointer size
// (alternatively a bump allocator would work very well here)
// idea
came_from: [Option<S>; 256],
cheapest_score: [f32; 256],
}
// ideas:
// * merge hashmaps
// * "chunked" exploration
// * things we put on priority queue don't need to point into a hashmap (i.e. we
// only need a hashmap to map from new/unknown nodes to whatever
// datastructure)
#[derive(Clone)]
pub struct Astar<S, Hasher> {
iter: usize,
max_iters: usize,
potential_nodes: BinaryHeap<PathEntry<S>>, // cost, node pairs
// converting to single hash structure: 11349 ms -> 10462 ms / 10612 ms
// with two hash structures (came_from and cheapest_scores): 10861 ms
visited_nodes: HashMap<S, NodeEntry<S>, Hasher>,
// -> 25055 ms -> 15771 ms with Box -> fixed bugs 10731 ms, hmmm
clusters: HashMap<S, Box<Cluster<S>>, Hasher>, // TODO: Box cluster?
//came_from: HashMap<S, S, Hasher>,
//cheapest_scores: HashMap<S, f32, Hasher>,
//final_scores: HashMap<S, f32, Hasher>,
//visited: HashSet<S, Hasher>,
clusters: HashMap<S, Box<Cluster<S>>, Hasher>,
start_node: S,
cheapest_node: Option<S>,
cheapest_cost: Option<f32>,
@ -120,29 +119,10 @@ impl<S: Clone + Eq + Hash, H: BuildHasher + Clone> Astar<S, H> {
max_iters,
iter: 0,
potential_nodes: core::iter::once(PathEntry {
priority: 0.0,
//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));
s
},
*/
visited_nodes: {
let mut s = HashMap::with_capacity_and_hasher(1, hasher.clone());
s.extend(core::iter::once((start.clone(), NodeEntry {
@ -161,26 +141,21 @@ impl<S: Clone + Eq + Hash, H: BuildHasher + Clone> Astar<S, H> {
pub fn poll<I>(
&mut self,
iters: usize,
// estimate how far we are from the target? but we are given two nodes... (current,
// 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,
// cost of edge between these two nodes
// I assume this is (source, destination)?
mut transition: impl FnMut(&S, &S) -> f32,
// have we reached a/the target?
// have we reached target?
mut satisfied: impl FnMut(&S) -> bool,
// this function clusters nodes together for cache locality purposes
// output (cluster base, offset in cluster)
cluster: impl Fn(&S) -> (S, u8),
) -> PathResult<S>
where
// Combining transition into this: 9913 ms -> 8204 ms (~1.7 out of ~6.5 seconds)
I: Iterator<Item = (S, f32)>,
I: Iterator<Item = (S, f32)>, // (node, transition cost)
{
/*
*/
if self.clusters.is_empty() {
let (key, index) = cluster(&self.start_node);
let mut came_from = std::array::from_fn(|_| None);
@ -193,14 +168,14 @@ impl<S: Clone + Eq + Hash, H: BuildHasher + Clone> Astar<S, H> {
}),
);
}
*/
let iter_limit = self.max_iters.min(self.iter + iters);
while self.iter < iter_limit {
// pop highest priority node
if let Some(PathEntry { node, .. }) = self.potential_nodes.pop() {
// if this is the destination, we return
if satisfied(&node) {
return PathResult::Path(self.reconstruct_path_to(node, cluster));
} else {
/*
let (cluster_key, index) = cluster(&node);
let (node_cheapest, came_from) = self
.clusters
@ -212,30 +187,18 @@ impl<S: Clone + Eq + Hash, H: BuildHasher + Clone> Astar<S, H> {
)
})
.unwrap();
// regression
//if node_cheapest < cost {
// we already processed it
// continue;
//}
// 10700 ms -> 10477 ms (moving this out of the loop)
// we have to fetch this even though it was put into the priority queu
/*
let node_cheapest = self
*/
// we have to fetch this even though it was put into the priority queue
let (node_cheapest, came_from) = self
.visited_nodes
.get(&node)
.map_or(f32::MAX, |n| n.cheapest_score);
*/
// otherwise we iterate neighbors
// TODO: try for_each here
// 6879 ms -> 6989 ms (regression using for_each)
//neighbors(&node).for_each(|(neighbor, transition)| {
.map(|n| (n.cheapest_score, n.came_from.clone()))
.unwrap();
for (neighbor, transition) in neighbors(&node) {
// skipping here: 10694 ms -> 9913 ms (almost whole second out of 7 taken
// for this, this is because the `transition` call is fairly expensive)
if neighbor == came_from {
continue;
//return;
}
/*
let (cluster_key, index) = cluster(&neighbor);
let mut previously_visited = false;
let neighbor_cheapest = self
@ -247,44 +210,18 @@ impl<S: Clone + Eq + Hash, H: BuildHasher + Clone> Astar<S, H> {
previously_visited.then(|| c.cheapest_score[usize::from(index)])
})
.unwrap_or(f32::MAX);
/*
*/
let neighbor_cheapest = self
.visited_nodes
.get(&neighbor)
.map_or(f32::MAX, |n| n.cheapest_score);
*/
// 10573 ms -> 11546 ms (with entry api appears to be regression)
/*
let mut previously_visited = true;
let neighbor_entry = self
.visited_nodes
.entry(neighbor.clone())
.or_insert_with(|| {
previously_visited = false;
NodeEntry {
came_from: node.clone(),
cheapest_score: f32::MAX,
}
});
let neighbor_cheapest = neighbor_entry.cheapest_score;
*/
/*
let node_cheapest = *self.cheapest_scores.get(&node).unwrap_or(&f32::MAX);
let neighbor_cheapest =
*self.cheapest_scores.get(&neighbor).unwrap_or(&f32::MAX);
*/
// TODO: have caller provide transition cost with neighbors iterator (so
// that duplicate costs in `transition` can be avoided?)
// compute cost to traverse to each neighbor
let cost = node_cheapest + transition; //transition(&node, &neighbor);
// if this is cheaper than existing cost for that neighbor (or neighbor
// hasn't been visited)
// can we convince ourselves that this is always true if node was not
// visited?
let cost = node_cheapest + transition;
if cost < neighbor_cheapest {
//neighbor_entry.cheapest_score = cost;
/*
neighbor_entry.cheapest_score = cost;
// note: unconditional insert, same cost as overwriting if it already
// exists
let previously_visited = self
@ -293,7 +230,6 @@ impl<S: Clone + Eq + Hash, H: BuildHasher + Clone> Astar<S, H> {
.is_some();
self.cheapest_scores.insert(neighbor.clone(), cost);
*/
/*
let previously_visited = self
.visited_nodes
.insert(neighbor.clone(), NodeEntry {
@ -301,7 +237,7 @@ impl<S: Clone + Eq + Hash, H: BuildHasher + Clone> Astar<S, H> {
cheapest_score: cost,
})
.is_some();
*/
/*
let cluster_mut =
self.clusters.entry(cluster_key).or_insert_with(|| {
Box::new(Cluster {
@ -312,37 +248,28 @@ impl<S: Clone + Eq + Hash, H: BuildHasher + Clone> Astar<S, H> {
cluster_mut.came_from[usize::from(index)] = Some(node.clone());
cluster_mut.cheapest_score[usize::from(index)] = cost;
*/
let h = heuristic(&neighbor, &node);
// note that cheapest_scores does not include the heuristic
// this is what final_scores does, priority queue does include
// heuristic
let priority = cost + h;
// note this is literally unused, removing saves ~350 ms out of 11349
// (note this is all of startup time)
//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) {
self.cheapest_node = Some(node.clone());
self.cheapest_cost = Some(h);
};
// commenting out if here: 11349 ms -> 12498 ms (but may give better
// paths?) (about 1 extra second or +10% time)
// with single hashmap change this has much more impact:
// 3473 ms -> 11981 ms
// TODO: I think the if here should be removed
// if we hadn't already visted, add this to potential nodes, what about
// its neighbors, wouldn't they need to be revisted???
if !previously_visited {
self.potential_nodes.push(PathEntry {
priority,
//cost,
cost_estimate,
node: neighbor,
});
}
}
}
//});
}
} else {
return PathResult::None(
@ -370,29 +297,29 @@ impl<S: Clone + Eq + Hash, H: BuildHasher + Clone> Astar<S, H> {
pub fn get_cheapest_cost(&self) -> Option<f32> { self.cheapest_cost }
// At least in world site pathfinding this is super cheap compared to actually
// finding the path!
fn reconstruct_path_to(&mut self, end: S, cluster: impl Fn(&S) -> (S, u8)) -> Path<S> {
let mut path = vec![end.clone()];
let mut cnode = &end;
let (mut ckey, mut ci) = cluster(cnode);
while let Some(node) = self
.clusters
.get(&ckey)
.and_then(|c| c.came_from[usize::from(ci)].as_ref())
.filter(|n| *n != cnode)
/*
self
let (mut ckey, mut ci) = cluster(cnode);
while let Some(node) =
self
.clusters
.get(&ckey)
.and_then(|c| c.came_from[usize::from(ci)].as_ref())
.filter(|n| *n != cnode)
*/
/*
*/
while let Some(node) = self
.visited_nodes
.get(cnode)
.map(|n| &n.came_from)
.filter(|n| *n != cnode)
*/
//self.came_from.get(cnode)
{
path.push(node.clone());
cnode = node;
(ckey, ci) = cluster(cnode);
//(ckey, ci) = cluster(cnode);
}
path.into_iter().rev().collect()
}

View File

@ -55,16 +55,10 @@ pub struct Civs {
/// (3) we have 8-byte keys (for which FxHash is fastest).
pub track_map: DHashMap<Id<Site>, DHashMap<Id<Site>, Id<Track>>>,
// 8249 ms -> 7680 ms (change when switching to ahash)
// 7495 ms -> 8057 ms -> 7481 ms (ahash -> sip13 -> fxhasher)
// TODO: deterministic(?), this is certainly faster, presumably due to less collisions
pub bridges: hashbrown::HashMap<
Vec2<i32>,
(Vec2<i32>, Id<Site>),
//std::hash::BuildHasherDefault<siphasher::sip::SipHasher13>,
std::hash::BuildHasherDefault<fxhash::FxHasher64>,
//std::hash::BuildHasherDefault<fxhash::FxHasher>,
//std::hash::BuildHasherDefault<fxhash::FxHasher32>, // too many collisions!
>,
pub sites: Store<Site>,
@ -527,8 +521,6 @@ impl Civs {
}
}
}
dbg!(CC.load(Ordering::Relaxed));
}
// TODO: Move this
@ -1120,7 +1112,6 @@ impl Civs {
loc: Vec2<i32>,
site_fn: impl FnOnce(Id<Place>) -> Site,
) -> Id<Site> {
prof_span!("establish site inner");
let place = match ctx.sim.get(loc).and_then(|site| site.place) {
Some(place) => place,
None => civs.establish_place(ctx, loc, SITE_AREA),
@ -1132,11 +1123,15 @@ impl Civs {
let site = establish_site(self, ctx, loc, site_fn);
// Find neighbors
prof_span!(guard, "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 297. So I think this is a reasonible
// limit (although the relationship between distance and pathfinding iterations
// can be a bit variable).
const MAX_NEIGHBOR_DISTANCE: f32 = 350.0;
let mut nearby = self
.sites
.iter()
.filter(|&(id, _)| id != site)
.filter(|(_, p)| {
matches!(
p.kind,
@ -1152,7 +1147,6 @@ impl Civs {
.filter(|(_, dist)| *dist < MAX_NEIGHBOR_DISTANCE)
.collect::<Vec<_>>();
nearby.sort_by_key(|(_, dist)| *dist as i32);
drop(guard);
if let SiteKind::Refactor
| SiteKind::Settlement
@ -1161,111 +1155,107 @@ impl Civs {
| SiteKind::DesertCity
| SiteKind::Castle = self.sites[site].kind
{
for (nearby, _) in nearby.into_iter().take(5) {
for (nearby, _) in nearby.into_iter().take(4) {
prof_span!("for nearby");
// Find a novel path
let maybe_path = {
prof_span!("find path");
find_path(
ctx,
|start| self.bridges.get(&start).map(|(end, _)| *end),
loc,
self.sites.get(nearby).center,
)
};
if maybe_path.is_some() {
info!("Succeed");
} else {
info!("Fail");
}
if let Some((path, cost)) = maybe_path {
prof_span!("with path");
// Find a path using existing paths
if self
// Find a route using existing paths
//
// If the novel path isn't efficient compared to this, don't use it
let max_novel_cost = {
let max_novel_cost = 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]) {
//dbg!("here"); called 18 times
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;
.map_or(f32::MAX, |(_, route_cost)| route_cost / 3.0);
max_novel_cost
};
let start = loc;
let end = self.sites.get(nearby).center;
// Find a novel path.
//
// We rely on the cost at least being equal to the distance, to avoid
// unnecessary novel pathfinding.
let maybe_path = ((start.distance_squared(end) as f32).sqrt() < max_novel_cost)
.then(|| {
prof_span!("find path");
let get_bridge = |start| self.bridges.get(&start).map(|(end, _)| *end);
find_path(ctx, get_bridge, start, end)
})
.flatten()
.filter(|&(_, cost)| cost < max_novel_cost);
if let Some((path, cost)) = maybe_path {
// 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]) {
//dbg!("here"); called 18 times
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);
}
}
}
@ -1341,30 +1331,18 @@ fn find_path(
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
// diagonals can only cost `1.0` if a path exists and since bridges have
// zero cost (and cover multiple tiles).
// 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 neighbors = |l: &Vec2<i32>| {
let l = *l;
let bridge = get_bridge(l);
/*
NEIGHBORS
.iter()
.filter_map(move |dir| walk_in_dir(sim, bridge, l, *dir))
*/
/*
*/
// Using walk_in_all_dirs saves ~500 ms
let potential = walk_in_all_dirs(sim, bridge, l);
potential.into_iter().filter_map(|p| p)
};
// transition cost?
let transition = |a: &Vec2<i32>, b: &Vec2<i32>| {
// factoring this out: 7463 ms -> 7356 ms
let bridge = get_bridge(*a);
1.0 + walk_in_dir(sim, bridge, *a, (*b - *a).map(|e| e.signum()))
.map_or(10000.0, |(_, cost)| cost)
potential
.into_iter()
.filter_map(|p| p.map(|(node, cost)| (node, cost + 1.0)))
};
let satisfied = |l: &Vec2<i32>| *l == b;
let cluster = |l: &Vec2<i32>| {
@ -1384,21 +1362,17 @@ fn find_path(
BuildHasherDefault::<FxHasher64>::default(),
);
astar
.poll(
MAX_PATH_ITERS,
heuristic,
neighbors,
transition,
satisfied,
cluster,
)
.poll(MAX_PATH_ITERS, heuristic, neighbors, satisfied, cluster)
.into_path()
.and_then(|path| astar.get_cheapest_cost().map(|cost| (path, cost)))
}
use core::sync::atomic::{AtomicUsize, Ordering};
static CC: AtomicUsize = AtomicUsize::new(0);
/// 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?)
/// Return tuple: (final location, cost)
///
/// For efficiency, this computes for all 8 directions at once.
fn walk_in_all_dirs(
sim: &WorldSim,
bridge: Option<Vec2<i32>>,
@ -1407,9 +1381,10 @@ fn walk_in_all_dirs(
let mut potential = [None; 8];
let mut adjacents = [a; 8];
for i in 0..8 {
adjacents[i] += NEIGHBORS[i];
}
NEIGHBORS
.iter()
.zip(&mut adjacents)
.for_each(|(&dir, adj)| *adj += dir);
let Some(a_chunk) = sim.get(a) else { return potential };
let mut chunks = [None; 8];
@ -1418,6 +1393,7 @@ fn walk_in_all_dirs(
chunks[i] = sim.get(adjacents[i]);
}
}
for i in 0..8 {
let Some(b_chunk) = chunks[i] else { continue };
@ -1436,7 +1412,7 @@ fn walk_in_all_dirs(
// Look for potential bridge spots in the cardinal directions if
// `loc_suitable_for_wallking` was false for the adjacent chunk.
for i in 0..4 {
// These happen to be the dirs where: dir.x == 0 || dir.y == 0
// These happen to be indices for dirs where: dir.x == 0 || dir.y == 0
let i = i * 2;
if potential[i].is_none() {
let dir = NEIGHBORS[i];
@ -1463,50 +1439,9 @@ fn walk_in_all_dirs(
potential
}
/// 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?)
//
// Return tuple: (final location, cost)
fn walk_in_dir(
sim: &WorldSim,
// Is there a bridge at `a`?
bridge: Option<Vec2<i32>>,
a: Vec2<i32>,
dir: Vec2<i32>,
) -> Option<(Vec2<i32>, f32)> {
//CC.fetch_add(1, Ordering::Relaxed);
if let Some(p) = bridge.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)?;
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 {
// if we can skip over unsuitable area with a bridge
(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
}
}
/// 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() {
// 7181 ms -> 6868 ms (300 ms! almost 10% of pathfinding time)
!NEIGHBORS
.iter()
.map(|n| {

View File

@ -124,7 +124,6 @@ pub enum RiverKind {
impl RiverKind {
pub fn is_ocean(&self) -> bool { matches!(*self, RiverKind::Ocean) }
#[inline(always)] // saves ~100 ms on current `world_generate_time`
pub fn is_river(&self) -> bool { matches!(*self, RiverKind::River { .. }) }
pub fn is_lake(&self) -> bool { matches!(*self, RiverKind::Lake { .. }) }
@ -213,11 +212,7 @@ impl RiverData {
pub fn near_river(&self) -> bool { self.is_river() || !self.neighbor_rivers.is_empty() }
pub fn near_water(&self) -> bool {
// 7408 ms -> 7270 ms (only 50 ms difference now)
self.river_kind.is_some() || !self.neighbor_rivers.is_empty()
//self.near_river() || self.is_lake() || self.is_ocean()
}
pub fn near_water(&self) -> bool { self.near_river() || self.is_lake() || self.is_ocean() }
}
/// Draw rivers and assign them heights, widths, and velocities. Take some