1#![expect(missing_docs, reason = "internal module; items are self-explanatory")]
3
4use std::mem;
5
6use rustc_hash::{FxBuildHasher, FxHashMap};
7use serde::{Deserialize, Serialize};
8use smallvec::SmallVec;
9use steel_utils::ChunkPos;
10
11use crate::chunk::{chunk_pyramid::GENERATION_PYRAMID, status::ChunkStatus};
12
13pub const MAX_SUPPORTED_VIEW_DISTANCE: u8 = 128;
15const FULL_CHUNK_LEVEL_RAW: u8 = MAX_SUPPORTED_VIEW_DISTANCE + 2;
16const RADIUS_AROUND_FULL_CHUNK: u8 = GENERATION_PYRAMID
17 .get_step_to(ChunkStatus::Full)
18 .accumulated_dependencies
19 .get_radius_of(ChunkStatus::Empty) as u8;
20const MAX_LEVEL_RAW: u8 = FULL_CHUNK_LEVEL_RAW + RADIUS_AROUND_FULL_CHUNK;
21pub(crate) const PORTAL_TICKET_RADIUS: u8 = 3;
22const PORTAL_TICKET_TIMEOUT_TICKS: i64 = 300;
23pub(crate) const ENDER_PEARL_TICKET_TIMEOUT_TICKS: u32 = 40;
24const ENDER_PEARL_TICKET_RADIUS: u8 = 2;
25
26#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
32pub struct ChunkTicketLevel(u8);
33
34impl ChunkTicketLevel {
35 pub const STRONGEST: Self = Self(0);
37 pub const ENTITY_TICKING_CHUNK: Self = Self(MAX_SUPPORTED_VIEW_DISTANCE);
39 pub const BLOCK_TICKING_CHUNK: Self = Self(MAX_SUPPORTED_VIEW_DISTANCE + 1);
41 pub const FULL_CHUNK: Self = Self(FULL_CHUNK_LEVEL_RAW);
43 pub const MAX: Self = Self(MAX_LEVEL_RAW);
45
46 #[must_use]
48 pub const fn new(raw: u8) -> Option<Self> {
49 if raw <= MAX_LEVEL_RAW {
50 Some(Self(raw))
51 } else {
52 None
53 }
54 }
55
56 #[must_use]
58 pub const fn for_full_chunk_radius(radius: u8) -> Self {
59 Self(FULL_CHUNK_LEVEL_RAW.saturating_sub(radius))
60 }
61
62 #[must_use]
64 pub const fn for_entity_ticking_radius(radius: u8) -> Self {
65 Self(Self::ENTITY_TICKING_CHUNK.0.saturating_sub(radius))
66 }
67
68 #[must_use]
70 pub const fn raw(self) -> u8 {
71 self.0
72 }
73
74 #[must_use]
76 pub const fn full_status(self) -> FullChunkStatus {
77 if self.0 <= Self::ENTITY_TICKING_CHUNK.0 {
78 FullChunkStatus::EntityTicking
79 } else if self.0 <= Self::BLOCK_TICKING_CHUNK.0 {
80 FullChunkStatus::BlockTicking
81 } else if self.0 <= Self::FULL_CHUNK.0 {
82 FullChunkStatus::Full
83 } else {
84 FullChunkStatus::Inaccessible
85 }
86 }
87
88 #[must_use]
89 pub const fn is_full(self) -> bool {
90 self.0 <= Self::FULL_CHUNK.0
91 }
92
93 #[must_use]
94 pub const fn is_block_ticking(self) -> bool {
95 self.0 <= Self::BLOCK_TICKING_CHUNK.0
96 }
97
98 #[must_use]
99 pub const fn is_entity_ticking(self) -> bool {
100 self.0 <= Self::ENTITY_TICKING_CHUNK.0
101 }
102
103 #[must_use]
104 const fn with_distance(self, distance: u8) -> Option<Self> {
105 let level = self.0.saturating_add(distance);
106 Self::new(level)
107 }
108
109 #[must_use]
110 const fn distance_to_max(self) -> u8 {
111 MAX_LEVEL_RAW - self.0
112 }
113
114 #[must_use]
115 const fn distance_to_block_ticking(self) -> u8 {
116 ChunkTicketLevel::BLOCK_TICKING_CHUNK
117 .0
118 .saturating_sub(self.0)
119 }
120}
121
122#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
124pub enum FullChunkStatus {
125 Inaccessible,
126 Full,
127 BlockTicking,
128 EntityTicking,
129}
130
131impl FullChunkStatus {
132 #[must_use]
133 pub const fn is_or_after(self, status: Self) -> bool {
134 self as u8 >= status as u8
135 }
136}
137
138#[derive(Debug, Clone, Copy, PartialEq, Eq)]
140pub struct ChunkTicket {
141 load_level: ChunkTicketLevel,
142 simulation_level: Option<ChunkTicketLevel>,
143}
144
145impl ChunkTicket {
146 #[must_use]
148 pub const fn loading(load_level: ChunkTicketLevel) -> Self {
149 Self {
150 load_level,
151 simulation_level: None,
152 }
153 }
154
155 #[must_use]
157 pub const fn full_chunks(radius: u8) -> Self {
158 Self::loading(ChunkTicketLevel::for_full_chunk_radius(radius))
159 }
160
161 #[must_use]
163 pub const fn simulated_full_chunks(radius: u8) -> Self {
164 let level = ChunkTicketLevel::for_full_chunk_radius(radius);
165 Self {
166 load_level: level,
167 simulation_level: Some(level),
168 }
169 }
170
171 #[must_use]
173 pub const fn full_chunks_with_entity_ticking(
174 load_radius: u8,
175 entity_ticking_radius: u8,
176 ) -> Self {
177 let entity_ticking_radius = if entity_ticking_radius > load_radius {
178 load_radius
179 } else {
180 entity_ticking_radius
181 };
182
183 Self {
184 load_level: ChunkTicketLevel::for_full_chunk_radius(load_radius),
185 simulation_level: Some(ChunkTicketLevel::for_entity_ticking_radius(
186 entity_ticking_radius,
187 )),
188 }
189 }
190
191 #[must_use]
197 pub const fn player(view_distance: u8, simulation_radius: u8) -> Self {
198 let simulation_radius = if simulation_radius > view_distance {
199 view_distance
200 } else {
201 simulation_radius
202 };
203
204 Self {
205 load_level: ChunkTicketLevel::for_entity_ticking_radius(view_distance),
206 simulation_level: Some(ChunkTicketLevel::for_entity_ticking_radius(
207 simulation_radius,
208 )),
209 }
210 }
211
212 #[must_use]
213 pub const fn load_level(self) -> ChunkTicketLevel {
214 self.load_level
215 }
216
217 #[must_use]
218 pub const fn simulation_level(self) -> Option<ChunkTicketLevel> {
219 self.simulation_level
220 }
221}
222
223#[must_use]
224pub const fn is_full(level: ChunkTicketLevel) -> bool {
225 level.is_full()
226}
227
228#[must_use]
229pub const fn full_status(level: Option<ChunkTicketLevel>) -> FullChunkStatus {
230 match level {
231 Some(level) => level.full_status(),
232 None => FullChunkStatus::Inaccessible,
233 }
234}
235
236#[must_use]
237pub const fn is_block_ticking(level: Option<ChunkTicketLevel>) -> bool {
238 match level {
239 Some(level) => level.is_block_ticking(),
240 None => false,
241 }
242}
243
244#[must_use]
245pub const fn is_entity_ticking(level: Option<ChunkTicketLevel>) -> bool {
246 match level {
247 Some(level) => level.is_entity_ticking(),
248 None => false,
249 }
250}
251
252#[must_use]
253pub const fn generation_status(level: Option<ChunkTicketLevel>) -> Option<ChunkStatus> {
254 match level {
255 None => None,
256 Some(level) => {
257 if is_full(level) {
258 Some(ChunkStatus::Full)
259 } else {
260 let distance = (level.raw() - FULL_CHUNK_LEVEL_RAW) as usize;
261 GENERATION_PYRAMID
263 .get_step_to(ChunkStatus::Full)
264 .accumulated_dependencies
265 .get(distance)
266 }
267 }
268 }
269}
270
271#[must_use]
276pub const fn ticket_level_for_status(status: ChunkStatus) -> ChunkTicketLevel {
277 if matches!(status, ChunkStatus::Full) {
278 ChunkTicketLevel::FULL_CHUNK
279 } else {
280 ChunkTicketLevel(
281 FULL_CHUNK_LEVEL_RAW
282 + GENERATION_PYRAMID
283 .get_step_to(ChunkStatus::Full)
284 .accumulated_dependencies
285 .get_radius_of(status) as u8,
286 )
287 }
288}
289
290type TicketLevels = SmallVec<[ChunkTicket; 4]>;
292
293#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)]
295pub(crate) struct PersistentChunkTickets {
296 #[serde(default)]
297 tickets: Vec<PersistentChunkTicket>,
298}
299
300#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
301struct PersistentChunkTicket {
302 #[serde(rename = "type")]
303 kind: PersistentChunkTicketKind,
304 chunk_x: i32,
305 chunk_z: i32,
306 ticks_left: i64,
307}
308
309#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
310#[serde(rename_all = "snake_case")]
311enum PersistentChunkTicketKind {
312 Portal,
313}
314
315#[derive(Debug, Default)]
317pub(crate) struct TimedChunkTickets {
318 tickets: Vec<TimedChunkTicket>,
319}
320
321impl TimedChunkTickets {
322 pub(crate) fn from_persistent(persistent: PersistentChunkTickets) -> Self {
324 let mut timed_tickets = Self::default();
325 for ticket in persistent.tickets {
326 timed_tickets.add_loaded_persistent_ticket(ticket);
327 }
328 timed_tickets
329 }
330
331 pub(crate) fn to_persistent(&self) -> PersistentChunkTickets {
333 PersistentChunkTickets {
334 tickets: self
335 .tickets
336 .iter()
337 .copied()
338 .filter_map(TimedChunkTicket::to_persistent)
339 .collect(),
340 }
341 }
342
343 pub(crate) fn activate_all(&self, ticket_manager: &mut ChunkTicketManager) {
345 for ticket in &self.tickets {
346 ticket_manager.add_ticket(ticket.pos, ticket.ticket);
347 }
348 }
349
350 pub(crate) fn add_portal_ticket(&mut self, pos: ChunkPos) -> Option<ChunkTicket> {
352 self.add_or_reset(
353 TimedChunkTicketKind::Portal,
354 pos,
355 portal_ticket(),
356 PORTAL_TICKET_TIMEOUT_TICKS,
357 )
358 }
359
360 pub(crate) fn add_ender_pearl_ticket(&mut self, pos: ChunkPos) -> Option<ChunkTicket> {
362 self.add_or_reset(
363 TimedChunkTicketKind::EnderPearl,
364 pos,
365 ender_pearl_ticket(),
366 i64::from(ENDER_PEARL_TICKET_TIMEOUT_TICKS),
367 )
368 }
369
370 pub(crate) fn tick(
372 &mut self,
373 mut can_expire: impl FnMut(ChunkPos) -> bool,
374 ) -> Vec<(ChunkPos, ChunkTicket)> {
375 let mut expired_tickets = Vec::new();
376 let mut index = 0;
377 while index < self.tickets.len() {
378 let ticket = &mut self.tickets[index];
379 if !can_expire(ticket.pos) {
380 index += 1;
381 continue;
382 }
383
384 ticket.ticks_left -= 1;
385 if ticket.ticks_left >= 0 {
386 index += 1;
387 continue;
388 }
389
390 let expired = self.tickets.swap_remove(index);
391 expired_tickets.push((expired.pos, expired.ticket));
392 }
393 expired_tickets
394 }
395
396 fn add_or_reset(
397 &mut self,
398 kind: TimedChunkTicketKind,
399 pos: ChunkPos,
400 ticket: ChunkTicket,
401 ticks_left: i64,
402 ) -> Option<ChunkTicket> {
403 if let Some(existing) = self
404 .tickets
405 .iter_mut()
406 .find(|entry| entry.kind == kind && entry.pos == pos && entry.ticket == ticket)
407 {
408 existing.ticks_left = ticks_left;
409 return None;
410 }
411
412 self.tickets.push(TimedChunkTicket {
413 kind,
414 pos,
415 ticket,
416 ticks_left,
417 });
418 Some(ticket)
419 }
420
421 fn add_loaded_persistent_ticket(&mut self, persistent: PersistentChunkTicket) {
422 match persistent.kind {
423 PersistentChunkTicketKind::Portal => {
424 self.add_loaded_portal_ticket(
425 ChunkPos::new(persistent.chunk_x, persistent.chunk_z),
426 persistent.ticks_left,
427 );
428 }
429 }
430 }
431
432 fn add_loaded_portal_ticket(&mut self, pos: ChunkPos, ticks_left: i64) {
433 if let Some(existing) = self.tickets.iter_mut().find(|entry| {
434 entry.kind == TimedChunkTicketKind::Portal
435 && entry.pos == pos
436 && entry.ticket == portal_ticket()
437 }) {
438 existing.ticks_left = PORTAL_TICKET_TIMEOUT_TICKS;
439 return;
440 }
441
442 self.tickets.push(TimedChunkTicket::portal(pos, ticks_left));
443 }
444
445 #[cfg(test)]
446 #[must_use]
447 const fn len(&self) -> usize {
448 self.tickets.len()
449 }
450}
451
452#[derive(Debug, Clone, Copy, PartialEq, Eq)]
453struct TimedChunkTicket {
454 kind: TimedChunkTicketKind,
455 pos: ChunkPos,
456 ticket: ChunkTicket,
457 ticks_left: i64,
458}
459
460impl TimedChunkTicket {
461 const fn portal(pos: ChunkPos, ticks_left: i64) -> Self {
462 Self {
463 kind: TimedChunkTicketKind::Portal,
464 pos,
465 ticket: portal_ticket(),
466 ticks_left,
467 }
468 }
469
470 const fn to_persistent(self) -> Option<PersistentChunkTicket> {
471 match self.kind {
472 TimedChunkTicketKind::Portal => Some(PersistentChunkTicket {
473 kind: PersistentChunkTicketKind::Portal,
474 chunk_x: self.pos.0.x,
475 chunk_z: self.pos.0.y,
476 ticks_left: self.ticks_left,
477 }),
478 TimedChunkTicketKind::EnderPearl => None,
479 }
480 }
481}
482
483#[derive(Debug, Clone, Copy, PartialEq, Eq)]
484enum TimedChunkTicketKind {
485 Portal,
486 EnderPearl,
487}
488
489#[must_use]
490const fn portal_ticket() -> ChunkTicket {
491 ChunkTicket::simulated_full_chunks(PORTAL_TICKET_RADIUS)
492}
493
494#[must_use]
495const fn ender_pearl_ticket() -> ChunkTicket {
496 ChunkTicket::simulated_full_chunks(ENDER_PEARL_TICKET_RADIUS)
497}
498
499#[derive(Debug, Clone, Copy, PartialEq, Eq)]
501pub struct LevelChange {
502 pub pos: ChunkPos,
503 pub new_level: Option<ChunkTicketLevel>,
505 pub new_simulation_level: Option<ChunkTicketLevel>,
507}
508
509#[derive(Debug)]
512pub struct ChunkTicketManager {
513 tickets: FxHashMap<ChunkPos, TicketLevels>,
514 levels: FxHashMap<ChunkPos, ChunkTicketLevel>,
515 simulation_levels: FxHashMap<ChunkPos, ChunkTicketLevel>,
516 dirty: bool,
517 changes: Vec<LevelChange>,
519}
520
521impl Default for ChunkTicketManager {
522 fn default() -> Self {
523 Self::new()
524 }
525}
526
527impl ChunkTicketManager {
528 #[must_use]
529 pub fn new() -> Self {
530 Self {
531 tickets: FxHashMap::default(),
532 levels: FxHashMap::default(),
533 simulation_levels: FxHashMap::default(),
534 dirty: false,
535 changes: Vec::new(),
536 }
537 }
538
539 pub fn add_ticket(&mut self, pos: ChunkPos, ticket: ChunkTicket) {
541 self.tickets.entry(pos).or_default().push(ticket);
542 self.dirty = true;
543 }
544
545 pub fn remove_ticket(&mut self, pos: ChunkPos, ticket: ChunkTicket) -> bool {
547 if let Some(tickets) = self.tickets.get_mut(&pos)
548 && let Some(idx) = tickets.iter().position(|stored| *stored == ticket)
549 {
550 tickets.swap_remove(idx);
551 self.dirty = true;
552 if tickets.is_empty() {
553 self.tickets.remove(&pos);
554 }
555 return true;
556 }
557 false
558 }
559
560 pub fn remove_all_tickets_at(&mut self, pos: ChunkPos) -> bool {
562 let removed = self.tickets.remove(&pos).is_some();
563 if removed {
564 self.dirty = true;
565 }
566 removed
567 }
568
569 #[must_use]
571 pub fn get_ticket(&self, pos: ChunkPos) -> Option<ChunkTicketLevel> {
572 self.tickets
573 .get(&pos)
574 .and_then(|tickets| tickets.iter().map(|ticket| ticket.load_level()).min())
575 }
576
577 pub fn tickets_at(&self, pos: ChunkPos) -> impl Iterator<Item = ChunkTicket> + '_ {
579 self.tickets
580 .get(&pos)
581 .into_iter()
582 .flat_map(|tickets| tickets.iter().copied())
583 }
584
585 pub fn tickets(&self) -> impl Iterator<Item = (ChunkPos, ChunkTicketLevel)> + '_ {
587 self.tickets.iter().filter_map(|(&pos, tickets)| {
588 tickets
589 .iter()
590 .map(|ticket| ticket.load_level())
591 .min()
592 .map(|level| (pos, level))
593 })
594 }
595
596 #[must_use]
597 pub fn ticket_count(&self) -> usize {
598 self.tickets.values().map(smallvec::SmallVec::len).sum()
599 }
600
601 #[must_use]
602 pub fn ticket_position_count(&self) -> usize {
603 self.tickets.len()
604 }
605
606 pub fn run_all_updates(&mut self) -> &[LevelChange] {
609 self.changes.clear();
610
611 if !self.dirty {
612 return &self.changes;
613 }
614
615 let old_capacity = self.levels.capacity();
617 let old_levels = mem::replace(
618 &mut self.levels,
619 FxHashMap::with_capacity_and_hasher(old_capacity, FxBuildHasher),
620 );
621 let old_simulation_capacity = self.simulation_levels.capacity();
622 let old_simulation_levels = mem::replace(
623 &mut self.simulation_levels,
624 FxHashMap::with_capacity_and_hasher(old_simulation_capacity, FxBuildHasher),
625 );
626
627 self.dirty = false;
628
629 for (&source_pos, tickets) in &self.tickets {
631 let Some(source_level) = tickets.iter().map(|ticket| ticket.load_level()).min() else {
632 continue;
633 };
634
635 let radius = i32::from(source_level.distance_to_max());
636 let sx = source_pos.0.x;
637 let sy = source_pos.0.y;
638
639 for dy in -radius..=radius {
640 for dx in -radius..=radius {
641 let distance = dx.abs().max(dy.abs()) as u8;
642 let Some(level) = source_level.with_distance(distance) else {
643 continue;
644 };
645
646 let pos = ChunkPos::new(sx + dx, sy + dy);
647 self.levels
648 .entry(pos)
649 .and_modify(|e| *e = (*e).min(level))
650 .or_insert(level);
651 }
652 }
653
654 let Some(simulation_level) = tickets
655 .iter()
656 .filter_map(|ticket| ticket.simulation_level())
657 .min()
658 else {
659 continue;
660 };
661
662 let radius = i32::from(simulation_level.distance_to_block_ticking());
663 for dy in -radius..=radius {
664 for dx in -radius..=radius {
665 let distance = dx.abs().max(dy.abs()) as u8;
666 let Some(level) = simulation_level.with_distance(distance) else {
667 continue;
668 };
669
670 let pos = ChunkPos::new(sx + dx, sy + dy);
671 self.simulation_levels
672 .entry(pos)
673 .and_modify(|e| *e = (*e).min(level))
674 .or_insert(level);
675 }
676 }
677 }
678
679 for (&pos, &new_level) in &self.levels {
681 match old_levels.get(&pos) {
682 Some(&old_level) if old_level == new_level => {} _ => self.changes.push(LevelChange {
684 pos,
685 new_level: Some(new_level),
686 new_simulation_level: self.simulation_levels.get(&pos).copied(),
687 }),
688 }
689 }
690
691 for &pos in old_levels.keys() {
693 if !self.levels.contains_key(&pos) {
694 self.changes.push(LevelChange {
695 pos,
696 new_level: None,
697 new_simulation_level: None,
698 });
699 }
700 }
701
702 self.record_simulation_only_changes(&old_levels, &old_simulation_levels);
703
704 &self.changes
705 }
706
707 pub(crate) fn take_changes(&mut self) -> Vec<LevelChange> {
709 mem::take(&mut self.changes)
710 }
711
712 pub(crate) fn recycle_changes(&mut self, mut changes: Vec<LevelChange>) {
714 debug_assert!(self.changes.is_empty());
715 changes.clear();
716 self.changes = changes;
717 }
718
719 fn record_simulation_only_changes(
720 &mut self,
721 old_levels: &FxHashMap<ChunkPos, ChunkTicketLevel>,
722 old_simulation_levels: &FxHashMap<ChunkPos, ChunkTicketLevel>,
723 ) {
724 for (&pos, &new_level) in &self.simulation_levels {
725 let load_changed = old_levels.get(&pos) != self.levels.get(&pos);
726 if load_changed {
727 continue;
728 }
729
730 match old_simulation_levels.get(&pos) {
731 Some(&old_level) if old_level == new_level => {}
732 _ => self.changes.push(LevelChange {
733 pos,
734 new_level: self.levels.get(&pos).copied(),
735 new_simulation_level: Some(new_level),
736 }),
737 }
738 }
739
740 for &pos in old_simulation_levels.keys() {
741 let load_changed = old_levels.get(&pos) != self.levels.get(&pos);
742 if load_changed || self.simulation_levels.contains_key(&pos) {
743 continue;
744 }
745
746 self.changes.push(LevelChange {
747 pos,
748 new_level: self.levels.get(&pos).copied(),
749 new_simulation_level: None,
750 });
751 }
752 }
753
754 #[must_use]
756 pub fn get_level(&self, pos: ChunkPos) -> Option<ChunkTicketLevel> {
757 self.levels.get(&pos).copied()
758 }
759
760 #[must_use]
762 pub fn get_simulation_level(&self, pos: ChunkPos) -> Option<ChunkTicketLevel> {
763 self.simulation_levels.get(&pos).copied()
764 }
765
766 #[cfg(test)]
767 #[must_use]
768 const fn is_dirty(&self) -> bool {
769 self.dirty
770 }
771
772 #[expect(dead_code, reason = "utility method for tests and future use")]
773 fn clear(&mut self) {
774 self.tickets.clear();
775 self.levels.clear();
776 self.simulation_levels.clear();
777 self.dirty = false;
778 self.changes.clear();
779 }
780
781 pub fn iter_levels(&self) -> impl Iterator<Item = (ChunkPos, ChunkTicketLevel)> + '_ {
782 self.levels.iter().map(|(&pos, &level)| (pos, level))
783 }
784
785 pub fn iter_simulation_levels(
786 &self,
787 ) -> impl Iterator<Item = (ChunkPos, ChunkTicketLevel)> + '_ {
788 self.simulation_levels
789 .iter()
790 .map(|(&pos, &level)| (pos, level))
791 }
792}
793
794#[cfg(test)]
795mod tests {
796 use super::*;
797
798 fn add_portal_ticket(
799 manager: &mut ChunkTicketManager,
800 timed_tickets: &mut TimedChunkTickets,
801 pos: ChunkPos,
802 ) {
803 if let Some(ticket) = timed_tickets.add_portal_ticket(pos) {
804 manager.add_ticket(pos, ticket);
805 }
806 }
807
808 fn add_ender_pearl_ticket(
809 manager: &mut ChunkTicketManager,
810 timed_tickets: &mut TimedChunkTickets,
811 pos: ChunkPos,
812 ) {
813 if let Some(ticket) = timed_tickets.add_ender_pearl_ticket(pos) {
814 manager.add_ticket(pos, ticket);
815 }
816 }
817
818 fn tick_timed_tickets(
819 manager: &mut ChunkTicketManager,
820 timed_tickets: &mut TimedChunkTickets,
821 can_expire: impl FnMut(ChunkPos) -> bool,
822 ) {
823 for (pos, ticket) in timed_tickets.tick(can_expire) {
824 manager.remove_ticket(pos, ticket);
825 }
826 }
827
828 #[test]
829 fn test_single_ticket_propagation() {
830 let mut manager = ChunkTicketManager::new();
831 manager.add_ticket(
832 ChunkPos::new(0, 0),
833 ChunkTicket::loading(ChunkTicketLevel::STRONGEST),
834 );
835 manager.run_all_updates();
836
837 assert_eq!(
838 manager.get_level(ChunkPos::new(0, 0)),
839 ChunkTicketLevel::new(0)
840 );
841 assert_eq!(
842 manager.get_level(ChunkPos::new(-1, -1)),
843 ChunkTicketLevel::new(1)
844 );
845 assert_eq!(
846 manager.get_level(ChunkPos::new(0, -1)),
847 ChunkTicketLevel::new(1)
848 );
849 assert_eq!(
850 manager.get_level(ChunkPos::new(1, 0)),
851 ChunkTicketLevel::new(1)
852 );
853 assert_eq!(
854 manager.get_level(ChunkPos::new(-2, -2)),
855 ChunkTicketLevel::new(2)
856 );
857 }
858
859 #[test]
860 fn test_deferred_updates() {
861 let mut manager = ChunkTicketManager::new();
862 manager.add_ticket(
863 ChunkPos::new(0, 0),
864 ChunkTicket::loading(ChunkTicketLevel::STRONGEST),
865 );
866
867 assert!(manager.is_dirty());
868 assert_eq!(manager.get_level(ChunkPos::new(0, 0)), None);
869
870 manager.run_all_updates();
871 assert!(!manager.is_dirty());
872 assert_eq!(
873 manager.get_level(ChunkPos::new(0, 0)),
874 ChunkTicketLevel::new(0)
875 );
876 }
877
878 #[test]
879 fn test_multiple_tickets_same_position() {
880 let mut manager = ChunkTicketManager::new();
881 manager.add_ticket(
882 ChunkPos::new(0, 0),
883 ChunkTicket::loading(ChunkTicketLevel::new(2).expect("test level is valid")),
884 );
885 manager.add_ticket(
886 ChunkPos::new(0, 0),
887 ChunkTicket::loading(ChunkTicketLevel::STRONGEST),
888 );
889 manager.add_ticket(
890 ChunkPos::new(0, 0),
891 ChunkTicket::loading(ChunkTicketLevel::new(1).expect("test level is valid")),
892 );
893 manager.run_all_updates();
894
895 assert_eq!(
896 manager.get_ticket(ChunkPos::new(0, 0)),
897 ChunkTicketLevel::new(0)
898 );
899 assert_eq!(
900 manager.get_level(ChunkPos::new(0, 0)),
901 ChunkTicketLevel::new(0)
902 );
903 }
904
905 #[test]
906 fn test_overlapping_propagation() {
907 let mut manager = ChunkTicketManager::new();
908 manager.add_ticket(
909 ChunkPos::new(0, 0),
910 ChunkTicket::loading(ChunkTicketLevel::STRONGEST),
911 );
912 manager.add_ticket(
913 ChunkPos::new(3, 0),
914 ChunkTicket::loading(ChunkTicketLevel::STRONGEST),
915 );
916 manager.run_all_updates();
917
918 assert_eq!(
919 manager.get_level(ChunkPos::new(1, 0)),
920 ChunkTicketLevel::new(1)
921 );
922 assert_eq!(
923 manager.get_level(ChunkPos::new(2, 0)),
924 ChunkTicketLevel::new(1)
925 );
926 }
927
928 #[test]
929 fn test_remove_ticket() {
930 let mut manager = ChunkTicketManager::new();
931 let ticket = ChunkTicket::loading(ChunkTicketLevel::STRONGEST);
932 manager.add_ticket(ChunkPos::new(0, 0), ticket);
933 manager.add_ticket(ChunkPos::new(5, 0), ticket);
934 manager.run_all_updates();
935
936 assert_eq!(
937 manager.get_level(ChunkPos::new(0, 0)),
938 ChunkTicketLevel::new(0)
939 );
940 assert_eq!(
941 manager.get_level(ChunkPos::new(5, 0)),
942 ChunkTicketLevel::new(0)
943 );
944
945 assert!(manager.remove_ticket(ChunkPos::new(0, 0), ticket));
946 manager.run_all_updates();
947
948 assert_eq!(
949 manager.get_level(ChunkPos::new(0, 0)),
950 ChunkTicketLevel::new(5)
951 );
952 assert_eq!(
953 manager.get_level(ChunkPos::new(5, 0)),
954 ChunkTicketLevel::new(0)
955 );
956 }
957
958 #[test]
959 fn test_remove_all_tickets_at_position() {
960 let mut manager = ChunkTicketManager::new();
961 let ticket = ChunkTicket::loading(ChunkTicketLevel::STRONGEST);
962 manager.add_ticket(ChunkPos::new(0, 0), ticket);
963 manager.run_all_updates();
964
965 manager.remove_ticket(ChunkPos::new(0, 0), ticket);
966 manager.run_all_updates();
967
968 assert_eq!(manager.get_level(ChunkPos::new(0, 0)), None);
969 }
970
971 #[test]
972 fn test_multiple_tickets_same_position_with_removal() {
973 let mut manager = ChunkTicketManager::new();
974 let level_0 = ChunkTicket::loading(ChunkTicketLevel::STRONGEST);
975 let level_1 = ChunkTicket::loading(ChunkTicketLevel::new(1).expect("test level is valid"));
976 let level_2 = ChunkTicket::loading(ChunkTicketLevel::new(2).expect("test level is valid"));
977 manager.add_ticket(ChunkPos::new(0, 0), level_0);
978 manager.add_ticket(ChunkPos::new(0, 0), level_2);
979 manager.add_ticket(ChunkPos::new(0, 0), level_1);
980 manager.run_all_updates();
981
982 assert_eq!(
983 manager.get_ticket(ChunkPos::new(0, 0)),
984 ChunkTicketLevel::new(0)
985 );
986 assert_eq!(manager.ticket_count(), 3);
987
988 manager.remove_ticket(ChunkPos::new(0, 0), level_0);
989 manager.run_all_updates();
990 assert_eq!(
991 manager.get_ticket(ChunkPos::new(0, 0)),
992 ChunkTicketLevel::new(1)
993 );
994
995 manager.remove_ticket(ChunkPos::new(0, 0), level_1);
996 manager.run_all_updates();
997 assert_eq!(
998 manager.get_ticket(ChunkPos::new(0, 0)),
999 ChunkTicketLevel::new(2)
1000 );
1001 }
1002
1003 #[test]
1004 fn test_duplicate_tickets_same_level() {
1005 let mut manager = ChunkTicketManager::new();
1006 let ticket = ChunkTicket::loading(ChunkTicketLevel::STRONGEST);
1007 manager.add_ticket(ChunkPos::new(0, 0), ticket);
1008 manager.add_ticket(ChunkPos::new(0, 0), ticket);
1009 manager.run_all_updates();
1010
1011 assert_eq!(manager.ticket_count(), 2);
1012
1013 manager.remove_ticket(ChunkPos::new(0, 0), ticket);
1014 manager.run_all_updates();
1015 assert_eq!(manager.ticket_count(), 1);
1016 assert_eq!(
1017 manager.get_level(ChunkPos::new(0, 0)),
1018 ChunkTicketLevel::new(0)
1019 );
1020
1021 manager.remove_ticket(ChunkPos::new(0, 0), ticket);
1022 manager.run_all_updates();
1023 assert_eq!(manager.ticket_count(), 0);
1024 assert_eq!(manager.get_level(ChunkPos::new(0, 0)), None);
1025 }
1026
1027 #[test]
1028 fn portal_timed_ticket_loads_simulates_resets_and_expires_like_vanilla() {
1029 let mut manager = ChunkTicketManager::new();
1030 let mut timed_tickets = TimedChunkTickets::default();
1031 let center = ChunkPos::new(0, 0);
1032
1033 add_portal_ticket(&mut manager, &mut timed_tickets, center);
1034 add_portal_ticket(&mut manager, &mut timed_tickets, center);
1035 manager.run_all_updates();
1036
1037 assert_eq!(timed_tickets.len(), 1);
1038 assert_eq!(manager.ticket_count(), 1);
1039 assert!(is_full(
1040 manager.get_level(center).expect("ticket should load")
1041 ));
1042 assert!(is_entity_ticking(manager.get_simulation_level(center)));
1043 assert!(is_entity_ticking(
1044 manager.get_simulation_level(ChunkPos::new(1, 0))
1045 ));
1046 assert!(is_block_ticking(
1047 manager.get_simulation_level(ChunkPos::new(2, 0))
1048 ));
1049 assert!(!is_entity_ticking(
1050 manager.get_simulation_level(ChunkPos::new(2, 0))
1051 ));
1052 assert_eq!(manager.get_simulation_level(ChunkPos::new(3, 0)), None);
1053 assert!(is_full(
1054 manager
1055 .get_level(ChunkPos::new(3, 0))
1056 .expect("portal ticket should load the full outer ring")
1057 ));
1058 assert!(!manager.get_level(ChunkPos::new(4, 0)).is_some_and(is_full));
1059
1060 for _ in 0..PORTAL_TICKET_TIMEOUT_TICKS {
1061 tick_timed_tickets(&mut manager, &mut timed_tickets, |_| true);
1062 }
1063 manager.run_all_updates();
1064 assert_eq!(manager.ticket_count(), 1);
1065
1066 tick_timed_tickets(&mut manager, &mut timed_tickets, |_| true);
1067 manager.run_all_updates();
1068 assert_eq!(manager.ticket_count(), 0);
1069 assert_eq!(manager.get_level(center), None);
1070 assert_eq!(manager.get_simulation_level(center), None);
1071 }
1072
1073 #[test]
1074 fn timed_ticket_does_not_age_until_chunk_can_expire() {
1075 let mut manager = ChunkTicketManager::new();
1076 let mut timed_tickets = TimedChunkTickets::default();
1077 let center = ChunkPos::new(0, 0);
1078
1079 add_portal_ticket(&mut manager, &mut timed_tickets, center);
1080 for _ in 0..=PORTAL_TICKET_TIMEOUT_TICKS {
1081 tick_timed_tickets(&mut manager, &mut timed_tickets, |_| false);
1082 }
1083 manager.run_all_updates();
1084 assert_eq!(manager.ticket_count(), 1);
1085
1086 for _ in 0..=PORTAL_TICKET_TIMEOUT_TICKS {
1087 tick_timed_tickets(&mut manager, &mut timed_tickets, |_| true);
1088 }
1089 manager.run_all_updates();
1090 assert_eq!(manager.ticket_count(), 0);
1091 }
1092
1093 #[test]
1094 fn persistent_portal_ticket_round_trips_remaining_ticks() {
1095 let persistent = PersistentChunkTickets {
1096 tickets: vec![PersistentChunkTicket {
1097 kind: PersistentChunkTicketKind::Portal,
1098 chunk_x: -4,
1099 chunk_z: 7,
1100 ticks_left: 123,
1101 }],
1102 };
1103
1104 let timed_tickets = TimedChunkTickets::from_persistent(persistent);
1105 let restored = timed_tickets.to_persistent();
1106
1107 assert_eq!(
1108 restored,
1109 PersistentChunkTickets {
1110 tickets: vec![PersistentChunkTicket {
1111 kind: PersistentChunkTicketKind::Portal,
1112 chunk_x: -4,
1113 chunk_z: 7,
1114 ticks_left: 123,
1115 }],
1116 }
1117 );
1118 }
1119
1120 #[test]
1121 fn duplicate_persistent_portal_ticket_resets_timeout_like_vanilla_activation() {
1122 let persistent = PersistentChunkTickets {
1123 tickets: vec![
1124 PersistentChunkTicket {
1125 kind: PersistentChunkTicketKind::Portal,
1126 chunk_x: 2,
1127 chunk_z: 3,
1128 ticks_left: 10,
1129 },
1130 PersistentChunkTicket {
1131 kind: PersistentChunkTicketKind::Portal,
1132 chunk_x: 2,
1133 chunk_z: 3,
1134 ticks_left: 20,
1135 },
1136 ],
1137 };
1138
1139 let restored = TimedChunkTickets::from_persistent(persistent).to_persistent();
1140
1141 assert_eq!(
1142 restored,
1143 PersistentChunkTickets {
1144 tickets: vec![PersistentChunkTicket {
1145 kind: PersistentChunkTicketKind::Portal,
1146 chunk_x: 2,
1147 chunk_z: 3,
1148 ticks_left: PORTAL_TICKET_TIMEOUT_TICKS,
1149 }],
1150 }
1151 );
1152 }
1153
1154 #[test]
1155 fn test_no_recalculation_when_clean() {
1156 let mut manager = ChunkTicketManager::new();
1157 manager.add_ticket(
1158 ChunkPos::new(0, 0),
1159 ChunkTicket::loading(ChunkTicketLevel::STRONGEST),
1160 );
1161 manager.run_all_updates();
1162
1163 assert!(!manager.is_dirty());
1164 manager.run_all_updates();
1165 assert!(!manager.is_dirty());
1166 }
1167
1168 #[test]
1169 fn simulated_full_ticket_propagates_simulation_only_to_block_ticking_area() {
1170 let mut manager = ChunkTicketManager::new();
1171 manager.add_ticket(ChunkPos::new(0, 0), ChunkTicket::simulated_full_chunks(1));
1172 manager.run_all_updates();
1173
1174 assert!(is_block_ticking(
1175 manager.get_simulation_level(ChunkPos::new(0, 0))
1176 ));
1177 assert!(!is_entity_ticking(
1178 manager.get_simulation_level(ChunkPos::new(0, 0))
1179 ));
1180 assert_eq!(manager.get_simulation_level(ChunkPos::new(1, 1)), None);
1181 assert!(is_full(
1182 manager
1183 .get_level(ChunkPos::new(1, 1))
1184 .expect("ticket should load the full outer ring")
1185 ));
1186 assert_eq!(
1187 manager.get_level(ChunkPos::new(1, 1)),
1188 Some(ChunkTicketLevel::FULL_CHUNK)
1189 );
1190 }
1191
1192 #[test]
1193 fn player_ticket_keeps_full_loading_moat_and_caps_simulation_radius() {
1194 let mut manager = ChunkTicketManager::new();
1195 let center = ChunkPos::new(0, 0);
1196 manager.add_ticket(center, ChunkTicket::player(1, 3));
1197 manager.run_all_updates();
1198
1199 assert!(is_entity_ticking(manager.get_level(center)));
1200 assert_eq!(
1201 manager.get_level(ChunkPos::new(1, 0)),
1202 Some(ChunkTicketLevel::ENTITY_TICKING_CHUNK)
1203 );
1204 assert_eq!(
1205 manager.get_level(ChunkPos::new(2, 0)),
1206 Some(ChunkTicketLevel::BLOCK_TICKING_CHUNK)
1207 );
1208 assert_eq!(
1209 manager.get_level(ChunkPos::new(3, 0)),
1210 Some(ChunkTicketLevel::FULL_CHUNK)
1211 );
1212
1213 assert!(is_entity_ticking(manager.get_simulation_level(center)));
1214 assert!(is_entity_ticking(
1215 manager.get_simulation_level(ChunkPos::new(1, 0))
1216 ));
1217 assert!(is_block_ticking(
1218 manager.get_simulation_level(ChunkPos::new(2, 0))
1219 ));
1220 assert!(!is_entity_ticking(
1221 manager.get_simulation_level(ChunkPos::new(2, 0))
1222 ));
1223 assert_eq!(manager.get_simulation_level(ChunkPos::new(3, 0)), None);
1224 }
1225
1226 #[test]
1227 fn maximum_player_view_distance_fits_ticket_level() {
1228 let ticket = ChunkTicket::player(MAX_SUPPORTED_VIEW_DISTANCE, MAX_SUPPORTED_VIEW_DISTANCE);
1229
1230 assert_eq!(ChunkTicketLevel::ENTITY_TICKING_CHUNK.raw(), 128);
1231 assert_eq!(ChunkTicketLevel::BLOCK_TICKING_CHUNK.raw(), 129);
1232 assert_eq!(ChunkTicketLevel::FULL_CHUNK.raw(), 130);
1233 assert_eq!(ticket.load_level().raw(), 0);
1234 assert_eq!(
1235 ticket.simulation_level().map(ChunkTicketLevel::raw),
1236 Some(0)
1237 );
1238 }
1239
1240 #[test]
1241 fn full_chunk_status_matches_vanilla_ticket_thresholds() {
1242 assert_eq!(
1243 ChunkTicketLevel::ENTITY_TICKING_CHUNK.full_status(),
1244 FullChunkStatus::EntityTicking
1245 );
1246 assert_eq!(
1247 ChunkTicketLevel::BLOCK_TICKING_CHUNK.full_status(),
1248 FullChunkStatus::BlockTicking
1249 );
1250 assert_eq!(
1251 ChunkTicketLevel::FULL_CHUNK.full_status(),
1252 FullChunkStatus::Full
1253 );
1254 assert_eq!(full_status(None), FullChunkStatus::Inaccessible);
1255 }
1256
1257 #[test]
1258 fn ticket_level_for_status_allows_requested_status() {
1259 for index in 0..=ChunkStatus::Full.get_index() {
1260 let status = ChunkStatus::from_index(index).expect("index is in status range");
1261 let ticket_level = ticket_level_for_status(status);
1262 let allowed = generation_status(Some(ticket_level));
1263 assert!(
1264 allowed.is_some_and(|allowed| allowed >= status),
1265 "{status:?} request mapped to level {ticket_level:?}, which allows {allowed:?}"
1266 );
1267 }
1268 }
1269
1270 #[test]
1271 fn non_full_ticket_level_maps_to_generation_status() {
1272 let ticket_level = ticket_level_for_status(ChunkStatus::StructureStarts);
1273
1274 assert!(!ticket_level.is_full());
1275 assert!(generation_status(Some(ticket_level)).is_some_and(|status| {
1276 status >= ChunkStatus::StructureStarts && status != ChunkStatus::Full
1277 }));
1278 }
1279
1280 #[test]
1281 fn ticket_level_for_status_creates_required_dependency_holders() {
1282 for index in 0..=ChunkStatus::Full.get_index() {
1283 let status = ChunkStatus::from_index(index).expect("index is in status range");
1284 let ticket_level = ticket_level_for_status(status);
1285 let propagation_radius = usize::from(ticket_level.distance_to_max());
1286 let required_radius = GENERATION_PYRAMID
1287 .get_step_to(status)
1288 .accumulated_dependencies
1289 .get_radius();
1290
1291 assert!(
1292 propagation_radius >= required_radius,
1293 "{status:?} request maps to level {ticket_level:?}, propagation radius {propagation_radius}, required radius {required_radius}"
1294 );
1295 }
1296 }
1297
1298 #[test]
1299 fn ender_pearl_timed_ticket_loads_simulates_resets_and_expires_like_vanilla() {
1300 let mut manager = ChunkTicketManager::new();
1301 let mut timed_tickets = TimedChunkTickets::default();
1302 let center = ChunkPos::new(0, 0);
1303
1304 add_ender_pearl_ticket(&mut manager, &mut timed_tickets, center);
1305 add_ender_pearl_ticket(&mut manager, &mut timed_tickets, center);
1306 manager.run_all_updates();
1307
1308 assert_eq!(timed_tickets.len(), 1);
1309 assert_eq!(manager.ticket_count(), 1);
1310 assert!(is_full(
1311 manager.get_level(center).expect("ticket should load")
1312 ));
1313 assert!(is_full(
1314 manager
1315 .get_level(ChunkPos::new(i32::from(ENDER_PEARL_TICKET_RADIUS), 0))
1316 .expect("ender pearl ticket should load the full outer ring")
1317 ));
1318 assert!(
1319 !manager
1320 .get_level(ChunkPos::new(i32::from(ENDER_PEARL_TICKET_RADIUS) + 1, 0))
1321 .is_some_and(is_full)
1322 );
1323
1324 for _ in 0..ENDER_PEARL_TICKET_TIMEOUT_TICKS {
1325 tick_timed_tickets(&mut manager, &mut timed_tickets, |_| true);
1326 }
1327 manager.run_all_updates();
1328 assert_eq!(manager.ticket_count(), 1);
1329
1330 tick_timed_tickets(&mut manager, &mut timed_tickets, |_| true);
1331 manager.run_all_updates();
1332 assert_eq!(manager.ticket_count(), 0);
1333 assert_eq!(manager.get_level(center), None);
1334 assert_eq!(manager.get_simulation_level(center), None);
1335 }
1336
1337 #[test]
1338 fn ender_pearl_timed_ticket_does_not_age_until_chunk_can_expire() {
1339 let mut manager = ChunkTicketManager::new();
1340 let mut timed_tickets = TimedChunkTickets::default();
1341 let center = ChunkPos::new(0, 0);
1342
1343 add_ender_pearl_ticket(&mut manager, &mut timed_tickets, center);
1344 for _ in 0..=ENDER_PEARL_TICKET_TIMEOUT_TICKS {
1345 tick_timed_tickets(&mut manager, &mut timed_tickets, |_| false);
1346 }
1347 manager.run_all_updates();
1348 assert_eq!(manager.ticket_count(), 1);
1349
1350 for _ in 0..=ENDER_PEARL_TICKET_TIMEOUT_TICKS {
1351 tick_timed_tickets(&mut manager, &mut timed_tickets, |_| true);
1352 }
1353 manager.run_all_updates();
1354 assert_eq!(manager.ticket_count(), 0);
1355 }
1356
1357 #[test]
1358 fn ender_pearl_timed_ticket_propagates_like_a_manual_simulated_ticket() {
1359 let mut timed = ChunkTicketManager::new();
1360 let mut timed_tickets = TimedChunkTickets::default();
1361 let mut manual = ChunkTicketManager::new();
1362 let pos = ChunkPos::new(0, 0);
1363 add_ender_pearl_ticket(&mut timed, &mut timed_tickets, pos);
1364 manual.add_ticket(
1365 pos,
1366 ChunkTicket::simulated_full_chunks(ENDER_PEARL_TICKET_RADIUS),
1367 );
1368 timed.run_all_updates();
1369 manual.run_all_updates();
1370
1371 for dx in -i32::from(ENDER_PEARL_TICKET_RADIUS)..=i32::from(ENDER_PEARL_TICKET_RADIUS) {
1372 for dz in -i32::from(ENDER_PEARL_TICKET_RADIUS)..=i32::from(ENDER_PEARL_TICKET_RADIUS) {
1373 let p = ChunkPos::new(dx, dz);
1374 assert_eq!(timed.get_level(p), manual.get_level(p));
1375 assert_eq!(
1376 timed.get_simulation_level(p),
1377 manual.get_simulation_level(p)
1378 );
1379 }
1380 }
1381 }
1382
1383 #[test]
1384 fn ender_pearl_timed_ticket_is_not_persisted() {
1385 let mut manager = ChunkTicketManager::new();
1386 let mut timed_tickets = TimedChunkTickets::default();
1387
1388 add_portal_ticket(&mut manager, &mut timed_tickets, ChunkPos::new(0, 0));
1389 add_ender_pearl_ticket(&mut manager, &mut timed_tickets, ChunkPos::new(1, 0));
1390
1391 assert_eq!(
1392 timed_tickets.to_persistent(),
1393 PersistentChunkTickets {
1394 tickets: vec![PersistentChunkTicket {
1395 kind: PersistentChunkTicketKind::Portal,
1396 chunk_x: 0,
1397 chunk_z: 0,
1398 ticks_left: PORTAL_TICKET_TIMEOUT_TICKS,
1399 }],
1400 }
1401 );
1402 }
1403}