1use std::sync::Arc;
4
5use rustc_hash::FxHashSet;
6use steel_utils::ChunkPos;
7
8use crate::chunk::{
9 chunk_holder::ChunkHolder,
10 chunk_map::ChunkMap,
11 chunk_scheduler::ChunkTicketReceipt,
12 chunk_ticket_manager::{ChunkTicketLevel, ticket_level_for_status},
13 status::ChunkStatus,
14};
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
21pub enum ChunkTicketKind {
22 Player,
24 PlayerSpawn,
26 SpawnSearch,
28 StructureLocate,
30 Pregen,
32 Command,
34 Portal,
36}
37
38pub struct ChunkRequest {
40 pub status: ChunkStatus,
42 pub positions: Vec<ChunkPos>,
44 pub ticket_kind: ChunkTicketKind,
46}
47
48#[derive(Debug, Clone, Copy, PartialEq, Eq)]
50pub enum ChunkRequestState {
51 Pending {
53 ready: usize,
55 total: usize,
57 },
58 Ready,
60 Cancelled,
62}
63
64pub struct ReadyChunks {
66 pub status: ChunkStatus,
68 pub holders: Vec<Arc<ChunkHolder>>,
70}
71
72pub(crate) struct ChunkRequestLease {
75 chunk_map: Arc<ChunkMap>,
76 positions: Box<[ChunkPos]>,
77 ticket_level: ChunkTicketLevel,
78 pub(crate) submission_receipt: Option<ChunkTicketReceipt>,
79}
80
81impl ChunkRequestLease {
82 pub(crate) fn new(
83 chunk_map: Arc<ChunkMap>,
84 positions: Box<[ChunkPos]>,
85 ticket_level: ChunkTicketLevel,
86 ) -> Self {
87 let submission_receipt = chunk_map.acquire_chunk_request_leases(&positions, ticket_level);
88 Self {
89 chunk_map,
90 positions,
91 ticket_level,
92 submission_receipt,
93 }
94 }
95}
96
97impl Drop for ChunkRequestLease {
98 fn drop(&mut self) {
99 let _ = self
100 .chunk_map
101 .release_chunk_request_leases(&self.positions, self.ticket_level);
102 }
103}
104
105struct ChunkRequestInner {
106 lease: ChunkRequestLease,
107 status: ChunkStatus,
108 ticket_kind: ChunkTicketKind,
109}
110
111pub struct ChunkRequestHandle {
117 inner: Option<ChunkRequestInner>,
118}
119
120impl ChunkRequestHandle {
121 pub(crate) fn new(chunk_map: Arc<ChunkMap>, request: ChunkRequest) -> Self {
122 let positions = dedupe_positions(request.positions);
123 let ticket_level = ticket_level_for_status(request.status);
124 let lease = ChunkRequestLease::new(chunk_map, positions, ticket_level);
125
126 Self {
127 inner: Some(ChunkRequestInner {
128 lease,
129 status: request.status,
130 ticket_kind: request.ticket_kind,
131 }),
132 }
133 }
134
135 #[must_use]
137 pub fn status(&self) -> Option<ChunkStatus> {
138 self.inner.as_ref().map(|inner| inner.status)
139 }
140
141 #[must_use]
143 pub fn ticket_kind(&self) -> Option<ChunkTicketKind> {
144 self.inner.as_ref().map(|inner| inner.ticket_kind)
145 }
146
147 #[must_use]
149 pub fn positions(&self) -> &[ChunkPos] {
150 self.inner
151 .as_ref()
152 .map_or(&[], |inner| inner.lease.positions.as_ref())
153 }
154
155 #[must_use]
158 pub fn poll(&self) -> ChunkRequestState {
159 let Some(inner) = &self.inner else {
160 return ChunkRequestState::Cancelled;
161 };
162 if inner.lease.positions.is_empty() {
163 return ChunkRequestState::Ready;
164 }
165 let ticket_receipt_committed = inner
166 .lease
167 .submission_receipt
168 .is_none_or(|receipt| inner.lease.chunk_map.is_ticket_receipt_committed(receipt));
169
170 let mut ready = 0;
171 for &pos in &inner.lease.positions {
172 let Some(holder) = inner
173 .lease
174 .chunk_map
175 .chunks
176 .read_sync(&pos, |_, holder| holder.clone())
177 else {
178 continue;
179 };
180
181 if holder.try_chunk(inner.status).is_some() {
182 ready += 1;
183 }
184 }
185
186 if ticket_receipt_committed && ready == inner.lease.positions.len() {
187 ChunkRequestState::Ready
188 } else {
189 ChunkRequestState::Pending {
190 ready,
191 total: inner.lease.positions.len(),
192 }
193 }
194 }
195
196 #[must_use]
198 pub fn ready_chunks(&self) -> Option<ReadyChunks> {
199 let inner = self.inner.as_ref()?;
200 if inner
201 .lease
202 .submission_receipt
203 .is_some_and(|receipt| !inner.lease.chunk_map.is_ticket_receipt_committed(receipt))
204 {
205 return None;
206 }
207 let mut holders = Vec::with_capacity(inner.lease.positions.len());
208
209 for &pos in &inner.lease.positions {
210 let holder = inner
211 .lease
212 .chunk_map
213 .chunks
214 .read_sync(&pos, |_, holder| holder.clone())?;
215 {
216 let _chunk = holder.try_chunk(inner.status)?;
217 }
218 holders.push(holder);
219 }
220
221 Some(ReadyChunks {
222 status: inner.status,
223 holders,
224 })
225 }
226
227 pub fn cancel(&mut self) {
229 drop(self.inner.take());
230 }
231}
232
233impl ChunkMap {
234 #[must_use]
239 pub fn request_chunks(self: &Arc<Self>, request: ChunkRequest) -> ChunkRequestHandle {
240 ChunkRequestHandle::new(self.clone(), request)
241 }
242
243 #[must_use]
245 pub fn request_chunk(
246 self: &Arc<Self>,
247 pos: ChunkPos,
248 status: ChunkStatus,
249 ticket_kind: ChunkTicketKind,
250 ) -> ChunkRequestHandle {
251 self.request_chunks(ChunkRequest {
252 status,
253 positions: vec![pos],
254 ticket_kind,
255 })
256 }
257
258 #[must_use]
260 pub fn request_square(
261 self: &Arc<Self>,
262 center: ChunkPos,
263 radius: u8,
264 status: ChunkStatus,
265 ticket_kind: ChunkTicketKind,
266 ) -> ChunkRequestHandle {
267 let radius = i32::from(radius);
268 let diameter = radius * 2 + 1;
269 let capacity = (diameter * diameter) as usize;
270 let mut positions = Vec::with_capacity(capacity);
271
272 for dz in -radius..=radius {
273 for dx in -radius..=radius {
274 positions.push(ChunkPos::new(center.0.x + dx, center.0.y + dz));
275 }
276 }
277
278 self.request_chunks(ChunkRequest {
279 status,
280 positions,
281 ticket_kind,
282 })
283 }
284}
285
286fn dedupe_positions(positions: Vec<ChunkPos>) -> Box<[ChunkPos]> {
287 let mut seen = FxHashSet::default();
288 let mut deduped = Vec::with_capacity(positions.len());
289 for pos in positions {
290 if seen.insert(pos) {
291 deduped.push(pos);
292 }
293 }
294 deduped.into_boxed_slice()
295}
296
297#[cfg(test)]
298mod tests {
299 use super::*;
300 use crate::test_support::fresh_test_world;
301 use std::{thread, time::Duration};
302
303 fn drive_request_until_ready(chunk_map: &Arc<ChunkMap>, request: &ChunkRequestHandle) {
304 for _ in 0..10_000 {
305 chunk_map.advance_scheduling();
306 if request.poll() == ChunkRequestState::Ready {
307 return;
308 }
309 thread::sleep(Duration::from_millis(1));
310 }
311 panic!("chunk request did not become ready");
312 }
313
314 #[test]
315 fn dedupe_positions_preserves_first_occurrence_order() {
316 let positions = dedupe_positions(vec![
317 ChunkPos::new(1, 2),
318 ChunkPos::new(3, 4),
319 ChunkPos::new(1, 2),
320 ]);
321 assert_eq!(&*positions, &[ChunkPos::new(1, 2), ChunkPos::new(3, 4)]);
322 }
323
324 #[test]
325 fn ready_chunk_still_waits_for_its_ticket_receipt_to_commit() {
326 let world = fresh_test_world("chunk_request_receipt");
327 let pos = ChunkPos::new(4, -7);
328 let first =
329 world
330 .chunk_map
331 .request_chunk(pos, ChunkStatus::Empty, ChunkTicketKind::Command);
332 drive_request_until_ready(&world.chunk_map, &first);
333
334 let second =
335 world
336 .chunk_map
337 .request_chunk(pos, ChunkStatus::Empty, ChunkTicketKind::Command);
338
339 assert_eq!(
340 second.poll(),
341 ChunkRequestState::Pending { ready: 1, total: 1 }
342 );
343 assert!(second.ready_chunks().is_none());
344
345 drive_request_until_ready(&world.chunk_map, &second);
346 assert!(second.ready_chunks().is_some());
347 }
348}