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