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