Skip to main content

steel_core/chunk/full_chunk/
mod.rs

1//! Full-chunk behavior over the common owning [`Chunk`].
2use 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/// Borrowed capability for Full-only live world access.
53///
54/// Similar to Java's `LevelChunk`, this holds a weak reference to the world
55/// (called `level` in Java) for callbacks during block state changes. Ticking
56/// and initial sending additionally require the corresponding neighborhood
57/// readiness confirmation from `ChunkMap`.
58#[derive(Clone, Copy)]
59pub struct FullChunkRef<'a> {
60    chunk: &'a Chunk,
61}
62
63/// State that only exists once a common chunk crosses the Full boundary.
64pub(crate) struct FullChunkRuntime {
65    /// Section registries and exact listener selections owned by this retained chunk.
66    game_event_listeners: FullChunkGameEventListeners,
67    /// Main-boundary activation state and callbacks staged by background loading.
68    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
97/// Result of promoting a proto chunk to a full chunk.
98pub struct FullChunkPromotion<'a> {
99    /// Full capability initialized by this promotion.
100    pub chunk: FullChunkRef<'a>,
101    /// Entities that should be registered after the full chunk is published.
102    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
136/// Generates section-local random-tick positions with Vanilla's LCG and bit layout.
137pub(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    /// Returns the data retained across generation and Full runtime access.
182    #[must_use]
183    pub(crate) const fn common(&self) -> &Chunk {
184        self.chunk
185    }
186
187    /// Returns the sections shared with the generation phase.
188    #[must_use]
189    pub const fn sections(&self) -> &Sections {
190        self.chunk.sections()
191    }
192
193    /// Runs random block and fluid ticks for this chunk.
194    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            // Keep the guard across no-op samples. Extreme random tick speeds may
216            // delay concurrent writers, but avoid lock churn in normal ticking.
217            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                // Either callback may write this section. Reacquire lazily so
229                // the next sample observes all changes made by this tick.
230                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    /// Promotes this chunk to Full status and initializes its runtime state.
260    ///
261    /// Transfers the chunk's heightmaps after ensuring every final map is primed.
262    /// Recalculates section block counts for random tick optimization.
263    ///
264    /// # Panics
265    /// Panics if this chunk's light-section count does not match its world height.
266    ///
267    #[must_use]
268    pub(crate) fn promote_to_full(&self) -> FullChunkPromotion<'_> {
269        let proto_chunk = self;
270        // Generation-only caches are never retained by a Full chunk. Carvers normally
271        // consume them earlier; promotion is the defensive lifecycle boundary.
272        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        // Ensure full chunks always have populated final heightmaps. Some stages
277        // may not touch blocks (carvers are currently empty), so lazy final
278        // heightmaps are not guaranteed to exist before promotion.
279        {
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        // Recalculate section counts for random tick optimization
293        for section in &proto_chunk.sections.sections {
294            section.write().recalculate_counts();
295        }
296
297        // Vanilla keeps proto ticks pending through Full promotion. Retaining
298        // the same container also makes the promotion linearizable with any
299        // concurrent scheduling against the chunk.
300        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    /// Creates an owning Full chunk loaded from disk (not dirty).
328    ///
329    /// Recalculates section block counts for random tick optimization.
330    ///
331    /// # Arguments
332    /// * `sections` - The chunk sections
333    /// * `pos` - The chunk position
334    /// * `min_y` - The minimum Y coordinate of the world
335    /// * `height` - The total height of the world
336    /// * `level` - Weak reference to the world (mirrors Java's `LevelChunk.level`)
337    /// * `block_ticks` - Scheduled block ticks loaded from disk
338    /// * `fluid_ticks` - Scheduled fluid ticks loaded from disk
339    /// * `heightmaps` - Heightmaps loaded from disk
340    /// * `postprocessing` - Pending postprocessing offsets loaded from disk
341    /// * `light` - Chunk-owned light data loaded from disk
342    ///
343    /// # Panics
344    /// Panics if the loaded light-section count does not match the chunk's world height.
345    ///
346    #[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        // Disk payloads may omit maps that are derivable from section data.
366        // Full construction owns the invariant that every final map exists;
367        // callers cannot publish a partially initialized runtime chunk.
368        heightmaps.prime_from_sections(
369            HeightmapType::final_types(),
370            min_y,
371            height,
372            &sections.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    /// Revalidates proto block entities while retaining the same storage instance.
409    fn adopt_proto_block_entities(&self) {
410        // Vanilla's source is a HashMap. Preserve the storage's native order at
411        // this promotion-only boundary rather than imposing a new ordering.
412        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    /// Returns a reference to the world if it's still alive.
488    ///
489    /// This mirrors Java's `LevelChunk.getLevel()`.
490    #[must_use]
491    pub fn get_level(&self) -> Option<Arc<World>> {
492        self.chunk.get_level()
493    }
494
495    /// Returns the weak reference to the world.
496    ///
497    /// Use this when you need to pass the world reference to block entities
498    /// at construction time.
499    #[must_use]
500    pub fn level_weak(&self) -> Weak<World> {
501        self.chunk.level_weak()
502    }
503
504    /// Activates load-staged block entities at the serialized chunk lifecycle boundary.
505    ///
506    /// The holder is installed before callbacks so reentrant changes register directly
507    /// into the world ticker. Existing storage order is retained for this load-only step.
508    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(&current, holder));
516                if Arc::ptr_eq(&current, 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    /// Deactivates one finalized holder and returns callbacks staged before activation.
549    #[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(&current, holder));
559        if belongs {
560            activation.holder = Weak::new();
561        }
562        mem::take(&mut activation.pending_lifecycle_dispatchers)
563    }
564
565    /// Makes an unloading holder dormant without discarding wrapper identity.
566    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(&current, holder));
572        if belongs {
573            activation.holder = Weak::new();
574        }
575    }
576
577    /// Captures a live state only while `expected` remains the exact storage owner.
578    ///
579    /// The section read precedes the storage read, matching block-state writers. Both
580    /// guards are dropped before the caller selects or invokes behavior.
581    #[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    /// Re-selects one listener without retaining section, storage, or binding locks across the
640    /// block/provider callback.
641    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    /// Returns this chunk's stable scheduled-tick container.
749    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    /// Schedules through the world index, or through local pre-publication
802    /// storage when this chunk has no live world (as in focused unit tests).
803    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    /// Takes an owned persistence snapshot without exposing live scheduler data.
870    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    /// Fills the vanilla skylight-source cache from current section contents.
880    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    /// Drains pending vanilla generation postprocessing offsets.
889    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    /// Snapshots pending postprocessing offsets for chunk persistence.
901    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    /// Runs pending vanilla generation postprocessing at the r1 readiness transition.
911    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    /// Scans chunk sections for POI block states and populates world POI storage.
971    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        // Palette pre-check WITHOUT the global POI lock: collect only the
977        // sections that actually contain POI blocks. The vast majority of
978        // worldgen chunks have none, so they never touch the (heavily
979        // contended) `poi_storage` mutex and never do a per-block scan.
980        // `Vec::new()` doesn't allocate until the first push, so the common
981        // empty case is allocation-free.
982        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    /// Returns the minimum Y coordinate of the world.
1001    #[must_use]
1002    pub const fn min_y(&self) -> i32 {
1003        self.chunk.min_y()
1004    }
1005
1006    /// Returns the total height of the world.
1007    #[must_use]
1008    pub const fn height(&self) -> i32 {
1009        self.chunk.height()
1010    }
1011
1012    /// Gets the first available Y coordinate for a heightmap column.
1013    #[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    /// Gets the section index for a given Y coordinate.
1023    #[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    /// Marks the chunk as unsaved.
1034    fn mark_unsaved(&self) {
1035        self.chunk.dirty.store(true, Ordering::Release);
1036    }
1037
1038    /// Gets a block entity at the given position.
1039    ///
1040    /// Returns `None` if no block entity exists at the position.
1041    #[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    /// Gets a block entity, creating the live block's implementation when storage is missing.
1066    ///
1067    /// This is Vanilla's `EntityCreationType.IMMEDIATE` path used by `Level.getBlockEntity`.
1068    #[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                    // The block behavior owns its factory. A result created from an obsolete
1220                    // state must never consume a marker installed by the replacement block, even
1221                    // when both states accept the same block-entity type.
1222                    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            // Keep Steel's marker until this block gains a factory. Vanilla consumes it because
1252            // every Vanilla EntityBlock factory is implemented; retaining it prevents permanent
1253            // data loss for intentionally deferred block implementations.
1254            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    /// Attempts every packed promotion after generation postprocessing.
1268    ///
1269    /// Intentional `Unimplemented` markers remain packed until Steel gains their block factory.
1270    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    /// Returns packed block-entity positions without causing promotion.
1278    #[must_use]
1279    pub fn pending_block_entity_positions(&self) -> Vec<BlockPos> {
1280        self.chunk.block_entity_storage().pending_positions()
1281    }
1282
1283    /// Retains a Vanilla `DUMMY` marker for lazy promotion if no concrete entity exists.
1284    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    /// Removes a block entity at the given position.
1298    ///
1299    /// Marks the entity as removed and unbinds its world ticker.
1300    #[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    /// Removes only the entity that still owns its position.
1309    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    /// Adds a block entity and reconciles its state-selected world ticker.
1323    ///
1324    /// Note: The world reference should be passed at block entity construction time.
1325    /// Returns false when the entity's position or type does not match the live state.
1326    #[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                // Removal hooks may synchronously replace either the block or its entity.
1469                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    /// Returns all block entities in this chunk.
1517    #[must_use]
1518    pub fn get_block_entities(&self) -> Vec<SharedBlockEntity> {
1519        self.chunk.block_entity_storage().get_all()
1520    }
1521
1522    /// Clears entity ownership while deferring lifecycle callbacks to an outer-lock-free caller.
1523    #[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    /// Sets a block state at the given position.
1531    ///
1532    /// Returns the old block state, or `None` if nothing changed.
1533    ///
1534    /// The world scheduler serializes gameplay mutations. This method makes the palette and
1535    /// block-entity ownership transition atomic, but its later Vanilla-ordered callbacks and
1536    /// derived-cache updates are not a general concurrent transaction for the same position.
1537    ///
1538    /// # Arguments
1539    /// * `pos` - The absolute block position
1540    /// * `state` - The new block state to set
1541    /// * `flags` - Update flags controlling behavior
1542    ///
1543    /// # Panics
1544    ///
1545    /// Panics if the behavior registry has not been initialized.
1546    #[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            // Behavior decisions run without section/storage locks. The following palette write
1610            // verifies the exact observed state before using the decision.
1611            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            // The exact old owner was detached with the palette write, so a concurrent/reentrant
1703            // replacement cannot be drained or removed by this operation. Unlike Vanilla's
1704            // single-threaded map, the entity is no longer discoverable during this callback.
1705            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            // Notify neighbors that we were removed (for rails, etc.)
1717            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            // Removal callbacks may synchronously replace this position. Vanilla does not run
1729            // placement callbacks for the stale request.
1730            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            // Call on_place for the new block
1736            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        // Block-entity reconciliation is an exact-state transaction. Placement callbacks or
1742        // concurrent writers that replace this request own the resulting entity instead.
1743        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    /// Gets a block state at the given position.
1785    #[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        // `LevelChunk` returns air outside its section array; `World` handles void air.
1795        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    /// Returns the index of the highest filled section in this chunk, if any.
1814    #[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    /// Returns the Y coordinate of the highest filled section in this chunk.
1824    #[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    /// Extracts the chunk data for sending to the client.
1831    #[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        // Collect block entity data for client sync
1865        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    /// Extracts the light data for sending to the client.
1892    #[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;