Skip food searches for stacks out of reach

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

author
Marijn Besseling <njirambem@gmail.com> · 2026-10-24 21:23 UTC
commit
f85e87cb455e4b09fe81f6f99a7145f4440a4d81
parent
fc89a04078
tree
browse at this commit

3 files changed +52 -14

src/food.rs +2 -6

@@ -7,7 +7,7 @@ use crate::{
7 7 items::{Carrying, Item, ItemKind},
8 8 jobs::{CurrentJob, Reservations, Target},
9 9 needs::is_sleeping,
10 - pathfinding::nearest_tile,
10 + pathfinding::nearest_of,
11 11 sim::{SimSystems, SimTick, TICKS_PER_HOUR},
12 12 world::{TilePos, WorldMap},
13 13 };
@@ -84,11 +84,7 @@ pub fn find_food(
84 84 .map(|(_, pos)| pos.0)
85 85 .collect()
86 86 };
87 - let search = |tiles: HashSet<UVec2>| {
88 - (!tiles.is_empty())
89 - .then(|| nearest_tile(map, from, FOOD_SEARCH_TILES, |p| tiles.contains(&p)))
90 - .flatten()
91 - };
87 + let search = |tiles: HashSet<UVec2>| nearest_of(map, from, FOOD_SEARCH_TILES, &tiles);
92 88 search(tiles(|k| k == ItemKind::Meal)).or_else(|| search(tiles(ItemKind::is_raw_food)))
93 89 }
94 90

src/jobs/mod.rs +2 -5

@@ -18,7 +18,7 @@ use crate::{
18 18 },
19 19 light::{LIT_BRIGHTNESS, LightMap, brightness, dark_soon, light_from},
20 20 needs::{EXHAUSTED_REST, Rest, SleepContext, done_sleeping, wants_sleep},
21 - pathfinding::{find_path, find_path_adjacent, is_adjacent, nearest_tile},
21 + pathfinding::{find_path, find_path_adjacent, is_adjacent, nearest_of, nearest_tile},
22 22 rng::SimRng,
23 23 sim::{SimSystems, SimTick, hour_of_day},
24 24 structures::{Fuel, Structure, StructureAssets, StructureKind, spawn_structure},
@@ -682,10 +682,7 @@ pub fn raw_food_source(ctx: &BuildContext, from: UVec2) -> Option<UVec2> {
682 682 })
683 683 .map(|(_, pos)| pos.0)
684 684 .collect();
685 - if sources.is_empty() {
686 - return None;
687 - }
688 - nearest_tile(ctx.map, from, SEARCH_TILES, |p| sources.contains(&p))
685 + nearest_of(ctx.map, from, SEARCH_TILES, &sources)
689 686 }
690 687
691 688 pub fn find_cooking(ctx: &BuildContext, from: UVec2) -> Option<Task> {

src/pathfinding.rs +48 -3

@@ -48,18 +48,25 @@ pub fn nearest_tile(
48 48 max_tiles: usize,
49 49 mut is_target: impl FnMut(UVec2) -> bool,
50 50 ) -> Option<UVec2> {
51 - let mut seen = HashSet::from([start]);
51 + let index = |p: UVec2| (p.y * map.size.x + p.x) as usize;
52 + let mut seen = vec![false; (map.size.x * map.size.y) as usize];
53 + let mut seen_count = 1;
54 + if let Some(cell) = seen.get_mut(index(start)) {
55 + *cell = true;
56 + }
52 57 let mut queue = VecDeque::from([start]);
53 58 while let Some(pos) = queue.pop_front() {
54 59 if is_target(pos) {
55 60 return Some(pos);
56 61 }
57 - if seen.len() >= max_tiles {
62 + if seen_count >= max_tiles {
58 63 continue;
59 64 }
60 65 for step in NEIGHBORS {
61 66 let next = pos.as_ivec2() + step;
62 - if map.move_cost(next).is_some() && seen.insert(next.as_uvec2()) {
67 + if map.move_cost(next).is_some() && !seen[index(next.as_uvec2())] {
68 + seen[index(next.as_uvec2())] = true;
69 + seen_count += 1;
63 70 queue.push_back(next.as_uvec2());
64 71 }
65 72 }
@@ -67,6 +74,24 @@ pub fn nearest_tile(
67 74 None
68 75 }
69 76
77 +pub fn search_reach(max_tiles: usize) -> u32 {
78 + (max_tiles as f32).sqrt().ceil() as u32
79 +}
80 +
81 +pub fn nearest_of(
82 + map: &WorldMap,
83 + start: UVec2,
84 + max_tiles: usize,
85 + targets: &HashSet<UVec2>,
86 +) -> Option<UVec2> {
87 + let reach = search_reach(max_tiles) as i32;
88 + let in_reach = |p: &&UVec2| (p.as_ivec2() - start.as_ivec2()).abs().max_element() <= reach;
89 + if !targets.iter().any(|p| in_reach(&p)) {
90 + return None;
91 + }
92 + nearest_tile(map, start, max_tiles, |p| targets.contains(&p))
93 +}
94 +
70 95 fn astar(
71 96 map: &WorldMap,
72 97 start: UVec2,
@@ -151,6 +176,26 @@ mod tests {
151 176 use super::*;
152 177 use crate::world::Terrain;
153 178
179 + // Idle colonists look for food and cooking input several times per game minute. A stack far
180 + // beyond what the flood fill can cover made every one of those searches visit the full tile
181 + // budget and fail, which dominated frame time at high speeds.
182 + #[test]
183 + fn target_searches_skip_targets_beyond_their_reach() {
184 + let map = WorldMap::filled(UVec2::splat(300), Terrain::Grass);
185 + let start = UVec2::splat(10);
186 + let max_tiles = 400;
187 + let near = start + UVec2::new(search_reach(max_tiles) / 4, 0);
188 + let far = start + UVec2::new(search_reach(max_tiles) + 1, 0);
189 + assert_eq!(
190 + nearest_of(&map, start, max_tiles, &HashSet::from([far])),
191 + None
192 + );
193 + assert_eq!(
194 + nearest_of(&map, start, max_tiles, &HashSet::from([far, near])),
195 + Some(near)
196 + );
197 + }
198 +
154 199 fn map_from(rows: &[&str]) -> WorldMap {
155 200 let size = UVec2::new(rows[0].len() as u32, rows.len() as u32);
156 201 let mut map = WorldMap::filled(size, Terrain::Grass);