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
111impl ChunkRequestInner {
112 fn tickets_committed(&self) -> bool {
113 self.lease
114 .submission_receipt
115 .is_none_or(|receipt| self.lease.chunk_map.is_ticket_receipt_committed(receipt))
116 }
117}
118
119pub struct ChunkRequestHandle {
125 inner: Option<ChunkRequestInner>,
126}
127
128impl ChunkRequestHandle {
129 pub(crate) fn new(chunk_map: Arc<ChunkMap>, request: ChunkRequest) -> Self {
130 let positions = dedupe_positions(request.positions);
131 let ticket_level = ticket_level_for_status(request.status);
132 let lease = ChunkRequestLease::new(chunk_map, positions, ticket_level);
133
134 Self {
135 inner: Some(ChunkRequestInner {
136 lease,
137 status: request.status,
138 ticket_kind: request.ticket_kind,
139 }),
140 }
141 }
142
143 #[must_use]
145 pub fn status(&self) -> Option<ChunkStatus> {
146 self.inner.as_ref().map(|inner| inner.status)
147 }
148
149 #[must_use]
151 pub fn ticket_kind(&self) -> Option<ChunkTicketKind> {
152 self.inner.as_ref().map(|inner| inner.ticket_kind)
153 }
154
155 #[must_use]
157 pub fn positions(&self) -> &[ChunkPos] {
158 self.inner
159 .as_ref()
160 .map_or(&[], |inner| inner.lease.positions.as_ref())
161 }
162
163 #[must_use]
166 pub fn tickets_committed(&self) -> bool {
167 self.inner
168 .as_ref()
169 .is_some_and(ChunkRequestInner::tickets_committed)
170 }
171
172 #[must_use]
175 pub fn poll(&self) -> ChunkRequestState {
176 let Some(inner) = &self.inner else {
177 return ChunkRequestState::Cancelled;
178 };
179 if inner.lease.positions.is_empty() {
180 return ChunkRequestState::Ready;
181 }
182 let tickets_committed = inner.tickets_committed();
184 let mut ready = 0;
185 for &pos in &inner.lease.positions {
186 let Some(holder) = inner
187 .lease
188 .chunk_map
189 .chunks
190 .read_sync(&pos, |_, holder| Arc::clone(holder))
191 else {
192 continue;
193 };
194
195 if holder.try_chunk(inner.status).is_some() {
196 ready += 1;
197 }
198 }
199
200 if tickets_committed && ready == inner.lease.positions.len() {
201 ChunkRequestState::Ready
202 } else {
203 ChunkRequestState::Pending {
204 ready,
205 total: inner.lease.positions.len(),
206 }
207 }
208 }
209
210 #[must_use]
212 pub fn ready_chunks(&self) -> Option<ReadyChunks> {
213 let inner = self.inner.as_ref()?;
214 if !inner.tickets_committed() {
215 return None;
216 }
217 let mut holders = Vec::with_capacity(inner.lease.positions.len());
218
219 for &pos in &inner.lease.positions {
220 let holder = inner
221 .lease
222 .chunk_map
223 .chunks
224 .read_sync(&pos, |_, holder| Arc::clone(holder))?;
225 {
226 let _chunk = holder.try_chunk(inner.status)?;
227 }
228 holders.push(holder);
229 }
230
231 Some(ReadyChunks {
232 status: inner.status,
233 holders,
234 })
235 }
236
237 pub fn cancel(&mut self) {
239 drop(self.inner.take());
240 }
241}
242
243impl ChunkMap {
244 #[must_use]
249 pub fn request_chunks(self: &Arc<Self>, request: ChunkRequest) -> ChunkRequestHandle {
250 ChunkRequestHandle::new(Arc::clone(self), request)
251 }
252
253 #[must_use]
255 pub fn request_chunk(
256 self: &Arc<Self>,
257 pos: ChunkPos,
258 status: ChunkStatus,
259 ticket_kind: ChunkTicketKind,
260 ) -> ChunkRequestHandle {
261 self.request_chunks(ChunkRequest {
262 status,
263 positions: vec![pos],
264 ticket_kind,
265 })
266 }
267
268 #[must_use]
270 pub fn request_square(
271 self: &Arc<Self>,
272 center: ChunkPos,
273 radius: u8,
274 status: ChunkStatus,
275 ticket_kind: ChunkTicketKind,
276 ) -> ChunkRequestHandle {
277 let radius = i32::from(radius);
278 let diameter = radius * 2 + 1;
279 let capacity = (diameter * diameter) as usize;
280 let mut positions = Vec::with_capacity(capacity);
281
282 for dz in -radius..=radius {
283 for dx in -radius..=radius {
284 positions.push(ChunkPos::new(center.0.x + dx, center.0.y + dz));
285 }
286 }
287
288 self.request_chunks(ChunkRequest {
289 status,
290 positions,
291 ticket_kind,
292 })
293 }
294}
295
296fn dedupe_positions(positions: Vec<ChunkPos>) -> Box<[ChunkPos]> {
297 let mut seen = FxHashSet::default();
298 let mut deduped = Vec::with_capacity(positions.len());
299 for pos in positions {
300 if seen.insert(pos) {
301 deduped.push(pos);
302 }
303 }
304 deduped.into_boxed_slice()
305}
306
307#[cfg(test)]
308mod tests {
309 use super::*;
310 use crate::test_support::fresh_test_world;
311 use std::{thread, time::Duration};
312
313 fn drive_request_until_ready(chunk_map: &Arc<ChunkMap>, request: &ChunkRequestHandle) {
314 for _ in 0..10_000 {
315 chunk_map.advance_scheduling();
316 if request.poll() == ChunkRequestState::Ready {
317 return;
318 }
319 thread::sleep(Duration::from_millis(1));
320 }
321 panic!("chunk request did not become ready");
322 }
323
324 #[test]
325 fn dedupe_positions_preserves_first_occurrence_order() {
326 let positions = dedupe_positions(vec![
327 ChunkPos::new(1, 2),
328 ChunkPos::new(3, 4),
329 ChunkPos::new(1, 2),
330 ]);
331 assert_eq!(&*positions, &[ChunkPos::new(1, 2), ChunkPos::new(3, 4)]);
332 }
333
334 #[test]
335 fn ready_chunk_still_waits_for_its_ticket_receipt_to_commit() {
336 let world_fixture = fresh_test_world("chunk_request_receipt");
337 let world = &world_fixture.world;
338 let pos = ChunkPos::new(4, -7);
339 let first =
340 world
341 .chunk_map
342 .request_chunk(pos, ChunkStatus::Empty, ChunkTicketKind::Command);
343 drive_request_until_ready(&world.chunk_map, &first);
344
345 let second =
346 world
347 .chunk_map
348 .request_chunk(pos, ChunkStatus::Empty, ChunkTicketKind::Command);
349
350 assert_eq!(
351 second.poll(),
352 ChunkRequestState::Pending { ready: 1, total: 1 }
353 );
354 assert!(second.ready_chunks().is_none());
355 assert!(!second.tickets_committed());
356
357 drive_request_until_ready(&world.chunk_map, &second);
358 assert!(second.ready_chunks().is_some());
359 assert!(second.tickets_committed());
360 }
361
362 #[test]
363 fn a_cancelled_request_has_no_committed_tickets() {
364 let world_fixture = fresh_test_world("chunk_request_cancel");
365 let world = &world_fixture.world;
366 let mut request = world.chunk_map.request_chunk(
367 ChunkPos::new(1, 1),
368 ChunkStatus::Empty,
369 ChunkTicketKind::Command,
370 );
371 drive_request_until_ready(&world.chunk_map, &request);
372 assert!(request.tickets_committed());
373
374 request.cancel();
375 assert!(!request.tickets_committed());
376 }
377}