steel_core/world/player_spawn_finder/
chunk_load.rs1use std::sync::Arc;
2use std::time::{Duration, Instant};
3
4use steel_utils::ChunkPos;
5
6use crate::chunk::chunk_request::ChunkRequestState;
7use crate::world::World;
8
9use super::ChunkRequestHandle;
10use super::candidates::CandidateSequence;
11
12pub(super) struct PollBudget {
14 started: Instant,
15 limit: Duration,
16 evaluated: usize,
17}
18
19impl PollBudget {
20 pub(super) fn new(limit: Duration) -> Self {
21 Self {
22 started: Instant::now(),
23 limit,
24 evaluated: 0,
25 }
26 }
27
28 pub(super) fn exhausted(&self) -> bool {
30 self.evaluated > 0 && self.started.elapsed() >= self.limit
31 }
32
33 pub(super) const fn record_evaluation(&mut self) {
34 self.evaluated += 1;
35 }
36}
37
38pub(super) struct ChunkLoad {
40 pub(super) chunk: ChunkPos,
41 request: Option<ChunkRequestHandle>,
43 pub(super) next_scan: i32,
45}
46
47#[derive(Clone, Copy)]
48pub(super) enum ChunkLoadState {
49 Ready,
50 Waiting,
51 Cancelled,
52}
53
54pub(super) enum Visit<T> {
56 Skip,
58 Evaluated,
60 Stop(T),
62}
63
64pub(super) enum ScanEnd<T> {
66 Finished,
68 OutOfBudget,
70 Stopped(T),
72}
73
74impl ChunkLoad {
75 pub(super) fn open(world: &Arc<World>, chunk: ChunkPos, next_scan: i32, held: bool) -> Self {
76 Self {
77 chunk,
78 request: (!held).then(|| world.request_spawn_candidate_chunk(chunk)),
79 next_scan,
80 }
81 }
82
83 pub(super) fn poll(&self, world: &Arc<World>) -> ChunkLoadState {
84 let Some(request) = &self.request else {
85 return if world.has_full_chunk(self.chunk) {
86 ChunkLoadState::Ready
87 } else {
88 ChunkLoadState::Waiting
89 };
90 };
91 match request.poll() {
92 ChunkRequestState::Cancelled => ChunkLoadState::Cancelled,
93 ChunkRequestState::Ready => ChunkLoadState::Ready,
94 ChunkRequestState::Pending { .. } => ChunkLoadState::Waiting,
95 }
96 }
97
98 pub(super) fn scan<T>(
100 &mut self,
101 sequence: &CandidateSequence,
102 budget: &mut PollBudget,
103 mut visit: impl FnMut(i32, i32, i32) -> Visit<T>,
104 ) -> ScanEnd<T> {
105 while self.next_scan < sequence.count() {
106 if budget.exhausted() {
107 return ScanEnd::OutOfBudget;
108 }
109 let index = self.next_scan;
110 self.next_scan += 1;
111 if sequence.chunk(index) != self.chunk {
112 continue;
113 }
114 let (x, z) = sequence.column(index);
115 match visit(index, x, z) {
116 Visit::Skip => {}
117 Visit::Evaluated => budget.record_evaluation(),
118 Visit::Stop(value) => {
119 budget.record_evaluation();
120 return ScanEnd::Stopped(value);
121 }
122 }
123 }
124 ScanEnd::Finished
125 }
126}
127
128#[cfg(test)]
129mod tests {
130 use steel_utils::BlockPos;
131
132 use super::*;
133
134 #[test]
135 fn scan_resumes_without_skipping_or_repeating_candidates() {
136 for (suggestion, radius) in [
137 (BlockPos::new(0, 64, 0), 10),
138 (BlockPos::new(-1, 64, -1), 40),
139 (BlockPos::new(15, 64, 15), 1),
140 (BlockPos::new(0, 64, 0), 0),
141 ] {
142 let Ok(mut sequence) = CandidateSequence::new(suggestion, radius) else {
143 panic!("sequence should build");
144 };
145 for offset in [0, 1, sequence.count() - 1] {
146 sequence.offset = offset;
147 let expected: Vec<i32> = (0..sequence.count())
148 .filter(|&index| sequence.chunk(index) == sequence.chunk(0))
149 .collect();
150
151 let mut load = ChunkLoad {
153 chunk: sequence.chunk(0),
154 request: None,
155 next_scan: 0,
156 };
157 let mut visited = Vec::new();
158 loop {
159 let mut budget = PollBudget::new(Duration::ZERO);
160 let end = load.scan(&sequence, &mut budget, |index, _, _| {
161 visited.push(index);
162 Visit::<()>::Evaluated
163 });
164 if matches!(end, ScanEnd::Finished) {
165 break;
166 }
167 }
168 assert_eq!(visited, expected, "radius {radius}, offset {offset}");
169 }
170 }
171 }
172}