1use arc_swap::ArcSwap;
2use rayon::ThreadPool;
3use rustc_hash::{FxBuildHasher, FxHashMap, FxHashSet};
4use std::{
5 io, mem,
6 sync::{
7 Arc, Weak,
8 atomic::{AtomicBool, AtomicUsize, Ordering},
9 },
10 time::{Duration, Instant},
11};
12use steel_protocol::packet_traits::EncodedPacket;
13use steel_protocol::packets::game::{
14 BlockChange, CBlockUpdate, CLightUpdate, CSectionBlocksUpdate, CSetChunkCenter,
15};
16use steel_protocol::utils::ConnectionProtocol;
17use steel_registry::dimension_type::DimensionTypeRef;
18use steel_registry::{
19 blocks::{BlockRef, block_state_ext::BlockStateExt},
20 fluid::FluidRef,
21};
22use steel_utils::{BlockPos, BlockStateId, ChunkPos, PackedChunkPos, SectionPos, locks::SyncMutex};
23use tokio::runtime::Runtime;
24use tokio::sync::Notify;
25use tokio::time::sleep;
26use tokio_util::sync::CancellationToken;
27use tokio_util::task::TaskTracker;
28use tracing::instrument;
29
30use crate::behavior::{BLOCK_BEHAVIORS, FLUID_BEHAVIORS};
31use crate::block_entity::{BlockEntityLifecycleExt as _, ClearedBlockEntities, SharedBlockEntity};
32use crate::chunk::chunk_holder::{
33 ChunkHolder, ChunkSaveDependency, PostProcessGenerationError, TickingReadiness,
34};
35use crate::chunk::chunk_request::ChunkRequestLease;
36pub use crate::chunk::chunk_scheduler::ChunkMapSchedulingTimings;
37#[cfg(test)]
38use crate::chunk::chunk_scheduler::PlayerTicketOperation;
39use crate::chunk::chunk_scheduler::{ChunkSchedulingCoordinator, ChunkTicketReceipt};
40use crate::chunk::chunk_ticket_manager::{
41 ChunkTicketLevel, LoadLevelChange, generation_status, is_block_ticking, is_entity_ticking,
42 is_full,
43};
44use crate::chunk::chunk_ticket_storage::{
45 ChunkTicketStorage, ENDER_PEARL_TICKET_TIMEOUT_TICKS, PersistentChunkTickets,
46 TimedTicketExpiration,
47};
48use crate::chunk::full_chunk_readiness::{
49 FullNeighborhoodCounts, FullNeighborhoodError, FullNeighborhoodIndex, FullPublication,
50 FullPublicationQueue,
51};
52pub use crate::chunk::gameplay_chunk_lookup_cache::GameplayChunkLookupCacheStats;
53use crate::chunk::gameplay_chunk_lookup_cache::{
54 GameplayChunkLookupCacheScope, lookup_or_insert_with,
55};
56use crate::chunk::light::{
57 LIGHT_CACHE_RADIUS, LightCacheLayout, LightCacheSetupRadius, LightLayer,
58 LightSectionEmptinessChange, LightSectionRange, LightWorkWindowGate, LightWorkset,
59 build_chunk_light_update_packet_for_sections,
60 propagate_block_light_changes_with_empty_sections,
61 propagate_sky_light_changes_with_empty_sections,
62};
63use crate::chunk::player_chunk_view::PlayerChunkView;
64use crate::chunk::simulation_ticket_manager::SimulationLevelChange;
65use crate::chunk::{
66 Chunk,
67 chunk_generation_task::ChunkGenerationTask,
68 full_chunk::{BlockRandomPositionGenerator, FullChunkRef},
69 section::RandomTickSectionBits,
70 status::ChunkStatus,
71};
72use crate::chunk_saver::ChunkStorage;
73use crate::player::connection::NetworkConnection;
74use crate::world::World;
75use crate::world::tick_scheduler::{BlockTick, FluidTick, ScheduledTickRunBatch};
76use crate::worldgen::{ChunkGeneratorType, WorldGenContext};
77use crate::{entity::Entity, player::Player};
78
79mod generation_readiness;
80mod light_update_state;
81mod light_updates;
82mod persistence;
83mod player_tracking;
84mod scheduled_ticks;
85
86#[cfg(test)]
87use light_update_state::PendingLightUpdates;
88use light_update_state::{InFlightLightUpdates, LightUpdateState, PendingChunkLightUpdates};
89
90const GENERATION_THREAD_MULTIPLE: usize = 2;
91const MAX_SCHEDULED_TICKS_PER_TICK: usize = 65_536;
93
94pub const ENDER_PEARL_TICKET_TIMEOUT: i64 = ENDER_PEARL_TICKET_TIMEOUT_TICKS;
98
99#[derive(Debug, Default)]
101pub struct ChunkMapGameTickTimings {
102 pub scheduling: ChunkMapSchedulingTimings,
104 pub broadcast_changes: Duration,
106 pub collect_tickable: Duration,
108 pub tick_chunks: Duration,
110 pub tick_block_entities: Duration,
112 pub tickable_count: usize,
114 pub total_chunks: usize,
116 pub lookup_cache: GameplayChunkLookupCacheStats,
118}
119
120#[derive(Clone)]
121struct TickableChunk {
122 pos: ChunkPos,
123 holder: Arc<ChunkHolder>,
124 randomly_ticking_sections: Arc<RandomTickSectionBits>,
125}
126
127#[derive(Default)]
134struct TickingChunkSnapshot {
135 block: Box<[TickableChunk]>,
136 random_chunk_indices: Box<[usize]>,
137 entity_indices: Box<[usize]>,
138}
139
140struct FinalizedBlockEntityUnload {
141 holder: Arc<ChunkHolder>,
142 lifecycle_dispatchers: Vec<SharedBlockEntity>,
143 positions: Vec<BlockPos>,
144}
145
146struct BlockTickBatchGuard<'a> {
147 world: &'a World,
148 batch: Arc<ScheduledTickRunBatch<BlockRef>>,
149}
150
151impl<'a> BlockTickBatchGuard<'a> {
152 fn new(world: &'a World, ticks: Vec<BlockTick>) -> Self {
153 Self {
154 world,
155 batch: world.begin_scheduled_block_tick_batch(ticks),
156 }
157 }
158
159 fn ticks(&self) -> &[BlockTick] {
160 self.batch.ticks()
161 }
162
163 fn start(&self, index: usize) {
164 self.batch.start(index);
165 }
166}
167
168impl Drop for BlockTickBatchGuard<'_> {
169 fn drop(&mut self) {
170 self.world.end_scheduled_block_tick_batch(&self.batch);
171 }
172}
173
174struct FluidTickBatchGuard<'a> {
175 world: &'a World,
176 batch: Arc<ScheduledTickRunBatch<FluidRef>>,
177}
178
179impl<'a> FluidTickBatchGuard<'a> {
180 fn new(world: &'a World, ticks: Vec<FluidTick>) -> Self {
181 Self {
182 world,
183 batch: world.begin_scheduled_fluid_tick_batch(ticks),
184 }
185 }
186
187 fn ticks(&self) -> &[FluidTick] {
188 self.batch.ticks()
189 }
190
191 fn start(&self, index: usize) {
192 self.batch.start(index);
193 }
194}
195
196impl Drop for FluidTickBatchGuard<'_> {
197 fn drop(&mut self) {
198 self.world.end_scheduled_fluid_tick_batch(&self.batch);
199 }
200}
201
202struct TickingReadinessCandidate {
203 pos: ChunkPos,
204 holder: Arc<ChunkHolder>,
205 desired: TickingReadiness,
206 target: TickingReadiness,
207}
208
209#[derive(Debug, Clone, Copy)]
210struct DeferredChunkRevival {
211 load_level: ChunkTicketLevel,
212}
213
214#[derive(Default)]
215struct ReadinessReconcileResult {
216 snapshot_changed: bool,
217 post_process_generation: Duration,
218 post_process_chunk_count: usize,
219 post_process_position_count: usize,
220 candidate_count: usize,
221}
222
223pub struct ChunkMap {
225 pub(crate) chunks: scc::HashMap<ChunkPos, Arc<ChunkHolder>, FxBuildHasher>,
227 pub(crate) unloading_chunks: scc::HashMap<ChunkPos, Arc<ChunkHolder>, FxBuildHasher>,
229 deferred_revivals: SyncMutex<FxHashMap<ChunkPos, DeferredChunkRevival>>,
231 pub pending_generation_tasks: SyncMutex<Vec<Arc<ChunkGenerationTask>>>,
233 pub task_tracker: TaskTracker,
235 scheduling: ChunkSchedulingCoordinator,
237 source_phase_guard: SyncMutex<()>,
239 full_publications: Arc<FullPublicationQueue>,
241 full_neighborhood: SyncMutex<FullNeighborhoodIndex>,
243 ticking_chunks: ArcSwap<TickingChunkSnapshot>,
245 finalized_block_entity_unloads: SyncMutex<Vec<FinalizedBlockEntityUnload>>,
247 pub world_gen_context: Arc<WorldGenContext>,
249 pub generation_pool: Arc<ThreadPool>,
251 chunk_encoding_pool: Arc<ThreadPool>,
253 pub chunk_runtime: Arc<Runtime>,
257 pub storage: Arc<ChunkStorage>,
259 pub chunks_to_broadcast: SyncMutex<Vec<Arc<ChunkHolder>>>,
261 light_updates: SyncMutex<LightUpdateState>,
263 light_updates_progress_notify: Notify,
265 light_work_window_gate: Arc<LightWorkWindowGate>,
267 running_generation_tasks: AtomicUsize,
269 generation_refill_notify: Notify,
271 generation_refill_cancel_token: CancellationToken,
273 generation_refill_stopped: AtomicBool,
275 generation_refill_started: AtomicBool,
277 pub cancel_token: CancellationToken,
280}
281
282#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
283struct GenerationTaskPriority {
284 simulation_bucket: u8,
285 simulation_level: ChunkTicketLevel,
286 load_level: ChunkTicketLevel,
287}
288
289impl GenerationTaskPriority {
290 const fn for_levels(
291 load_level: Option<ChunkTicketLevel>,
292 simulation_level: Option<ChunkTicketLevel>,
293 ) -> Self {
294 let simulation_bucket = if simulation_level.is_some() { 0 } else { 1 };
295 Self {
296 simulation_bucket,
297 simulation_level: match simulation_level {
298 Some(level) => level,
299 None => ChunkTicketLevel::MAX,
300 },
301 load_level: match load_level {
302 Some(level) => level,
303 None => ChunkTicketLevel::MAX,
304 },
305 }
306 }
307}
308
309struct RunningGenerationTaskPermit {
310 chunk_map: Arc<ChunkMap>,
311 task: Arc<ChunkGenerationTask>,
312}
313
314impl Drop for RunningGenerationTaskPermit {
315 fn drop(&mut self) {
316 self.task
317 .center_holder()
318 .clear_generation_task_if_current(&self.task);
319 self.chunk_map
320 .running_generation_tasks
321 .fetch_sub(1, Ordering::AcqRel);
322 self.chunk_map.notify_generation_refill();
323 }
324}
325
326impl ChunkMap {
327 #[must_use]
331 #[expect(
332 clippy::too_many_arguments,
333 reason = "chunk-map construction requires its world and scheduling configuration"
334 )]
335 pub fn new_with_storage(
336 chunk_runtime: Arc<Runtime>,
337 world: Weak<World>,
338 dimension_type: DimensionTypeRef,
339 sea_level: i32,
340 view_distance: u8,
341 simulation_distance: u8,
342 storage: Arc<ChunkStorage>,
343 generator: Arc<ChunkGeneratorType>,
344 generation_pool: Arc<ThreadPool>,
345 ) -> Self {
346 let chunk_encoding_pool = Arc::clone(&generation_pool);
347 Self::new_with_storage_and_ticket_storage(
348 chunk_runtime,
349 world,
350 dimension_type,
351 sea_level,
352 storage,
353 generator,
354 generation_pool,
355 chunk_encoding_pool,
356 view_distance,
357 simulation_distance,
358 ChunkTicketStorage::new(),
359 )
360 }
361
362 #[must_use]
363 #[expect(
364 clippy::too_many_arguments,
365 reason = "extends ChunkMap::new_with_storage with restored runtime ticket state"
366 )]
367 pub(crate) fn new_with_storage_and_ticket_storage(
368 chunk_runtime: Arc<Runtime>,
369 world: Weak<World>,
370 dimension_type: DimensionTypeRef,
371 sea_level: i32,
372 storage: Arc<ChunkStorage>,
373 generator: Arc<ChunkGeneratorType>,
374 generation_pool: Arc<ThreadPool>,
375 chunk_encoding_pool: Arc<ThreadPool>,
376 view_distance: u8,
377 simulation_distance: u8,
378 ticket_storage: ChunkTicketStorage,
379 ) -> Self {
380 let full_publications = Arc::new(FullPublicationQueue::default());
381
382 Self {
383 chunks: scc::HashMap::default(),
384 unloading_chunks: scc::HashMap::default(),
385 deferred_revivals: SyncMutex::new(FxHashMap::default()),
386 pending_generation_tasks: SyncMutex::new(Vec::new()),
387 task_tracker: TaskTracker::new(),
388 scheduling: ChunkSchedulingCoordinator::new(
389 ticket_storage,
390 view_distance,
391 simulation_distance,
392 ),
393 source_phase_guard: SyncMutex::new(()),
394 full_publications,
395 full_neighborhood: SyncMutex::new(FullNeighborhoodIndex::default()),
396 ticking_chunks: ArcSwap::from_pointee(TickingChunkSnapshot::default()),
397 finalized_block_entity_unloads: SyncMutex::new(Vec::new()),
398 world_gen_context: Arc::new(WorldGenContext::new(
399 generator,
400 world,
401 dimension_type.min_y,
402 dimension_type.height,
403 sea_level,
404 )),
405 generation_pool,
406 chunk_encoding_pool,
407 chunk_runtime,
408 storage,
409 chunks_to_broadcast: SyncMutex::new(Vec::new()),
410 light_updates: SyncMutex::new(LightUpdateState::default()),
411 light_updates_progress_notify: Notify::new(),
412 light_work_window_gate: Arc::new(LightWorkWindowGate::new()),
413 running_generation_tasks: AtomicUsize::new(0),
414 generation_refill_notify: Notify::new(),
415 generation_refill_cancel_token: CancellationToken::new(),
416 generation_refill_stopped: AtomicBool::new(false),
417 generation_refill_started: AtomicBool::new(false),
418 cancel_token: CancellationToken::new(),
419 }
420 }
421
422 pub(crate) fn light_work_window_gate(&self) -> Arc<LightWorkWindowGate> {
423 Arc::clone(&self.light_work_window_gate)
424 }
425
426 pub fn start_generation_refill_loop(self: &Arc<Self>) {
428 if self.generation_refill_started.swap(true, Ordering::AcqRel) {
429 return;
430 }
431
432 let chunk_map = Arc::clone(self);
433 self.task_tracker.spawn_on(
434 async move {
435 loop {
436 tokio::select! {
437 () = chunk_map.generation_refill_cancel_token.cancelled() => break,
438 () = chunk_map.generation_refill_notify.notified() => {
439 chunk_map.run_generation_tasks_b();
440 }
441 }
442 }
443 },
444 self.chunk_runtime.handle(),
445 );
446 }
447
448 pub fn stop_generation_refill_loop(&self) {
451 self.generation_refill_stopped
452 .store(true, Ordering::Release);
453 self.generation_refill_cancel_token.cancel();
454 self.generation_refill_notify.notify_waiters();
455
456 let pending = mem::take(&mut *self.pending_generation_tasks.lock());
457 for task in pending {
458 task.cancel();
459 task.center_holder().clear_generation_task_if_current(&task);
460 }
461 }
462
463 pub(crate) fn notify_generation_refill(&self) {
464 self.generation_refill_notify.notify_one();
465 }
466
467 fn run_or_notify_generation_refill(&self) {
468 if self.generation_refill_started.load(Ordering::Acquire) {
469 self.notify_generation_refill();
470 } else {
471 self.run_generation_tasks_b();
472 }
473 }
474
475 pub fn with_full_chunk<F, R>(&self, pos: ChunkPos, f: F) -> Option<R>
478 where
479 F: FnOnce(FullChunkRef<'_>) -> R,
480 {
481 let holder = self.lookup_active_holder(pos)?;
482 if holder.is_status_disallowed(ChunkStatus::Full) {
483 return None;
484 }
485 holder.try_full_chunk().map(f)
486 }
487
488 pub(crate) fn active_full_chunk_holder(&self, pos: ChunkPos) -> Option<Arc<ChunkHolder>> {
490 let holder = self.lookup_active_holder(pos)?;
491 if holder.is_status_disallowed(ChunkStatus::Full)
492 || holder.try_chunk(ChunkStatus::Full).is_none()
493 {
494 return None;
495 }
496 Some(holder)
497 }
498
499 #[doc(hidden)]
504 #[cfg(feature = "benchmark-support")]
505 pub fn insert_benchmark_chunk_holder(&self, pos: ChunkPos, holder: Arc<ChunkHolder>) {
506 assert!(holder.simulation_level().is_none());
507 assert!(self.ticking_chunks.load().block.is_empty());
508 let _ = self.chunks.insert_sync(pos, holder);
509 }
510
511 #[inline]
512 fn lookup_active_holder(&self, pos: ChunkPos) -> Option<Arc<ChunkHolder>> {
513 lookup_or_insert_with(self, pos, || {
514 self.chunks.read_sync(&pos, |_, holder| Arc::clone(holder))
515 })
516 }
517
518 #[must_use]
520 pub(crate) fn is_block_ticking_full_chunk_loaded(&self, pos: ChunkPos) -> bool {
521 self.lookup_active_holder(pos).is_some_and(|holder| {
522 is_block_ticking(holder.load_level())
523 && holder.ticking_readiness_snapshot().is_block_ticking()
524 })
525 }
526
527 #[must_use]
529 pub(crate) fn is_block_ticking_full_chunk_simulated(&self, pos: ChunkPos) -> bool {
530 self.lookup_active_holder(pos).is_some_and(|holder| {
531 is_block_ticking(holder.simulation_level())
532 && holder.ticking_readiness_snapshot().is_block_ticking()
533 })
534 }
535
536 pub(crate) fn with_chunk_at_status<F, R>(
539 &self,
540 pos: ChunkPos,
541 status: ChunkStatus,
542 f: F,
543 ) -> Option<R>
544 where
545 F: FnOnce(&Chunk) -> R,
546 {
547 let chunk_holder = self.lookup_active_holder(pos)?;
548 if chunk_holder.is_status_disallowed(status) {
551 return None;
552 }
553 let chunk = chunk_holder.try_chunk(status)?;
554 Some(f(chunk))
555 }
556
557 #[cfg(test)]
558 pub(crate) fn queue_test_player_ticket_add(
559 &self,
560 pos: ChunkPos,
561 player_id: uuid::Uuid,
562 ) -> ChunkTicketReceipt {
563 self.scheduling
564 .queue_player_ticket_operation(PlayerTicketOperation::Add { pos, player_id })
565 }
566
567 #[cfg(test)]
568 pub(crate) fn queue_test_player_ticket_remove(
569 &self,
570 pos: ChunkPos,
571 player_id: uuid::Uuid,
572 ) -> ChunkTicketReceipt {
573 self.scheduling
574 .queue_player_ticket_operation(PlayerTicketOperation::Remove { pos, player_id })
575 }
576
577 pub(crate) fn acquire_chunk_request_leases(
578 &self,
579 positions: &[ChunkPos],
580 ticket_level: ChunkTicketLevel,
581 ) -> Option<ChunkTicketReceipt> {
582 self.scheduling
583 .acquire_chunk_request_leases(positions.iter().copied(), ticket_level)
584 }
585
586 pub(crate) fn release_chunk_request_leases(
587 &self,
588 positions: &[ChunkPos],
589 ticket_level: ChunkTicketLevel,
590 ) -> Option<ChunkTicketReceipt> {
591 self.scheduling
592 .release_chunk_request_leases(positions.iter().copied(), ticket_level)
593 }
594
595 pub(crate) fn is_ticket_receipt_committed(&self, receipt: ChunkTicketReceipt) -> bool {
596 self.scheduling.is_receipt_committed(receipt)
597 }
598
599 pub(crate) async fn with_full_chunks_in_radius<F, R>(
602 self: &Arc<Self>,
603 center: ChunkPos,
604 radius: u8,
605 f: F,
606 ) -> Option<R>
607 where
608 F: FnOnce() -> R,
609 {
610 let ticket_level = ChunkTicketLevel::for_full_chunk_radius(radius);
611 let lease = ChunkRequestLease::new(Arc::clone(self), Box::new([center]), ticket_level);
612 let Some(ticket_receipt) = lease.submission_receipt else {
613 unreachable!("one chunk request lease must produce a receipt");
614 };
615 let radius = i32::from(radius);
616
617 loop {
618 self.advance_scheduling();
619 if self.is_ticket_receipt_committed(ticket_receipt)
620 && self.full_square_is_ready(center, radius)
621 {
622 break;
623 }
624
625 if self.cancel_token.is_cancelled() {
626 drop(lease);
627 self.advance_scheduling();
628 return None;
629 }
630
631 sleep(Duration::from_millis(10)).await;
632 }
633
634 let result = f();
635 drop(lease);
636 self.advance_scheduling();
637
638 Some(result)
639 }
640
641 pub(crate) fn place_portal_ticket(&self, ticket_position: BlockPos) {
643 let center = ChunkPos::from_block_pos(ticket_position);
644 self.scheduling.add_or_refresh_portal_ticket(center);
645 }
646
647 pub(crate) fn tick_timed_tickets(&self) {
649 let expirations = self.eligible_timed_ticket_expirations();
650 self.scheduling.tick_timed_tickets(&expirations);
651 }
652
653 fn eligible_timed_ticket_expirations(&self) -> Vec<TimedTicketExpiration> {
654 let mut expirations = self.scheduling.timed_ticket_expirations();
656 expirations.retain(|expiration| {
657 expiration.can_expire_if_unloaded() || self.can_timed_ticket_expire(expiration.pos())
658 });
659 expirations
660 }
661
662 pub(crate) fn persistent_chunk_tickets(&self) -> PersistentChunkTickets {
663 self.scheduling.persistent_chunk_tickets()
664 }
665
666 fn can_timed_ticket_expire(&self, pos: ChunkPos) -> bool {
667 self.chunks
668 .read_sync(&pos, |_, holder| holder.is_ready_for_saving())
669 .unwrap_or(true)
670 }
671
672 fn full_square_is_ready(&self, center: ChunkPos, radius: i32) -> bool {
673 for dz in -radius..=radius {
674 for dx in -radius..=radius {
675 let pos = ChunkPos::new(center.0.x + dx, center.0.y + dz);
676 let Some(holder) = self.chunks.read_sync(&pos, |_, holder| holder.clone()) else {
677 return false;
678 };
679 if holder.try_chunk(ChunkStatus::Full).is_none() {
680 return false;
681 }
682 }
683 }
684 true
685 }
686
687 #[expect(
689 clippy::too_many_lines,
690 reason = "block and light packet construction share the same holder drain"
691 )]
692 pub fn broadcast_changed_chunks(&self) {
693 self.propagate_queued_light_changes();
694
695 let holders = {
696 let mut guard = self.chunks_to_broadcast.lock();
697 if guard.is_empty() {
698 return;
699 }
700 mem::take(&mut *guard)
701 };
702
703 let mut world = None;
704
705 for holder in holders {
706 let chunk_pos = holder.get_pos();
707 let world = world.get_or_insert_with(|| self.world_gen_context.world());
709 let has_skylight = world.dimension_type.has_skylight;
710 let min_y = holder.min_y();
711 holder.clear_broadcast_queued();
712
713 let light_changes = holder.take_changed_light_sections();
714 let changes_by_section = holder.take_changed_blocks();
716 let has_publishable_light_changes =
717 !light_changes.block.is_empty() || (has_skylight && !light_changes.sky.is_empty());
718
719 if !has_publishable_light_changes && changes_by_section.is_empty() {
720 continue;
721 }
722
723 if has_publishable_light_changes
724 && let Some(chunk) = holder.try_chunk(ChunkStatus::Full)
725 {
726 let tracking_players = world.get_light_packet_tracking_players(chunk_pos);
727 if !tracking_players.is_empty() {
728 let light_data = {
729 let light = chunk.light();
730 let sky_sections = if has_skylight {
731 light_changes.sky.as_slice()
732 } else {
733 &[]
734 };
735 build_chunk_light_update_packet_for_sections(
736 chunk_pos,
737 &light,
738 has_skylight,
739 sky_sections,
740 &light_changes.block,
741 )
742 };
743 let light_packet = CLightUpdate {
744 x: chunk_pos.0.x,
745 z: chunk_pos.0.y,
746 light_data,
747 };
748
749 let Ok(encoded) = EncodedPacket::from_bare(
750 light_packet,
751 world.compression,
752 ConnectionProtocol::Play,
753 ) else {
754 log::warn!("Failed to encode light update packet");
755 continue;
756 };
757
758 for entity_id in &tracking_players {
759 if let Some(player) = world.players.get_by_entity_id(*entity_id) {
760 player.connection.send_encoded(encoded.clone());
761 }
762 }
763 }
764 }
765
766 if changes_by_section.is_empty() {
767 continue;
768 }
769
770 let tracking_players = world.get_packet_tracking_players(chunk_pos);
772 if tracking_players.is_empty() {
773 continue;
774 }
775
776 for (section_index, changed_positions) in changes_by_section {
778 let section_y = min_y / 16 + section_index as i32;
779 let section_pos = SectionPos::new(chunk_pos.0.x, section_y, chunk_pos.0.y);
780
781 if changed_positions.len() == 1 {
782 let Some(&packed) = changed_positions.iter().next() else {
784 continue;
785 };
786 let block_pos = section_pos.relative_to_block_pos(packed);
787 let block_state = world.get_block_state(block_pos);
788
789 tracing::trace!(
790 ?block_pos,
791 ?block_state,
792 player_count = tracking_players.len(),
793 "Broadcasting single block update"
794 );
795
796 let update_packet = CBlockUpdate {
797 pos: block_pos,
798 block_state,
799 };
800
801 let Ok(encoded) = EncodedPacket::from_bare(
802 update_packet,
803 world.compression,
804 ConnectionProtocol::Play,
805 ) else {
806 log::warn!("Failed to encode block update packet");
807 continue;
808 };
809
810 for entity_id in &tracking_players {
811 if let Some(player) = world.players.get_by_entity_id(*entity_id) {
812 player.connection.send_encoded(encoded.clone());
813 }
814 }
815 world.broadcast_block_entity_if_needed(block_pos);
816 } else {
817 let changes: Vec<BlockChange> = changed_positions
819 .iter()
820 .map(|&packed| {
821 let block_pos = section_pos.relative_to_block_pos(packed);
822 let block_state = world.get_block_state(block_pos);
823 BlockChange {
824 pos: packed,
825 block_state,
826 }
827 })
828 .collect();
829
830 tracing::trace!(
831 change_count = changes.len(),
832 ?section_pos,
833 player_count = tracking_players.len(),
834 "Broadcasting section block updates"
835 );
836
837 let packet = CSectionBlocksUpdate {
838 section_pos,
839 changes,
840 };
841
842 let Ok(encoded) = EncodedPacket::from_bare(
843 packet,
844 world.compression,
845 ConnectionProtocol::Play,
846 ) else {
847 log::warn!("Failed to encode section block update packet");
848 continue;
849 };
850
851 for entity_id in &tracking_players {
852 if let Some(player) = world.players.get_by_entity_id(*entity_id) {
853 player.connection.send_encoded(encoded.clone());
854 }
855 }
856 for &packed in &changed_positions {
857 let block_pos = section_pos.relative_to_block_pos(packed);
858 world.broadcast_block_entity_if_needed(block_pos);
859 }
860 }
861 }
862 }
863 }
864
865 #[instrument(level = "trace", skip(self, world), name = "chunk_map_game_tick")]
872 pub fn tick_game(
873 self: &Arc<Self>,
874 world: &Arc<World>,
875 tick_count: u64,
876 random_tick_speed: u32,
877 runs_normally: bool,
878 ) -> ChunkMapGameTickTimings {
879 let _source_phase_guard = self.source_phase_guard.lock();
880 let mut timings = ChunkMapGameTickTimings::default();
881
882 if tick_count.is_multiple_of(100) {
883 tracing::debug!(
884 chunks = self.chunks.len(),
885 unloading = self.unloading_chunks.len(),
886 "Chunk map status"
887 );
888 }
889
890 if runs_normally {
891 let lookup_cache_scope = GameplayChunkLookupCacheScope::enter(self);
892 let _span = tracing::trace_span!("collect_tickable").entered();
893 let start = Instant::now();
894 let tickable_chunks = self.ticking_chunks.load();
895 timings.collect_tickable = start.elapsed();
896 if !tickable_chunks.block.is_empty() {
897 let _span = tracing::trace_span!(
898 "scheduled_ticks",
899 block_ticking_count = tickable_chunks.block.len(),
900 total_chunks = self.chunks.len()
901 )
902 .entered();
903 let start = Instant::now();
904 let current_tick = world.game_time();
907 let ready_block_ticks =
908 Self::collect_scheduled_block_ticks(world, &tickable_chunks, current_tick);
909 Self::execute_scheduled_block_ticks(world, ready_block_ticks);
910
911 let ready_fluid_ticks =
912 Self::collect_scheduled_fluid_ticks(world, &tickable_chunks, current_tick);
913 Self::execute_scheduled_fluid_ticks(world, ready_fluid_ticks);
914 timings.tick_chunks += start.elapsed();
915 }
916 timings.lookup_cache.merge(lookup_cache_scope.finish());
917 }
918
919 if runs_normally {
922 self.tick_timed_tickets();
923 }
924 timings.scheduling = self.run_chunk_source_updates();
925
926 {
927 let lookup_cache_scope = GameplayChunkLookupCacheScope::enter(self);
928 let _span = tracing::trace_span!("collect_tickable").entered();
929 let start = Instant::now();
930 let tickable_chunks = self.ticking_chunks.load();
931 timings.collect_tickable += start.elapsed();
932 timings.total_chunks = self.chunks.len();
933 timings.tickable_count = tickable_chunks.block.len();
934
935 if runs_normally && random_tick_speed > 0 && !tickable_chunks.block.is_empty() {
936 let _span = tracing::trace_span!(
937 "random_chunk_ticks",
938 block_ticking_count = tickable_chunks.block.len(),
939 total_chunks = timings.total_chunks
940 )
941 .entered();
942 let start = Instant::now();
943 let mut random_positions = BlockRandomPositionGenerator::from_runtime_rng();
946 for &index in &tickable_chunks.random_chunk_indices {
947 let tickable_chunk = &tickable_chunks.block[index];
950 if tickable_chunk.randomly_ticking_sections.is_empty() {
951 continue;
952 }
953 if let Some(chunk) = tickable_chunk.holder.try_full_chunk() {
954 chunk.tick_random_blocks(world, random_tick_speed, &mut random_positions);
955 }
956 }
957 timings.tick_chunks += start.elapsed();
958 }
959 timings.lookup_cache.merge(lookup_cache_scope.finish());
960 }
961
962 {
963 let _span = tracing::trace_span!("broadcast_changes").entered();
964 let start = Instant::now();
965 self.broadcast_changed_chunks();
966 timings.broadcast_changes = start.elapsed();
967 }
968
969 {
970 let _span = tracing::trace_span!("process_unloads").entered();
971 let start = Instant::now();
972 self.process_pending_unloads();
973 timings.scheduling.process_unloads = start.elapsed();
974 }
975
976 timings
977 }
978
979 #[instrument(level = "trace", skip(self), name = "advance_chunk_scheduling")]
986 pub(crate) fn advance_scheduling(self: &Arc<Self>) -> ChunkMapSchedulingTimings {
987 let _source_phase_guard = self.source_phase_guard.lock();
988 let mut timings = self.run_chunk_source_updates();
989 let start = Instant::now();
990 self.process_pending_unloads();
991 timings.process_unloads = start.elapsed();
992 timings
993 }
994
995 fn apply_simulation_changes(&self, changes: &[SimulationLevelChange]) -> bool {
996 let world = self.world_gen_context.world();
997 let mut snapshot_changed = false;
998 for change in changes {
999 let Some(holder) = self
1000 .chunks
1001 .read_sync(&change.pos, |_, holder| Arc::clone(holder))
1002 else {
1003 continue;
1004 };
1005 let previous_level = holder.simulation_level();
1006 if previous_level == change.new_level {
1007 continue;
1008 }
1009
1010 let readiness = holder.ticking_readiness_snapshot().readiness();
1011 let previous_visibility = holder.entity_visibility();
1012 holder.set_simulation_level(change.new_level);
1013 let previous_membership = Self::ticking_snapshot_membership(readiness, previous_level);
1014 let current_membership = Self::ticking_snapshot_membership(readiness, change.new_level);
1015 snapshot_changed |= previous_membership != current_membership;
1016 let new_visibility = holder.entity_visibility();
1017 if previous_visibility != new_visibility
1018 && holder.try_chunk(ChunkStatus::Empty).is_some()
1019 {
1020 world.update_entity_chunk_visibility(change.pos, new_visibility);
1021 }
1022 }
1023 snapshot_changed
1024 }
1025
1026 #[expect(
1027 clippy::too_many_lines,
1028 reason = "the ordered chunk-source commit is kept together so its Vanilla phase guarantees stay auditable"
1029 )]
1030 fn run_chunk_source_updates(self: &Arc<Self>) -> ChunkMapSchedulingTimings {
1031 debug_assert!(!GameplayChunkLookupCacheScope::is_active_for(self));
1034 let mut timings = ChunkMapSchedulingTimings::default();
1035 let mut batch = {
1036 let _span = tracing::trace_span!("ticket_updates").entered();
1037 let start = Instant::now();
1038 let world = self.world_gen_context.world();
1039 let batch = self
1040 .scheduling
1041 .run_all_updates(world.view_distance, world.simulation_distance);
1042 timings.ticket_updates = start.elapsed();
1043 batch
1044 };
1045
1046 self.merge_deferred_revivals(&mut batch.load_changes);
1047
1048 {
1049 let _span = tracing::trace_span!("block_entity_unloads").entered();
1050 let start = Instant::now();
1051 self.finish_block_entity_unloads();
1054 timings.block_entity_unloads = start.elapsed();
1055 }
1056
1057 let simulation_snapshot_changed = self.apply_simulation_changes(&batch.simulation_changes);
1058
1059 let (changed_positions, mut rebuild_ticking_snapshot, rebuild_readiness) = {
1060 let _span = tracing::trace_span!("readiness_demotions").entered();
1061 let start = Instant::now();
1062 let changed_positions = batch
1063 .load_changes
1064 .iter()
1065 .map(|change| change.pos)
1066 .collect::<Vec<_>>();
1067 let mut rebuild_ticking_snapshot = simulation_snapshot_changed;
1068 let rebuild_readiness = match self
1069 .prepare_ticking_readiness_demotions(&batch.load_changes)
1070 {
1071 Ok(changed) => {
1072 rebuild_ticking_snapshot |= changed;
1073 false
1074 }
1075 Err(error) => {
1076 tracing::error!(
1077 ?error,
1078 "Full-neighborhood index invariant failed before lifecycle commit; rebuilding after the commit"
1079 );
1080 self.clear_all_ticking_readiness();
1081 *self.full_neighborhood.lock() = FullNeighborhoodIndex::default();
1082 true
1083 }
1084 };
1085 timings.readiness_demotions = start.elapsed();
1086 (
1087 changed_positions,
1088 rebuild_ticking_snapshot,
1089 rebuild_readiness,
1090 )
1091 };
1092
1093 let holders_to_schedule = {
1094 let _span = tracing::trace_span!("lifecycle_commit").entered();
1095 let start = Instant::now();
1096 let holders: Vec<(Arc<ChunkHolder>, ChunkTicketLevel)> = batch
1097 .load_changes
1098 .drain(..)
1099 .filter_map(|change| {
1100 self.update_chunk_level(change.pos, change.new_level)
1101 .zip(change.new_level)
1102 })
1103 .collect();
1104 timings.lifecycle_commit = start.elapsed();
1105 holders
1106 };
1107
1108 let lookup_cache_scope = GameplayChunkLookupCacheScope::enter(self);
1109 let readiness_result = {
1110 let _span = tracing::trace_span!("readiness_reconcile").entered();
1111 let start = Instant::now();
1112 let result = if rebuild_readiness {
1113 rebuild_ticking_snapshot = true;
1114 match self.rebuild_ticking_readiness() {
1115 Ok(result) => result,
1116 Err(error) => self.recover_ticking_readiness_index(error),
1117 }
1118 } else {
1119 match self.reconcile_ticking_readiness_measured(&changed_positions) {
1120 Ok(result) => {
1121 rebuild_ticking_snapshot |= result.snapshot_changed;
1122 result
1123 }
1124 Err(error) => {
1125 rebuild_ticking_snapshot = true;
1126 self.recover_ticking_readiness_index(error)
1127 }
1128 }
1129 };
1130 timings.readiness_reconcile = start.elapsed();
1131 result
1132 };
1133 timings.lookup_cache = lookup_cache_scope.finish();
1134 timings.post_process_generation = readiness_result.post_process_generation;
1135 timings.post_process_chunk_count = readiness_result.post_process_chunk_count;
1136 timings.post_process_position_count = readiness_result.post_process_position_count;
1137 timings.readiness_candidate_count = readiness_result.candidate_count;
1138
1139 if rebuild_ticking_snapshot {
1140 let _span = tracing::trace_span!("ticking_snapshot_rebuild").entered();
1141 let start = Instant::now();
1142 timings.rebuilt_ticking_chunk_count = self.rebuild_ticking_chunk_snapshot();
1143 timings.ticking_snapshot_rebuild = start.elapsed();
1144 }
1145
1146 {
1147 let _span = tracing::trace_span!("schedule_generation").entered();
1148 let start = Instant::now();
1149 timings.scheduled_count = holders_to_schedule
1150 .iter()
1151 .filter(|(holder, level)| {
1152 let Some(status) = generation_status(Some(*level)) else {
1153 return false;
1154 };
1155 holder.schedule_chunk_generation_task_b(status, self)
1156 })
1157 .count();
1158 timings.schedule_generation = start.elapsed();
1159 }
1160
1161 {
1162 let _span = tracing::trace_span!("run_generation").entered();
1163 let start = Instant::now();
1164 self.run_or_notify_generation_refill();
1165 timings.run_generation = start.elapsed();
1166 }
1167
1168 let through_receipt = batch.through_receipt;
1169 if self.deferred_revivals.lock().is_empty() {
1172 self.scheduling.publish_committed(through_receipt);
1173 }
1174 self.scheduling.recycle_update_batch(batch);
1175 timings
1176 }
1177
1178 fn process_pending_unloads(self: &Arc<Self>) {
1179 let staged_revivals = self
1180 .deferred_revivals
1181 .lock()
1182 .keys()
1183 .copied()
1184 .collect::<FxHashSet<_>>();
1185 self.process_unloads(&staged_revivals);
1186 }
1187
1188 pub fn tickable_full_chunk_positions(&self) -> Vec<ChunkPos> {
1190 let snapshot = self.ticking_chunks.load();
1191 snapshot
1192 .entity_indices
1193 .iter()
1194 .map(|&index| snapshot.block[index].pos)
1195 .collect()
1196 }
1197
1198 pub(crate) fn is_entity_ticking_full_chunk_loaded(&self, pos: ChunkPos) -> bool {
1200 self.chunks
1201 .read_sync(&pos, |_, holder| holder.entity_visibility().is_ticking())
1202 .unwrap_or(false)
1203 }
1204
1205 pub(crate) fn block_entity_tick_state_if_owned(
1210 &self,
1211 holder: &Arc<ChunkHolder>,
1212 pos: BlockPos,
1213 expected: &SharedBlockEntity,
1214 ) -> Option<BlockStateId> {
1215 let chunk_pos = ChunkPos::from_block_pos(pos);
1216 let active = self
1217 .chunks
1218 .read_sync(&chunk_pos, |_, current| Arc::ptr_eq(current, holder))
1219 .unwrap_or(false);
1220 if !active
1221 || !is_block_ticking(holder.simulation_level())
1222 || !holder.ticking_readiness_snapshot().is_block_ticking()
1223 {
1224 return None;
1225 }
1226
1227 holder
1228 .try_full_chunk()?
1229 .block_entity_tick_state_if_owned(pos, expected)
1230 }
1231
1232 pub(crate) fn reconcile_block_entity_ticker(&self, holder: &Arc<ChunkHolder>, pos: BlockPos) {
1235 let world = self.world_gen_context.world();
1236 let chunk_pos = ChunkPos::from_block_pos(pos);
1237 let active = self
1238 .chunks
1239 .read_sync(&chunk_pos, |_, current| Arc::ptr_eq(current, holder))
1240 .unwrap_or(false);
1241 if !active {
1242 world.block_entity_tickers().remove(holder, pos);
1243 return;
1244 }
1245
1246 let target = {
1247 let Some(chunk) = holder.try_full_chunk() else {
1248 world.block_entity_tickers().remove(holder, pos);
1249 return;
1250 };
1251 chunk.block_entity_tick_target(pos)
1252 };
1253 let Some((state, block_entity)) = target else {
1254 world.block_entity_tickers().remove(holder, pos);
1255 return;
1256 };
1257 if block_entity.is_removed() {
1258 world.block_entity_tickers().remove(holder, pos);
1259 return;
1260 }
1261
1262 let behavior = BLOCK_BEHAVIORS.get_behavior(state.get_block());
1263 let ticker = behavior.get_block_entity_ticker(&world, state, block_entity.get_type());
1264 let ticker = ticker.filter(|ticker| {
1265 let valid = ticker.accepts(block_entity.get_type());
1266 if !valid {
1267 tracing::error!(
1268 block = %state.get_block().key,
1269 block_entity_type = %block_entity.get_type().key,
1270 ?pos,
1271 "Block behavior returned a ticker for the wrong block-entity type"
1272 );
1273 }
1274 valid
1275 });
1276 world
1277 .block_entity_tickers()
1278 .reconcile(holder, block_entity, ticker);
1279 }
1280
1281 pub(crate) fn activate_block_entities<'a>(
1282 &self,
1283 holders: impl IntoIterator<Item = &'a Arc<ChunkHolder>>,
1284 ) {
1285 for holder in holders {
1286 if !holder.load_level().is_some_and(is_full)
1287 || !self
1288 .chunks
1289 .read_sync(&holder.get_pos(), |_, active| Arc::ptr_eq(active, holder))
1290 .unwrap_or(false)
1291 || !holder.is_full_status_initialized()
1292 || holder.published_status() != Some(ChunkStatus::Full)
1293 {
1294 continue;
1295 }
1296 let batch = {
1297 let Some(chunk) = holder.try_full_chunk() else {
1298 continue;
1299 };
1300 chunk.prepare_block_entity_activation(holder)
1301 };
1302 let Some(batch) = batch else {
1303 continue;
1304 };
1305 for block_entity in batch.lifecycle_dispatchers {
1306 block_entity.dispatch_lifecycle_events();
1307 }
1308 for pos in batch.positions {
1309 {
1310 let Some(chunk) = holder.try_full_chunk() else {
1311 break;
1312 };
1313 chunk.reconcile_block_entity_game_event_listener(pos);
1314 }
1315 self.reconcile_block_entity_ticker(holder, pos);
1316 }
1317 }
1318 }
1319
1320 fn finish_block_entity_unloads(&self) {
1321 let finalized = mem::take(&mut *self.finalized_block_entity_unloads.lock());
1322 if finalized.is_empty() {
1323 return;
1324 }
1325
1326 let world = self.world_gen_context.world();
1327 for mut unload in finalized {
1328 let mut lifecycle_dispatchers = unload
1329 .holder
1330 .try_full_chunk()
1331 .map(|chunk| chunk.deactivate_block_entities(&unload.holder))
1332 .unwrap_or_default();
1333 world
1334 .block_entity_tickers()
1335 .remove_positions(&unload.holder, &unload.positions);
1336 lifecycle_dispatchers.append(&mut unload.lifecycle_dispatchers);
1337 for block_entity in lifecycle_dispatchers {
1338 block_entity.dispatch_lifecycle_events();
1339 }
1340 }
1341 }
1342
1343 pub fn place_ender_pearl_ticket(&self, chunk: ChunkPos) {
1352 self.scheduling.add_or_refresh_ender_pearl_ticket(chunk);
1353 }
1354}
1355
1356#[cfg(test)]
1357mod tests;