1use std::{
3 fmt,
4 io::Cursor,
5 mem,
6 sync::{Arc, Weak, atomic::Ordering},
7};
8
9use rustc_hash::FxHashSet;
10use steel_protocol::packets::game::{
11 BlockEntityInfo, ChunkPacketData, HeightmapType as ProtocolHeightmapType, Heightmaps,
12 LightUpdatePacketData,
13};
14use steel_registry::{
15 REGISTRY, RegistryEntry,
16 blocks::{BlockRef, block_state_ext::BlockStateExt},
17 fluid::FluidRef,
18 vanilla_blocks,
19};
20use steel_utils::{
21 BlockPos, BlockStateId, ChunkPos, Direction, PackedChunkLocalXZ, SectionPos, types::UpdateFlags,
22};
23
24use steel_utils::locks::SyncMutex;
25
26use crate::behavior::{BLOCK_BEHAVIORS, BlockEntityCreation, FLUID_BEHAVIORS};
27use crate::block_entity::{
28 BlockEntity, BlockEntityInsert, BlockEntityLifecycleExt as _, BlockEntityLookup,
29 ClearedBlockEntities, DetachedBlockEntity, LifecycleDispatchers, SharedBlockEntity,
30};
31use crate::chunk::{
32 Chunk,
33 block_entity_listener::{FullChunkGameEventListeners, ListenerSelectionCommit},
34 chunk_holder::ChunkHolder,
35 data::empty_postprocessing,
36 heightmap::{ChunkHeightmaps, HeightmapType},
37 light::{
38 ChunkLightData, LightSectionEmptinessChange, build_chunk_light_update_packet,
39 has_different_light_properties,
40 },
41 section::Sections,
42 status::ChunkStatus,
43};
44use crate::entity::SharedEntity;
45use crate::world::tick_scheduler::{
46 BlockTickList, ChunkTickContainer, FluidTickList, ScheduledTickSnapshot, TickPriority,
47 TickSchedulerError,
48};
49use crate::world::{World, game_event::GameEventListenerCount};
50use steel_worldgen::structure::{StructureReferenceMap, StructureStartMap};
51
52#[derive(Clone, Copy)]
59pub struct FullChunkRef<'a> {
60 chunk: &'a Chunk,
61}
62
63pub(crate) struct FullChunkRuntime {
65 game_event_listeners: FullChunkGameEventListeners,
67 block_entity_activation: SyncMutex<BlockEntityActivation>,
69}
70
71impl FullChunkRuntime {
72 fn new(listener_count: Arc<GameEventListenerCount>) -> Self {
73 Self {
74 game_event_listeners: FullChunkGameEventListeners::new(listener_count),
75 block_entity_activation: SyncMutex::new(BlockEntityActivation::default()),
76 }
77 }
78}
79
80impl fmt::Debug for FullChunkRuntime {
81 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
82 f.debug_struct("FullChunkRuntime").finish_non_exhaustive()
83 }
84}
85
86#[derive(Default)]
87struct BlockEntityActivation {
88 holder: Weak<ChunkHolder>,
89 pending_lifecycle_dispatchers: Vec<SharedBlockEntity>,
90}
91
92pub(crate) struct BlockEntityActivationBatch {
93 pub(crate) lifecycle_dispatchers: Vec<SharedBlockEntity>,
94 pub(crate) positions: Vec<BlockPos>,
95}
96
97pub struct FullChunkPromotion<'a> {
99 pub chunk: FullChunkRef<'a>,
101 pub pending_entities: Vec<SharedEntity>,
103}
104
105#[derive(Clone, Copy, Debug, Eq, PartialEq)]
106pub(crate) enum FullChunkBlockSetResult {
107 Changed(BlockStateId),
108 Unchanged,
109 Stale(BlockStateId),
110}
111
112enum PendingPromotionCommit {
113 Retry,
114 Complete {
115 block_entity: Option<SharedBlockEntity>,
116 lifecycle_dispatchers: LifecycleDispatchers,
117 },
118}
119
120enum ProtoBlockEntityAdoption {
121 Retry,
122 Gone,
123 Discarded,
124 Adopted(LifecycleDispatchers),
125}
126
127fn random_tick_kinds(state: BlockStateId) -> Option<(bool, Option<FluidRef>)> {
128 let metadata = state.get_ticking_metadata();
129 let tick_block = metadata.randomly_ticking_block();
130 let tick_fluid = metadata
131 .randomly_ticking_fluid()
132 .then_some(metadata.fluid_state().fluid_id);
133 (tick_block || tick_fluid.is_some()).then_some((tick_block, tick_fluid))
134}
135
136pub(crate) struct BlockRandomPositionGenerator {
138 value: i32,
139}
140
141impl BlockRandomPositionGenerator {
142 pub(crate) fn from_runtime_rng() -> Self {
143 Self::from_seed(rand::random())
144 }
145
146 const fn from_seed(value: i32) -> Self {
147 Self { value }
148 }
149
150 const fn next_local(&mut self) -> (usize, usize, usize) {
151 self.value = self.value.wrapping_mul(3).wrapping_add(1_013_904_223);
152 let value = self.value >> 2;
153 (
154 (value & 15) as usize,
155 ((value >> 16) & 15) as usize,
156 ((value >> 8) & 15) as usize,
157 )
158 }
159}
160
161#[expect(
162 clippy::trivially_copy_pass_by_ref,
163 reason = "FullChunkRef is a borrowed capability with a conventional shared-receiver API"
164)]
165impl FullChunkRef<'_> {
166 pub(crate) const fn from_full_context(chunk: &Chunk) -> FullChunkRef<'_> {
167 FullChunkRef { chunk }
168 }
169
170 fn runtime(&self) -> &FullChunkRuntime {
171 let Some(runtime) = self.chunk.full_runtime() else {
172 panic!("Full chunk view was exposed without initialized runtime state");
173 };
174 runtime
175 }
176
177 pub(crate) fn game_event_listeners(&self) -> &FullChunkGameEventListeners {
178 &self.runtime().game_event_listeners
179 }
180
181 #[must_use]
183 pub(crate) const fn common(&self) -> &Chunk {
184 self.chunk
185 }
186
187 #[must_use]
189 pub const fn sections(&self) -> &Sections {
190 self.chunk.sections()
191 }
192
193 pub(crate) fn tick_random_blocks(
195 &self,
196 world: &Arc<World>,
197 random_tick_speed: u32,
198 random_positions: &mut BlockRandomPositionGenerator,
199 ) {
200 if random_tick_speed == 0 {
201 return;
202 }
203
204 let block_behaviors = &*BLOCK_BEHAVIORS;
205 let fluid_behaviors = &*FLUID_BEHAVIORS;
206 let chunk_base_x = self.chunk.pos.0.x * 16;
207 let chunk_base_z = self.chunk.pos.0.y * 16;
208 let random_tick_sections = self.chunk.sections.random_tick_sections();
209 let mut next_section_index = 0;
210
211 while let Some(section_index) = random_tick_sections.next(next_section_index) {
212 next_section_index = section_index + 1;
213 let section = &self.chunk.sections.sections[section_index];
214 let section_base_y = self.min_y() + (section_index as i32 * 16);
215 let mut section_guard = None;
218
219 for _ in 0..random_tick_speed {
220 let (local_x, local_y, local_z) = random_positions.next_local();
221 let state = section_guard
222 .get_or_insert_with(|| section.read())
223 .states
224 .get(local_x, local_y, local_z);
225 let Some((tick_block, tick_fluid)) = random_tick_kinds(state) else {
226 continue;
227 };
228 drop(section_guard.take());
231 let pos = BlockPos::new(
232 chunk_base_x + local_x as i32,
233 section_base_y + local_y as i32,
234 chunk_base_z + local_z as i32,
235 );
236
237 if tick_block {
238 block_behaviors
239 .get_behavior(state.get_block())
240 .random_tick(state, world, pos);
241 }
242 if let Some(fluid) = tick_fluid {
243 fluid_behaviors.get_behavior(fluid).random_tick(world, pos);
244 }
245 }
246 }
247 }
248}
249
250impl Chunk {
251 fn initialize_full_runtime_state(&self, listener_count: Arc<GameEventListenerCount>) {
252 let runtime = FullChunkRuntime::new(listener_count);
253 assert!(
254 self.initialize_full_runtime(runtime).is_ok(),
255 "Full chunk runtime was initialized more than once"
256 );
257 }
258
259 #[must_use]
268 pub(crate) fn promote_to_full(&self) -> FullChunkPromotion<'_> {
269 let proto_chunk = self;
270 proto_chunk.clear_transient_generation_state();
273 let min_y = proto_chunk.min_y();
274 let height = proto_chunk.height();
275 let level = proto_chunk.level_weak();
276 {
280 let mut heightmaps = proto_chunk.heightmaps.write();
281 heightmaps.prime_from_sections(
282 HeightmapType::final_types(),
283 min_y,
284 height,
285 &proto_chunk.sections.sections,
286 );
287 for &heightmap_type in HeightmapType::final_types() {
288 let _ = heightmaps.get_final(heightmap_type);
289 }
290 }
291
292 for section in &proto_chunk.sections.sections {
294 section.write().recalculate_counts();
295 }
296
297 assert!(
301 proto_chunk.scheduled_tick_container().promote_to_full(),
302 "Proto chunk scheduled-tick container was already promoted"
303 );
304 let pending_entities = proto_chunk.entities.close_and_drain();
305 if let Err(error) = proto_chunk
306 .light
307 .write()
308 .refresh_emptiness_maps_from_sections(&proto_chunk.sections)
309 {
310 panic!("invalid proto chunk light emptiness map length: {error:?}");
311 }
312
313 FullChunkRef::populate_poi(&level, &proto_chunk.sections, proto_chunk.pos, min_y);
314 let game_event_listener_count = level
315 .upgrade()
316 .map_or_else(GameEventListenerCount::shared, |world| {
317 world.game_event_listener_count()
318 });
319
320 proto_chunk.initialize_full_runtime_state(game_event_listener_count);
321 let full = FullChunkRef::from_full_context(proto_chunk);
322 full.adopt_proto_block_entities();
323 FullChunkPromotion {
324 chunk: full,
325 pending_entities,
326 }
327 }
328
329 #[must_use]
349 #[expect(
350 clippy::too_many_arguments,
351 reason = "all parameters are required to fully restore a chunk from disk"
352 )]
353 pub(crate) fn from_full_disk(
354 sections: Sections,
355 pos: ChunkPos,
356 min_y: i32,
357 height: i32,
358 level: Weak<World>,
359 block_ticks: BlockTickList,
360 fluid_ticks: FluidTickList,
361 mut heightmaps: ChunkHeightmaps,
362 postprocessing: Vec<Vec<u16>>,
363 structure_starts: StructureStartMap,
364 structure_references: StructureReferenceMap,
365 light: ChunkLightData,
366 ) -> Chunk {
367 heightmaps.prime_from_sections(
371 HeightmapType::final_types(),
372 min_y,
373 height,
374 §ions.sections,
375 );
376
377 let chunk = Chunk::from_disk(
378 sections,
379 pos,
380 ChunkStatus::Full,
381 min_y,
382 height,
383 heightmaps,
384 structure_starts,
385 structure_references,
386 None,
387 postprocessing,
388 block_ticks,
389 fluid_ticks,
390 level.clone(),
391 light,
392 );
393
394 FullChunkRef::populate_poi(&level, &chunk.sections, pos, min_y);
395 let game_event_listener_count = level
396 .upgrade()
397 .map_or_else(GameEventListenerCount::shared, |world| {
398 world.game_event_listener_count()
399 });
400
401 chunk.initialize_full_runtime_state(game_event_listener_count);
402 chunk
403 }
404}
405
406#[expect(
407 clippy::trivially_copy_pass_by_ref,
408 reason = "FullChunkRef is a borrowed capability with a conventional shared-receiver API"
409)]
410impl FullChunkRef<'_> {
411 fn adopt_proto_block_entities(&self) {
413 let block_entities = self
416 .chunk
417 .block_entity_storage()
418 .get_all_without_lifecycle_filter();
419 for block_entity in block_entities {
420 let pos = block_entity.get_block_pos();
421 if ChunkPos::from_block_pos(pos) != self.chunk.pos {
422 log::warn!(
423 "Trying to promote block entity {} at {pos:?} in chunk {:?}",
424 block_entity.get_type().key,
425 self.chunk.pos,
426 );
427 self.chunk
428 .block_entity_storage()
429 .discard_if_same_without_lifecycle(pos, &block_entity);
430 continue;
431 }
432
433 loop {
434 let state = self.chunk.get_block_state(pos);
435 let valid = state.has_block_entity() && block_entity.is_valid_block_state(state);
436 let adoption = self.with_locked_block_state(pos, |live_state| {
437 if live_state != state {
438 return ProtoBlockEntityAdoption::Retry;
439 }
440 if !valid {
441 return if self
442 .chunk
443 .block_entity_storage()
444 .discard_if_same_without_lifecycle(pos, &block_entity)
445 {
446 ProtoBlockEntityAdoption::Discarded
447 } else {
448 ProtoBlockEntityAdoption::Gone
449 };
450 }
451 self.chunk
452 .block_entity_storage()
453 .adopt_if_same_staged(pos, &block_entity, state)
454 .map_or(
455 ProtoBlockEntityAdoption::Gone,
456 ProtoBlockEntityAdoption::Adopted,
457 )
458 });
459
460 match adoption {
461 ProtoBlockEntityAdoption::Retry => {}
462 ProtoBlockEntityAdoption::Gone => break,
463 ProtoBlockEntityAdoption::Discarded => {
464 log::warn!(
465 "Discarding promoted block entity {} at {pos:?}: block {} does not accept that type",
466 block_entity.get_type().key,
467 state.get_block().key,
468 );
469 break;
470 }
471 ProtoBlockEntityAdoption::Adopted(lifecycle_dispatchers) => {
472 let cached_state = block_entity.get_block_state();
473 if state.get_block() != cached_state.get_block() {
474 log::warn!(
475 "Updating mismatched block entity {} state at {pos:?}: {} != {}",
476 block_entity.get_type().key,
477 state.get_block().key,
478 cached_state.get_block().key,
479 );
480 }
481 self.finish_block_entity_change(pos, lifecycle_dispatchers);
482 self.mark_unsaved();
483 break;
484 }
485 }
486 }
487 }
488 }
489
490 #[must_use]
494 pub fn get_level(&self) -> Option<Arc<World>> {
495 self.chunk.get_level()
496 }
497
498 #[must_use]
503 pub fn level_weak(&self) -> Weak<World> {
504 self.chunk.level_weak()
505 }
506
507 pub(crate) fn prepare_block_entity_activation(
512 &self,
513 holder: &Arc<ChunkHolder>,
514 ) -> Option<BlockEntityActivationBatch> {
515 let lifecycle_dispatchers = {
516 let mut activation = self.runtime().block_entity_activation.lock();
517 if let Some(current) = activation.holder.upgrade() {
518 debug_assert!(Arc::ptr_eq(¤t, holder));
519 if Arc::ptr_eq(¤t, holder) {
520 return None;
521 }
522 }
523 activation.holder = Arc::downgrade(holder);
524 mem::take(&mut activation.pending_lifecycle_dispatchers)
525 };
526
527 let mut known_positions = FxHashSet::default();
528 let mut positions = self
529 .chunk
530 .block_entity_storage()
531 .get_all_without_lifecycle_filter()
532 .into_iter()
533 .map(|block_entity| block_entity.get_block_pos())
534 .inspect(|pos| {
535 known_positions.insert(*pos);
536 })
537 .collect::<Vec<_>>();
538 positions.extend(
539 self.runtime()
540 .game_event_listeners
541 .block_entity_positions()
542 .into_iter()
543 .filter(|pos| known_positions.insert(*pos)),
544 );
545 Some(BlockEntityActivationBatch {
546 lifecycle_dispatchers,
547 positions,
548 })
549 }
550
551 #[must_use]
553 pub(crate) fn deactivate_block_entities(
554 &self,
555 holder: &Arc<ChunkHolder>,
556 ) -> Vec<SharedBlockEntity> {
557 let mut activation = self.runtime().block_entity_activation.lock();
558 let belongs = activation
559 .holder
560 .upgrade()
561 .is_some_and(|current| Arc::ptr_eq(¤t, holder));
562 if belongs {
563 activation.holder = Weak::new();
564 }
565 mem::take(&mut activation.pending_lifecycle_dispatchers)
566 }
567
568 pub(crate) fn suspend_block_entities(&self, holder: &Arc<ChunkHolder>) {
570 let mut activation = self.runtime().block_entity_activation.lock();
571 let belongs = activation
572 .holder
573 .upgrade()
574 .is_some_and(|current| Arc::ptr_eq(¤t, holder));
575 if belongs {
576 activation.holder = Weak::new();
577 }
578 }
579
580 #[must_use]
585 pub(crate) fn block_entity_tick_state_if_owned(
586 &self,
587 pos: BlockPos,
588 expected: &SharedBlockEntity,
589 ) -> Option<BlockStateId> {
590 let (state, owner_matches) = self.with_locked_block_state(pos, |state| {
591 (
592 state,
593 self.chunk
594 .block_entity_storage()
595 .contains_same(pos, expected),
596 )
597 });
598 owner_matches.then_some(state)
599 }
600
601 pub(crate) fn block_entity_tick_target(
602 &self,
603 pos: BlockPos,
604 ) -> Option<(BlockStateId, SharedBlockEntity)> {
605 self.with_locked_block_state(pos, |state| {
606 self.chunk
607 .block_entity_storage()
608 .get(pos)
609 .map(|entity| (state, entity))
610 })
611 }
612
613 fn finish_block_entity_change(
614 &self,
615 pos: BlockPos,
616 lifecycle_dispatchers: LifecycleDispatchers,
617 ) {
618 {
619 let mut activation = self.runtime().block_entity_activation.lock();
620 if activation.holder.upgrade().is_none() {
621 activation
622 .pending_lifecycle_dispatchers
623 .extend(lifecycle_dispatchers);
624 return;
625 }
626 }
627 let current = self
628 .chunk
629 .block_entity_storage()
630 .get(pos)
631 .filter(|block_entity| !block_entity.is_removed());
632 self.runtime()
633 .game_event_listeners
634 .remove_obsolete(pos, current.as_ref());
635 for block_entity in lifecycle_dispatchers {
636 block_entity.dispatch_lifecycle_events();
637 }
638 self.reconcile_block_entity_game_event_listener(pos);
639 self.refresh_block_entity_ticker(pos);
640 }
641
642 pub(crate) fn reconcile_block_entity_game_event_listener(&self, pos: BlockPos) {
645 loop {
646 let current = self
647 .chunk
648 .block_entity_storage()
649 .get(pos)
650 .filter(|block_entity| !block_entity.is_removed());
651 self.runtime()
652 .game_event_listeners
653 .remove_obsolete(pos, current.as_ref());
654 let Some(block_entity) = current else {
655 return;
656 };
657 if self
658 .runtime()
659 .game_event_listeners
660 .is_selected(&block_entity)
661 {
662 return;
663 }
664
665 let Some((state, live_entity)) = self.block_entity_tick_target(pos) else {
666 continue;
667 };
668 if !Arc::ptr_eq(&block_entity, &live_entity) || live_entity.is_removed() {
669 continue;
670 }
671 let Some(world) = self.get_level() else {
672 return;
673 };
674 let behavior = BLOCK_BEHAVIORS.get_behavior(state.get_block());
675 let listener = behavior.get_game_event_listener(&world, live_entity.as_ref());
676
677 let still_owned = self.with_locked_block_state(pos, |live_state| {
678 live_state == state
679 && !live_entity.is_removed()
680 && self
681 .chunk
682 .block_entity_storage()
683 .contains_same(pos, &live_entity)
684 });
685 if !still_owned {
686 continue;
687 }
688 if self
689 .runtime()
690 .block_entity_activation
691 .lock()
692 .holder
693 .upgrade()
694 .is_none()
695 {
696 return;
697 }
698
699 match self
700 .runtime()
701 .game_event_listeners
702 .commit_selection(live_entity, listener)
703 {
704 ListenerSelectionCommit::Committed | ListenerSelectionCommit::AlreadySelected => {
705 return;
706 }
707 ListenerSelectionCommit::Occupied => {}
708 }
709 }
710 }
711
712 fn refresh_block_entity_ticker(&self, pos: BlockPos) {
713 let holder = {
714 let activation = self.runtime().block_entity_activation.lock();
715 activation.holder.upgrade()
716 };
717 let Some(holder) = holder else {
718 return;
719 };
720 let Some(world) = self.get_level() else {
721 return;
722 };
723 let Some((state, block_entity)) = self.block_entity_tick_target(pos) else {
724 world.block_entity_tickers().remove(&holder, pos);
725 return;
726 };
727 if block_entity.is_removed() {
728 world.block_entity_tickers().remove(&holder, pos);
729 return;
730 }
731
732 let behavior = BLOCK_BEHAVIORS.get_behavior(state.get_block());
733 let ticker = behavior.get_block_entity_ticker(&world, state, block_entity.get_type());
734 let ticker = ticker.filter(|ticker| {
735 let valid = ticker.accepts(block_entity.get_type());
736 if !valid {
737 tracing::error!(
738 block = %state.get_block().key,
739 block_entity_type = %block_entity.get_type().key,
740 ?pos,
741 "Block behavior returned a ticker for the wrong block-entity type"
742 );
743 }
744 valid
745 });
746 world
747 .block_entity_tickers()
748 .reconcile(&holder, block_entity, ticker);
749 }
750
751 pub(crate) const fn scheduled_tick_container(&self) -> &Arc<ChunkTickContainer> {
753 self.chunk.scheduled_tick_container()
754 }
755
756 pub(crate) fn schedule_unregistered_block_tick(
757 &self,
758 block: BlockRef,
759 pos: BlockPos,
760 trigger_tick: i64,
761 priority: TickPriority,
762 sub_tick_order: i64,
763 ) -> Option<bool> {
764 self.chunk
765 .scheduled_tick_container()
766 .schedule_unregistered_block(block, pos, trigger_tick, priority, sub_tick_order)
767 }
768
769 pub(crate) fn schedule_unregistered_fluid_tick(
770 &self,
771 fluid: FluidRef,
772 pos: BlockPos,
773 trigger_tick: i64,
774 priority: TickPriority,
775 sub_tick_order: i64,
776 ) -> Option<bool> {
777 self.chunk
778 .scheduled_tick_container()
779 .schedule_unregistered_fluid(fluid, pos, trigger_tick, priority, sub_tick_order)
780 }
781
782 pub(crate) fn has_scheduled_block_tick(
783 &self,
784 pos: BlockPos,
785 block: BlockRef,
786 ) -> Result<bool, TickSchedulerError> {
787 self.chunk
788 .scheduled_tick_container()
789 .has_block(pos, block)
790 .ok_or(TickSchedulerError::MissingContainer(self.chunk.pos))
791 }
792
793 pub(crate) fn has_scheduled_fluid_tick(
794 &self,
795 pos: BlockPos,
796 fluid: FluidRef,
797 ) -> Result<bool, TickSchedulerError> {
798 self.chunk
799 .scheduled_tick_container()
800 .has_fluid(pos, fluid)
801 .ok_or(TickSchedulerError::MissingContainer(self.chunk.pos))
802 }
803
804 pub(crate) fn schedule_block_tick(
807 &self,
808 pos: BlockPos,
809 block: BlockRef,
810 trigger_tick: i64,
811 priority: TickPriority,
812 sub_tick_order: i64,
813 ) {
814 let result = if let Some(world) = self.get_level() {
815 world.schedule_block_tick_for_chunk(
816 *self,
817 pos,
818 block,
819 trigger_tick,
820 priority,
821 sub_tick_order,
822 )
823 } else {
824 self.schedule_unregistered_block_tick(
825 block,
826 pos,
827 trigger_tick,
828 priority,
829 sub_tick_order,
830 )
831 .ok_or(TickSchedulerError::MissingContainer(self.chunk.pos))
832 };
833
834 match result {
835 Ok(true) => self.chunk.dirty.store(true, Ordering::Release),
836 Ok(false) => {}
837 Err(error) => panic!("Full chunk scheduled-tick ownership invariant failed: {error:?}"),
838 }
839 }
840
841 pub(crate) fn schedule_fluid_tick(
842 &self,
843 pos: BlockPos,
844 fluid: FluidRef,
845 trigger_tick: i64,
846 priority: TickPriority,
847 sub_tick_order: i64,
848 ) {
849 let result = if let Some(world) = self.get_level() {
850 world.schedule_fluid_tick_for_chunk(
851 *self,
852 pos,
853 fluid,
854 trigger_tick,
855 priority,
856 sub_tick_order,
857 )
858 } else {
859 self.schedule_unregistered_fluid_tick(
860 fluid,
861 pos,
862 trigger_tick,
863 priority,
864 sub_tick_order,
865 )
866 .ok_or(TickSchedulerError::MissingContainer(self.chunk.pos))
867 };
868
869 match result {
870 Ok(true) => self.chunk.dirty.store(true, Ordering::Release),
871 Ok(false) => {}
872 Err(error) => panic!("Full chunk scheduled-tick ownership invariant failed: {error:?}"),
873 }
874 }
875
876 pub(crate) fn scheduled_tick_snapshot(&self) -> ScheduledTickSnapshot {
878 let current_tick = self.get_level().map_or(0, |world| world.game_time());
879 let result = self
880 .chunk
881 .scheduled_tick_container()
882 .snapshot(current_tick)
883 .ok_or(TickSchedulerError::MissingContainer(self.chunk.pos));
884 match result {
885 Ok(snapshot) => snapshot,
886 Err(error) => panic!("Full chunk scheduled-tick ownership invariant failed: {error:?}"),
887 }
888 }
889
890 pub fn initialize_light_sources(&self) {
892 self.refresh_light_emptiness_maps();
893 self.chunk
894 .sky_light_sources
895 .write()
896 .fill_from_sections(&self.chunk.sections);
897 }
898
899 pub(crate) fn take_postprocessing(&self) -> Option<Box<[Vec<u16>]>> {
901 let mut postprocessing = self.chunk.postprocessing.lock();
902 if postprocessing.iter().all(Vec::is_empty) {
903 return None;
904 }
905
906 let pending = mem::replace(&mut *postprocessing, empty_postprocessing(self.height()));
907 self.chunk.dirty.store(true, Ordering::Release);
908 Some(pending)
909 }
910
911 pub(crate) fn postprocessing_for_serialization(&self) -> Vec<Vec<u16>> {
913 self.chunk
914 .postprocessing
915 .lock()
916 .iter()
917 .map(Vec::clone)
918 .collect()
919 }
920
921 pub(crate) fn post_process_generation(
923 world: &Arc<World>,
924 chunk_pos: ChunkPos,
925 min_y: i32,
926 postprocessing: Box<[Vec<u16>]>,
927 ) {
928 for (section_index, packed_offsets) in postprocessing.into_vec().into_iter().enumerate() {
929 if packed_offsets.is_empty() {
930 continue;
931 }
932
933 let section_y = Self::section_y_from_section_index(min_y, section_index);
934 for packed in packed_offsets {
935 let pos = Chunk::unpack_postprocessing_offset(packed, section_y, chunk_pos);
936 let state = world.get_block_state(pos);
937 let fluid_state = state.get_fluid_state();
938
939 if !fluid_state.is_empty() {
940 FLUID_BEHAVIORS.get_behavior(fluid_state.fluid_id).tick(
941 world,
942 pos,
943 state,
944 fluid_state,
945 );
946 }
947
948 if state.get_block().config.liquid {
949 BLOCK_BEHAVIORS
950 .get_behavior(state.get_block())
951 .tick(state, world, pos);
952 } else {
953 let new_state = Self::update_from_neighbor_shapes(world, state, pos);
954 if new_state != state {
955 let flags = UpdateFlags::UPDATE_INVISIBLE
956 | UpdateFlags::UPDATE_KNOWN_SHAPE
957 | UpdateFlags::UPDATE_SKIP_BLOCK_ENTITY_SIDEEFFECTS;
958 world.set_block(pos, new_state, flags);
959 }
960 }
961 }
962 }
963 }
964
965 fn update_from_neighbor_shapes(
966 world: &Arc<World>,
967 state: BlockStateId,
968 pos: BlockPos,
969 ) -> BlockStateId {
970 let mut updated = state;
971 for direction in Direction::UPDATE_SHAPE_ORDER {
972 let neighbor_pos = pos.relative(direction);
973 let neighbor_state = world.get_block_state(neighbor_pos);
974 let behavior = BLOCK_BEHAVIORS.get_behavior(updated.get_block());
975 updated =
976 behavior.update_shape(updated, world, pos, direction, neighbor_pos, neighbor_state);
977 }
978 updated
979 }
980
981 fn populate_poi(level: &Weak<World>, sections: &Sections, pos: ChunkPos, min_y: i32) {
983 let Some(world) = level.upgrade() else {
984 return;
985 };
986
987 let mut poi_sections: Vec<(usize, SectionPos)> = Vec::new();
994 for (i, section) in sections.sections.iter().enumerate() {
995 if section.read().contains_poi() {
996 let section_y = min_y / 16 + i as i32;
997 poi_sections.push((i, SectionPos::new(pos.0.x, section_y, pos.0.y)));
998 }
999 }
1000 if poi_sections.is_empty() {
1001 return;
1002 }
1003
1004 let mut poi_storage = world.poi_storage.lock();
1005 for (i, section_pos) in poi_sections {
1006 let guard = sections.sections[i].read();
1007 poi_storage.scan_and_populate(&guard, section_pos);
1008 }
1009 }
1010
1011 #[must_use]
1013 pub const fn min_y(&self) -> i32 {
1014 self.chunk.min_y()
1015 }
1016
1017 #[must_use]
1019 pub const fn height(&self) -> i32 {
1020 self.chunk.height()
1021 }
1022
1023 #[must_use]
1025 pub fn get_height(&self, heightmap_type: HeightmapType, local_x: usize, local_z: usize) -> i32 {
1026 self.chunk
1027 .heightmaps
1028 .read()
1029 .get_final(heightmap_type)
1030 .get_first_available(local_x, local_z)
1031 }
1032
1033 #[must_use]
1035 const fn get_section_index(&self, y: i32) -> usize {
1036 ((y - self.min_y()) / 16) as usize
1037 }
1038
1039 #[must_use]
1040 const fn section_y_from_section_index(min_y: i32, index: usize) -> i32 {
1041 min_y.div_euclid(16) + index as i32
1042 }
1043
1044 fn mark_unsaved(&self) {
1046 self.chunk.dirty.store(true, Ordering::Release);
1047 }
1048
1049 #[must_use]
1053 pub fn get_block_entity(&self, pos: BlockPos) -> Option<SharedBlockEntity> {
1054 loop {
1055 match self.chunk.block_entity_storage().lookup(pos) {
1056 BlockEntityLookup::Concrete(block_entity) => {
1057 if block_entity.is_removed() {
1058 if self
1059 .chunk
1060 .block_entity_storage()
1061 .remove_if_same_and_removed(pos, &block_entity)
1062 {
1063 self.finish_block_entity_change(pos, LifecycleDispatchers::new());
1064 return None;
1065 }
1066 continue;
1067 }
1068 return Some(block_entity);
1069 }
1070 BlockEntityLookup::Pending => return self.promote_pending_block_entity(pos),
1071 BlockEntityLookup::Absent => return None,
1072 }
1073 }
1074 }
1075
1076 #[must_use]
1080 pub(crate) fn get_block_entity_immediate(&self, pos: BlockPos) -> Option<SharedBlockEntity> {
1081 if ChunkPos::from_block_pos(pos) != self.chunk.pos {
1082 return None;
1083 }
1084 loop {
1085 match self.chunk.block_entity_storage().lookup(pos) {
1086 BlockEntityLookup::Concrete(block_entity) => {
1087 if block_entity.is_removed() {
1088 if self
1089 .chunk
1090 .block_entity_storage()
1091 .remove_if_same_and_removed(pos, &block_entity)
1092 {
1093 self.finish_block_entity_change(pos, LifecycleDispatchers::new());
1094 }
1095 continue;
1096 }
1097 return Some(block_entity);
1098 }
1099 BlockEntityLookup::Pending => return self.promote_pending_block_entity(pos),
1100 BlockEntityLookup::Absent => {}
1101 }
1102
1103 let state = self.get_block_state(pos);
1104 if !state.has_block_entity() {
1105 let state_unchanged =
1106 self.with_locked_block_state(pos, |live_state| live_state == state);
1107 if state_unchanged {
1108 return None;
1109 }
1110 continue;
1111 }
1112
1113 let behavior = BLOCK_BEHAVIORS.get_behavior(state.get_block());
1114 match behavior.new_block_entity(self.chunk.level_weak(), pos, state) {
1115 BlockEntityCreation::Created(block_entity) => {
1116 let valid = block_entity.get_block_pos() == pos
1117 && block_entity.is_valid_block_state(state);
1118 let inserted = self.with_locked_block_state(pos, |live_state| {
1119 if live_state != state {
1120 return None;
1121 }
1122 if !valid {
1123 return Some(None);
1124 }
1125 Some(Some(
1126 self.chunk
1127 .block_entity_storage()
1128 .insert_if_absent_staged(&block_entity, state),
1129 ))
1130 });
1131 let Some(inserted) = inserted else {
1132 continue;
1133 };
1134 let inserted = inserted?;
1135 match inserted {
1136 BlockEntityInsert::Existing(existing) => return Some(existing),
1137 BlockEntityInsert::Inserted(lifecycle_dispatchers) => {
1138 self.finish_block_entity_change(pos, lifecycle_dispatchers);
1139 self.mark_unsaved();
1140 return Some(block_entity);
1141 }
1142 }
1143 }
1144 BlockEntityCreation::NoEntity => {
1145 let state_unchanged =
1146 self.with_locked_block_state(pos, |live_state| live_state == state);
1147 if state_unchanged {
1148 return None;
1149 }
1150 }
1151 BlockEntityCreation::Unimplemented => {
1152 let inserted = self.with_locked_block_state(pos, |live_state| {
1153 live_state == state && self.chunk.block_entity_storage().set_pending(pos)
1154 });
1155 if inserted {
1156 self.mark_unsaved();
1157 }
1158 return None;
1159 }
1160 }
1161 }
1162 }
1163
1164 fn promote_pending_block_entity(&self, pos: BlockPos) -> Option<SharedBlockEntity> {
1165 loop {
1166 match self.chunk.block_entity_storage().lookup(pos) {
1167 BlockEntityLookup::Concrete(block_entity) => {
1168 if block_entity.is_removed() {
1169 if self
1170 .chunk
1171 .block_entity_storage()
1172 .remove_if_same_and_removed(pos, &block_entity)
1173 {
1174 self.finish_block_entity_change(pos, LifecycleDispatchers::new());
1175 }
1176 continue;
1177 }
1178 return Some(block_entity);
1179 }
1180 BlockEntityLookup::Pending => {}
1181 BlockEntityLookup::Absent => return None,
1182 }
1183
1184 let state = self.get_block_state(pos);
1185 if !state.has_block_entity() {
1186 let state_unchanged = self.with_locked_block_state(pos, |live_state| {
1187 if live_state != state {
1188 return false;
1189 }
1190 self.chunk.block_entity_storage().remove_pending(pos);
1191 true
1192 });
1193 if !state_unchanged {
1194 continue;
1195 }
1196 log::warn!(
1197 "Tried to promote a pending block entity at {pos:?}, but block {} does not allow one",
1198 state.get_block().key,
1199 );
1200 return None;
1201 }
1202
1203 let behavior = BLOCK_BEHAVIORS.get_behavior(state.get_block());
1204 let creation = behavior.new_block_entity(self.chunk.level_weak(), pos, state);
1205 match self.commit_pending_creation(pos, state, creation) {
1206 PendingPromotionCommit::Retry => {}
1207 PendingPromotionCommit::Complete {
1208 block_entity,
1209 lifecycle_dispatchers,
1210 } => {
1211 self.finish_block_entity_change(pos, lifecycle_dispatchers);
1212 return block_entity;
1213 }
1214 }
1215 }
1216 }
1217
1218 fn commit_pending_creation(
1219 &self,
1220 pos: BlockPos,
1221 expected_state: BlockStateId,
1222 creation: BlockEntityCreation,
1223 ) -> PendingPromotionCommit {
1224 match creation {
1225 BlockEntityCreation::Created(block_entity) => {
1226 let valid = block_entity.get_block_pos() == pos
1227 && ChunkPos::from_block_pos(pos) == self.chunk.pos
1228 && block_entity.is_valid_block_state(expected_state);
1229 self.with_locked_block_state(pos, |live_state| {
1230 if live_state != expected_state {
1234 return PendingPromotionCommit::Retry;
1235 }
1236 if !valid {
1237 return PendingPromotionCommit::Complete {
1238 block_entity: None,
1239 lifecycle_dispatchers: LifecycleDispatchers::new(),
1240 };
1241 }
1242 let (block_entity, lifecycle_dispatchers) = self
1243 .chunk
1244 .block_entity_storage()
1245 .promote_staged(pos, expected_state, block_entity);
1246 PendingPromotionCommit::Complete {
1247 block_entity,
1248 lifecycle_dispatchers,
1249 }
1250 })
1251 }
1252 BlockEntityCreation::NoEntity => self.with_locked_block_state(pos, |live_state| {
1253 if live_state != expected_state {
1254 return PendingPromotionCommit::Retry;
1255 }
1256 self.chunk.block_entity_storage().remove_pending(pos);
1257 PendingPromotionCommit::Complete {
1258 block_entity: None,
1259 lifecycle_dispatchers: LifecycleDispatchers::new(),
1260 }
1261 }),
1262 BlockEntityCreation::Unimplemented => self.with_locked_block_state(pos, |live_state| {
1266 if live_state == expected_state {
1267 PendingPromotionCommit::Complete {
1268 block_entity: None,
1269 lifecycle_dispatchers: LifecycleDispatchers::new(),
1270 }
1271 } else {
1272 PendingPromotionCommit::Retry
1273 }
1274 }),
1275 }
1276 }
1277
1278 pub(crate) fn promote_pending_block_entities(&self) {
1282 let positions = self.pending_block_entity_positions();
1283 for pos in positions {
1284 let _ = self.promote_pending_block_entity(pos);
1285 }
1286 }
1287
1288 #[must_use]
1290 pub fn pending_block_entity_positions(&self) -> Vec<BlockPos> {
1291 self.chunk.block_entity_storage().pending_positions()
1292 }
1293
1294 pub fn set_pending_block_entity(&self, pos: BlockPos) {
1296 if ChunkPos::from_block_pos(pos) != self.chunk.pos {
1297 log::warn!(
1298 "Trying to set a pending block entity at {pos:?} in chunk {:?}",
1299 self.chunk.pos,
1300 );
1301 return;
1302 }
1303 if self.chunk.block_entity_storage().set_pending(pos) {
1304 self.mark_unsaved();
1305 }
1306 }
1307
1308 #[must_use]
1312 pub fn remove_block_entity(&self, pos: BlockPos) -> bool {
1313 let (removed, lifecycle_dispatchers) = self.chunk.block_entity_storage().remove_staged(pos);
1314 self.finish_block_entity_change(pos, lifecycle_dispatchers);
1315 self.mark_unsaved();
1316 removed
1317 }
1318
1319 pub(crate) fn remove_block_entity_if_same(&self, expected: &dyn BlockEntity) -> bool {
1321 let pos = expected.get_block_pos();
1322 let (removed, lifecycle_dispatchers) = self
1323 .chunk
1324 .block_entity_storage()
1325 .remove_if_same_staged(pos, expected);
1326 self.finish_block_entity_change(pos, lifecycle_dispatchers);
1327 if removed {
1328 self.mark_unsaved();
1329 }
1330 removed
1331 }
1332
1333 #[must_use]
1338 pub fn add_and_register_block_entity(&self, block_entity: SharedBlockEntity) -> bool {
1339 let pos = block_entity.get_block_pos();
1340 let (valid, lifecycle_dispatchers) =
1341 self.add_and_register_block_entity_staged(block_entity);
1342 self.finish_block_entity_change(pos, lifecycle_dispatchers);
1343 valid
1344 }
1345
1346 fn add_and_register_block_entity_staged(
1347 &self,
1348 block_entity: SharedBlockEntity,
1349 ) -> (bool, LifecycleDispatchers) {
1350 let pos = block_entity.get_block_pos();
1351 if ChunkPos::from_block_pos(pos) != self.chunk.pos {
1352 log::warn!(
1353 "Trying to set block entity {} at {pos:?} in chunk {:?}",
1354 block_entity.get_type().key,
1355 self.chunk.pos,
1356 );
1357 return (false, LifecycleDispatchers::new());
1358 }
1359
1360 loop {
1361 let state = self.get_block_state(pos);
1362 let valid = state.has_block_entity() && block_entity.is_valid_block_state(state);
1363 if !valid {
1364 let state_unchanged =
1365 self.with_locked_block_state(pos, |live_state| live_state == state);
1366 if !state_unchanged {
1367 continue;
1368 }
1369 log::warn!(
1370 "Trying to set block entity {} at {pos:?}, but block {} does not accept that type",
1371 block_entity.get_type().key,
1372 state.get_block().key,
1373 );
1374 return (false, LifecycleDispatchers::new());
1375 }
1376
1377 let cached_state = block_entity.get_block_state();
1378 let committed = self.with_locked_block_state(pos, |live_state| {
1379 if live_state != state {
1380 return None;
1381 }
1382 Some(
1383 self.chunk
1384 .block_entity_storage()
1385 .add_staged(&block_entity, state),
1386 )
1387 });
1388 let Some((_, lifecycle_dispatchers)) = committed else {
1389 continue;
1390 };
1391 if state.get_block() != cached_state.get_block() {
1392 log::warn!(
1393 "Updating mismatched block entity {} state at {pos:?}: {} != {}",
1394 block_entity.get_type().key,
1395 state.get_block().key,
1396 cached_state.get_block().key,
1397 );
1398 }
1399
1400 self.mark_unsaved();
1401 return (true, lifecycle_dispatchers);
1402 }
1403 }
1404
1405 fn with_locked_block_state<R>(&self, pos: BlockPos, f: impl FnOnce(BlockStateId) -> R) -> R {
1406 let y = pos.y();
1407 if y < self.min_y() || y >= self.min_y() + self.height() {
1408 return f(REGISTRY.blocks.get_base_state_id(&vanilla_blocks::AIR));
1409 }
1410
1411 let section_index = self.get_section_index(y);
1412 if section_index >= self.chunk.sections.sections.len() {
1413 return f(REGISTRY.blocks.get_base_state_id(&vanilla_blocks::AIR));
1414 }
1415
1416 let section = self.chunk.sections.sections[section_index].read();
1417 let state = if section.is_empty() {
1418 REGISTRY.blocks.get_base_state_id(&vanilla_blocks::AIR)
1419 } else {
1420 section.states.get(
1421 (pos.x() & 15) as usize,
1422 (y & 15) as usize,
1423 (pos.z() & 15) as usize,
1424 )
1425 };
1426 f(state)
1427 }
1428
1429 fn reconcile_block_entity_after_set(&self, pos: BlockPos, state: BlockStateId) {
1430 loop {
1431 if self.get_block_state(pos) != state {
1432 return;
1433 }
1434
1435 if let Some(block_entity) = self.get_block_entity(pos) {
1436 if block_entity.is_valid_block_state(state) {
1437 let committed = self.with_locked_block_state(pos, |live_state| {
1438 if live_state != state {
1439 return None;
1440 }
1441 Some(self.chunk.block_entity_storage().update_if_same_staged(
1442 pos,
1443 &block_entity,
1444 state,
1445 ))
1446 });
1447 let Some((updated, lifecycle_dispatchers)) = committed else {
1448 return;
1449 };
1450 self.finish_block_entity_change(pos, lifecycle_dispatchers);
1451 if updated {
1452 return;
1453 }
1454 continue;
1455 }
1456
1457 let removed = self.with_locked_block_state(pos, |live_state| {
1458 if live_state != state {
1459 return None;
1460 }
1461 Some(
1462 self.chunk
1463 .block_entity_storage()
1464 .remove_if_same_staged(pos, block_entity.as_ref()),
1465 )
1466 });
1467 let Some((removed, lifecycle_dispatchers)) = removed else {
1468 return;
1469 };
1470 self.finish_block_entity_change(pos, lifecycle_dispatchers);
1471 if !removed {
1472 continue;
1473 }
1474 log::warn!(
1475 "Removed mismatched block entity at {pos:?}: type = {}, state = {}",
1476 block_entity.get_type().key,
1477 state.get_block().key,
1478 );
1479 continue;
1481 }
1482
1483 let behavior = BLOCK_BEHAVIORS.get_behavior(state.get_block());
1484 match behavior.new_block_entity(self.chunk.level_weak(), pos, state) {
1485 BlockEntityCreation::Created(block_entity) => {
1486 let valid = block_entity.get_block_pos() == pos
1487 && block_entity.is_valid_block_state(state);
1488 let inserted = self.with_locked_block_state(pos, |live_state| {
1489 if live_state != state {
1490 return None;
1491 }
1492 if !valid {
1493 return Some(None);
1494 }
1495 Some(Some(
1496 self.chunk
1497 .block_entity_storage()
1498 .insert_if_absent_staged(&block_entity, state),
1499 ))
1500 });
1501 let Some(inserted) = inserted else {
1502 return;
1503 };
1504 let Some(inserted) = inserted else {
1505 debug_assert!(false, "block-entity factory returned an invalid entity");
1506 return;
1507 };
1508 if let BlockEntityInsert::Inserted(lifecycle_dispatchers) = inserted {
1509 self.finish_block_entity_change(pos, lifecycle_dispatchers);
1510 return;
1511 }
1512 }
1513 BlockEntityCreation::NoEntity => return,
1514 BlockEntityCreation::Unimplemented => {
1515 let inserted = self.with_locked_block_state(pos, |live_state| {
1516 live_state == state && self.chunk.block_entity_storage().set_pending(pos)
1517 });
1518 if inserted {
1519 self.mark_unsaved();
1520 }
1521 return;
1522 }
1523 }
1524 }
1525 }
1526
1527 #[must_use]
1529 pub fn get_block_entities(&self) -> Vec<SharedBlockEntity> {
1530 self.chunk.block_entity_storage().get_all()
1531 }
1532
1533 #[must_use]
1535 pub(crate) fn clear_all_block_entities_staged(&self) -> ClearedBlockEntities {
1536 self.chunk
1537 .block_entity_storage()
1538 .clear_and_stage_lifecycle_callbacks()
1539 }
1540
1541 #[must_use]
1558 pub fn set_block_state(
1559 &self,
1560 pos: BlockPos,
1561 state: BlockStateId,
1562 flags: UpdateFlags,
1563 ) -> Option<BlockStateId> {
1564 match self.set_block_state_inner(pos, None, state, flags)? {
1565 FullChunkBlockSetResult::Changed(old_state) => Some(old_state),
1566 FullChunkBlockSetResult::Unchanged | FullChunkBlockSetResult::Stale(_) => None,
1567 }
1568 }
1569
1570 pub(crate) fn set_block_state_if_unchanged(
1571 &self,
1572 pos: BlockPos,
1573 expected_state: BlockStateId,
1574 new_state: BlockStateId,
1575 flags: UpdateFlags,
1576 ) -> Option<FullChunkBlockSetResult> {
1577 self.set_block_state_inner(pos, Some(expected_state), new_state, flags)
1578 }
1579
1580 #[expect(
1581 clippy::too_many_lines,
1582 reason = "block mutation keeps vanilla side effects in one ordered transaction"
1583 )]
1584 fn set_block_state_inner(
1585 &self,
1586 pos: BlockPos,
1587 expected_state: Option<BlockStateId>,
1588 state: BlockStateId,
1589 flags: UpdateFlags,
1590 ) -> Option<FullChunkBlockSetResult> {
1591 let y = pos.0.y;
1592
1593 if y < self.min_y() || y >= self.min_y() + self.height() {
1594 return None;
1595 }
1596
1597 let section_index = self.get_section_index(y);
1598
1599 if section_index >= self.chunk.sections.sections.len() {
1600 return None;
1601 }
1602
1603 let section = &self.chunk.sections.sections[section_index];
1604
1605 let local_x = (pos.0.x & 15) as usize;
1606 let local_y = (y & 15) as usize;
1607 let local_z = (pos.0.z & 15) as usize;
1608
1609 let mut keep_block_entity_decision = None;
1610 let (old_state, was_empty, is_empty, detached_block_entity) = loop {
1611 let mut section_guard = section.write();
1612 let observed_state = section_guard.states.get(local_x, local_y, local_z);
1613 if expected_state.is_some_and(|expected| observed_state != expected) {
1614 return Some(FullChunkBlockSetResult::Stale(observed_state));
1615 }
1616 if observed_state == state {
1617 return Some(FullChunkBlockSetResult::Unchanged);
1618 }
1619
1620 let old_block = observed_state.get_block();
1623 let new_block = state.get_block();
1624 let block_changed = old_block != new_block;
1625 let detach_block_entity = if block_changed && observed_state.has_block_entity() {
1626 let Some((decision_state, should_keep)) = keep_block_entity_decision else {
1627 drop(section_guard);
1628 let should_keep = BLOCK_BEHAVIORS
1629 .get_behavior(new_block)
1630 .should_keep_block_entity(observed_state, state);
1631 keep_block_entity_decision = Some((observed_state, should_keep));
1632 continue;
1633 };
1634 if decision_state != observed_state {
1635 drop(section_guard);
1636 keep_block_entity_decision = None;
1637 continue;
1638 }
1639 !should_keep
1640 } else {
1641 false
1642 };
1643
1644 let was_empty = section_guard.is_empty();
1645 let old_state = section_guard.set_block_state(local_x, local_y, local_z, state);
1646 debug_assert_eq!(old_state, observed_state);
1647 let detached_block_entity = detach_block_entity.then(|| {
1648 self.chunk
1649 .block_entity_storage()
1650 .detach_and_queue_removal(pos)
1651 });
1652 let is_empty = section_guard.is_empty();
1653 break (old_state, was_empty, is_empty, detached_block_entity);
1654 };
1655
1656 let min_y = self.min_y();
1657 let sections = &self.chunk.sections;
1658 self.chunk
1659 .heightmaps
1660 .write()
1661 .update_final(local_x, y, local_z, state, |lx, scan_y, lz| {
1662 let scan_section_index = ((scan_y - min_y) / 16) as usize;
1663 let scan_local_y = ((scan_y - min_y) % 16) as usize;
1664 sections.sections[scan_section_index]
1665 .read()
1666 .states
1667 .get(lx, scan_local_y, lz)
1668 });
1669
1670 let old_block = old_state.get_block();
1671 let new_block = state.get_block();
1672
1673 let empty_section_change = if was_empty == is_empty {
1674 None
1675 } else {
1676 self.update_light_section_emptiness(y, is_empty);
1677 Some(LightSectionEmptinessChange {
1678 section_pos: SectionPos::new(
1679 self.chunk.pos.0.x,
1680 SectionPos::block_to_section_coord(y),
1681 self.chunk.pos.0.y,
1682 ),
1683 empty: is_empty,
1684 })
1685 };
1686
1687 let light_properties_changed = has_different_light_properties(old_state, state);
1688 if light_properties_changed {
1689 self.update_sky_light_sources(local_x, y, local_z);
1690 }
1691
1692 let block_changed = old_block != new_block;
1693 let moved_by_piston = flags.contains(UpdateFlags::UPDATE_MOVE_BY_PISTON);
1694 let side_effects = !flags.contains(UpdateFlags::UPDATE_SKIP_BLOCK_ENTITY_SIDEEFFECTS);
1695
1696 let block_behaviors = &*BLOCK_BEHAVIORS;
1697 let old_behavior = block_behaviors.get_behavior(old_block);
1698 let new_behavior = block_behaviors.get_behavior(new_block);
1699
1700 let level = self.get_level();
1701 if let Some(level) = &level
1702 && (light_properties_changed || empty_section_change.is_some())
1703 {
1704 level.queue_light_change_after_block_set(pos, old_state, state, empty_section_change);
1705 }
1706
1707 if let Some(DetachedBlockEntity {
1708 entity,
1709 dispatch_removed,
1710 }) = detached_block_entity
1711 && let Some(block_entity) = entity
1712 {
1713 if side_effects && level.is_some() {
1717 block_entity.pre_remove_side_effects(pos, old_state);
1718 }
1719 let mut lifecycle_dispatchers = LifecycleDispatchers::new();
1720 if dispatch_removed {
1721 lifecycle_dispatchers.push(block_entity);
1722 }
1723 self.finish_block_entity_change(pos, lifecycle_dispatchers);
1724 }
1725
1726 if let Some(level) = level {
1727 if (block_changed || new_behavior.is_rail())
1729 && (flags.contains(UpdateFlags::UPDATE_NEIGHBORS) || moved_by_piston)
1730 {
1731 old_behavior.affect_neighbors_after_removal(
1732 old_state,
1733 &level,
1734 pos,
1735 moved_by_piston,
1736 );
1737 }
1738
1739 let current_state = section.read().states.get(local_x, local_y, local_z);
1742 if current_state.get_block() != new_block {
1743 return Some(FullChunkBlockSetResult::Stale(current_state));
1744 }
1745
1746 if !flags.contains(UpdateFlags::UPDATE_SKIP_ON_PLACE) {
1748 new_behavior.on_place(state, &level, pos, old_state, moved_by_piston);
1749 }
1750 }
1751
1752 if state.has_block_entity() {
1755 self.reconcile_block_entity_after_set(pos, state);
1756 }
1757
1758 self.mark_unsaved();
1759 Some(FullChunkBlockSetResult::Changed(old_state))
1760 }
1761
1762 fn update_light_section_emptiness(&self, y: i32, is_empty: bool) {
1763 let section_y = SectionPos::block_to_section_coord(y);
1764 self.chunk
1765 .light
1766 .write()
1767 .set_section_empty(section_y, is_empty);
1768 }
1769
1770 fn update_sky_light_sources(&self, local_x: usize, y: i32, local_z: usize) {
1771 let chunk_min_x = self.chunk.pos.0.x * 16;
1772 let chunk_min_z = self.chunk.pos.0.y * 16;
1773 self.chunk.sky_light_sources.write().update(
1774 local_x,
1775 y,
1776 local_z,
1777 |scan_x, scan_y, scan_z| {
1778 self.get_block_state(BlockPos::new(
1779 chunk_min_x + scan_x as i32,
1780 scan_y,
1781 chunk_min_z + scan_z as i32,
1782 ))
1783 },
1784 );
1785 }
1786
1787 pub(crate) fn refresh_light_emptiness_maps(&self) {
1788 if let Err(error) = self
1789 .chunk
1790 .light
1791 .write()
1792 .refresh_emptiness_maps_from_sections(&self.chunk.sections)
1793 {
1794 panic!("invalid chunk light emptiness map length: {error:?}");
1795 }
1796 }
1797
1798 #[must_use]
1800 pub fn get_block_state(&self, pos: BlockPos) -> BlockStateId {
1801 let y = pos.0.y;
1802 if y < self.min_y() || y >= self.min_y() + self.height() {
1803 return REGISTRY.blocks.get_base_state_id(&vanilla_blocks::AIR);
1804 }
1805
1806 let section_index = self.get_section_index(y);
1807
1808 if section_index >= self.chunk.sections.sections.len() {
1810 return REGISTRY.blocks.get_base_state_id(&vanilla_blocks::AIR);
1811 }
1812
1813 let section = &self.chunk.sections.sections[section_index];
1814 let section_guard = section.read();
1815
1816 if section_guard.is_empty() {
1817 return REGISTRY.blocks.get_base_state_id(&vanilla_blocks::AIR);
1818 }
1819
1820 let local_x = (pos.0.x & 15) as usize;
1821 let local_y = (y & 15) as usize;
1822 let local_z = (pos.0.z & 15) as usize;
1823
1824 section_guard.states.get(local_x, local_y, local_z)
1825 }
1826
1827 #[must_use]
1829 pub fn highest_filled_section_index(&self) -> Option<usize> {
1830 self.chunk
1831 .sections
1832 .sections
1833 .iter()
1834 .rposition(|section| !section.read().is_empty())
1835 }
1836
1837 #[must_use]
1839 pub fn highest_section_position(&self) -> i32 {
1840 self.highest_filled_section_index()
1841 .map_or(self.min_y(), |index| self.min_y() + index as i32 * 16)
1842 }
1843
1844 #[must_use]
1846 pub fn extract_chunk_data(&self) -> ChunkPacketData {
1847 let data = Vec::new();
1848
1849 let mut cursor = Cursor::new(data);
1850 self.chunk.sections.sections.iter().for_each(|section| {
1851 section.read().write(&mut cursor);
1852 });
1853
1854 let heightmaps = {
1855 let heightmaps = self.chunk.heightmaps.read();
1856 vec![
1857 (
1858 ProtocolHeightmapType::WorldSurface,
1859 heightmaps
1860 .get_final(HeightmapType::WorldSurface)
1861 .get_raw_data(),
1862 ),
1863 (
1864 ProtocolHeightmapType::MotionBlocking,
1865 heightmaps
1866 .get_final(HeightmapType::MotionBlocking)
1867 .get_raw_data(),
1868 ),
1869 (
1870 ProtocolHeightmapType::MotionBlockingNoLeaves,
1871 heightmaps
1872 .get_final(HeightmapType::MotionBlockingNoLeaves)
1873 .get_raw_data(),
1874 ),
1875 ]
1876 };
1877
1878 let block_entities: Vec<BlockEntityInfo> = self
1880 .chunk
1881 .block_entity_storage()
1882 .get_all()
1883 .iter()
1884 .map(|entity| {
1885 let pos = entity.get_block_pos();
1886 let type_id = entity.get_type().id() as i32;
1887 let update_tag = entity.get_update_tag();
1888
1889 BlockEntityInfo {
1890 packed_xz: PackedChunkLocalXZ::from_block_pos(pos),
1891 y: pos.0.y as i16,
1892 type_id,
1893 data: update_tag.into(),
1894 }
1895 })
1896 .collect();
1897
1898 ChunkPacketData {
1899 heightmaps: Heightmaps { heightmaps },
1900 data: cursor.into_inner(),
1901 block_entities,
1902 }
1903 }
1904
1905 #[must_use]
1907 pub fn extract_light_data(&self, has_skylight: bool) -> LightUpdatePacketData {
1908 let light = self.chunk.light.read();
1909 build_chunk_light_update_packet(&light, has_skylight)
1910 }
1911}
1912
1913#[cfg(test)]
1914mod game_event_tests;
1915
1916#[cfg(test)]
1917mod tests;