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::ChunkTicketRevision,
12 chunk_ticket_manager::{ChunkTicket, ticket_level_for_status},
13 status::ChunkStatus,
14};
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18pub enum ChunkTicketKind {
19 Player,
21 PlayerSpawn,
23 SpawnSearch,
25 StructureLocate,
27 Pregen,
29 Command,
31 Portal,
33}
34
35pub struct ChunkRequest {
37 pub status: ChunkStatus,
39 pub positions: Vec<ChunkPos>,
41 pub ticket_kind: ChunkTicketKind,
43}
44
45#[derive(Debug, Clone, Copy, PartialEq, Eq)]
47pub enum ChunkRequestState {
48 Pending {
50 ready: usize,
52 total: usize,
54 },
55 Ready,
57 Cancelled,
59}
60
61pub struct ReadyChunks {
63 pub status: ChunkStatus,
65 pub holders: Vec<Arc<ChunkHolder>>,
67}
68
69struct ChunkRequestInner {
70 chunk_map: Arc<ChunkMap>,
71 positions: Box<[ChunkPos]>,
72 status: ChunkStatus,
73 ticket_kind: ChunkTicketKind,
74 ticket: ChunkTicket,
75 submission_revision: Option<ChunkTicketRevision>,
76}
77
78pub struct ChunkRequestHandle {
84 inner: Option<ChunkRequestInner>,
85}
86
87impl ChunkRequestHandle {
88 pub(crate) fn new(chunk_map: Arc<ChunkMap>, request: ChunkRequest) -> Self {
89 let ticket = ChunkTicket::loading(ticket_level_for_status(request.status));
90 Self::new_with_ticket(chunk_map, request, ticket)
91 }
92
93 fn new_with_ticket(
94 chunk_map: Arc<ChunkMap>,
95 request: ChunkRequest,
96 ticket: ChunkTicket,
97 ) -> Self {
98 let positions = dedupe_positions(request.positions);
99 let submission_revision = chunk_map.add_chunk_tickets(&positions, ticket);
100
101 Self {
102 inner: Some(ChunkRequestInner {
103 chunk_map,
104 positions,
105 status: request.status,
106 ticket_kind: request.ticket_kind,
107 ticket,
108 submission_revision,
109 }),
110 }
111 }
112
113 #[must_use]
115 pub fn status(&self) -> Option<ChunkStatus> {
116 self.inner.as_ref().map(|inner| inner.status)
117 }
118
119 #[must_use]
121 pub fn ticket_kind(&self) -> Option<ChunkTicketKind> {
122 self.inner.as_ref().map(|inner| inner.ticket_kind)
123 }
124
125 #[must_use]
127 pub fn positions(&self) -> &[ChunkPos] {
128 self.inner
129 .as_ref()
130 .map_or(&[], |inner| inner.positions.as_ref())
131 }
132
133 #[must_use]
136 pub fn poll(&self) -> ChunkRequestState {
137 let Some(inner) = &self.inner else {
138 return ChunkRequestState::Cancelled;
139 };
140 if inner.positions.is_empty() {
141 return ChunkRequestState::Ready;
142 }
143 let ticket_revision_committed = inner
144 .submission_revision
145 .is_none_or(|revision| inner.chunk_map.is_ticket_revision_committed(revision));
146
147 let mut ready = 0;
148 for &pos in &inner.positions {
149 let Some(holder) = inner
150 .chunk_map
151 .chunks
152 .read_sync(&pos, |_, holder| holder.clone())
153 else {
154 continue;
155 };
156
157 if holder.try_chunk(inner.status).is_some() {
158 ready += 1;
159 }
160 }
161
162 if ticket_revision_committed && ready == inner.positions.len() {
163 ChunkRequestState::Ready
164 } else {
165 ChunkRequestState::Pending {
166 ready,
167 total: inner.positions.len(),
168 }
169 }
170 }
171
172 #[must_use]
174 pub fn ready_chunks(&self) -> Option<ReadyChunks> {
175 let inner = self.inner.as_ref()?;
176 if inner
177 .submission_revision
178 .is_some_and(|revision| !inner.chunk_map.is_ticket_revision_committed(revision))
179 {
180 return None;
181 }
182 let mut holders = Vec::with_capacity(inner.positions.len());
183
184 for &pos in &inner.positions {
185 let holder = inner
186 .chunk_map
187 .chunks
188 .read_sync(&pos, |_, holder| holder.clone())?;
189 {
190 let _chunk = holder.try_chunk(inner.status)?;
191 }
192 holders.push(holder);
193 }
194
195 Some(ReadyChunks {
196 status: inner.status,
197 holders,
198 })
199 }
200
201 pub fn cancel(&mut self) {
203 self.release_tickets();
204 }
205
206 fn release_tickets(&mut self) {
207 let Some(inner) = self.inner.take() else {
208 return;
209 };
210
211 let _ = inner
212 .chunk_map
213 .remove_chunk_tickets(&inner.positions, inner.ticket);
214 }
215}
216
217impl Drop for ChunkRequestHandle {
218 fn drop(&mut self) {
219 self.release_tickets();
220 }
221}
222
223impl ChunkMap {
224 #[must_use]
229 pub fn request_chunks(self: &Arc<Self>, request: ChunkRequest) -> ChunkRequestHandle {
230 ChunkRequestHandle::new(self.clone(), request)
231 }
232
233 #[must_use]
235 pub fn request_chunk(
236 self: &Arc<Self>,
237 pos: ChunkPos,
238 status: ChunkStatus,
239 ticket_kind: ChunkTicketKind,
240 ) -> ChunkRequestHandle {
241 self.request_chunks(ChunkRequest {
242 status,
243 positions: vec![pos],
244 ticket_kind,
245 })
246 }
247
248 #[must_use]
250 pub fn request_square(
251 self: &Arc<Self>,
252 center: ChunkPos,
253 radius: u8,
254 status: ChunkStatus,
255 ticket_kind: ChunkTicketKind,
256 ) -> ChunkRequestHandle {
257 let radius = i32::from(radius);
258 let diameter = radius * 2 + 1;
259 let capacity = (diameter * diameter) as usize;
260 let mut positions = Vec::with_capacity(capacity);
261
262 for dz in -radius..=radius {
263 for dx in -radius..=radius {
264 positions.push(ChunkPos::new(center.0.x + dx, center.0.y + dz));
265 }
266 }
267
268 self.request_chunks(ChunkRequest {
269 status,
270 positions,
271 ticket_kind,
272 })
273 }
274}
275
276fn dedupe_positions(positions: Vec<ChunkPos>) -> Box<[ChunkPos]> {
277 let mut seen = FxHashSet::default();
278 let mut deduped = Vec::with_capacity(positions.len());
279 for pos in positions {
280 if seen.insert(pos) {
281 deduped.push(pos);
282 }
283 }
284 deduped.into_boxed_slice()
285}
286
287#[cfg(test)]
288mod tests {
289 use super::*;
290 use crate::test_support::fresh_test_world;
291 use std::{thread, time::Duration};
292
293 fn drive_request_until_ready(chunk_map: &Arc<ChunkMap>, request: &ChunkRequestHandle) {
294 for _ in 0..10_000 {
295 chunk_map.advance_scheduling();
296 if request.poll() == ChunkRequestState::Ready {
297 return;
298 }
299 thread::sleep(Duration::from_millis(1));
300 }
301 panic!("chunk request did not become ready");
302 }
303
304 #[test]
305 fn dedupe_positions_preserves_first_occurrence_order() {
306 let positions = dedupe_positions(vec![
307 ChunkPos::new(1, 2),
308 ChunkPos::new(3, 4),
309 ChunkPos::new(1, 2),
310 ]);
311 assert_eq!(&*positions, &[ChunkPos::new(1, 2), ChunkPos::new(3, 4)]);
312 }
313
314 #[test]
315 fn ready_chunk_still_waits_for_its_ticket_revision_to_commit() {
316 let world = fresh_test_world("chunk_request_revision");
317 let pos = ChunkPos::new(4, -7);
318 let first =
319 world
320 .chunk_map
321 .request_chunk(pos, ChunkStatus::Empty, ChunkTicketKind::Command);
322 drive_request_until_ready(&world.chunk_map, &first);
323
324 let second =
325 world
326 .chunk_map
327 .request_chunk(pos, ChunkStatus::Empty, ChunkTicketKind::Command);
328
329 assert_eq!(
330 second.poll(),
331 ChunkRequestState::Pending { ready: 1, total: 1 }
332 );
333 assert!(second.ready_chunks().is_none());
334
335 drive_request_until_ready(&world.chunk_map, &second);
336 assert!(second.ready_chunks().is_some());
337 }
338}