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 proto_chunk
306 .light
307 .write()
308 .refresh_emptiness_maps_from_sections(&proto_chunk.sections)
309 .expect("invalid proto chunk light emptiness map length");
310
311 FullChunkRef::populate_poi(&level, &proto_chunk.sections, proto_chunk.pos, min_y);
312 let game_event_listener_count = level
313 .upgrade()
314 .map_or_else(GameEventListenerCount::shared, |world| {
315 world.game_event_listener_count()
316 });
317
318 proto_chunk.initialize_full_runtime_state(game_event_listener_count);
319 let full = FullChunkRef::from_full_context(proto_chunk);
320 full.adopt_proto_block_entities();
321 FullChunkPromotion {
322 chunk: full,
323 pending_entities,
324 }
325 }
326
327 #[must_use]
347 #[expect(
348 clippy::too_many_arguments,
349 reason = "all parameters are required to fully restore a chunk from disk"
350 )]
351 pub(crate) fn from_full_disk(
352 sections: Sections,
353 pos: ChunkPos,
354 min_y: i32,
355 height: i32,
356 level: Weak<World>,
357 block_ticks: BlockTickList,
358 fluid_ticks: FluidTickList,
359 mut heightmaps: ChunkHeightmaps,
360 postprocessing: Vec<Vec<u16>>,
361 structure_starts: StructureStartMap,
362 structure_references: StructureReferenceMap,
363 light: ChunkLightData,
364 ) -> Chunk {
365 heightmaps.prime_from_sections(
369 HeightmapType::final_types(),
370 min_y,
371 height,
372 §ions.sections,
373 );
374
375 let chunk = Chunk::from_disk(
376 sections,
377 pos,
378 ChunkStatus::Full,
379 min_y,
380 height,
381 heightmaps,
382 structure_starts,
383 structure_references,
384 postprocessing,
385 block_ticks,
386 fluid_ticks,
387 Weak::clone(&level),
388 light,
389 );
390
391 FullChunkRef::populate_poi(&level, &chunk.sections, pos, min_y);
392 let game_event_listener_count = level
393 .upgrade()
394 .map_or_else(GameEventListenerCount::shared, |world| {
395 world.game_event_listener_count()
396 });
397
398 chunk.initialize_full_runtime_state(game_event_listener_count);
399 chunk
400 }
401}
402
403#[expect(
404 clippy::trivially_copy_pass_by_ref,
405 reason = "FullChunkRef is a borrowed capability with a conventional shared-receiver API"
406)]
407impl FullChunkRef<'_> {
408 fn adopt_proto_block_entities(&self) {
410 let block_entities = self
413 .chunk
414 .block_entity_storage()
415 .get_all_without_lifecycle_filter();
416 for block_entity in block_entities {
417 let pos = block_entity.get_block_pos();
418 if ChunkPos::from_block_pos(pos) != self.chunk.pos {
419 log::warn!(
420 "Trying to promote block entity {} at {pos:?} in chunk {:?}",
421 block_entity.get_type().key,
422 self.chunk.pos,
423 );
424 self.chunk
425 .block_entity_storage()
426 .discard_if_same_without_lifecycle(pos, &block_entity);
427 continue;
428 }
429
430 loop {
431 let state = self.chunk.get_block_state(pos);
432 let valid = state.has_block_entity() && block_entity.is_valid_block_state(state);
433 let adoption = self.with_locked_block_state(pos, |live_state| {
434 if live_state != state {
435 return ProtoBlockEntityAdoption::Retry;
436 }
437 if !valid {
438 return if self
439 .chunk
440 .block_entity_storage()
441 .discard_if_same_without_lifecycle(pos, &block_entity)
442 {
443 ProtoBlockEntityAdoption::Discarded
444 } else {
445 ProtoBlockEntityAdoption::Gone
446 };
447 }
448 self.chunk
449 .block_entity_storage()
450 .adopt_if_same_staged(pos, &block_entity, state)
451 .map_or(
452 ProtoBlockEntityAdoption::Gone,
453 ProtoBlockEntityAdoption::Adopted,
454 )
455 });
456
457 match adoption {
458 ProtoBlockEntityAdoption::Retry => {}
459 ProtoBlockEntityAdoption::Gone => break,
460 ProtoBlockEntityAdoption::Discarded => {
461 log::warn!(
462 "Discarding promoted block entity {} at {pos:?}: block {} does not accept that type",
463 block_entity.get_type().key,
464 state.get_block().key,
465 );
466 break;
467 }
468 ProtoBlockEntityAdoption::Adopted(lifecycle_dispatchers) => {
469 let cached_state = block_entity.get_block_state();
470 if state.get_block() != cached_state.get_block() {
471 log::warn!(
472 "Updating mismatched block entity {} state at {pos:?}: {} != {}",
473 block_entity.get_type().key,
474 state.get_block().key,
475 cached_state.get_block().key,
476 );
477 }
478 self.finish_block_entity_change(pos, lifecycle_dispatchers);
479 self.mark_unsaved();
480 break;
481 }
482 }
483 }
484 }
485 }
486
487 #[must_use]
491 pub fn get_level(&self) -> Option<Arc<World>> {
492 self.chunk.get_level()
493 }
494
495 #[must_use]
500 pub fn level_weak(&self) -> Weak<World> {
501 self.chunk.level_weak()
502 }
503
504 pub(crate) fn prepare_block_entity_activation(
509 &self,
510 holder: &Arc<ChunkHolder>,
511 ) -> Option<BlockEntityActivationBatch> {
512 let lifecycle_dispatchers = {
513 let mut activation = self.runtime().block_entity_activation.lock();
514 if let Some(current) = activation.holder.upgrade() {
515 debug_assert!(Arc::ptr_eq(¤t, holder));
516 if Arc::ptr_eq(¤t, holder) {
517 return None;
518 }
519 }
520 activation.holder = Arc::downgrade(holder);
521 mem::take(&mut activation.pending_lifecycle_dispatchers)
522 };
523
524 let mut known_positions = FxHashSet::default();
525 let mut positions = self
526 .chunk
527 .block_entity_storage()
528 .get_all_without_lifecycle_filter()
529 .into_iter()
530 .map(|block_entity| block_entity.get_block_pos())
531 .inspect(|pos| {
532 known_positions.insert(*pos);
533 })
534 .collect::<Vec<_>>();
535 positions.extend(
536 self.runtime()
537 .game_event_listeners
538 .block_entity_positions()
539 .into_iter()
540 .filter(|pos| known_positions.insert(*pos)),
541 );
542 Some(BlockEntityActivationBatch {
543 lifecycle_dispatchers,
544 positions,
545 })
546 }
547
548 #[must_use]
550 pub(crate) fn deactivate_block_entities(
551 &self,
552 holder: &Arc<ChunkHolder>,
553 ) -> Vec<SharedBlockEntity> {
554 let mut activation = self.runtime().block_entity_activation.lock();
555 let belongs = activation
556 .holder
557 .upgrade()
558 .is_some_and(|current| Arc::ptr_eq(¤t, holder));
559 if belongs {
560 activation.holder = Weak::new();
561 }
562 mem::take(&mut activation.pending_lifecycle_dispatchers)
563 }
564
565 pub(crate) fn suspend_block_entities(&self, holder: &Arc<ChunkHolder>) {
567 let mut activation = self.runtime().block_entity_activation.lock();
568 let belongs = activation
569 .holder
570 .upgrade()
571 .is_some_and(|current| Arc::ptr_eq(¤t, holder));
572 if belongs {
573 activation.holder = Weak::new();
574 }
575 }
576
577 #[must_use]
582 pub(crate) fn block_entity_tick_state_if_owned(
583 &self,
584 pos: BlockPos,
585 expected: &SharedBlockEntity,
586 ) -> Option<BlockStateId> {
587 let (state, owner_matches) = self.with_locked_block_state(pos, |state| {
588 (
589 state,
590 self.chunk
591 .block_entity_storage()
592 .contains_same(pos, expected),
593 )
594 });
595 owner_matches.then_some(state)
596 }
597
598 pub(crate) fn block_entity_tick_target(
599 &self,
600 pos: BlockPos,
601 ) -> Option<(BlockStateId, SharedBlockEntity)> {
602 self.with_locked_block_state(pos, |state| {
603 self.chunk
604 .block_entity_storage()
605 .get(pos)
606 .map(|entity| (state, entity))
607 })
608 }
609
610 fn finish_block_entity_change(
611 &self,
612 pos: BlockPos,
613 lifecycle_dispatchers: LifecycleDispatchers,
614 ) {
615 {
616 let mut activation = self.runtime().block_entity_activation.lock();
617 if activation.holder.upgrade().is_none() {
618 activation
619 .pending_lifecycle_dispatchers
620 .extend(lifecycle_dispatchers);
621 return;
622 }
623 }
624 let current = self
625 .chunk
626 .block_entity_storage()
627 .get(pos)
628 .filter(|block_entity| !block_entity.is_removed());
629 self.runtime()
630 .game_event_listeners
631 .remove_obsolete(pos, current.as_ref());
632 for block_entity in lifecycle_dispatchers {
633 block_entity.dispatch_lifecycle_events();
634 }
635 self.reconcile_block_entity_game_event_listener(pos);
636 self.refresh_block_entity_ticker(pos);
637 }
638
639 pub(crate) fn reconcile_block_entity_game_event_listener(&self, pos: BlockPos) {
642 loop {
643 let current = self
644 .chunk
645 .block_entity_storage()
646 .get(pos)
647 .filter(|block_entity| !block_entity.is_removed());
648 self.runtime()
649 .game_event_listeners
650 .remove_obsolete(pos, current.as_ref());
651 let Some(block_entity) = current else {
652 return;
653 };
654 if self
655 .runtime()
656 .game_event_listeners
657 .is_selected(&block_entity)
658 {
659 return;
660 }
661
662 let Some((state, live_entity)) = self.block_entity_tick_target(pos) else {
663 continue;
664 };
665 if !Arc::ptr_eq(&block_entity, &live_entity) || live_entity.is_removed() {
666 continue;
667 }
668 let Some(world) = self.get_level() else {
669 return;
670 };
671 let behavior = BLOCK_BEHAVIORS.get_behavior(state.get_block());
672 let listener = behavior.get_game_event_listener(&world, live_entity.as_ref());
673
674 let still_owned = self.with_locked_block_state(pos, |live_state| {
675 live_state == state
676 && !live_entity.is_removed()
677 && self
678 .chunk
679 .block_entity_storage()
680 .contains_same(pos, &live_entity)
681 });
682 if !still_owned {
683 continue;
684 }
685 if self
686 .runtime()
687 .block_entity_activation
688 .lock()
689 .holder
690 .upgrade()
691 .is_none()
692 {
693 return;
694 }
695
696 match self
697 .runtime()
698 .game_event_listeners
699 .commit_selection(live_entity, listener)
700 {
701 ListenerSelectionCommit::Committed | ListenerSelectionCommit::AlreadySelected => {
702 return;
703 }
704 ListenerSelectionCommit::Occupied => {}
705 }
706 }
707 }
708
709 fn refresh_block_entity_ticker(&self, pos: BlockPos) {
710 let holder = {
711 let activation = self.runtime().block_entity_activation.lock();
712 activation.holder.upgrade()
713 };
714 let Some(holder) = holder else {
715 return;
716 };
717 let Some(world) = self.get_level() else {
718 return;
719 };
720 let Some((state, block_entity)) = self.block_entity_tick_target(pos) else {
721 world.block_entity_tickers().remove(&holder, pos);
722 return;
723 };
724 if block_entity.is_removed() {
725 world.block_entity_tickers().remove(&holder, pos);
726 return;
727 }
728
729 let behavior = BLOCK_BEHAVIORS.get_behavior(state.get_block());
730 let ticker = behavior.get_block_entity_ticker(&world, state, block_entity.get_type());
731 let ticker = ticker.filter(|ticker| {
732 let valid = ticker.accepts(block_entity.get_type());
733 if !valid {
734 tracing::error!(
735 block = %state.get_block().key,
736 block_entity_type = %block_entity.get_type().key,
737 ?pos,
738 "Block behavior returned a ticker for the wrong block-entity type"
739 );
740 }
741 valid
742 });
743 world
744 .block_entity_tickers()
745 .reconcile(&holder, block_entity, ticker);
746 }
747
748 pub(crate) const fn scheduled_tick_container(&self) -> &Arc<ChunkTickContainer> {
750 self.chunk.scheduled_tick_container()
751 }
752
753 pub(crate) fn schedule_unregistered_block_tick(
754 &self,
755 block: BlockRef,
756 pos: BlockPos,
757 trigger_tick: i64,
758 priority: TickPriority,
759 sub_tick_order: i64,
760 ) -> Option<bool> {
761 self.chunk
762 .scheduled_tick_container()
763 .schedule_unregistered_block(block, pos, trigger_tick, priority, sub_tick_order)
764 }
765
766 pub(crate) fn schedule_unregistered_fluid_tick(
767 &self,
768 fluid: FluidRef,
769 pos: BlockPos,
770 trigger_tick: i64,
771 priority: TickPriority,
772 sub_tick_order: i64,
773 ) -> Option<bool> {
774 self.chunk
775 .scheduled_tick_container()
776 .schedule_unregistered_fluid(fluid, pos, trigger_tick, priority, sub_tick_order)
777 }
778
779 pub(crate) fn has_scheduled_block_tick(
780 &self,
781 pos: BlockPos,
782 block: BlockRef,
783 ) -> Result<bool, TickSchedulerError> {
784 self.chunk
785 .scheduled_tick_container()
786 .has_block(pos, block)
787 .ok_or(TickSchedulerError::MissingContainer(self.chunk.pos))
788 }
789
790 pub(crate) fn has_scheduled_fluid_tick(
791 &self,
792 pos: BlockPos,
793 fluid: FluidRef,
794 ) -> Result<bool, TickSchedulerError> {
795 self.chunk
796 .scheduled_tick_container()
797 .has_fluid(pos, fluid)
798 .ok_or(TickSchedulerError::MissingContainer(self.chunk.pos))
799 }
800
801 pub(crate) fn schedule_block_tick(
804 &self,
805 pos: BlockPos,
806 block: BlockRef,
807 trigger_tick: i64,
808 priority: TickPriority,
809 sub_tick_order: i64,
810 ) {
811 let result = if let Some(world) = self.get_level() {
812 world.schedule_block_tick_for_chunk(
813 *self,
814 pos,
815 block,
816 trigger_tick,
817 priority,
818 sub_tick_order,
819 )
820 } else {
821 self.schedule_unregistered_block_tick(
822 block,
823 pos,
824 trigger_tick,
825 priority,
826 sub_tick_order,
827 )
828 .ok_or(TickSchedulerError::MissingContainer(self.chunk.pos))
829 };
830
831 if result.expect("Full chunk scheduled-tick ownership invariant failed") {
832 self.chunk.dirty.store(true, Ordering::Release);
833 }
834 }
835
836 pub(crate) fn schedule_fluid_tick(
837 &self,
838 pos: BlockPos,
839 fluid: FluidRef,
840 trigger_tick: i64,
841 priority: TickPriority,
842 sub_tick_order: i64,
843 ) {
844 let result = if let Some(world) = self.get_level() {
845 world.schedule_fluid_tick_for_chunk(
846 *self,
847 pos,
848 fluid,
849 trigger_tick,
850 priority,
851 sub_tick_order,
852 )
853 } else {
854 self.schedule_unregistered_fluid_tick(
855 fluid,
856 pos,
857 trigger_tick,
858 priority,
859 sub_tick_order,
860 )
861 .ok_or(TickSchedulerError::MissingContainer(self.chunk.pos))
862 };
863
864 if result.expect("Full chunk scheduled-tick ownership invariant failed") {
865 self.chunk.dirty.store(true, Ordering::Release);
866 }
867 }
868
869 pub(crate) fn scheduled_tick_snapshot(&self) -> ScheduledTickSnapshot {
871 let current_tick = self.get_level().map_or(0, |world| world.game_time());
872 self.chunk
873 .scheduled_tick_container()
874 .snapshot(current_tick)
875 .ok_or(TickSchedulerError::MissingContainer(self.chunk.pos))
876 .expect("Full chunk scheduled-tick ownership invariant failed")
877 }
878
879 pub fn initialize_light_sources(&self) {
881 self.refresh_light_emptiness_maps();
882 self.chunk
883 .sky_light_sources
884 .write()
885 .fill_from_sections(&self.chunk.sections);
886 }
887
888 pub(crate) fn take_postprocessing(&self) -> Option<Box<[Vec<u16>]>> {
890 let mut postprocessing = self.chunk.postprocessing.lock();
891 if postprocessing.iter().all(Vec::is_empty) {
892 return None;
893 }
894
895 let pending = mem::replace(&mut *postprocessing, empty_postprocessing(self.height()));
896 self.chunk.dirty.store(true, Ordering::Release);
897 Some(pending)
898 }
899
900 pub(crate) fn postprocessing_for_serialization(&self) -> Vec<Vec<u16>> {
902 self.chunk
903 .postprocessing
904 .lock()
905 .iter()
906 .map(Vec::clone)
907 .collect()
908 }
909
910 pub(crate) fn post_process_generation(
912 world: &Arc<World>,
913 chunk_pos: ChunkPos,
914 min_y: i32,
915 postprocessing: Box<[Vec<u16>]>,
916 ) {
917 for (section_index, packed_offsets) in postprocessing.into_vec().into_iter().enumerate() {
918 if packed_offsets.is_empty() {
919 continue;
920 }
921
922 let section_y = Self::section_y_from_section_index(min_y, section_index);
923 for packed in packed_offsets {
924 let pos = Chunk::unpack_postprocessing_offset(packed, section_y, chunk_pos);
925 let state = world.get_block_state(pos);
926 let fluid_state = state.get_fluid_state();
927
928 if !fluid_state.is_empty() {
929 FLUID_BEHAVIORS.get_behavior(fluid_state.fluid_id).tick(
930 world,
931 pos,
932 state,
933 fluid_state,
934 );
935 }
936
937 if state.get_block().config.liquid {
938 BLOCK_BEHAVIORS
939 .get_behavior(state.get_block())
940 .tick(state, world, pos);
941 } else {
942 let new_state = Self::update_from_neighbor_shapes(world, state, pos);
943 if new_state != state {
944 let flags = UpdateFlags::UPDATE_INVISIBLE
945 | UpdateFlags::UPDATE_KNOWN_SHAPE
946 | UpdateFlags::UPDATE_SKIP_BLOCK_ENTITY_SIDEEFFECTS;
947 world.set_block(pos, new_state, flags);
948 }
949 }
950 }
951 }
952 }
953
954 fn update_from_neighbor_shapes(
955 world: &Arc<World>,
956 state: BlockStateId,
957 pos: BlockPos,
958 ) -> BlockStateId {
959 let mut updated = state;
960 for direction in Direction::UPDATE_SHAPE_ORDER {
961 let neighbor_pos = pos.relative(direction);
962 let neighbor_state = world.get_block_state(neighbor_pos);
963 let behavior = BLOCK_BEHAVIORS.get_behavior(updated.get_block());
964 updated =
965 behavior.update_shape(updated, world, pos, direction, neighbor_pos, neighbor_state);
966 }
967 updated
968 }
969
970 fn populate_poi(level: &Weak<World>, sections: &Sections, pos: ChunkPos, min_y: i32) {
972 let Some(world) = level.upgrade() else {
973 return;
974 };
975
976 let mut poi_sections: Vec<(usize, SectionPos)> = Vec::new();
983 for (i, section) in sections.sections.iter().enumerate() {
984 if section.read().contains_poi() {
985 let section_y = min_y / 16 + i as i32;
986 poi_sections.push((i, SectionPos::new(pos.0.x, section_y, pos.0.y)));
987 }
988 }
989 if poi_sections.is_empty() {
990 return;
991 }
992
993 let mut poi_storage = world.poi_storage.lock();
994 for (i, section_pos) in poi_sections {
995 let guard = sections.sections[i].read();
996 poi_storage.scan_and_populate(&guard, section_pos);
997 }
998 }
999
1000 #[must_use]
1002 pub const fn min_y(&self) -> i32 {
1003 self.chunk.min_y()
1004 }
1005
1006 #[must_use]
1008 pub const fn height(&self) -> i32 {
1009 self.chunk.height()
1010 }
1011
1012 #[must_use]
1014 pub fn get_height(&self, heightmap_type: HeightmapType, local_x: usize, local_z: usize) -> i32 {
1015 self.chunk
1016 .heightmaps
1017 .read()
1018 .get_final(heightmap_type)
1019 .get_first_available(local_x, local_z)
1020 }
1021
1022 #[must_use]
1024 const fn get_section_index(&self, y: i32) -> usize {
1025 ((y - self.min_y()) / 16) as usize
1026 }
1027
1028 #[must_use]
1029 const fn section_y_from_section_index(min_y: i32, index: usize) -> i32 {
1030 min_y.div_euclid(16) + index as i32
1031 }
1032
1033 fn mark_unsaved(&self) {
1035 self.chunk.dirty.store(true, Ordering::Release);
1036 }
1037
1038 #[must_use]
1042 pub fn get_block_entity(&self, pos: BlockPos) -> Option<SharedBlockEntity> {
1043 loop {
1044 match self.chunk.block_entity_storage().lookup(pos) {
1045 BlockEntityLookup::Concrete(block_entity) => {
1046 if block_entity.is_removed() {
1047 if self
1048 .chunk
1049 .block_entity_storage()
1050 .remove_if_same_and_removed(pos, &block_entity)
1051 {
1052 self.finish_block_entity_change(pos, LifecycleDispatchers::new());
1053 return None;
1054 }
1055 continue;
1056 }
1057 return Some(block_entity);
1058 }
1059 BlockEntityLookup::Pending => return self.promote_pending_block_entity(pos),
1060 BlockEntityLookup::Absent => return None,
1061 }
1062 }
1063 }
1064
1065 #[must_use]
1069 pub(crate) fn get_block_entity_immediate(&self, pos: BlockPos) -> Option<SharedBlockEntity> {
1070 if ChunkPos::from_block_pos(pos) != self.chunk.pos {
1071 return None;
1072 }
1073 loop {
1074 match self.chunk.block_entity_storage().lookup(pos) {
1075 BlockEntityLookup::Concrete(block_entity) => {
1076 if block_entity.is_removed() {
1077 if self
1078 .chunk
1079 .block_entity_storage()
1080 .remove_if_same_and_removed(pos, &block_entity)
1081 {
1082 self.finish_block_entity_change(pos, LifecycleDispatchers::new());
1083 }
1084 continue;
1085 }
1086 return Some(block_entity);
1087 }
1088 BlockEntityLookup::Pending => return self.promote_pending_block_entity(pos),
1089 BlockEntityLookup::Absent => {}
1090 }
1091
1092 let state = self.get_block_state(pos);
1093 if !state.has_block_entity() {
1094 let state_unchanged =
1095 self.with_locked_block_state(pos, |live_state| live_state == state);
1096 if state_unchanged {
1097 return None;
1098 }
1099 continue;
1100 }
1101
1102 let behavior = BLOCK_BEHAVIORS.get_behavior(state.get_block());
1103 match behavior.new_block_entity(self.chunk.level_weak(), pos, state) {
1104 BlockEntityCreation::Created(block_entity) => {
1105 let valid = block_entity.get_block_pos() == pos
1106 && block_entity.is_valid_block_state(state);
1107 let inserted = self.with_locked_block_state(pos, |live_state| {
1108 if live_state != state {
1109 return None;
1110 }
1111 if !valid {
1112 return Some(None);
1113 }
1114 Some(Some(
1115 self.chunk
1116 .block_entity_storage()
1117 .insert_if_absent_staged(&block_entity, state),
1118 ))
1119 });
1120 let Some(inserted) = inserted else {
1121 continue;
1122 };
1123 let inserted = inserted?;
1124 match inserted {
1125 BlockEntityInsert::Existing(existing) => return Some(existing),
1126 BlockEntityInsert::Inserted(lifecycle_dispatchers) => {
1127 self.finish_block_entity_change(pos, lifecycle_dispatchers);
1128 self.mark_unsaved();
1129 return Some(block_entity);
1130 }
1131 }
1132 }
1133 BlockEntityCreation::NoEntity => {
1134 let state_unchanged =
1135 self.with_locked_block_state(pos, |live_state| live_state == state);
1136 if state_unchanged {
1137 return None;
1138 }
1139 }
1140 BlockEntityCreation::Unimplemented => {
1141 let inserted = self.with_locked_block_state(pos, |live_state| {
1142 live_state == state && self.chunk.block_entity_storage().set_pending(pos)
1143 });
1144 if inserted {
1145 self.mark_unsaved();
1146 }
1147 return None;
1148 }
1149 }
1150 }
1151 }
1152
1153 fn promote_pending_block_entity(&self, pos: BlockPos) -> Option<SharedBlockEntity> {
1154 loop {
1155 match self.chunk.block_entity_storage().lookup(pos) {
1156 BlockEntityLookup::Concrete(block_entity) => {
1157 if block_entity.is_removed() {
1158 if self
1159 .chunk
1160 .block_entity_storage()
1161 .remove_if_same_and_removed(pos, &block_entity)
1162 {
1163 self.finish_block_entity_change(pos, LifecycleDispatchers::new());
1164 }
1165 continue;
1166 }
1167 return Some(block_entity);
1168 }
1169 BlockEntityLookup::Pending => {}
1170 BlockEntityLookup::Absent => return None,
1171 }
1172
1173 let state = self.get_block_state(pos);
1174 if !state.has_block_entity() {
1175 let state_unchanged = self.with_locked_block_state(pos, |live_state| {
1176 if live_state != state {
1177 return false;
1178 }
1179 self.chunk.block_entity_storage().remove_pending(pos);
1180 true
1181 });
1182 if !state_unchanged {
1183 continue;
1184 }
1185 log::warn!(
1186 "Tried to promote a pending block entity at {pos:?}, but block {} does not allow one",
1187 state.get_block().key,
1188 );
1189 return None;
1190 }
1191
1192 let behavior = BLOCK_BEHAVIORS.get_behavior(state.get_block());
1193 let creation = behavior.new_block_entity(self.chunk.level_weak(), pos, state);
1194 match self.commit_pending_creation(pos, state, creation) {
1195 PendingPromotionCommit::Retry => {}
1196 PendingPromotionCommit::Complete {
1197 block_entity,
1198 lifecycle_dispatchers,
1199 } => {
1200 self.finish_block_entity_change(pos, lifecycle_dispatchers);
1201 return block_entity;
1202 }
1203 }
1204 }
1205 }
1206
1207 fn commit_pending_creation(
1208 &self,
1209 pos: BlockPos,
1210 expected_state: BlockStateId,
1211 creation: BlockEntityCreation,
1212 ) -> PendingPromotionCommit {
1213 match creation {
1214 BlockEntityCreation::Created(block_entity) => {
1215 let valid = block_entity.get_block_pos() == pos
1216 && ChunkPos::from_block_pos(pos) == self.chunk.pos
1217 && block_entity.is_valid_block_state(expected_state);
1218 self.with_locked_block_state(pos, |live_state| {
1219 if live_state != expected_state {
1223 return PendingPromotionCommit::Retry;
1224 }
1225 if !valid {
1226 return PendingPromotionCommit::Complete {
1227 block_entity: None,
1228 lifecycle_dispatchers: LifecycleDispatchers::new(),
1229 };
1230 }
1231 let (block_entity, lifecycle_dispatchers) = self
1232 .chunk
1233 .block_entity_storage()
1234 .promote_staged(pos, expected_state, block_entity);
1235 PendingPromotionCommit::Complete {
1236 block_entity,
1237 lifecycle_dispatchers,
1238 }
1239 })
1240 }
1241 BlockEntityCreation::NoEntity => self.with_locked_block_state(pos, |live_state| {
1242 if live_state != expected_state {
1243 return PendingPromotionCommit::Retry;
1244 }
1245 self.chunk.block_entity_storage().remove_pending(pos);
1246 PendingPromotionCommit::Complete {
1247 block_entity: None,
1248 lifecycle_dispatchers: LifecycleDispatchers::new(),
1249 }
1250 }),
1251 BlockEntityCreation::Unimplemented => self.with_locked_block_state(pos, |live_state| {
1255 if live_state == expected_state {
1256 PendingPromotionCommit::Complete {
1257 block_entity: None,
1258 lifecycle_dispatchers: LifecycleDispatchers::new(),
1259 }
1260 } else {
1261 PendingPromotionCommit::Retry
1262 }
1263 }),
1264 }
1265 }
1266
1267 pub(crate) fn promote_pending_block_entities(&self) {
1271 let positions = self.pending_block_entity_positions();
1272 for pos in positions {
1273 let _ = self.promote_pending_block_entity(pos);
1274 }
1275 }
1276
1277 #[must_use]
1279 pub fn pending_block_entity_positions(&self) -> Vec<BlockPos> {
1280 self.chunk.block_entity_storage().pending_positions()
1281 }
1282
1283 pub fn set_pending_block_entity(&self, pos: BlockPos) {
1285 if ChunkPos::from_block_pos(pos) != self.chunk.pos {
1286 log::warn!(
1287 "Trying to set a pending block entity at {pos:?} in chunk {:?}",
1288 self.chunk.pos,
1289 );
1290 return;
1291 }
1292 if self.chunk.block_entity_storage().set_pending(pos) {
1293 self.mark_unsaved();
1294 }
1295 }
1296
1297 #[must_use]
1301 pub fn remove_block_entity(&self, pos: BlockPos) -> bool {
1302 let (removed, lifecycle_dispatchers) = self.chunk.block_entity_storage().remove_staged(pos);
1303 self.finish_block_entity_change(pos, lifecycle_dispatchers);
1304 self.mark_unsaved();
1305 removed
1306 }
1307
1308 pub(crate) fn remove_block_entity_if_same(&self, expected: &dyn BlockEntity) -> bool {
1310 let pos = expected.get_block_pos();
1311 let (removed, lifecycle_dispatchers) = self
1312 .chunk
1313 .block_entity_storage()
1314 .remove_if_same_staged(pos, expected);
1315 self.finish_block_entity_change(pos, lifecycle_dispatchers);
1316 if removed {
1317 self.mark_unsaved();
1318 }
1319 removed
1320 }
1321
1322 #[must_use]
1327 pub fn add_and_register_block_entity(&self, block_entity: SharedBlockEntity) -> bool {
1328 let pos = block_entity.get_block_pos();
1329 let (valid, lifecycle_dispatchers) =
1330 self.add_and_register_block_entity_staged(block_entity);
1331 self.finish_block_entity_change(pos, lifecycle_dispatchers);
1332 valid
1333 }
1334
1335 fn add_and_register_block_entity_staged(
1336 &self,
1337 block_entity: SharedBlockEntity,
1338 ) -> (bool, LifecycleDispatchers) {
1339 let pos = block_entity.get_block_pos();
1340 if ChunkPos::from_block_pos(pos) != self.chunk.pos {
1341 log::warn!(
1342 "Trying to set block entity {} at {pos:?} in chunk {:?}",
1343 block_entity.get_type().key,
1344 self.chunk.pos,
1345 );
1346 return (false, LifecycleDispatchers::new());
1347 }
1348
1349 loop {
1350 let state = self.get_block_state(pos);
1351 let valid = state.has_block_entity() && block_entity.is_valid_block_state(state);
1352 if !valid {
1353 let state_unchanged =
1354 self.with_locked_block_state(pos, |live_state| live_state == state);
1355 if !state_unchanged {
1356 continue;
1357 }
1358 log::warn!(
1359 "Trying to set block entity {} at {pos:?}, but block {} does not accept that type",
1360 block_entity.get_type().key,
1361 state.get_block().key,
1362 );
1363 return (false, LifecycleDispatchers::new());
1364 }
1365
1366 let cached_state = block_entity.get_block_state();
1367 let committed = self.with_locked_block_state(pos, |live_state| {
1368 if live_state != state {
1369 return None;
1370 }
1371 Some(
1372 self.chunk
1373 .block_entity_storage()
1374 .add_staged(&block_entity, state),
1375 )
1376 });
1377 let Some((_, lifecycle_dispatchers)) = committed else {
1378 continue;
1379 };
1380 if state.get_block() != cached_state.get_block() {
1381 log::warn!(
1382 "Updating mismatched block entity {} state at {pos:?}: {} != {}",
1383 block_entity.get_type().key,
1384 state.get_block().key,
1385 cached_state.get_block().key,
1386 );
1387 }
1388
1389 self.mark_unsaved();
1390 return (true, lifecycle_dispatchers);
1391 }
1392 }
1393
1394 fn with_locked_block_state<R>(&self, pos: BlockPos, f: impl FnOnce(BlockStateId) -> R) -> R {
1395 let y = pos.y();
1396 if y < self.min_y() || y >= self.min_y() + self.height() {
1397 return f(REGISTRY.blocks.get_base_state_id(&vanilla_blocks::AIR));
1398 }
1399
1400 let section_index = self.get_section_index(y);
1401 if section_index >= self.chunk.sections.sections.len() {
1402 return f(REGISTRY.blocks.get_base_state_id(&vanilla_blocks::AIR));
1403 }
1404
1405 let section = self.chunk.sections.sections[section_index].read();
1406 let state = if section.is_empty() {
1407 REGISTRY.blocks.get_base_state_id(&vanilla_blocks::AIR)
1408 } else {
1409 section.states().get(
1410 (pos.x() & 15) as usize,
1411 (y & 15) as usize,
1412 (pos.z() & 15) as usize,
1413 )
1414 };
1415 f(state)
1416 }
1417
1418 fn reconcile_block_entity_after_set(&self, pos: BlockPos, state: BlockStateId) {
1419 loop {
1420 if self.get_block_state(pos) != state {
1421 return;
1422 }
1423
1424 if let Some(block_entity) = self.get_block_entity(pos) {
1425 if block_entity.is_valid_block_state(state) {
1426 let committed = self.with_locked_block_state(pos, |live_state| {
1427 if live_state != state {
1428 return None;
1429 }
1430 Some(self.chunk.block_entity_storage().update_if_same_staged(
1431 pos,
1432 &block_entity,
1433 state,
1434 ))
1435 });
1436 let Some((updated, lifecycle_dispatchers)) = committed else {
1437 return;
1438 };
1439 self.finish_block_entity_change(pos, lifecycle_dispatchers);
1440 if updated {
1441 return;
1442 }
1443 continue;
1444 }
1445
1446 let removed = self.with_locked_block_state(pos, |live_state| {
1447 if live_state != state {
1448 return None;
1449 }
1450 Some(
1451 self.chunk
1452 .block_entity_storage()
1453 .remove_if_same_staged(pos, block_entity.as_ref()),
1454 )
1455 });
1456 let Some((removed, lifecycle_dispatchers)) = removed else {
1457 return;
1458 };
1459 self.finish_block_entity_change(pos, lifecycle_dispatchers);
1460 if !removed {
1461 continue;
1462 }
1463 log::warn!(
1464 "Removed mismatched block entity at {pos:?}: type = {}, state = {}",
1465 block_entity.get_type().key,
1466 state.get_block().key,
1467 );
1468 continue;
1470 }
1471
1472 let behavior = BLOCK_BEHAVIORS.get_behavior(state.get_block());
1473 match behavior.new_block_entity(self.chunk.level_weak(), pos, state) {
1474 BlockEntityCreation::Created(block_entity) => {
1475 let valid = block_entity.get_block_pos() == pos
1476 && block_entity.is_valid_block_state(state);
1477 let inserted = self.with_locked_block_state(pos, |live_state| {
1478 if live_state != state {
1479 return None;
1480 }
1481 if !valid {
1482 return Some(None);
1483 }
1484 Some(Some(
1485 self.chunk
1486 .block_entity_storage()
1487 .insert_if_absent_staged(&block_entity, state),
1488 ))
1489 });
1490 let Some(inserted) = inserted else {
1491 return;
1492 };
1493 let Some(inserted) = inserted else {
1494 debug_assert!(false, "block-entity factory returned an invalid entity");
1495 return;
1496 };
1497 if let BlockEntityInsert::Inserted(lifecycle_dispatchers) = inserted {
1498 self.finish_block_entity_change(pos, lifecycle_dispatchers);
1499 return;
1500 }
1501 }
1502 BlockEntityCreation::NoEntity => return,
1503 BlockEntityCreation::Unimplemented => {
1504 let inserted = self.with_locked_block_state(pos, |live_state| {
1505 live_state == state && self.chunk.block_entity_storage().set_pending(pos)
1506 });
1507 if inserted {
1508 self.mark_unsaved();
1509 }
1510 return;
1511 }
1512 }
1513 }
1514 }
1515
1516 #[must_use]
1518 pub fn get_block_entities(&self) -> Vec<SharedBlockEntity> {
1519 self.chunk.block_entity_storage().get_all()
1520 }
1521
1522 #[must_use]
1524 pub(crate) fn clear_all_block_entities_staged(&self) -> ClearedBlockEntities {
1525 self.chunk
1526 .block_entity_storage()
1527 .clear_and_stage_lifecycle_callbacks()
1528 }
1529
1530 #[must_use]
1547 pub fn set_block_state(
1548 &self,
1549 pos: BlockPos,
1550 state: BlockStateId,
1551 flags: UpdateFlags,
1552 ) -> Option<BlockStateId> {
1553 match self.set_block_state_inner(pos, None, state, flags)? {
1554 FullChunkBlockSetResult::Changed(old_state) => Some(old_state),
1555 FullChunkBlockSetResult::Unchanged | FullChunkBlockSetResult::Stale(_) => None,
1556 }
1557 }
1558
1559 pub(crate) fn set_block_state_if_unchanged(
1560 &self,
1561 pos: BlockPos,
1562 expected_state: BlockStateId,
1563 new_state: BlockStateId,
1564 flags: UpdateFlags,
1565 ) -> Option<FullChunkBlockSetResult> {
1566 self.set_block_state_inner(pos, Some(expected_state), new_state, flags)
1567 }
1568
1569 #[expect(
1570 clippy::too_many_lines,
1571 reason = "block mutation keeps vanilla side effects in one ordered transaction"
1572 )]
1573 fn set_block_state_inner(
1574 &self,
1575 pos: BlockPos,
1576 expected_state: Option<BlockStateId>,
1577 state: BlockStateId,
1578 flags: UpdateFlags,
1579 ) -> Option<FullChunkBlockSetResult> {
1580 let y = pos.0.y;
1581
1582 if y < self.min_y() || y >= self.min_y() + self.height() {
1583 return None;
1584 }
1585
1586 let section_index = self.get_section_index(y);
1587
1588 if section_index >= self.chunk.sections.sections.len() {
1589 return None;
1590 }
1591
1592 let section = &self.chunk.sections.sections[section_index];
1593
1594 let local_x = (pos.0.x & 15) as usize;
1595 let local_y = (y & 15) as usize;
1596 let local_z = (pos.0.z & 15) as usize;
1597
1598 let mut keep_block_entity_decision = None;
1599 let (old_state, was_empty, is_empty, detached_block_entity) = loop {
1600 let mut section_guard = section.write();
1601 let observed_state = section_guard.states().get(local_x, local_y, local_z);
1602 if expected_state.is_some_and(|expected| observed_state != expected) {
1603 return Some(FullChunkBlockSetResult::Stale(observed_state));
1604 }
1605 if observed_state == state {
1606 return Some(FullChunkBlockSetResult::Unchanged);
1607 }
1608
1609 let old_block = observed_state.get_block();
1612 let new_block = state.get_block();
1613 let block_changed = old_block != new_block;
1614 let detach_block_entity = if block_changed && observed_state.has_block_entity() {
1615 let Some((decision_state, should_keep)) = keep_block_entity_decision else {
1616 drop(section_guard);
1617 let should_keep = BLOCK_BEHAVIORS
1618 .get_behavior(new_block)
1619 .should_keep_block_entity(observed_state, state);
1620 keep_block_entity_decision = Some((observed_state, should_keep));
1621 continue;
1622 };
1623 if decision_state != observed_state {
1624 drop(section_guard);
1625 keep_block_entity_decision = None;
1626 continue;
1627 }
1628 !should_keep
1629 } else {
1630 false
1631 };
1632
1633 let was_empty = section_guard.is_empty();
1634 let old_state = section_guard.set_block_state(local_x, local_y, local_z, state);
1635 debug_assert_eq!(old_state, observed_state);
1636 let detached_block_entity = detach_block_entity.then(|| {
1637 self.chunk
1638 .block_entity_storage()
1639 .detach_and_queue_removal(pos)
1640 });
1641 let is_empty = section_guard.is_empty();
1642 break (old_state, was_empty, is_empty, detached_block_entity);
1643 };
1644
1645 let min_y = self.min_y();
1646 let sections = &self.chunk.sections;
1647 self.chunk
1648 .heightmaps
1649 .write()
1650 .update_final(local_x, y, local_z, state, |lx, scan_y, lz| {
1651 let scan_section_index = ((scan_y - min_y) / 16) as usize;
1652 let scan_local_y = ((scan_y - min_y) % 16) as usize;
1653 sections.sections[scan_section_index]
1654 .read()
1655 .states()
1656 .get(lx, scan_local_y, lz)
1657 });
1658
1659 let old_block = old_state.get_block();
1660 let new_block = state.get_block();
1661
1662 let empty_section_change = if was_empty == is_empty {
1663 None
1664 } else {
1665 self.update_light_section_emptiness(y, is_empty);
1666 Some(LightSectionEmptinessChange {
1667 section_pos: SectionPos::new(
1668 self.chunk.pos.0.x,
1669 SectionPos::block_to_section_coord(y),
1670 self.chunk.pos.0.y,
1671 ),
1672 empty: is_empty,
1673 })
1674 };
1675
1676 let light_properties_changed = has_different_light_properties(old_state, state);
1677 if light_properties_changed {
1678 self.update_sky_light_sources(local_x, y, local_z);
1679 }
1680
1681 let block_changed = old_block != new_block;
1682 let moved_by_piston = flags.contains(UpdateFlags::UPDATE_MOVE_BY_PISTON);
1683 let side_effects = !flags.contains(UpdateFlags::UPDATE_SKIP_BLOCK_ENTITY_SIDEEFFECTS);
1684
1685 let block_behaviors = &*BLOCK_BEHAVIORS;
1686 let old_behavior = block_behaviors.get_behavior(old_block);
1687 let new_behavior = block_behaviors.get_behavior(new_block);
1688
1689 let level = self.get_level();
1690 if let Some(level) = &level
1691 && (light_properties_changed || empty_section_change.is_some())
1692 {
1693 level.queue_light_change_after_block_set(pos, old_state, state, empty_section_change);
1694 }
1695
1696 if let Some(DetachedBlockEntity {
1697 entity,
1698 dispatch_removed,
1699 }) = detached_block_entity
1700 && let Some(block_entity) = entity
1701 {
1702 if side_effects && level.is_some() {
1706 block_entity.pre_remove_side_effects(pos, old_state);
1707 }
1708 let mut lifecycle_dispatchers = LifecycleDispatchers::new();
1709 if dispatch_removed {
1710 lifecycle_dispatchers.push(block_entity);
1711 }
1712 self.finish_block_entity_change(pos, lifecycle_dispatchers);
1713 }
1714
1715 if let Some(level) = level {
1716 if (block_changed || new_behavior.is_rail())
1718 && (flags.contains(UpdateFlags::UPDATE_NEIGHBORS) || moved_by_piston)
1719 {
1720 old_behavior.affect_neighbors_after_removal(
1721 old_state,
1722 &level,
1723 pos,
1724 moved_by_piston,
1725 );
1726 }
1727
1728 let current_state = section.read().states().get(local_x, local_y, local_z);
1731 if current_state.get_block() != new_block {
1732 return Some(FullChunkBlockSetResult::Stale(current_state));
1733 }
1734
1735 if !flags.contains(UpdateFlags::UPDATE_SKIP_ON_PLACE) {
1737 new_behavior.on_place(state, &level, pos, old_state, moved_by_piston);
1738 }
1739 }
1740
1741 if state.has_block_entity() {
1744 self.reconcile_block_entity_after_set(pos, state);
1745 }
1746
1747 self.mark_unsaved();
1748 Some(FullChunkBlockSetResult::Changed(old_state))
1749 }
1750
1751 fn update_light_section_emptiness(&self, y: i32, is_empty: bool) {
1752 let section_y = SectionPos::block_to_section_coord(y);
1753 self.chunk
1754 .light
1755 .write()
1756 .set_section_empty(section_y, is_empty);
1757 }
1758
1759 fn update_sky_light_sources(&self, local_x: usize, y: i32, local_z: usize) {
1760 let chunk_min_x = self.chunk.pos.0.x * 16;
1761 let chunk_min_z = self.chunk.pos.0.y * 16;
1762 self.chunk.sky_light_sources.write().update(
1763 local_x,
1764 y,
1765 local_z,
1766 |scan_x, scan_y, scan_z| {
1767 self.get_block_state(BlockPos::new(
1768 chunk_min_x + scan_x as i32,
1769 scan_y,
1770 chunk_min_z + scan_z as i32,
1771 ))
1772 },
1773 );
1774 }
1775
1776 pub(crate) fn refresh_light_emptiness_maps(&self) {
1777 self.chunk
1778 .light
1779 .write()
1780 .refresh_emptiness_maps_from_sections(&self.chunk.sections)
1781 .expect("invalid chunk light emptiness map length");
1782 }
1783
1784 #[must_use]
1786 pub fn get_block_state(&self, pos: BlockPos) -> BlockStateId {
1787 let y = pos.0.y;
1788 if y < self.min_y() || y >= self.min_y() + self.height() {
1789 return REGISTRY.blocks.get_base_state_id(&vanilla_blocks::AIR);
1790 }
1791
1792 let section_index = self.get_section_index(y);
1793
1794 if section_index >= self.chunk.sections.sections.len() {
1796 return REGISTRY.blocks.get_base_state_id(&vanilla_blocks::AIR);
1797 }
1798
1799 let section = &self.chunk.sections.sections[section_index];
1800 let section_guard = section.read();
1801
1802 if section_guard.is_empty() {
1803 return REGISTRY.blocks.get_base_state_id(&vanilla_blocks::AIR);
1804 }
1805
1806 let local_x = (pos.0.x & 15) as usize;
1807 let local_y = (y & 15) as usize;
1808 let local_z = (pos.0.z & 15) as usize;
1809
1810 section_guard.states().get(local_x, local_y, local_z)
1811 }
1812
1813 #[must_use]
1815 pub fn highest_filled_section_index(&self) -> Option<usize> {
1816 self.chunk
1817 .sections
1818 .sections
1819 .iter()
1820 .rposition(|section| !section.read().is_empty())
1821 }
1822
1823 #[must_use]
1825 pub fn highest_section_position(&self) -> i32 {
1826 self.highest_filled_section_index()
1827 .map_or(self.min_y(), |index| self.min_y() + index as i32 * 16)
1828 }
1829
1830 #[must_use]
1832 pub fn extract_chunk_data(&self) -> ChunkPacketData {
1833 let data = Vec::new();
1834
1835 let mut cursor = Cursor::new(data);
1836 self.chunk.sections.sections.iter().for_each(|section| {
1837 section.read().write(&mut cursor);
1838 });
1839
1840 let heightmaps = {
1841 let heightmaps = self.chunk.heightmaps.read();
1842 vec![
1843 (
1844 ProtocolHeightmapType::WorldSurface,
1845 heightmaps
1846 .get_final(HeightmapType::WorldSurface)
1847 .get_raw_data(),
1848 ),
1849 (
1850 ProtocolHeightmapType::MotionBlocking,
1851 heightmaps
1852 .get_final(HeightmapType::MotionBlocking)
1853 .get_raw_data(),
1854 ),
1855 (
1856 ProtocolHeightmapType::MotionBlockingNoLeaves,
1857 heightmaps
1858 .get_final(HeightmapType::MotionBlockingNoLeaves)
1859 .get_raw_data(),
1860 ),
1861 ]
1862 };
1863
1864 let block_entities: Vec<BlockEntityInfo> = self
1866 .chunk
1867 .block_entity_storage()
1868 .get_all()
1869 .iter()
1870 .map(|entity| {
1871 let pos = entity.get_block_pos();
1872 let type_id = entity.get_type().id() as i32;
1873 let update_tag = entity.get_update_tag();
1874
1875 BlockEntityInfo {
1876 packed_xz: PackedChunkLocalXZ::from_block_pos(pos),
1877 y: pos.0.y as i16,
1878 type_id,
1879 data: update_tag.into(),
1880 }
1881 })
1882 .collect();
1883
1884 ChunkPacketData {
1885 heightmaps: Heightmaps { heightmaps },
1886 data: cursor.into_inner(),
1887 block_entities,
1888 }
1889 }
1890
1891 #[must_use]
1893 pub fn extract_light_data(&self, has_skylight: bool) -> LightUpdatePacketData {
1894 let light = self.chunk.light.read();
1895 build_chunk_light_update_packet(&light, has_skylight)
1896 }
1897}
1898
1899#[cfg(test)]
1900mod game_event_tests;
1901
1902#[cfg(test)]
1903mod tests;