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};
35pub(crate) use crate::chunk::chunk_scheduler::ChunkMapSchedulingTimings;
36use crate::chunk::chunk_scheduler::{
37 ChunkMapPreparationTimings, ChunkSchedulingBoundaryStep, ChunkSchedulingCoordinator,
38 ChunkTicketOperation, ChunkTicketRevision, PreparedChunkSchedulingEpoch,
39};
40use crate::chunk::chunk_ticket_manager::{
41 ChunkTicket, ChunkTicketLevel, ChunkTicketManager, ENDER_PEARL_TICKET_TIMEOUT_TICKS,
42 LevelChange, PersistentChunkTickets, TimedChunkTickets, generation_status, is_block_ticking,
43 is_entity_ticking, is_full,
44};
45use crate::chunk::full_chunk_readiness::{
46 FullNeighborhoodCounts, FullNeighborhoodError, FullNeighborhoodIndex, FullPublication,
47 FullPublicationQueue,
48};
49pub use crate::chunk::gameplay_chunk_lookup_cache::GameplayChunkLookupCacheStats;
50use crate::chunk::gameplay_chunk_lookup_cache::{
51 GameplayChunkLookupCacheScope, lookup_or_insert_with,
52};
53use crate::chunk::light::{
54 LIGHT_CACHE_RADIUS, LightCacheLayout, LightCacheSetupRadius, LightLayer,
55 LightSectionEmptinessChange, LightSectionRange, LightWorkWindowGate, LightWorkset,
56 build_chunk_light_update_packet_for_sections,
57 propagate_block_light_changes_with_empty_sections,
58 propagate_sky_light_changes_with_empty_sections,
59};
60use crate::chunk::player_chunk_view::PlayerChunkView;
61use crate::chunk::{
62 Chunk,
63 chunk_generation_task::ChunkGenerationTask,
64 full_chunk::{BlockRandomPositionGenerator, FullChunkRef},
65 section::RandomTickSectionBits,
66 status::ChunkStatus,
67};
68use crate::chunk_saver::ChunkStorage;
69use crate::player::connection::NetworkConnection;
70use crate::world::World;
71use crate::world::tick_scheduler::{BlockTick, FluidTick, ScheduledTickRunBatch};
72use crate::worldgen::{ChunkGeneratorType, WorldGenContext};
73use crate::{entity::Entity, player::Player};
74
75mod generation_readiness;
76mod light_update_state;
77mod light_updates;
78mod persistence;
79mod player_tracking;
80mod scheduled_ticks;
81
82#[cfg(test)]
83use light_update_state::PendingLightUpdates;
84use light_update_state::{InFlightLightUpdates, LightUpdateState, PendingChunkLightUpdates};
85
86const GENERATION_THREAD_MULTIPLE: usize = 2;
87const MAX_SCHEDULED_TICKS_PER_TICK: usize = 65_536;
89
90pub const ENDER_PEARL_TICKET_TIMEOUT: u32 = ENDER_PEARL_TICKET_TIMEOUT_TICKS;
94
95#[derive(Debug, Default)]
97pub struct ChunkMapGameTickTimings {
98 pub broadcast_changes: Duration,
100 pub collect_tickable: Duration,
102 pub tick_chunks: Duration,
104 pub tick_block_entities: Duration,
106 pub tickable_count: usize,
108 pub total_chunks: usize,
110 pub lookup_cache: GameplayChunkLookupCacheStats,
112}
113
114#[derive(Clone)]
115struct TickableChunk {
116 pos: ChunkPos,
117 holder: Arc<ChunkHolder>,
118 randomly_ticking_sections: Arc<RandomTickSectionBits>,
119}
120
121#[derive(Default)]
128struct TickingChunkSnapshot {
129 block: Box<[TickableChunk]>,
130 random_chunk_indices: Box<[usize]>,
131 entity_indices: Box<[usize]>,
132}
133
134struct FinalizedBlockEntityUnload {
135 holder: Arc<ChunkHolder>,
136 lifecycle_dispatchers: Vec<SharedBlockEntity>,
137 positions: Vec<BlockPos>,
138}
139
140struct BlockTickBatchGuard<'a> {
141 world: &'a World,
142 batch: Arc<ScheduledTickRunBatch<BlockRef>>,
143}
144
145impl<'a> BlockTickBatchGuard<'a> {
146 fn new(world: &'a World, ticks: Vec<BlockTick>) -> Self {
147 Self {
148 world,
149 batch: world.begin_scheduled_block_tick_batch(ticks),
150 }
151 }
152
153 fn ticks(&self) -> &[BlockTick] {
154 self.batch.ticks()
155 }
156
157 fn start(&self, index: usize) {
158 self.batch.start(index);
159 }
160}
161
162impl Drop for BlockTickBatchGuard<'_> {
163 fn drop(&mut self) {
164 self.world.end_scheduled_block_tick_batch(&self.batch);
165 }
166}
167
168struct FluidTickBatchGuard<'a> {
169 world: &'a World,
170 batch: Arc<ScheduledTickRunBatch<FluidRef>>,
171}
172
173impl<'a> FluidTickBatchGuard<'a> {
174 fn new(world: &'a World, ticks: Vec<FluidTick>) -> Self {
175 Self {
176 world,
177 batch: world.begin_scheduled_fluid_tick_batch(ticks),
178 }
179 }
180
181 fn ticks(&self) -> &[FluidTick] {
182 self.batch.ticks()
183 }
184
185 fn start(&self, index: usize) {
186 self.batch.start(index);
187 }
188}
189
190impl Drop for FluidTickBatchGuard<'_> {
191 fn drop(&mut self) {
192 self.world.end_scheduled_fluid_tick_batch(&self.batch);
193 }
194}
195
196struct TickingReadinessCandidate {
197 pos: ChunkPos,
198 holder: Arc<ChunkHolder>,
199 desired: TickingReadiness,
200 target: TickingReadiness,
201}
202
203#[derive(Debug, Clone, Copy)]
204struct DeferredChunkRevival {
205 load_level: ChunkTicketLevel,
206 simulation_level: Option<ChunkTicketLevel>,
207}
208
209#[derive(Default)]
210struct ReadinessReconcileResult {
211 snapshot_changed: bool,
212 post_process_generation: Duration,
213 post_process_chunk_count: usize,
214 post_process_position_count: usize,
215 candidate_count: usize,
216}
217
218pub struct ChunkMap {
220 pub(crate) chunks: scc::HashMap<ChunkPos, Arc<ChunkHolder>, FxBuildHasher>,
222 pub(crate) unloading_chunks: scc::HashMap<ChunkPos, Arc<ChunkHolder>, FxBuildHasher>,
224 deferred_revivals: SyncMutex<FxHashMap<ChunkPos, DeferredChunkRevival>>,
226 pub pending_generation_tasks: SyncMutex<Vec<Arc<ChunkGenerationTask>>>,
228 pub task_tracker: TaskTracker,
230 scheduling: ChunkSchedulingCoordinator,
232 full_publications: Arc<FullPublicationQueue>,
234 full_neighborhood: SyncMutex<FullNeighborhoodIndex>,
236 ticking_chunks: ArcSwap<TickingChunkSnapshot>,
238 finalized_block_entity_unloads: SyncMutex<Vec<FinalizedBlockEntityUnload>>,
240 timed_chunk_tickets: SyncMutex<TimedChunkTickets>,
242 pub world_gen_context: Arc<WorldGenContext>,
244 pub generation_pool: Arc<ThreadPool>,
246 chunk_encoding_pool: Arc<ThreadPool>,
248 pub chunk_runtime: Arc<Runtime>,
252 pub storage: Arc<ChunkStorage>,
254 pub chunks_to_broadcast: SyncMutex<Vec<Arc<ChunkHolder>>>,
256 light_updates: SyncMutex<LightUpdateState>,
258 light_updates_progress_notify: Notify,
260 light_work_window_gate: Arc<LightWorkWindowGate>,
262 running_generation_tasks: AtomicUsize,
264 generation_refill_notify: Notify,
266 generation_refill_cancel_token: CancellationToken,
268 generation_refill_stopped: AtomicBool,
270 generation_refill_started: AtomicBool,
272 pub cancel_token: CancellationToken,
275}
276
277#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
278struct GenerationTaskPriority {
279 simulation_bucket: u8,
280 simulation_level: ChunkTicketLevel,
281 load_level: ChunkTicketLevel,
282}
283
284impl GenerationTaskPriority {
285 const fn for_levels(
286 load_level: Option<ChunkTicketLevel>,
287 simulation_level: Option<ChunkTicketLevel>,
288 ) -> Self {
289 let simulation_bucket = if simulation_level.is_some() { 0 } else { 1 };
290 Self {
291 simulation_bucket,
292 simulation_level: match simulation_level {
293 Some(level) => level,
294 None => ChunkTicketLevel::MAX,
295 },
296 load_level: match load_level {
297 Some(level) => level,
298 None => ChunkTicketLevel::MAX,
299 },
300 }
301 }
302}
303
304struct RunningGenerationTaskPermit {
305 chunk_map: Arc<ChunkMap>,
306}
307
308impl Drop for RunningGenerationTaskPermit {
309 fn drop(&mut self) {
310 self.chunk_map
311 .running_generation_tasks
312 .fetch_sub(1, Ordering::AcqRel);
313 self.chunk_map.notify_generation_refill();
314 }
315}
316
317impl ChunkMap {
318 #[must_use]
322 pub fn new_with_storage(
323 chunk_runtime: Arc<Runtime>,
324 world: Weak<World>,
325 dimension_type: DimensionTypeRef,
326 sea_level: i32,
327 storage: Arc<ChunkStorage>,
328 generator: Arc<ChunkGeneratorType>,
329 generation_pool: Arc<ThreadPool>,
330 ) -> Self {
331 let chunk_encoding_pool = Arc::clone(&generation_pool);
332 Self::new_with_storage_and_timed_tickets(
333 chunk_runtime,
334 world,
335 dimension_type,
336 sea_level,
337 storage,
338 generator,
339 generation_pool,
340 chunk_encoding_pool,
341 TimedChunkTickets::default(),
342 )
343 }
344
345 #[must_use]
346 #[expect(
347 clippy::too_many_arguments,
348 reason = "extends ChunkMap::new_with_storage with restored runtime ticket state"
349 )]
350 pub(crate) fn new_with_storage_and_timed_tickets(
351 chunk_runtime: Arc<Runtime>,
352 world: Weak<World>,
353 dimension_type: DimensionTypeRef,
354 sea_level: i32,
355 storage: Arc<ChunkStorage>,
356 generator: Arc<ChunkGeneratorType>,
357 generation_pool: Arc<ThreadPool>,
358 chunk_encoding_pool: Arc<ThreadPool>,
359 timed_chunk_tickets: TimedChunkTickets,
360 ) -> Self {
361 let mut chunk_tickets = ChunkTicketManager::new();
362 timed_chunk_tickets.activate_all(&mut chunk_tickets);
363 let full_publications = Arc::new(FullPublicationQueue::default());
364
365 Self {
366 chunks: scc::HashMap::default(),
367 unloading_chunks: scc::HashMap::default(),
368 deferred_revivals: SyncMutex::new(FxHashMap::default()),
369 pending_generation_tasks: SyncMutex::new(Vec::new()),
370 task_tracker: TaskTracker::new(),
371 scheduling: ChunkSchedulingCoordinator::new(chunk_tickets),
372 full_publications,
373 full_neighborhood: SyncMutex::new(FullNeighborhoodIndex::default()),
374 ticking_chunks: ArcSwap::from_pointee(TickingChunkSnapshot::default()),
375 finalized_block_entity_unloads: SyncMutex::new(Vec::new()),
376 timed_chunk_tickets: SyncMutex::new(timed_chunk_tickets),
377 world_gen_context: Arc::new(WorldGenContext::new(
378 generator,
379 world,
380 dimension_type.min_y,
381 dimension_type.height,
382 sea_level,
383 )),
384 generation_pool,
385 chunk_encoding_pool,
386 chunk_runtime,
387 storage,
388 chunks_to_broadcast: SyncMutex::new(Vec::new()),
389 light_updates: SyncMutex::new(LightUpdateState::default()),
390 light_updates_progress_notify: Notify::new(),
391 light_work_window_gate: Arc::new(LightWorkWindowGate::new()),
392 running_generation_tasks: AtomicUsize::new(0),
393 generation_refill_notify: Notify::new(),
394 generation_refill_cancel_token: CancellationToken::new(),
395 generation_refill_stopped: AtomicBool::new(false),
396 generation_refill_started: AtomicBool::new(false),
397 cancel_token: CancellationToken::new(),
398 }
399 }
400
401 pub(crate) fn light_work_window_gate(&self) -> Arc<LightWorkWindowGate> {
402 Arc::clone(&self.light_work_window_gate)
403 }
404
405 pub fn start_generation_refill_loop(self: &Arc<Self>) {
407 if self.generation_refill_started.swap(true, Ordering::AcqRel) {
408 return;
409 }
410
411 let chunk_map = Arc::clone(self);
412 self.task_tracker.spawn_on(
413 async move {
414 loop {
415 tokio::select! {
416 () = chunk_map.generation_refill_cancel_token.cancelled() => break,
417 () = chunk_map.generation_refill_notify.notified() => {
418 chunk_map.run_generation_tasks_b();
419 }
420 }
421 }
422 },
423 self.chunk_runtime.handle(),
424 );
425 }
426
427 pub fn stop_generation_refill_loop(&self) {
429 self.generation_refill_stopped
430 .store(true, Ordering::Release);
431 self.generation_refill_cancel_token.cancel();
432 self.generation_refill_notify.notify_waiters();
433 }
434
435 pub(crate) fn notify_generation_refill(&self) {
436 self.generation_refill_notify.notify_one();
437 }
438
439 fn run_or_notify_generation_refill(&self) {
440 if self.generation_refill_started.load(Ordering::Acquire) {
441 self.notify_generation_refill();
442 } else {
443 self.run_generation_tasks_b();
444 }
445 }
446
447 pub fn with_full_chunk<F, R>(&self, pos: ChunkPos, f: F) -> Option<R>
450 where
451 F: FnOnce(FullChunkRef<'_>) -> R,
452 {
453 let holder = self.lookup_active_holder(pos)?;
454 if holder.is_status_disallowed(ChunkStatus::Full) {
455 return None;
456 }
457 holder.try_full_chunk().map(f)
458 }
459
460 pub(crate) fn active_full_chunk_holder(&self, pos: ChunkPos) -> Option<Arc<ChunkHolder>> {
462 let holder = self.lookup_active_holder(pos)?;
463 if holder.is_status_disallowed(ChunkStatus::Full)
464 || holder.try_chunk(ChunkStatus::Full).is_none()
465 {
466 return None;
467 }
468 Some(holder)
469 }
470
471 #[doc(hidden)]
476 #[cfg(feature = "benchmark-support")]
477 pub fn insert_benchmark_chunk_holder(&self, pos: ChunkPos, holder: Arc<ChunkHolder>) {
478 assert!(holder.simulation_level().is_none());
479 assert!(self.ticking_chunks.load().block.is_empty());
480 let _ = self.chunks.insert_sync(pos, holder);
481 }
482
483 #[inline]
484 fn lookup_active_holder(&self, pos: ChunkPos) -> Option<Arc<ChunkHolder>> {
485 lookup_or_insert_with(self, pos, || {
486 self.chunks.read_sync(&pos, |_, holder| Arc::clone(holder))
487 })
488 }
489
490 #[must_use]
492 pub(crate) fn is_block_ticking_full_chunk_loaded(&self, pos: ChunkPos) -> bool {
493 self.lookup_active_holder(pos).is_some_and(|holder| {
494 is_block_ticking(holder.load_level())
495 && holder.ticking_readiness_snapshot().is_block_ticking()
496 })
497 }
498
499 #[must_use]
501 pub(crate) fn is_block_ticking_full_chunk_simulated(&self, pos: ChunkPos) -> bool {
502 self.lookup_active_holder(pos).is_some_and(|holder| {
503 is_block_ticking(holder.simulation_level())
504 && holder.ticking_readiness_snapshot().is_block_ticking()
505 })
506 }
507
508 pub(crate) fn with_chunk_at_status<F, R>(
511 &self,
512 pos: ChunkPos,
513 status: ChunkStatus,
514 f: F,
515 ) -> Option<R>
516 where
517 F: FnOnce(&Chunk) -> R,
518 {
519 let chunk_holder = self.lookup_active_holder(pos)?;
520 if chunk_holder.is_status_disallowed(status) {
523 return None;
524 }
525 let chunk = chunk_holder.try_chunk(status)?;
526 Some(f(chunk))
527 }
528
529 pub(crate) fn add_chunk_ticket(
530 &self,
531 pos: ChunkPos,
532 ticket: ChunkTicket,
533 ) -> ChunkTicketRevision {
534 self.scheduling
535 .queue_ticket_operation(ChunkTicketOperation::Add { pos, ticket })
536 }
537
538 pub(crate) fn add_chunk_tickets(
539 &self,
540 positions: &[ChunkPos],
541 ticket: ChunkTicket,
542 ) -> Option<ChunkTicketRevision> {
543 self.scheduling.queue_ticket_operations(
544 positions
545 .iter()
546 .copied()
547 .map(|pos| ChunkTicketOperation::Add { pos, ticket }),
548 )
549 }
550
551 pub(crate) fn remove_chunk_ticket(
552 &self,
553 pos: ChunkPos,
554 ticket: ChunkTicket,
555 ) -> ChunkTicketRevision {
556 self.scheduling
557 .queue_ticket_operation(ChunkTicketOperation::Remove { pos, ticket })
558 }
559
560 pub(crate) fn remove_chunk_tickets(
561 &self,
562 positions: &[ChunkPos],
563 ticket: ChunkTicket,
564 ) -> Option<ChunkTicketRevision> {
565 self.scheduling.queue_ticket_operations(
566 positions
567 .iter()
568 .copied()
569 .map(|pos| ChunkTicketOperation::Remove { pos, ticket }),
570 )
571 }
572
573 fn replace_chunk_ticket(
574 &self,
575 old_pos: ChunkPos,
576 old_ticket: ChunkTicket,
577 new_pos: ChunkPos,
578 new_ticket: ChunkTicket,
579 ) {
580 let operations = [
581 ChunkTicketOperation::Remove {
582 pos: old_pos,
583 ticket: old_ticket,
584 },
585 ChunkTicketOperation::Add {
586 pos: new_pos,
587 ticket: new_ticket,
588 },
589 ];
590 let _ = self.scheduling.queue_ticket_operations(operations);
591 }
592
593 pub(crate) fn is_ticket_revision_committed(&self, revision: ChunkTicketRevision) -> bool {
594 self.scheduling.is_revision_committed(revision)
595 }
596
597 pub(crate) async fn with_full_chunks_in_radius<F, R>(
600 self: &Arc<Self>,
601 center: ChunkPos,
602 radius: u8,
603 f: F,
604 ) -> Option<R>
605 where
606 F: FnOnce() -> R,
607 {
608 let ticket = ChunkTicket::full_chunks(radius);
609
610 let ticket_revision = self.add_chunk_ticket(center, ticket);
611 let radius = i32::from(radius);
612
613 loop {
614 self.advance_scheduling();
615 if self.is_ticket_revision_committed(ticket_revision)
616 && self.full_square_is_ready(center, radius)
617 {
618 break;
619 }
620
621 if self.cancel_token.is_cancelled() {
622 self.remove_chunk_ticket(center, ticket);
623 self.advance_scheduling();
624 return None;
625 }
626
627 sleep(Duration::from_millis(10)).await;
628 }
629
630 let result = f();
631 self.remove_chunk_ticket(center, ticket);
632 self.advance_scheduling();
633
634 Some(result)
635 }
636
637 pub(crate) fn place_portal_ticket(&self, ticket_position: BlockPos) {
639 let center = ChunkPos::from_block_pos(ticket_position);
640 let mut timed_tickets = self.timed_chunk_tickets.lock();
641 let ticket = timed_tickets.add_portal_ticket(center);
642 if let Some(ticket) = ticket {
643 self.add_chunk_ticket(center, ticket);
644 }
645 }
646
647 pub(crate) fn tick_timed_tickets(&self) {
649 let mut timed_tickets = self.timed_chunk_tickets.lock();
650 let expired = timed_tickets.tick(|pos| self.can_timed_ticket_expire(pos));
651 let _ = self.scheduling.queue_ticket_operations(
652 expired
653 .into_iter()
654 .map(|(pos, ticket)| ChunkTicketOperation::Remove { pos, ticket }),
655 );
656 }
657
658 pub(crate) fn persistent_chunk_tickets(&self) -> PersistentChunkTickets {
659 self.timed_chunk_tickets.lock().to_persistent()
660 }
661
662 fn can_timed_ticket_expire(&self, pos: ChunkPos) -> bool {
663 self.chunks
664 .read_sync(&pos, |_, holder| holder.is_ready_for_saving())
665 .unwrap_or(true)
666 }
667
668 fn full_square_is_ready(&self, center: ChunkPos, radius: i32) -> bool {
669 for dz in -radius..=radius {
670 for dx in -radius..=radius {
671 let pos = ChunkPos::new(center.0.x + dx, center.0.y + dz);
672 let Some(holder) = self.chunks.read_sync(&pos, |_, holder| holder.clone()) else {
673 return false;
674 };
675 if holder.try_chunk(ChunkStatus::Full).is_none() {
676 return false;
677 }
678 }
679 }
680 true
681 }
682
683 #[expect(
685 clippy::too_many_lines,
686 reason = "block and light packet construction share the same holder drain"
687 )]
688 pub fn broadcast_changed_chunks(&self) {
689 self.propagate_queued_light_changes();
690
691 let holders = {
692 let mut guard = self.chunks_to_broadcast.lock();
693 if guard.is_empty() {
694 return;
695 }
696 mem::take(&mut *guard)
697 };
698
699 let mut world = None;
700
701 for holder in holders {
702 let chunk_pos = holder.get_pos();
703 let world = world.get_or_insert_with(|| self.world_gen_context.world());
705 let has_skylight = world.dimension_type.has_skylight;
706 let min_y = holder.min_y();
707 holder.clear_broadcast_queued();
708
709 let light_changes = holder.take_changed_light_sections();
710 let changes_by_section = holder.take_changed_blocks();
712 let has_publishable_light_changes =
713 !light_changes.block.is_empty() || (has_skylight && !light_changes.sky.is_empty());
714
715 if !has_publishable_light_changes && changes_by_section.is_empty() {
716 continue;
717 }
718
719 if has_publishable_light_changes
720 && let Some(chunk) = holder.try_chunk(ChunkStatus::Full)
721 {
722 let tracking_players = world.get_light_packet_tracking_players(chunk_pos);
723 if !tracking_players.is_empty() {
724 let light_data = {
725 let light = chunk.light();
726 let sky_sections = if has_skylight {
727 light_changes.sky.as_slice()
728 } else {
729 &[]
730 };
731 build_chunk_light_update_packet_for_sections(
732 chunk_pos,
733 &light,
734 has_skylight,
735 sky_sections,
736 &light_changes.block,
737 )
738 };
739 let light_packet = CLightUpdate {
740 x: chunk_pos.0.x,
741 z: chunk_pos.0.y,
742 light_data,
743 };
744
745 let Ok(encoded) = EncodedPacket::from_bare(
746 light_packet,
747 world.compression,
748 ConnectionProtocol::Play,
749 ) else {
750 log::warn!("Failed to encode light update packet");
751 continue;
752 };
753
754 for entity_id in &tracking_players {
755 if let Some(player) = world.players.get_by_entity_id(*entity_id) {
756 player.connection.send_encoded(encoded.clone());
757 }
758 }
759 }
760 }
761
762 if changes_by_section.is_empty() {
763 continue;
764 }
765
766 let tracking_players = world.get_packet_tracking_players(chunk_pos);
768 if tracking_players.is_empty() {
769 continue;
770 }
771
772 for (section_index, changed_positions) in changes_by_section {
774 let section_y = min_y / 16 + section_index as i32;
775 let section_pos = SectionPos::new(chunk_pos.0.x, section_y, chunk_pos.0.y);
776
777 if changed_positions.len() == 1 {
778 let Some(&packed) = changed_positions.iter().next() else {
780 continue;
781 };
782 let block_pos = section_pos.relative_to_block_pos(packed);
783 let block_state = world.get_block_state(block_pos);
784
785 tracing::trace!(
786 ?block_pos,
787 ?block_state,
788 player_count = tracking_players.len(),
789 "Broadcasting single block update"
790 );
791
792 let update_packet = CBlockUpdate {
793 pos: block_pos,
794 block_state,
795 };
796
797 let Ok(encoded) = EncodedPacket::from_bare(
798 update_packet,
799 world.compression,
800 ConnectionProtocol::Play,
801 ) else {
802 log::warn!("Failed to encode block update packet");
803 continue;
804 };
805
806 for entity_id in &tracking_players {
807 if let Some(player) = world.players.get_by_entity_id(*entity_id) {
808 player.connection.send_encoded(encoded.clone());
809 }
810 }
811 world.broadcast_block_entity_if_needed(block_pos);
812 } else {
813 let changes: Vec<BlockChange> = changed_positions
815 .iter()
816 .map(|&packed| {
817 let block_pos = section_pos.relative_to_block_pos(packed);
818 let block_state = world.get_block_state(block_pos);
819 BlockChange {
820 pos: packed,
821 block_state,
822 }
823 })
824 .collect();
825
826 tracing::trace!(
827 change_count = changes.len(),
828 ?section_pos,
829 player_count = tracking_players.len(),
830 "Broadcasting section block updates"
831 );
832
833 let packet = CSectionBlocksUpdate {
834 section_pos,
835 changes,
836 };
837
838 let Ok(encoded) = EncodedPacket::from_bare(
839 packet,
840 world.compression,
841 ConnectionProtocol::Play,
842 ) else {
843 log::warn!("Failed to encode section block update packet");
844 continue;
845 };
846
847 for entity_id in &tracking_players {
848 if let Some(player) = world.players.get_by_entity_id(*entity_id) {
849 player.connection.send_encoded(encoded.clone());
850 }
851 }
852 for &packed in &changed_positions {
853 let block_pos = section_pos.relative_to_block_pos(packed);
854 world.broadcast_block_entity_if_needed(block_pos);
855 }
856 }
857 }
858 }
859 }
860
861 #[instrument(level = "trace", skip(self, world), name = "chunk_map_game_tick")]
869 pub fn tick_game(
870 self: &Arc<Self>,
871 world: &Arc<World>,
872 tick_count: u64,
873 random_tick_speed: u32,
874 runs_normally: bool,
875 ) -> ChunkMapGameTickTimings {
876 let mut timings = ChunkMapGameTickTimings::default();
877
878 if tick_count.is_multiple_of(100) {
879 tracing::debug!(
880 chunks = self.chunks.len(),
881 unloading = self.unloading_chunks.len(),
882 "Chunk map status"
883 );
884 }
885
886 if !runs_normally {
887 let _span = tracing::trace_span!("broadcast_changes").entered();
888 let start = Instant::now();
889 self.broadcast_changed_chunks();
890 timings.broadcast_changes = start.elapsed();
891 return timings;
892 }
893
894 {
895 let _span = tracing::trace_span!("collect_tickable").entered();
896 let start = Instant::now();
897 let tickable_chunks = self.ticking_chunks.load();
898 timings.collect_tickable = start.elapsed();
899 timings.total_chunks = self.chunks.len();
900 timings.tickable_count = tickable_chunks.block.len();
901
902 if !tickable_chunks.block.is_empty() {
903 let _span = tracing::trace_span!(
904 "tick_chunks",
905 block_ticking_count = tickable_chunks.block.len(),
906 total_chunks = timings.total_chunks
907 )
908 .entered();
909 let start = Instant::now();
910 let current_tick = world.game_time();
913 let ready_block_ticks =
914 Self::collect_scheduled_block_ticks(world, &tickable_chunks, current_tick);
915 Self::execute_scheduled_block_ticks(world, ready_block_ticks);
916
917 let ready_fluid_ticks =
918 Self::collect_scheduled_fluid_ticks(world, &tickable_chunks, current_tick);
919 Self::execute_scheduled_fluid_ticks(world, ready_fluid_ticks);
920
921 if random_tick_speed > 0 {
922 let mut random_positions = BlockRandomPositionGenerator::from_runtime_rng();
925 for &index in &tickable_chunks.random_chunk_indices {
926 let tickable_chunk = &tickable_chunks.block[index];
929 if tickable_chunk.randomly_ticking_sections.is_empty() {
930 continue;
931 }
932 if let Some(chunk) = tickable_chunk.holder.try_full_chunk() {
933 chunk.tick_random_blocks(
934 world,
935 random_tick_speed,
936 &mut random_positions,
937 );
938 }
939 }
940 }
941 timings.tick_chunks = start.elapsed();
942 }
943 }
944
945 {
946 let _span = tracing::trace_span!("broadcast_changes").entered();
947 let start = Instant::now();
948 self.broadcast_changed_chunks();
949 timings.broadcast_changes = start.elapsed();
950 }
951
952 timings
953 }
954
955 #[instrument(level = "trace", skip(self), name = "advance_chunk_scheduling")]
963 pub(crate) fn advance_scheduling(self: &Arc<Self>) -> ChunkMapSchedulingTimings {
964 match self.scheduling.take_boundary_step() {
965 ChunkSchedulingBoundaryStep::Running => ChunkMapSchedulingTimings::default(),
966 ChunkSchedulingBoundaryStep::Start {
967 ticket_manager,
968 applied_revision,
969 } => {
970 self.spawn_scheduling_epoch(ticket_manager, applied_revision, Vec::new());
971 ChunkMapSchedulingTimings::default()
972 }
973 ChunkSchedulingBoundaryStep::Commit(epoch) => self.commit_scheduling_epoch(epoch),
974 }
975 }
976
977 fn commit_scheduling_epoch(
978 self: &Arc<Self>,
979 epoch: PreparedChunkSchedulingEpoch,
980 ) -> ChunkMapSchedulingTimings {
981 let PreparedChunkSchedulingEpoch {
982 mut ticket_manager,
983 applied_revision,
984 mut changes,
985 timings,
986 } = epoch;
987 let mut timings = timings.into_scheduling_timings();
988
989 self.merge_deferred_revivals(&mut changes);
990
991 {
992 let _span = tracing::trace_span!("block_entity_unloads").entered();
993 let start = Instant::now();
994 self.finish_block_entity_unloads();
997 timings.block_entity_unloads = start.elapsed();
998 }
999
1000 let (changed_positions, mut rebuild_ticking_snapshot, rebuild_readiness) = {
1001 let _span = tracing::trace_span!("readiness_demotions").entered();
1002 let start = Instant::now();
1003 let changed_positions = changes.iter().map(|change| change.pos).collect::<Vec<_>>();
1004 let mut rebuild_ticking_snapshot = self.simulation_changes_ticking_snapshot(&changes);
1005 let rebuild_readiness = match self.prepare_ticking_readiness_demotions(&changes) {
1006 Ok(changed) => {
1007 rebuild_ticking_snapshot |= changed;
1008 false
1009 }
1010 Err(error) => {
1011 tracing::error!(
1012 ?error,
1013 "Full-neighborhood index invariant failed before lifecycle commit; rebuilding after the commit"
1014 );
1015 self.clear_all_ticking_readiness();
1016 *self.full_neighborhood.lock() = FullNeighborhoodIndex::default();
1017 true
1018 }
1019 };
1020 timings.readiness_demotions = start.elapsed();
1021 (
1022 changed_positions,
1023 rebuild_ticking_snapshot,
1024 rebuild_readiness,
1025 )
1026 };
1027
1028 let holders_to_schedule = {
1029 let _span = tracing::trace_span!("lifecycle_commit").entered();
1030 let start = Instant::now();
1031 let holders = changes
1032 .drain(..)
1033 .filter_map(|change| {
1034 self.update_chunk_level(
1035 change.pos,
1036 change.new_level,
1037 change.new_simulation_level,
1038 )
1039 .zip(change.new_level)
1040 })
1041 .collect();
1042 timings.lifecycle_commit = start.elapsed();
1043 holders
1044 };
1045
1046 let lookup_cache_scope = GameplayChunkLookupCacheScope::enter(self);
1047 let readiness_result = {
1048 let _span = tracing::trace_span!("readiness_reconcile").entered();
1049 let start = Instant::now();
1050 let result = if rebuild_readiness {
1051 rebuild_ticking_snapshot = true;
1052 match self.rebuild_ticking_readiness() {
1053 Ok(result) => result,
1054 Err(error) => self.recover_ticking_readiness_index(error),
1055 }
1056 } else {
1057 match self.reconcile_ticking_readiness_measured(&changed_positions) {
1058 Ok(result) => {
1059 rebuild_ticking_snapshot |= result.snapshot_changed;
1060 result
1061 }
1062 Err(error) => {
1063 rebuild_ticking_snapshot = true;
1064 self.recover_ticking_readiness_index(error)
1065 }
1066 }
1067 };
1068 timings.readiness_reconcile = start.elapsed();
1069 result
1070 };
1071 timings.lookup_cache = lookup_cache_scope.finish();
1072 timings.post_process_generation = readiness_result.post_process_generation;
1073 timings.post_process_chunk_count = readiness_result.post_process_chunk_count;
1074 timings.post_process_position_count = readiness_result.post_process_position_count;
1075 timings.readiness_candidate_count = readiness_result.candidate_count;
1076
1077 if rebuild_ticking_snapshot {
1078 let _span = tracing::trace_span!("ticking_snapshot_rebuild").entered();
1079 let start = Instant::now();
1080 timings.rebuilt_ticking_chunk_count = self.rebuild_ticking_chunk_snapshot();
1081 timings.ticking_snapshot_rebuild = start.elapsed();
1082 }
1083
1084 ticket_manager.recycle_changes(changes);
1085 self.scheduling.publish_committed_revision(applied_revision);
1086 self.spawn_scheduling_epoch(ticket_manager, applied_revision, holders_to_schedule);
1087 timings
1088 }
1089
1090 fn spawn_scheduling_epoch(
1091 self: &Arc<Self>,
1092 ticket_manager: ChunkTicketManager,
1093 applied_revision: ChunkTicketRevision,
1094 holders_to_schedule: Vec<(Arc<ChunkHolder>, ChunkTicketLevel)>,
1095 ) {
1096 let chunk_map = Arc::clone(self);
1097 drop(self.task_tracker.spawn_blocking_on(
1099 move || {
1100 let epoch = chunk_map.prepare_scheduling_epoch(
1101 ticket_manager,
1102 applied_revision,
1103 holders_to_schedule,
1104 );
1105 chunk_map.scheduling.finish_epoch(epoch);
1106 },
1107 self.chunk_runtime.handle(),
1108 ));
1109 }
1110
1111 #[instrument(level = "trace", skip(self, ticket_manager, holders_to_schedule))]
1112 fn prepare_scheduling_epoch(
1113 self: &Arc<Self>,
1114 mut ticket_manager: ChunkTicketManager,
1115 applied_revision: ChunkTicketRevision,
1116 holders_to_schedule: Vec<(Arc<ChunkHolder>, ChunkTicketLevel)>,
1117 ) -> PreparedChunkSchedulingEpoch {
1118 let mut timings = ChunkMapPreparationTimings::default();
1119
1120 let applied_revision = {
1121 let _span = tracing::trace_span!("ticket_updates").entered();
1122 let start = Instant::now();
1123 let revision = self
1124 .scheduling
1125 .apply_pending_ticket_operations(&mut ticket_manager, applied_revision);
1126 ticket_manager.run_all_updates();
1127 timings.ticket_updates = start.elapsed();
1128 revision
1129 };
1130 let changes = ticket_manager.take_changes();
1131
1132 {
1133 let _span = tracing::trace_span!("schedule_generation").entered();
1134 let start = Instant::now();
1135 timings.scheduled_count = holders_to_schedule
1136 .iter()
1137 .filter(|(holder, level)| {
1138 let Some(status) = generation_status(Some(*level)) else {
1139 return false;
1140 };
1141 holder.schedule_chunk_generation_task_b(status, self)
1142 })
1143 .count();
1144 timings.schedule_generation = start.elapsed();
1145 }
1146
1147 {
1148 let _span = tracing::trace_span!("run_generation").entered();
1149 let start = Instant::now();
1150 self.run_or_notify_generation_refill();
1151 timings.run_generation = start.elapsed();
1152 }
1153
1154 {
1155 let _span = tracing::trace_span!("process_unloads").entered();
1156 let start = Instant::now();
1157 let staged_revivals = changes
1158 .iter()
1159 .filter(|change| {
1160 change.new_level.is_some() && self.unloading_chunks.contains_sync(&change.pos)
1161 })
1162 .map(|change| change.pos)
1163 .chain(self.deferred_revivals.lock().keys().copied())
1164 .collect::<FxHashSet<_>>();
1165 self.process_unloads(&staged_revivals);
1166 timings.process_unloads = start.elapsed();
1167 }
1168
1169 PreparedChunkSchedulingEpoch {
1170 ticket_manager,
1171 applied_revision,
1172 changes,
1173 timings,
1174 }
1175 }
1176
1177 pub fn tickable_full_chunk_positions(&self) -> Vec<ChunkPos> {
1179 let snapshot = self.ticking_chunks.load();
1180 snapshot
1181 .entity_indices
1182 .iter()
1183 .map(|&index| snapshot.block[index].pos)
1184 .collect()
1185 }
1186
1187 pub(crate) fn is_entity_ticking_full_chunk_loaded(&self, pos: ChunkPos) -> bool {
1189 self.chunks
1190 .read_sync(&pos, |_, holder| holder.entity_visibility().is_ticking())
1191 .unwrap_or(false)
1192 }
1193
1194 pub(crate) fn block_entity_tick_state_if_owned(
1199 &self,
1200 holder: &Arc<ChunkHolder>,
1201 pos: BlockPos,
1202 expected: &SharedBlockEntity,
1203 ) -> Option<BlockStateId> {
1204 let chunk_pos = ChunkPos::from_block_pos(pos);
1205 let active = self
1206 .chunks
1207 .read_sync(&chunk_pos, |_, current| Arc::ptr_eq(current, holder))
1208 .unwrap_or(false);
1209 if !active
1210 || !is_block_ticking(holder.simulation_level())
1211 || !holder.ticking_readiness_snapshot().is_block_ticking()
1212 {
1213 return None;
1214 }
1215
1216 holder
1217 .try_full_chunk()?
1218 .block_entity_tick_state_if_owned(pos, expected)
1219 }
1220
1221 pub(crate) fn reconcile_block_entity_ticker(&self, holder: &Arc<ChunkHolder>, pos: BlockPos) {
1224 let world = self.world_gen_context.world();
1225 let chunk_pos = ChunkPos::from_block_pos(pos);
1226 let active = self
1227 .chunks
1228 .read_sync(&chunk_pos, |_, current| Arc::ptr_eq(current, holder))
1229 .unwrap_or(false);
1230 if !active {
1231 world.block_entity_tickers().remove(holder, pos);
1232 return;
1233 }
1234
1235 let target = {
1236 let Some(chunk) = holder.try_full_chunk() else {
1237 world.block_entity_tickers().remove(holder, pos);
1238 return;
1239 };
1240 chunk.block_entity_tick_target(pos)
1241 };
1242 let Some((state, block_entity)) = target else {
1243 world.block_entity_tickers().remove(holder, pos);
1244 return;
1245 };
1246 if block_entity.is_removed() {
1247 world.block_entity_tickers().remove(holder, pos);
1248 return;
1249 }
1250
1251 let behavior = BLOCK_BEHAVIORS.get_behavior(state.get_block());
1252 let ticker = behavior.get_block_entity_ticker(&world, state, block_entity.get_type());
1253 let ticker = ticker.filter(|ticker| {
1254 let valid = ticker.accepts(block_entity.get_type());
1255 if !valid {
1256 tracing::error!(
1257 block = %state.get_block().key,
1258 block_entity_type = %block_entity.get_type().key,
1259 ?pos,
1260 "Block behavior returned a ticker for the wrong block-entity type"
1261 );
1262 }
1263 valid
1264 });
1265 world
1266 .block_entity_tickers()
1267 .reconcile(holder, block_entity, ticker);
1268 }
1269
1270 pub(crate) fn activate_block_entities<'a>(
1271 &self,
1272 holders: impl IntoIterator<Item = &'a Arc<ChunkHolder>>,
1273 ) {
1274 for holder in holders {
1275 if !holder.load_level().is_some_and(is_full)
1276 || !self
1277 .chunks
1278 .read_sync(&holder.get_pos(), |_, active| Arc::ptr_eq(active, holder))
1279 .unwrap_or(false)
1280 || !holder.is_full_status_initialized()
1281 || holder.published_status() != Some(ChunkStatus::Full)
1282 {
1283 continue;
1284 }
1285 let batch = {
1286 let Some(chunk) = holder.try_full_chunk() else {
1287 continue;
1288 };
1289 chunk.prepare_block_entity_activation(holder)
1290 };
1291 let Some(batch) = batch else {
1292 continue;
1293 };
1294 for block_entity in batch.lifecycle_dispatchers {
1295 block_entity.dispatch_lifecycle_events();
1296 }
1297 for pos in batch.positions {
1298 {
1299 let Some(chunk) = holder.try_full_chunk() else {
1300 break;
1301 };
1302 chunk.reconcile_block_entity_game_event_listener(pos);
1303 }
1304 self.reconcile_block_entity_ticker(holder, pos);
1305 }
1306 }
1307 }
1308
1309 fn finish_block_entity_unloads(&self) {
1310 let finalized = mem::take(&mut *self.finalized_block_entity_unloads.lock());
1311 if finalized.is_empty() {
1312 return;
1313 }
1314
1315 let world = self.world_gen_context.world();
1316 for mut unload in finalized {
1317 let mut lifecycle_dispatchers = unload
1318 .holder
1319 .try_full_chunk()
1320 .map(|chunk| chunk.deactivate_block_entities(&unload.holder))
1321 .unwrap_or_default();
1322 world
1323 .block_entity_tickers()
1324 .remove_positions(&unload.holder, &unload.positions);
1325 lifecycle_dispatchers.append(&mut unload.lifecycle_dispatchers);
1326 for block_entity in lifecycle_dispatchers {
1327 block_entity.dispatch_lifecycle_events();
1328 }
1329 }
1330 }
1331
1332 pub fn place_ender_pearl_ticket(&self, chunk: ChunkPos) {
1342 let mut timed_tickets = self.timed_chunk_tickets.lock();
1343 let ticket = timed_tickets.add_ender_pearl_ticket(chunk);
1344 if let Some(ticket) = ticket {
1345 self.add_chunk_ticket(chunk, ticket);
1346 }
1347 }
1348}
1349
1350#[cfg(test)]
1351mod tests;