Skip to main content

steel_core/world/block_updates/
mod.rs

1use super::*;
2use crate::chunk::Chunk;
3
4mod neighbor_updater;
5
6pub(in crate::world) use neighbor_updater::{CollectingNeighborUpdater, ShapeUpdate};
7
8static LARGE_BLOCK_REGION_WARNING_EMITTED: AtomicBool = AtomicBool::new(false);
9
10impl World {
11    /// Vanilla block-update recursion limit (`Block.UPDATE_LIMIT`).
12    pub const UPDATE_LIMIT: i32 = 512;
13
14    /// Gets the block state at the given position.
15    ///
16    /// Returns void air out of bounds and air when the containing chunk is not loaded.
17    #[must_use]
18    pub fn get_block_state(&self, pos: BlockPos) -> BlockStateId {
19        if !self.is_in_valid_bounds(pos) {
20            return REGISTRY.blocks.get_base_state_id(&vanilla_blocks::VOID_AIR);
21        }
22
23        let chunk_pos = Self::chunk_pos_for_block(pos);
24        self.chunk_map
25            .with_full_chunk(chunk_pos, |chunk| chunk.get_block_state(pos))
26            .unwrap_or_else(|| REGISTRY.blocks.get_base_state_id(&vanilla_blocks::AIR))
27    }
28
29    ///Vanilla equivalent: `level.getBrightness()`
30    pub fn light_value_at(&self, layer: LightLayer, pos: BlockPos) -> u8 {
31        if layer == LightLayer::Sky && !self.dimension_type.has_skylight {
32            return 0;
33        }
34        if !self.is_in_valid_bounds_horizontal(pos) {
35            return self.default_light_value(layer);
36        }
37
38        let chunk_pos = Self::chunk_pos_for_block(pos);
39        self.chunk_map
40            .with_chunk_at_status(chunk_pos, ChunkStatus::Light, |chunk| {
41                let light = chunk.light();
42                light.get_light_value(layer, pos)
43            })
44            .unwrap_or_else(|| self.default_light_value(layer))
45    }
46
47    pub(crate) fn is_entity_ticking_chunk_loaded(&self, pos: BlockPos) -> bool {
48        self.chunk_map
49            .is_entity_ticking_full_chunk_loaded(Self::chunk_pos_for_block(pos))
50    }
51
52    pub(crate) fn is_block_ticking_chunk_loaded(&self, pos: BlockPos) -> bool {
53        self.chunk_map
54            .is_block_ticking_full_chunk_loaded(Self::chunk_pos_for_block(pos))
55    }
56
57    pub(crate) fn is_full_chunk_loaded_at(&self, pos: BlockPos) -> bool {
58        self.chunk_map
59            .with_full_chunk(Self::chunk_pos_for_block(pos), |_| ())
60            .is_some()
61    }
62
63    pub(crate) fn queue_light_change_after_block_set(
64        &self,
65        pos: BlockPos,
66        old_state: BlockStateId,
67        new_state: BlockStateId,
68        empty_section_change: Option<LightSectionEmptinessChange>,
69    ) {
70        let light_properties_changed = has_different_light_properties(old_state, new_state);
71        if !light_properties_changed && empty_section_change.is_none() {
72            return;
73        }
74
75        self.chunk_map
76            .queue_light_change(pos, light_properties_changed, empty_section_change);
77    }
78
79    pub(super) const fn default_light_value(&self, layer: LightLayer) -> u8 {
80        match layer {
81            LightLayer::Sky if self.dimension_type.has_skylight => MAX_LIGHT_LEVEL,
82            LightLayer::Sky | LightLayer::Block => 0,
83        }
84    }
85
86    /// Returns whether every block state in the vanilla AABB block range is air.
87    ///
88    /// Matches `BlockGetter.getBlockStates(AABB)` using
89    /// `BlockPos.betweenClosedStream(AABB)`: both min and max coordinates are
90    /// floored before iterating the inclusive block range. Large ranges fall back
91    /// to streaming reads instead of acquiring an unbounded section workset.
92    #[must_use]
93    pub fn block_states_in_aabb_are_air(&self, aabb: WorldAabb) -> bool {
94        let min_x = aabb.min_x().floor() as i32;
95        let min_y = aabb.min_y().floor() as i32;
96        let min_z = aabb.min_z().floor() as i32;
97        let max_x = aabb.max_x().floor() as i32;
98        let max_y = aabb.max_y().floor() as i32;
99        let max_z = aabb.max_z().floor() as i32;
100
101        let bounds = BlockRegionBounds::from_corners(
102            BlockPos::new(min_x, min_y, min_z),
103            BlockPos::new(max_x, max_y, max_z),
104        );
105
106        let streaming_read = || {
107            for y in min_y..=max_y {
108                for z in min_z..=max_z {
109                    for x in min_x..=max_x {
110                        if !self.get_block_state(BlockPos::new(x, y, z)).is_air() {
111                            return false;
112                        }
113                    }
114                }
115            }
116            true
117        };
118
119        let Some(all_air) = self.try_with_block_region(bounds, |region| {
120            for y in min_y..=max_y {
121                for z in min_z..=max_z {
122                    for x in min_x..=max_x {
123                        let Some(state) = region.get_block_state(BlockPos::new(x, y, z)) else {
124                            return false;
125                        };
126                        if !state.is_air() {
127                            return false;
128                        }
129                    }
130                }
131            }
132            true
133        }) else {
134            if !LARGE_BLOCK_REGION_WARNING_EMITTED.swap(true, Ordering::Relaxed) {
135                tracing::warn!(
136                    min_x,
137                    min_y,
138                    min_z,
139                    max_x,
140                    max_y,
141                    max_z,
142                    max_workset_slots = MAX_BLOCK_REGION_WORKSET_SLOTS,
143                    "Block-state AABB exceeds the bulk-read limit; using streaming reads"
144                );
145            }
146            return streaming_read();
147        };
148        all_air
149    }
150
151    /// Sets a block at the given position.
152    ///
153    /// Returns `true` if the block was successfully set, `false` otherwise.
154    /// Uses the default update limit of 512 (matching vanilla).
155    ///
156    /// Live gameplay callers must run in Steel's serialized world-mutation phase. Palette and
157    /// block-entity ownership claims are atomic, but the following Vanilla-ordered callbacks,
158    /// neighbor updates, and derived-cache writes are intentionally not one concurrent
159    /// transaction for the same position.
160    pub fn set_block(
161        self: &Arc<Self>,
162        pos: BlockPos,
163        block_state: BlockStateId,
164        flags: UpdateFlags,
165    ) -> bool {
166        self.set_block_with_limit(pos, block_state, flags, Self::UPDATE_LIMIT)
167    }
168
169    /// Sets a block at the given position with a custom update limit.
170    ///
171    /// The update limit bounds recursive shape propagation. The block mutation
172    /// itself still occurs when the limit is zero or negative, matching vanilla.
173    ///
174    /// Returns `true` if the block was successfully set, `false` otherwise.
175    /// See [`Self::set_block`] for the serialized world-mutation requirement.
176    pub fn set_block_with_limit(
177        self: &Arc<Self>,
178        pos: BlockPos,
179        block_state: BlockStateId,
180        flags: UpdateFlags,
181        update_limit: i32,
182    ) -> bool {
183        if !self.is_in_valid_bounds(pos) {
184            return false;
185        }
186
187        let chunk_pos = Self::chunk_pos_for_block(pos);
188        let Some(old_state) = self
189            .chunk_map
190            .with_full_chunk(chunk_pos, |chunk| {
191                chunk.set_block_state(pos, block_state, flags)
192            })
193            .flatten()
194        else {
195            return false;
196        };
197
198        self.finish_block_set(pos, old_state, block_state, flags, update_limit);
199        true
200    }
201
202    /// Replaces a block only if it still has `expected_state`.
203    ///
204    /// The comparison and palette write are performed under one chunk-section write lock. This
205    /// prevents two consumers of the same observed block state from both succeeding. Block
206    /// callbacks still run after that state claim, so callers must remain in a serialized
207    /// world-mutation phase such as an exclusive packet handler or ordered tick commit.
208    pub fn set_block_if_unchanged(
209        self: &Arc<Self>,
210        pos: BlockPos,
211        expected_state: BlockStateId,
212        new_state: BlockStateId,
213        flags: UpdateFlags,
214    ) -> ConditionalBlockSetResult {
215        self.set_block_if_unchanged_with_limit(
216            pos,
217            expected_state,
218            new_state,
219            flags,
220            Self::UPDATE_LIMIT,
221        )
222    }
223
224    /// Conditional variant of [`Self::set_block_with_limit`].
225    pub fn set_block_if_unchanged_with_limit(
226        self: &Arc<Self>,
227        pos: BlockPos,
228        expected_state: BlockStateId,
229        new_state: BlockStateId,
230        flags: UpdateFlags,
231        update_limit: i32,
232    ) -> ConditionalBlockSetResult {
233        if !self.is_in_valid_bounds(pos) {
234            return ConditionalBlockSetResult::Unavailable;
235        }
236
237        let chunk_pos = Self::chunk_pos_for_block(pos);
238        let Some(result) = self
239            .chunk_map
240            .with_full_chunk(chunk_pos, |chunk| {
241                chunk.set_block_state_if_unchanged(pos, expected_state, new_state, flags)
242            })
243            .flatten()
244        else {
245            return ConditionalBlockSetResult::Unavailable;
246        };
247
248        match result {
249            FullChunkBlockSetResult::Changed(old_state) => {
250                self.finish_block_set(pos, old_state, new_state, flags, update_limit);
251                ConditionalBlockSetResult::Changed
252            }
253            FullChunkBlockSetResult::Unchanged => ConditionalBlockSetResult::Unchanged,
254            FullChunkBlockSetResult::Stale(current_state) => {
255                ConditionalBlockSetResult::Stale(current_state)
256            }
257        }
258    }
259
260    pub(super) fn finish_block_set(
261        self: &Arc<Self>,
262        pos: BlockPos,
263        old_state: BlockStateId,
264        block_state: BlockStateId,
265        flags: UpdateFlags,
266        update_limit: i32,
267    ) {
268        let new_state = self.get_block_state(pos);
269        if new_state != block_state {
270            return;
271        }
272
273        let chunk_pos = Self::chunk_pos_for_block(pos);
274        if flags.contains(UpdateFlags::UPDATE_CLIENTS)
275            && self.chunk_map.is_block_ticking_full_chunk_loaded(chunk_pos)
276        {
277            self.chunk_map.block_changed(pos);
278            self.update_navigating_mobs_after_block_collision_change(pos, old_state, block_state);
279        }
280
281        if flags.contains(UpdateFlags::UPDATE_NEIGHBORS) {
282            self.update_neighbors_at(pos, old_state.get_block());
283            let behavior = BLOCK_BEHAVIORS.get_behavior(block_state.get_block());
284            if behavior.has_analog_output_signal(block_state) {
285                self.update_neighbor_for_output_signal(pos, block_state.get_block());
286            }
287        }
288
289        if !flags.contains(UpdateFlags::UPDATE_KNOWN_SHAPE) && update_limit > 0 {
290            let neighbor_flags =
291                flags & !(UpdateFlags::UPDATE_NEIGHBORS | UpdateFlags::UPDATE_SUPPRESS_DROPS);
292            let old_behavior = BLOCK_BEHAVIORS.get_behavior(old_state.get_block());
293            old_behavior.update_indirect_neighbour_shapes(
294                old_state,
295                self,
296                pos,
297                neighbor_flags,
298                update_limit - 1,
299            );
300            self.update_neighbour_shapes(block_state, pos, neighbor_flags, update_limit - 1);
301            let new_behavior = BLOCK_BEHAVIORS.get_behavior(block_state.get_block());
302            new_behavior.update_indirect_neighbour_shapes(
303                block_state,
304                self,
305                pos,
306                neighbor_flags,
307                update_limit - 1,
308            );
309        }
310
311        if REGISTRY.poi_types.type_id_for_state(old_state)
312            != REGISTRY.poi_types.type_id_for_state(new_state)
313        {
314            self.poi_storage
315                .lock()
316                .on_block_state_change(pos, old_state, new_state);
317        }
318    }
319
320    pub(super) fn update_navigating_mobs_after_block_collision_change(
321        self: &Arc<Self>,
322        pos: BlockPos,
323        old_state: BlockStateId,
324        new_state: BlockStateId,
325    ) {
326        let collision_shape_changed = self.block_collision_shape_changed(pos, old_state, new_state);
327        let game_time = self.game_time();
328        for entity_id in self.navigating_mob_ids() {
329            let Some(entity) = self.entity_manager.get_by_id(entity_id) else {
330                self.untrack_navigating_mob(entity_id);
331                continue;
332            };
333            let Some(pathfinder) = entity.as_pathfinder_mob() else {
334                self.untrack_navigating_mob(entity_id);
335                continue;
336            };
337            {
338                let mut navigation = pathfinder.mob_base().navigation().lock();
339                navigation.invalidate_path_type(pos);
340            }
341            if !collision_shape_changed {
342                continue;
343            }
344            if !pathfinder.is_path_finding() {
345                continue;
346            }
347
348            let should_recompute = {
349                let navigation = pathfinder.mob_base().navigation().lock();
350                navigation.should_recompute_path(pos, pathfinder.position())
351            };
352            if !should_recompute {
353                continue;
354            }
355
356            let request = {
357                let mut navigation = pathfinder.mob_base().navigation().lock();
358                navigation.request_recompute_path(game_time, pathfinder.can_update_path())
359            };
360            if let Some(request) = request {
361                pathfinder.recompute_path(request);
362            }
363        }
364    }
365
366    pub(super) fn navigating_mob_ids(&self) -> Vec<i32> {
367        self.navigating_mobs.ids()
368    }
369
370    pub(super) fn block_collision_shape_changed(
371        &self,
372        pos: BlockPos,
373        old_state: BlockStateId,
374        new_state: BlockStateId,
375    ) -> bool {
376        let old_shape = self.block_collision_shape(pos, old_state);
377        let new_shape = self.block_collision_shape(pos, new_state);
378        join_is_not_empty(old_shape, new_shape, BooleanOp::NotSame)
379    }
380
381    pub(super) fn block_collision_shape(&self, pos: BlockPos, state: BlockStateId) -> VoxelShape {
382        BLOCK_BEHAVIORS
383            .get_behavior(state.get_block())
384            .get_collision_shape(state, self, pos, BlockCollisionContext::empty())
385    }
386
387    /// Updates all neighbors of the given position about a block change.
388    ///
389    /// This is the Rust equivalent of vanilla's `Level.updateNeighborsAt()`.
390    pub fn update_neighbors_at(self: &Arc<Self>, pos: BlockPos, source_block: BlockRef) {
391        self.neighbor_updater
392            .update_neighbors_at_except_from_facing(self, pos, source_block, None);
393    }
394
395    /// Runs Vanilla's deferred command-placement neighbor notifications.
396    ///
397    /// `/fill`, `/setblock`, and `/clone` suppress ordinary neighbor updates
398    /// while mutating their regions, then replay this operation in a stable
399    /// second pass.
400    pub(crate) fn update_neighbors_on_block_set(
401        self: &Arc<Self>,
402        pos: BlockPos,
403        old_state: BlockStateId,
404    ) {
405        let state = self.get_block_state(pos);
406        let block = state.get_block();
407        if old_state.get_block() != block {
408            BLOCK_BEHAVIORS
409                .get_behavior(old_state.get_block())
410                .affect_neighbors_after_removal(old_state, self, pos, false);
411        }
412
413        self.update_neighbors_at(pos, block);
414        if BLOCK_BEHAVIORS
415            .get_behavior(block)
416            .has_analog_output_signal(state)
417        {
418            self.update_neighbor_for_output_signal(pos, block);
419        }
420    }
421
422    /// Updates all neighbors except the one in `skip_direction`.
423    ///
424    /// Mirrors vanilla `Level.updateNeighborsAtExceptFromFacing` without the
425    /// experimental redstone `Orientation` value.
426    pub fn update_neighbors_at_except_from_facing(
427        self: &Arc<Self>,
428        pos: BlockPos,
429        source_block: BlockRef,
430        skip_direction: Direction,
431    ) {
432        self.neighbor_updater
433            .update_neighbors_at_except_from_facing(self, pos, source_block, Some(skip_direction));
434    }
435
436    /// Updates all neighboring shapes around `pos`.
437    pub fn update_neighbor_shapes_at(
438        self: &Arc<Self>,
439        state: BlockStateId,
440        pos: BlockPos,
441        flags: UpdateFlags,
442        update_limit: i32,
443    ) {
444        for direction in Direction::UPDATE_SHAPE_ORDER {
445            let neighbor_pos = pos.relative(direction);
446            self.neighbor_shape_changed(
447                direction.opposite(),
448                neighbor_pos,
449                pos,
450                state,
451                flags,
452                update_limit,
453            );
454        }
455    }
456
457    /// Updates comparators that can read analog output from `pos`.
458    ///
459    /// Mirrors vanilla `Level.updateNeighbourForOutputSignal`.
460    /// Steel intentionally never synchronously loads the second neighbor chunk:
461    /// block-ticking chunks have a radius-one Full-chunk safety border, while
462    /// other call sites retain the game-tick no-blocking policy.
463    pub(crate) fn update_neighbor_for_output_signal(
464        self: &Arc<Self>,
465        pos: BlockPos,
466        changed_block: BlockRef,
467    ) {
468        for direction in Direction::HORIZONTAL {
469            let mut relative_pos = pos.relative(direction);
470            if !self.has_full_chunk(Self::chunk_pos_for_block(relative_pos)) {
471                continue;
472            }
473
474            let mut state = self.get_block_state(relative_pos);
475            if state.get_block() == &vanilla_blocks::COMPARATOR {
476                self.neighbor_changed_with_state(state, relative_pos, changed_block, false);
477                continue;
478            }
479
480            if !self.is_redstone_conductor(state, relative_pos) {
481                continue;
482            }
483
484            relative_pos = relative_pos.relative(direction);
485            if !self.has_full_chunk(Self::chunk_pos_for_block(relative_pos)) {
486                continue;
487            }
488            state = self.get_block_state(relative_pos);
489            if state.get_block() == &vanilla_blocks::COMPARATOR {
490                self.neighbor_changed_with_state(state, relative_pos, changed_block, false);
491            }
492        }
493    }
494
495    pub(crate) fn update_neighbour_shapes(
496        self: &Arc<Self>,
497        state: BlockStateId,
498        pos: BlockPos,
499        flags: UpdateFlags,
500        update_limit: i32,
501    ) {
502        for direction in Direction::UPDATE_SHAPE_ORDER {
503            let neighbor_pos = pos.relative(direction);
504            self.neighbor_shape_changed(
505                direction.opposite(),
506                neighbor_pos,
507                pos,
508                state,
509                flags,
510                update_limit,
511            );
512        }
513    }
514
515    /// Recomputes a state against all neighbors in vanilla shape-update order.
516    pub(crate) fn update_from_neighbor_shapes(
517        self: &Arc<Self>,
518        state: BlockStateId,
519        pos: BlockPos,
520    ) -> BlockStateId {
521        let mut updated = state;
522        for direction in Direction::UPDATE_SHAPE_ORDER {
523            let neighbor_pos = pos.relative(direction);
524            let neighbor_state = self.get_block_state(neighbor_pos);
525            updated = BLOCK_BEHAVIORS
526                .get_behavior(updated.get_block())
527                .update_shape(updated, self, pos, direction, neighbor_pos, neighbor_state);
528        }
529        updated
530    }
531
532    /// Called when a neighbor's shape changes, to update this block's state.
533    ///
534    /// This is the Rust equivalent of vanilla's `NeighborUpdater.executeShapeUpdate()`.
535    pub(crate) fn neighbor_shape_changed(
536        self: &Arc<Self>,
537        direction: Direction,
538        pos: BlockPos,
539        neighbor_pos: BlockPos,
540        neighbor_state: BlockStateId,
541        flags: UpdateFlags,
542        update_limit: i32,
543    ) {
544        self.neighbor_updater.shape_update(
545            self,
546            ShapeUpdate::new(
547                direction,
548                neighbor_state,
549                pos,
550                neighbor_pos,
551                flags,
552                update_limit,
553            ),
554        );
555    }
556
557    pub(super) fn execute_neighbor_shape_update(
558        self: &Arc<Self>,
559        direction: Direction,
560        pos: BlockPos,
561        neighbor_pos: BlockPos,
562        neighbor_state: BlockStateId,
563        flags: UpdateFlags,
564        update_limit: i32,
565    ) {
566        if !self.is_in_valid_bounds(pos) {
567            return;
568        }
569
570        let current_state = self.get_block_state(pos);
571
572        if flags.contains(UpdateFlags::UPDATE_SKIP_SHAPE_UPDATE_ON_WIRE)
573            && current_state.get_block() == &vanilla_blocks::REDSTONE_WIRE
574        {
575            return;
576        }
577
578        let block_behaviors = &*BLOCK_BEHAVIORS;
579        let behavior = block_behaviors.get_behavior(current_state.get_block());
580        let new_state = behavior.update_shape(
581            current_state,
582            self,
583            pos,
584            direction,
585            neighbor_pos,
586            neighbor_state,
587        );
588
589        self.update_or_destroy(current_state, new_state, pos, flags, update_limit);
590    }
591
592    pub(crate) fn update_or_destroy(
593        self: &Arc<World>,
594        old_state: BlockStateId,
595        new_state: BlockStateId,
596        pos: BlockPos,
597        flags: UpdateFlags,
598        recursion_left: i32,
599    ) {
600        if new_state == old_state {
601            return;
602        }
603
604        if new_state.is_air() {
605            self.destroy_block_with_limit(
606                pos,
607                !flags.contains(UpdateFlags::UPDATE_SUPPRESS_DROPS),
608                recursion_left,
609            );
610        } else {
611            self.set_block_with_limit(
612                pos,
613                new_state,
614                flags & !UpdateFlags::UPDATE_SUPPRESS_DROPS,
615                recursion_left,
616            );
617        }
618    }
619
620    /// Notifies a block that one of its neighbors changed.
621    ///
622    /// This is the Rust equivalent of vanilla's `Level.neighborChanged()`.
623    pub(crate) fn neighbor_changed(self: &Arc<Self>, pos: BlockPos, source_block: BlockRef) {
624        self.neighbor_updater
625            .neighbor_changed(self, pos, source_block);
626    }
627
628    pub(crate) fn neighbor_changed_with_state(
629        self: &Arc<Self>,
630        state: BlockStateId,
631        pos: BlockPos,
632        source_block: BlockRef,
633        moved_by_piston: bool,
634    ) {
635        self.neighbor_updater.neighbor_changed_with_state(
636            self,
637            state,
638            pos,
639            source_block,
640            moved_by_piston,
641        );
642    }
643
644    pub(super) fn execute_neighbor_update(
645        self: &Arc<Self>,
646        state: BlockStateId,
647        pos: BlockPos,
648        source_block: BlockRef,
649        moved_by_piston: bool,
650    ) {
651        if !self.is_in_valid_bounds(pos) {
652            return;
653        }
654        let block_behaviors = &*BLOCK_BEHAVIORS;
655        let behavior = block_behaviors.get_behavior(state.get_block());
656        behavior.handle_neighbor_changed(state, self, pos, source_block, moved_by_piston);
657    }
658
659    pub(super) const fn chunk_pos_for_block(pos: BlockPos) -> ChunkPos {
660        ChunkPos::new(
661            SectionPos::block_to_section_coord(pos.0.x),
662            SectionPos::block_to_section_coord(pos.0.z),
663        )
664    }
665
666    /// Gets a block entity at the given position.
667    ///
668    /// Returns `None` if the chunk is not loaded or there is no block entity at the position.
669    #[must_use]
670    pub fn get_block_entity(&self, pos: BlockPos) -> Option<SharedBlockEntity> {
671        let chunk_pos = Self::chunk_pos_for_block(pos);
672        self.chunk_map
673            .with_full_chunk(chunk_pos, |chunk| chunk.get_block_entity_immediate(pos))
674            .flatten()
675    }
676
677    /// Adds a block entity to the loaded full chunk at its position.
678    pub(crate) fn set_block_entity(&self, block_entity: SharedBlockEntity) -> bool {
679        let pos = block_entity.get_block_pos();
680        if !self.is_in_valid_bounds(pos) {
681            return false;
682        }
683
684        self.chunk_map
685            .with_full_chunk(Self::chunk_pos_for_block(pos), |chunk| {
686                chunk.add_and_register_block_entity(block_entity)
687            })
688            .unwrap_or(false)
689    }
690
691    /// Removes a block entity only while it still owns its position.
692    pub(crate) fn remove_block_entity_if_same(&self, expected: &dyn BlockEntity) -> bool {
693        let pos = expected.get_block_pos();
694        if !self.is_in_valid_bounds(pos) {
695            return false;
696        }
697
698        self.chunk_map
699            .with_full_chunk(Self::chunk_pos_for_block(pos), |chunk| {
700                chunk.remove_block_entity_if_same(expected)
701            })
702            .unwrap_or(false)
703    }
704
705    /// Called when a block entity's data changes.
706    ///
707    /// Marks the containing chunk as unsaved so it will be persisted to disk.
708    pub fn block_entity_changed(&self, pos: BlockPos) {
709        let chunk_pos = Self::chunk_pos_for_block(pos);
710        self.chunk_map.packet_content_changed(chunk_pos);
711        self.mark_chunk_dirty(chunk_pos);
712    }
713
714    /// Queues a same-state block update and its block-entity update packet.
715    ///
716    /// Mirrors vanilla `Level.sendBlockUpdated` for callers that changed only
717    /// block-entity data.
718    pub(crate) fn send_block_updated(&self, pos: BlockPos) {
719        self.chunk_map.block_changed(pos);
720    }
721
722    /// Marks a chunk as dirty (unsaved) so it will be persisted to disk.
723    ///
724    /// Called when entities move, are added/removed, or when block entities change.
725    pub fn mark_chunk_dirty(&self, chunk_pos: ChunkPos) {
726        self.chunk_map
727            .with_chunk_at_status(chunk_pos, ChunkStatus::Empty, Chunk::mark_dirty);
728    }
729}