1use std::fmt::{self, Formatter};
3use std::sync::{
4 Arc, OnceLock, Weak,
5 atomic::{AtomicBool, Ordering},
6};
7
8use parking_lot::{MappedRwLockWriteGuard, RwLockReadGuard, RwLockWriteGuard};
9use rustc_hash::FxHashMap;
10use steel_registry::{
11 REGISTRY,
12 blocks::{BlockRef, block_state_ext::BlockStateExt},
13 fluid::FluidRef,
14 vanilla_blocks,
15};
16use steel_utils::{
17 BlockPos, BlockStateId, ChunkPos, Downcast as _, DowncastType, ErasedType, SectionPos,
18 locks::{SyncMutex, SyncRwLock},
19 types::UpdateFlags,
20};
21
22use crate::behavior::{BLOCK_BEHAVIORS, BlockEntityCreation};
23use crate::block_entity::{BlockEntityLookup, BlockEntityStorage, SharedBlockEntity};
24use crate::chunk::{
25 full_chunk::FullChunkRuntime,
26 heightmap::{ChunkHeightmaps, HeightmapType},
27 light::{
28 ChunkLightData, ChunkSkyLightSources, LightSectionEmptinessChange,
29 has_different_light_properties,
30 },
31 section::Sections,
32 status::ChunkStatus,
33};
34use crate::entity::{EntityStorage, EntityStorageAddResult, SharedEntity};
35use crate::world::World;
36use crate::world::tick_scheduler::{
37 BlockTickList, ChunkTickContainer, ChunkTickLists, FluidTickList, TickPriority,
38};
39use crate::worldgen::carving_mask::CarvingMask;
40use steel_worldgen::structure::{StructureReferenceMap, StructureStartMap};
41
42pub(crate) fn empty_postprocessing(height: i32) -> Box<[Vec<u16>]> {
43 let section_count = (height / 16) as usize;
44 (0..section_count).map(|_| Vec::new()).collect()
45}
46
47pub(crate) fn postprocessing_from_disk(
48 height: i32,
49 mut postprocessing: Vec<Vec<u16>>,
50) -> Box<[Vec<u16>]> {
51 let section_count = (height / 16) as usize;
52 postprocessing.resize_with(section_count, Vec::new);
53 postprocessing.truncate(section_count);
54 postprocessing.into_boxed_slice()
55}
56
57#[derive(Default)]
58struct TransientGenerationState {
59 value: Option<Box<dyn ErasedType + Send + Sync>>,
60}
61
62impl fmt::Debug for TransientGenerationState {
63 fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
64 formatter
65 .debug_struct("TransientGenerationState")
66 .field(
67 "type_key",
68 &self.value.as_deref().map(ErasedType::downcast_type_key),
69 )
70 .finish()
71 }
72}
73
74#[derive(Debug)]
76pub struct Chunk {
77 pub sections: Sections,
79 pub pos: ChunkPos,
81 pub dirty: AtomicBool,
84 pub(crate) heightmaps: SyncRwLock<ChunkHeightmaps>,
86 min_y: i32,
88 height: i32,
90 level: Weak<World>,
92 pub(crate) block_entities: BlockEntityStorage,
94 pub(crate) entities: EntityStorage,
96 pub structure_starts: SyncRwLock<StructureStartMap>,
98 pub structure_references: SyncRwLock<StructureReferenceMap>,
100 pub carving_mask: SyncRwLock<Option<CarvingMask>>,
102 pub postprocessing: SyncMutex<Box<[Vec<u16>]>>,
104 pub(crate) scheduled_ticks: Arc<ChunkTickContainer>,
106 pub sky_light_sources: SyncRwLock<ChunkSkyLightSources>,
108 pub light: SyncRwLock<ChunkLightData>,
110 full_runtime: OnceLock<Box<FullChunkRuntime>>,
112 transient_generation_state: SyncMutex<TransientGenerationState>,
114}
115
116enum PendingPromotionCommit {
117 Retry,
118 Complete(Option<SharedBlockEntity>),
119}
120
121impl Chunk {
122 #[must_use]
124 pub fn new(
125 sections: Sections,
126 pos: ChunkPos,
127 min_y: i32,
128 height: i32,
129 level: Weak<World>,
130 ) -> Self {
131 Self {
132 sections,
133 pos,
134 dirty: AtomicBool::new(true), heightmaps: SyncRwLock::new(ChunkHeightmaps::empty()),
136 min_y,
137 height,
138 level,
139 block_entities: BlockEntityStorage::new(),
140 entities: EntityStorage::new(),
141 structure_starts: SyncRwLock::new(FxHashMap::default()),
142 structure_references: SyncRwLock::new(FxHashMap::default()),
143 carving_mask: SyncRwLock::new(None),
144 postprocessing: SyncMutex::new(empty_postprocessing(height)),
145 scheduled_ticks: Arc::new(ChunkTickContainer::new_proto(ChunkTickLists::new(
146 BlockTickList::new_pending(),
147 FluidTickList::new_pending(),
148 ))),
149 sky_light_sources: SyncRwLock::new(ChunkSkyLightSources::for_valid_world_height(
150 min_y, height,
151 )),
152 light: SyncRwLock::new(ChunkLightData::for_valid_world_height(min_y, height)),
153 full_runtime: OnceLock::new(),
154 transient_generation_state: SyncMutex::new(TransientGenerationState::default()),
155 }
156 }
157
158 #[expect(
164 clippy::too_many_arguments,
165 reason = "disk rehydration mirrors the persisted proto chunk fields"
166 )]
167 #[must_use]
168 pub(crate) fn from_disk(
169 sections: Sections,
170 pos: ChunkPos,
171 status: ChunkStatus,
172 min_y: i32,
173 height: i32,
174 heightmaps: ChunkHeightmaps,
175 structure_starts: StructureStartMap,
176 structure_references: StructureReferenceMap,
177 carving_mask: Option<CarvingMask>,
178 postprocessing: Vec<Vec<u16>>,
179 block_ticks: BlockTickList,
180 fluid_ticks: FluidTickList,
181 level: Weak<World>,
182 mut light: ChunkLightData,
183 ) -> Self {
184 if let Err(error) = light.refresh_emptiness_maps_from_sections(§ions) {
185 panic!("invalid loaded proto chunk light emptiness map length: {error:?}");
186 }
187
188 let chunk = Self {
189 sections,
190 pos,
191 dirty: AtomicBool::new(false),
192 heightmaps: SyncRwLock::new(heightmaps),
193 min_y,
194 height,
195 level,
196 block_entities: BlockEntityStorage::new(),
197 entities: if status == ChunkStatus::Full {
198 EntityStorage::new_closed()
199 } else {
200 EntityStorage::new()
201 },
202 structure_starts: SyncRwLock::new(structure_starts),
203 structure_references: SyncRwLock::new(structure_references),
204 carving_mask: SyncRwLock::new(carving_mask),
205 postprocessing: SyncMutex::new(postprocessing_from_disk(height, postprocessing)),
206 scheduled_ticks: Arc::new(if status == ChunkStatus::Full {
207 ChunkTickContainer::new(ChunkTickLists::new(block_ticks, fluid_ticks))
208 } else {
209 ChunkTickContainer::new_proto(ChunkTickLists::new(block_ticks, fluid_ticks))
210 }),
211 sky_light_sources: SyncRwLock::new(ChunkSkyLightSources::for_valid_world_height(
212 min_y, height,
213 )),
214 light: SyncRwLock::new(light),
215 full_runtime: OnceLock::new(),
216 transient_generation_state: SyncMutex::new(TransientGenerationState::default()),
217 };
218
219 if status >= ChunkStatus::InitializeLight {
220 chunk.initialize_light_sources();
221 }
222
223 chunk
224 }
225
226 #[must_use]
228 pub const fn min_y(&self) -> i32 {
229 self.min_y
230 }
231
232 #[must_use]
234 pub const fn height(&self) -> i32 {
235 self.height
236 }
237
238 #[must_use]
240 pub const fn pos(&self) -> ChunkPos {
241 self.pos
242 }
243
244 #[must_use]
246 pub const fn sections(&self) -> &Sections {
247 &self.sections
248 }
249
250 #[must_use]
252 pub fn is_dirty(&self) -> bool {
253 self.dirty.load(Ordering::Acquire)
254 }
255
256 pub fn mark_dirty(&self) {
258 self.dirty.store(true, Ordering::Release);
259 }
260
261 pub fn take_dirty(&self) -> bool {
263 self.dirty.swap(false, Ordering::AcqRel)
264 }
265
266 pub fn clear_dirty(&self) {
268 self.dirty.store(false, Ordering::Release);
269 }
270
271 #[must_use]
273 pub fn get_relative_block(
274 &self,
275 relative_x: usize,
276 relative_y: usize,
277 relative_z: usize,
278 ) -> Option<BlockStateId> {
279 self.sections
280 .get_relative_block(relative_x, relative_y, relative_z)
281 }
282
283 pub(crate) fn set_relative_block_for_generation(
285 &self,
286 status: ChunkStatus,
287 relative_x: usize,
288 relative_y: usize,
289 relative_z: usize,
290 value: BlockStateId,
291 ) {
292 if status >= ChunkStatus::InitializeLight {
293 self.sections
294 .set_relative_block(relative_x, relative_y, relative_z, value);
295 self.refresh_light_emptiness_maps();
296 } else {
297 self.sections
298 .set_relative_block_for_generation(relative_x, relative_y, relative_z, value);
299 }
300 self.mark_dirty();
301 self.update_status_heightmaps_after_block_change(
302 status,
303 relative_x,
304 self.min_y + relative_y as i32,
305 relative_z,
306 value,
307 );
308 }
309
310 pub(crate) fn write_block_batch_for_generation(
312 &self,
313 status: ChunkStatus,
314 blocks: &[(usize, usize, usize, BlockStateId)],
315 ) {
316 if blocks.is_empty() {
317 return;
318 }
319 if status < ChunkStatus::InitializeLight {
320 self.sections.write_block_batch(blocks);
321 } else {
322 self.sections.write_tracked_block_batch(blocks);
323 self.refresh_light_emptiness_maps();
324 }
325 self.mark_dirty();
326 }
327
328 pub(crate) fn write_column_blocks_for_generation(
330 &self,
331 status: ChunkStatus,
332 x: usize,
333 z: usize,
334 blocks: &[(usize, BlockStateId)],
335 ) {
336 if blocks.is_empty() {
337 return;
338 }
339 if status < ChunkStatus::InitializeLight {
340 self.sections.write_column_blocks(x, z, blocks);
341 } else {
342 for &(relative_y, value) in blocks {
343 self.sections.set_relative_block(x, relative_y, z, value);
344 }
345 self.refresh_light_emptiness_maps();
346 }
347 self.mark_dirty();
348 }
349
350 pub(crate) fn prime_heightmaps(&self, heightmap_types: &[HeightmapType]) {
352 self.heightmaps.write().prime_from_sections(
353 heightmap_types,
354 self.min_y,
355 self.height,
356 &self.sections.sections,
357 );
358 }
359
360 pub fn prime_final_heightmaps(&self) {
362 self.prime_heightmaps(HeightmapType::final_types());
363 }
364
365 #[must_use]
367 pub(crate) fn generation_height_at(
368 &self,
369 heightmap_type: HeightmapType,
370 local_x: usize,
371 local_z: usize,
372 ) -> i32 {
373 {
374 let heightmaps = self.heightmaps.read();
375 if let Some(heightmap) = heightmaps.get(heightmap_type) {
376 return heightmap.get_first_available(local_x, local_z);
377 }
378 }
379 self.prime_heightmaps(&[heightmap_type]);
380 let heightmaps = self.heightmaps.read();
381 let Some(heightmap) = heightmaps.get(heightmap_type) else {
382 panic!("heightmap {heightmap_type:?} missing after priming");
383 };
384 heightmap.get_first_available(local_x, local_z)
385 }
386
387 pub(crate) fn generation_heightmaps(&self) -> RwLockReadGuard<'_, ChunkHeightmaps> {
389 self.heightmaps.read()
390 }
391
392 pub(crate) fn update_heightmaps_after_direct_column_writes(
394 &self,
395 status: ChunkStatus,
396 local_x: usize,
397 local_z: usize,
398 relative_writes: &[(usize, BlockStateId)],
399 ) {
400 if relative_writes.is_empty() {
401 return;
402 }
403 self.update_status_heightmaps_after_column_block_changes(
404 status,
405 local_x,
406 local_z,
407 relative_writes,
408 );
409 }
410
411 pub fn sky_light_sources(&self) -> RwLockReadGuard<'_, ChunkSkyLightSources> {
413 self.sky_light_sources.read()
414 }
415
416 #[must_use]
418 pub fn block_light_sources(&self) -> Vec<BlockPos> {
419 self.sections.block_light_sources(self.pos, self.min_y)
420 }
421
422 pub fn light(&self) -> RwLockReadGuard<'_, ChunkLightData> {
424 self.light.read()
425 }
426
427 pub(crate) fn light_mut(&self) -> RwLockWriteGuard<'_, ChunkLightData> {
429 self.light.write()
430 }
431
432 pub fn structure_starts(&self) -> RwLockReadGuard<'_, StructureStartMap> {
434 self.structure_starts.read()
435 }
436
437 pub fn structure_starts_mut(&self) -> RwLockWriteGuard<'_, StructureStartMap> {
439 self.structure_starts.write()
440 }
441
442 pub fn structure_references(&self) -> RwLockReadGuard<'_, StructureReferenceMap> {
444 self.structure_references.read()
445 }
446
447 pub fn structure_references_mut(&self) -> RwLockWriteGuard<'_, StructureReferenceMap> {
449 self.structure_references.write()
450 }
451
452 pub(crate) fn initialize_full_runtime(
454 &self,
455 runtime: FullChunkRuntime,
456 ) -> Result<(), FullChunkRuntime> {
457 self.full_runtime
458 .set(Box::new(runtime))
459 .map_err(|runtime| *runtime)
460 }
461
462 #[must_use]
464 pub(crate) fn full_runtime(&self) -> Option<&FullChunkRuntime> {
465 self.full_runtime.get().map(Box::as_ref)
466 }
467
468 pub(crate) fn install_transient_generation_state<T>(&self, state: T)
474 where
475 T: DowncastType + Send + Sync,
476 {
477 let mut slot = self.transient_generation_state.lock();
478 if let Some(current) = slot.value.as_deref() {
479 panic!(
480 "chunk transient generation state {} was not consumed before installing {}",
481 current.downcast_type_key(),
482 T::TYPE_KEY
483 );
484 }
485 slot.value = Some(Box::new(state));
486 }
487
488 pub(crate) fn with_transient_generation_state_mut<T, R>(
497 &self,
498 f: impl FnOnce(&mut T) -> R,
499 ) -> Option<R>
500 where
501 T: DowncastType + Send + Sync,
502 {
503 let mut slot = self.transient_generation_state.lock();
504 let state = slot.value.as_deref_mut()?;
505 let actual_key = state.downcast_type_key();
506 let Some(state) = state.downcast_mut::<T>() else {
507 panic!(
508 "chunk transient generation state type mismatch: expected {}, found {}",
509 T::TYPE_KEY,
510 actual_key
511 );
512 };
513 Some(f(state))
514 }
515
516 pub(crate) fn consume_transient_generation_state<T, R>(
525 &self,
526 f: impl FnOnce(Option<&mut T>) -> R,
527 ) -> R
528 where
529 T: DowncastType + Send + Sync,
530 {
531 let state = self.transient_generation_state.lock().value.take();
532 let Some(mut state) = state else {
533 return f(None);
534 };
535 let actual_key = state.downcast_type_key();
536 let Some(state) = state.downcast_mut::<T>() else {
537 panic!(
538 "chunk transient generation state type mismatch: expected {}, found {}",
539 T::TYPE_KEY,
540 actual_key
541 );
542 };
543 f(Some(state))
544 }
545
546 pub(crate) fn clear_transient_generation_state(&self) {
548 self.transient_generation_state.lock().value = None;
549 }
550
551 pub(crate) fn get_or_create_carving_mask(&self) -> MappedRwLockWriteGuard<'_, CarvingMask> {
557 let mut guard = self.carving_mask.write();
558 if guard.is_none() {
559 *guard = Some(CarvingMask::new(self.height, self.min_y));
560 }
561 RwLockWriteGuard::map(guard, |opt| match opt {
562 Some(mask) => mask,
563 None => unreachable!("carving mask initialized immediately above"),
564 })
565 }
566
567 #[must_use]
569 pub const fn pack_postprocessing_offset(pos: BlockPos) -> u16 {
570 let x = (pos.0.x & 15) as u16;
571 let y = (pos.0.y & 15) as u16;
572 let z = (pos.0.z & 15) as u16;
573 x | (y << 4) | (z << 8)
574 }
575
576 #[must_use]
578 pub fn unpack_postprocessing_offset(
579 packed: u16,
580 section_y: i32,
581 chunk_pos: ChunkPos,
582 ) -> BlockPos {
583 let x = chunk_pos.0.x * 16 + i32::from(packed & 15);
584 let y = section_y * 16 + i32::from((packed >> 4) & 15);
585 let z = chunk_pos.0.y * 16 + i32::from((packed >> 8) & 15);
586 BlockPos::new(x, y, z)
587 }
588
589 pub(crate) fn mark_pos_for_postprocessing(&self, pos: BlockPos) {
591 let y = pos.0.y;
592 if y < self.min_y || y >= self.min_y + self.height {
593 return;
594 }
595
596 let section_index = self.get_section_index(y);
597 let packed = Self::pack_postprocessing_offset(pos);
598 self.postprocessing.lock()[section_index].push(packed);
599 self.mark_unsaved();
600 }
601
602 #[must_use]
604 const fn get_section_index(&self, y: i32) -> usize {
605 ((y - self.min_y) / 16) as usize
606 }
607
608 fn mark_unsaved(&self) {
610 self.dirty.store(true, Ordering::Release);
611 }
612
613 #[must_use]
615 pub(crate) fn level_weak(&self) -> Weak<World> {
616 self.level.clone()
617 }
618
619 #[must_use]
621 pub(crate) fn get_level(&self) -> Option<Arc<World>> {
622 self.level.upgrade()
623 }
624
625 #[must_use]
627 pub(crate) const fn block_entity_storage(&self) -> &BlockEntityStorage {
628 &self.block_entities
629 }
630
631 #[must_use]
633 pub(crate) const fn scheduled_tick_container(&self) -> &Arc<ChunkTickContainer> {
634 &self.scheduled_ticks
635 }
636
637 pub fn initialize_light_sources(&self) {
639 for section in &self.sections.sections {
640 section.write().recalculate_counts();
641 }
642 self.refresh_light_emptiness_maps();
643 self.sky_light_sources
644 .write()
645 .fill_from_sections(&self.sections);
646 }
647
648 #[must_use]
650 pub(crate) fn get_block_entity(&self, pos: BlockPos) -> Option<SharedBlockEntity> {
651 self.block_entities.get(pos)
652 }
653
654 #[must_use]
656 pub(crate) fn set_block_entity(&self, block_entity: SharedBlockEntity) -> bool {
657 let pos = block_entity.get_block_pos();
658 if ChunkPos::from_block_pos(pos) != self.pos {
659 log::warn!(
660 "Trying to set block entity {} at {pos:?} in proto chunk {:?}",
661 block_entity.get_type().key,
662 self.pos,
663 );
664 return false;
665 }
666
667 loop {
668 let state = self.get_block_state(pos);
669 let valid = state.has_block_entity() && block_entity.is_valid_block_state(state);
670 if !valid {
671 let state_unchanged =
672 self.with_locked_block_state(pos, |live_state| live_state == state);
673 if !state_unchanged {
674 continue;
675 }
676 log::warn!(
677 "Trying to set block entity {} at {pos:?}, but block {} does not accept that type",
678 block_entity.get_type().key,
679 state.get_block().key,
680 );
681 return false;
682 }
683
684 let committed = self.with_locked_block_state(pos, |live_state| {
685 if live_state != state {
686 return false;
687 }
688 let _ = self.block_entities.set_without_lifecycle(&block_entity);
689 true
690 });
691 if !committed {
692 continue;
693 }
694 self.mark_unsaved();
695 return true;
696 }
697 }
698
699 pub(crate) fn set_pending_block_entity(&self, pos: BlockPos) {
701 if ChunkPos::from_block_pos(pos) != self.pos {
702 log::warn!(
703 "Trying to set a pending block entity at {pos:?} in proto chunk {:?}",
704 self.pos,
705 );
706 return;
707 }
708 if self.block_entities.set_pending(pos) {
709 self.mark_unsaved();
710 }
711 }
712
713 pub(crate) fn set_pending_block_entity_if_state(
715 &self,
716 pos: BlockPos,
717 expected_state: BlockStateId,
718 ) -> bool {
719 if ChunkPos::from_block_pos(pos) != self.pos {
720 return false;
721 }
722 let inserted = self.with_locked_block_state(pos, |live_state| {
723 live_state == expected_state && self.block_entities.set_pending(pos)
724 });
725 if inserted {
726 self.mark_unsaved();
727 }
728 inserted
729 }
730
731 #[must_use]
733 pub fn pending_block_entity_positions(&self) -> Vec<BlockPos> {
734 self.block_entities.pending_positions()
735 }
736
737 pub(crate) fn promote_pending_block_entity(&self, pos: BlockPos) -> Option<SharedBlockEntity> {
739 if ChunkPos::from_block_pos(pos) != self.pos {
740 return None;
741 }
742 loop {
743 match self.block_entities.lookup(pos) {
744 BlockEntityLookup::Concrete(block_entity) => return Some(block_entity),
745 BlockEntityLookup::Pending => {}
746 BlockEntityLookup::Absent => return None,
747 }
748
749 let state = self.get_block_state(pos);
750 if !state.has_block_entity() {
751 let state_unchanged =
752 self.with_locked_block_state(pos, |live_state| live_state == state);
753 if state_unchanged {
754 return None;
755 }
756 continue;
757 }
758
759 let behavior = BLOCK_BEHAVIORS.get_behavior(state.get_block());
760 let creation = behavior.new_block_entity(self.level.clone(), pos, state);
761 match self.commit_pending_creation(pos, state, creation) {
762 PendingPromotionCommit::Retry => {}
763 PendingPromotionCommit::Complete(block_entity) => return block_entity,
764 }
765 }
766 }
767
768 fn commit_pending_creation(
769 &self,
770 pos: BlockPos,
771 expected_state: BlockStateId,
772 creation: BlockEntityCreation,
773 ) -> PendingPromotionCommit {
774 let BlockEntityCreation::Created(block_entity) = creation else {
775 return self.with_locked_block_state(pos, |live_state| {
778 if live_state == expected_state {
779 PendingPromotionCommit::Complete(None)
780 } else {
781 PendingPromotionCommit::Retry
782 }
783 });
784 };
785 let valid = block_entity.get_block_pos() == pos
786 && ChunkPos::from_block_pos(pos) == self.pos
787 && block_entity.is_valid_block_state(expected_state);
788 self.with_locked_block_state(pos, |live_state| {
789 if live_state != expected_state {
790 return PendingPromotionCommit::Retry;
791 }
792 if !valid {
793 return PendingPromotionCommit::Complete(None);
794 }
795 PendingPromotionCommit::Complete(
796 self.block_entities
797 .promote_without_lifecycle(pos, block_entity),
798 )
799 })
800 }
801
802 pub(crate) fn remove_block_entity(&self, pos: BlockPos) {
804 self.block_entities.remove_without_lifecycle(pos);
805 self.mark_unsaved();
806 }
807
808 pub(crate) fn remove_block_entity_if_state(
810 &self,
811 pos: BlockPos,
812 expected_state: BlockStateId,
813 ) -> bool {
814 if ChunkPos::from_block_pos(pos) != self.pos {
815 return false;
816 }
817 let removed = self.with_locked_block_state(pos, |live_state| {
818 live_state == expected_state && self.block_entities.remove_without_lifecycle(pos)
819 });
820 if removed {
821 self.mark_unsaved();
822 }
823 removed
824 }
825
826 pub(crate) fn clear_all_block_entities(&self) {
828 self.block_entities.clear_without_lifecycle();
829 }
830
831 #[must_use]
833 pub fn get_block_entities(&self) -> Vec<SharedBlockEntity> {
834 self.block_entities.get_all_without_lifecycle_filter()
835 }
836
837 pub(crate) fn add_entity(&self, entity: SharedEntity) -> bool {
839 match self.entities.add(entity) {
840 EntityStorageAddResult::Staged => {
841 self.mark_unsaved();
842 true
843 }
844 EntityStorageAddResult::Closed(entity) => {
845 drop(entity);
850 true
851 }
852 }
853 }
854
855 #[must_use]
857 pub fn get_entities(&self) -> Vec<SharedEntity> {
858 self.entities.get_all()
859 }
860
861 #[must_use]
863 pub(crate) fn get_saveable_entities(&self) -> Vec<SharedEntity> {
864 self.entities.get_saveable_entities()
865 }
866
867 pub(crate) fn schedule_block_tick(
873 &self,
874 pos: BlockPos,
875 block: BlockRef,
876 priority: TickPriority,
877 ) {
878 if self
879 .scheduled_ticks
880 .schedule_pending_block(block, pos, priority)
881 == Some(true)
882 {
883 self.mark_unsaved();
884 }
885 }
886
887 pub(crate) fn schedule_fluid_tick(
891 &self,
892 pos: BlockPos,
893 fluid: FluidRef,
894 priority: TickPriority,
895 ) {
896 if self
897 .scheduled_ticks
898 .schedule_pending_fluid(fluid, pos, priority)
899 == Some(true)
900 {
901 self.mark_unsaved();
902 }
903 }
904
905 pub(crate) fn set_block_state_for_generation(
909 &self,
910 status: ChunkStatus,
911 pos: BlockPos,
912 state: BlockStateId,
913 _flags: UpdateFlags,
914 ) -> Option<BlockStateId> {
915 let y = pos.0.y;
916
917 if y < self.min_y || y >= self.min_y + self.height {
918 return Some(
919 REGISTRY
920 .blocks
921 .get_default_state_id(&vanilla_blocks::VOID_AIR),
922 );
923 }
924
925 let local_x = (pos.0.x & 15) as usize;
926 let local_y = (y & 15) as usize;
927 let local_z = (pos.0.z & 15) as usize;
928
929 let section_index = self.get_section_index(y);
930 let section = &self.sections.sections[section_index];
931 let (old_state, empty_section_changed_to) = {
932 let mut section_guard = section.write();
933 if status >= ChunkStatus::InitializeLight {
934 let was_empty = section_guard.is_empty();
935 let old_state = section_guard.set_block_state(local_x, local_y, local_z, state);
936 let is_empty = section_guard.is_empty();
937 let empty_section_changed_to = (was_empty != is_empty).then_some(is_empty);
938 (old_state, empty_section_changed_to)
939 } else {
940 (
941 section_guard.set_block_state_for_generation(local_x, local_y, local_z, state),
942 None,
943 )
944 }
945 };
946
947 if old_state == state {
948 return None;
949 }
950
951 if status >= ChunkStatus::InitializeLight {
952 let empty_section_change = empty_section_changed_to.map(|is_empty| {
953 self.update_light_section_emptiness(y, is_empty);
954 LightSectionEmptinessChange {
955 section_pos: SectionPos::new(
956 self.pos.0.x,
957 SectionPos::block_to_section_coord(y),
958 self.pos.0.y,
959 ),
960 empty: is_empty,
961 }
962 });
963
964 let light_properties_changed = has_different_light_properties(old_state, state);
965 if light_properties_changed {
966 self.update_sky_light_sources(local_x, y, local_z);
967 }
968 if status >= ChunkStatus::Light
969 && (light_properties_changed || empty_section_change.is_some())
970 && let Some(level) = self.level.upgrade()
971 {
972 level.queue_light_change_after_block_set(
973 pos,
974 old_state,
975 state,
976 empty_section_change,
977 );
978 }
979 }
980
981 self.update_status_heightmaps_after_block_change(status, local_x, y, local_z, state);
982
983 self.mark_unsaved();
984 Some(old_state)
985 }
986
987 fn update_light_section_emptiness(&self, y: i32, is_empty: bool) {
988 let section_y = SectionPos::block_to_section_coord(y);
989 self.light.write().set_section_empty(section_y, is_empty);
990 }
991
992 fn update_sky_light_sources(&self, local_x: usize, y: i32, local_z: usize) {
993 let chunk_min_x = self.pos.0.x * 16;
994 let chunk_min_z = self.pos.0.y * 16;
995 self.sky_light_sources
996 .write()
997 .update(local_x, y, local_z, |scan_x, scan_y, scan_z| {
998 self.get_block_state(BlockPos::new(
999 chunk_min_x + scan_x as i32,
1000 scan_y,
1001 chunk_min_z + scan_z as i32,
1002 ))
1003 });
1004 }
1005
1006 pub(crate) fn refresh_light_emptiness_maps(&self) {
1007 if let Err(error) = self
1008 .light
1009 .write()
1010 .refresh_emptiness_maps_from_sections(&self.sections)
1011 {
1012 panic!("invalid proto chunk light emptiness map length: {error:?}");
1013 }
1014 }
1015
1016 pub(crate) fn update_status_heightmaps_after_block_change(
1021 &self,
1022 status: ChunkStatus,
1023 local_x: usize,
1024 y: i32,
1025 local_z: usize,
1026 state: BlockStateId,
1027 ) {
1028 self.update_heightmaps_after_block_change(
1029 status.heightmaps_after(),
1030 local_x,
1031 y,
1032 local_z,
1033 state,
1034 );
1035 }
1036
1037 pub(crate) fn update_status_heightmaps_after_column_block_changes(
1038 &self,
1039 status: ChunkStatus,
1040 local_x: usize,
1041 local_z: usize,
1042 relative_writes: &[(usize, BlockStateId)],
1043 ) {
1044 self.update_heightmaps_after_column_block_changes(
1045 status.heightmaps_after(),
1046 local_x,
1047 local_z,
1048 relative_writes,
1049 );
1050 }
1051
1052 fn update_heightmaps_after_block_change(
1053 &self,
1054 heightmap_types: &[HeightmapType],
1055 local_x: usize,
1056 y: i32,
1057 local_z: usize,
1058 state: BlockStateId,
1059 ) {
1060 let min_y = self.min_y;
1061 let height = self.height;
1062 let sections = &self.sections;
1063
1064 let get_block = |lx: usize, scan_y: i32, lz: usize| {
1065 let scan_section_index = ((scan_y - min_y) / 16) as usize;
1066 let scan_local_y = ((scan_y - min_y) % 16) as usize;
1067 sections.sections[scan_section_index]
1068 .read()
1069 .states
1070 .get(lx, scan_local_y, lz)
1071 };
1072
1073 let mut heightmaps = self.heightmaps.write();
1074 heightmaps.prime_from_sections(heightmap_types, min_y, height, §ions.sections);
1075
1076 for &hm_type in heightmap_types {
1077 let Some(heightmap) = heightmaps.get_mut(hm_type) else {
1078 panic!("heightmap {hm_type:?} missing after priming");
1079 };
1080 heightmap.update(local_x, y, local_z, state, get_block);
1081 }
1082 }
1083
1084 fn update_heightmaps_after_column_block_changes(
1085 &self,
1086 heightmap_types: &[HeightmapType],
1087 local_x: usize,
1088 local_z: usize,
1089 relative_writes: &[(usize, BlockStateId)],
1090 ) {
1091 if relative_writes.is_empty() {
1092 return;
1093 }
1094
1095 let min_y = self.min_y;
1096 let height = self.height;
1097 let sections = &self.sections;
1098
1099 let get_block = |lx: usize, scan_y: i32, lz: usize| {
1100 let scan_section_index = ((scan_y - min_y) / 16) as usize;
1101 let scan_local_y = ((scan_y - min_y) % 16) as usize;
1102 sections.sections[scan_section_index]
1103 .read()
1104 .states
1105 .get(lx, scan_local_y, lz)
1106 };
1107
1108 let mut heightmaps = self.heightmaps.write();
1109 heightmaps.prime_from_sections(heightmap_types, min_y, height, §ions.sections);
1110
1111 for &(relative_y, state) in relative_writes {
1112 let y = min_y + relative_y as i32;
1113 for &hm_type in heightmap_types {
1114 let Some(heightmap) = heightmaps.get_mut(hm_type) else {
1115 panic!("heightmap {hm_type:?} missing after priming");
1116 };
1117 heightmap.update(local_x, y, local_z, state, get_block);
1118 }
1119 }
1120 }
1121
1122 #[must_use]
1124 pub fn get_block_state(&self, pos: BlockPos) -> BlockStateId {
1125 let y = pos.0.y;
1126
1127 if y < self.min_y || y >= self.min_y + self.height {
1129 return REGISTRY.blocks.get_default_state_id(&vanilla_blocks::AIR);
1131 }
1132
1133 let section_index = self.get_section_index(y);
1134 let section = &self.sections.sections[section_index];
1135 let section_guard = section.read();
1136
1137 let local_x = (pos.0.x & 15) as usize;
1138 let local_y = (y & 15) as usize;
1139 let local_z = (pos.0.z & 15) as usize;
1140
1141 section_guard.states.get(local_x, local_y, local_z)
1142 }
1143
1144 fn with_locked_block_state<R>(&self, pos: BlockPos, f: impl FnOnce(BlockStateId) -> R) -> R {
1145 let y = pos.y();
1146 if y < self.min_y || y >= self.min_y + self.height {
1147 return f(REGISTRY.blocks.get_default_state_id(&vanilla_blocks::AIR));
1148 }
1149
1150 let section = self.sections.sections[self.get_section_index(y)].read();
1151 let state = section.states.get(
1152 (pos.x() & 15) as usize,
1153 (y & 15) as usize,
1154 (pos.z() & 15) as usize,
1155 );
1156 f(state)
1157 }
1158}
1159
1160#[cfg(test)]
1161mod tests {
1162 use std::sync::{
1163 Arc, Weak,
1164 atomic::{AtomicUsize, Ordering},
1165 };
1166
1167 use super::{Chunk, PendingPromotionCommit};
1168 use crate::behavior::{BlockEntityCreation, init_behaviors};
1169 use crate::block_entity::{
1170 BlockEntityLifecycleExt as _, SharedBlockEntity,
1171 entities::{RawBlockEntity, SignBlockEntity},
1172 init_block_entities,
1173 };
1174 use crate::chunk::{
1175 section::{ChunkSection, Sections},
1176 status::ChunkStatus,
1177 };
1178 use crate::world::tick_scheduler::TickPriority;
1179 use steel_registry::{init_vanilla_registry, vanilla_block_entity_types, vanilla_blocks};
1180 use steel_utils::{BlockPos, ChunkPos, DowncastType, DowncastTypeKey, types::UpdateFlags};
1181
1182 struct DropSentinel(Arc<AtomicUsize>);
1183
1184 unsafe impl DowncastType for DropSentinel {
1186 const TYPE_KEY: DowncastTypeKey =
1187 DowncastTypeKey::new("steel:test/chunk/transient_generation_state");
1188 }
1189
1190 impl Drop for DropSentinel {
1191 fn drop(&mut self) {
1192 self.0.fetch_add(1, Ordering::Relaxed);
1193 }
1194 }
1195
1196 #[test]
1197 fn transient_generation_state_survives_borrow_and_is_consumed_once() {
1198 init_vanilla_registry();
1199 let chunk = Chunk::new(
1200 Sections::from_owned(vec![ChunkSection::new_empty()].into_boxed_slice()),
1201 ChunkPos::new(0, 0),
1202 0,
1203 16,
1204 Weak::new(),
1205 );
1206 let drops = Arc::new(AtomicUsize::new(0));
1207 chunk.install_transient_generation_state(DropSentinel(Arc::clone(&drops)));
1208
1209 assert!(
1210 chunk
1211 .with_transient_generation_state_mut::<DropSentinel, _>(|_| ())
1212 .is_some()
1213 );
1214 assert_eq!(drops.load(Ordering::Relaxed), 0);
1215
1216 let present =
1217 chunk.consume_transient_generation_state::<DropSentinel, _>(|state| state.is_some());
1218 assert!(present);
1219 assert_eq!(drops.load(Ordering::Relaxed), 1);
1220 assert!(
1221 !chunk
1222 .consume_transient_generation_state::<DropSentinel, _>(|state| { state.is_some() })
1223 );
1224 assert_eq!(drops.load(Ordering::Relaxed), 1);
1225 }
1226
1227 #[test]
1228 fn full_promotion_drops_leftover_transient_generation_state() {
1229 init_vanilla_registry();
1230 init_behaviors();
1231 let chunk = Chunk::new(
1232 Sections::from_owned(vec![ChunkSection::new_empty()].into_boxed_slice()),
1233 ChunkPos::new(0, 0),
1234 0,
1235 16,
1236 Weak::new(),
1237 );
1238 let drops = Arc::new(AtomicUsize::new(0));
1239 chunk.install_transient_generation_state(DropSentinel(Arc::clone(&drops)));
1240
1241 let _ = chunk.promote_to_full();
1242
1243 assert_eq!(drops.load(Ordering::Relaxed), 1);
1244 }
1245
1246 #[test]
1247 fn postprocessing_offset_pack_unpack_matches_vanilla_layout() {
1248 let chunk_pos = ChunkPos::new(-2, 1);
1249 let section_y = -4;
1250 let pos = BlockPos::new(-17, -63, 31);
1251
1252 let packed = Chunk::pack_postprocessing_offset(pos);
1253
1254 assert_eq!(packed, 15 | (1 << 4) | (15 << 8));
1255 assert_eq!(
1256 Chunk::unpack_postprocessing_offset(packed, section_y, chunk_pos),
1257 pos
1258 );
1259 }
1260
1261 #[test]
1262 fn proto_scheduled_block_ticks_use_vanilla_zero_delay() {
1263 init_vanilla_registry();
1264 let proto = Chunk::new(
1265 Sections::from_owned(vec![ChunkSection::new_empty()].into_boxed_slice()),
1266 ChunkPos::new(0, 0),
1267 0,
1268 16,
1269 Weak::new(),
1270 );
1271 let pos = BlockPos::new(3, 4, 5);
1272
1273 proto.schedule_block_tick(pos, &vanilla_blocks::DIRT, TickPriority::Normal);
1274
1275 let Some(snapshot) = proto.scheduled_ticks.snapshot(0) else {
1276 panic!("proto chunk scheduled ticks should remain available");
1277 };
1278 let Some(tick) = snapshot.block.first() else {
1279 panic!("proto chunk should store scheduled block tick");
1280 };
1281
1282 assert_eq!(tick.pos, pos);
1283 assert_eq!(tick.tick_type, &vanilla_blocks::DIRT);
1284 assert_eq!(tick.delay, 0);
1285 assert_eq!(tick.priority, TickPriority::Normal);
1286 }
1287
1288 #[test]
1289 fn full_promotion_retains_and_closes_proto_scheduled_tick_container() {
1290 init_vanilla_registry();
1291 let proto = Chunk::new(
1292 Sections::from_owned(vec![ChunkSection::new_empty()].into_boxed_slice()),
1293 ChunkPos::new(0, 0),
1294 0,
1295 16,
1296 Weak::new(),
1297 );
1298 let scheduled_ticks = Arc::clone(&proto.scheduled_ticks);
1299 let pos = BlockPos::new(3, 4, 5);
1300 proto.schedule_block_tick(pos, &vanilla_blocks::DIRT, TickPriority::Normal);
1301
1302 let full = proto.promote_to_full().chunk;
1303
1304 assert_eq!(
1305 scheduled_ticks.schedule_pending_block(
1306 &vanilla_blocks::DIRT,
1307 BlockPos::new(6, 7, 8),
1308 TickPriority::Normal,
1309 ),
1310 None
1311 );
1312 assert!(Arc::ptr_eq(
1313 &scheduled_ticks,
1314 full.scheduled_tick_container()
1315 ));
1316 let snapshot = full.scheduled_tick_snapshot();
1317 assert_eq!(snapshot.block.len(), 1);
1318 assert_eq!(snapshot.block[0].pos, pos);
1319 }
1320
1321 #[test]
1322 fn proto_chunk_preserves_distinct_air_states_in_empty_sections() {
1323 init_vanilla_registry();
1324 init_behaviors();
1325 let proto = Chunk::new(
1326 Sections::from_owned(vec![ChunkSection::new_empty()].into_boxed_slice()),
1327 ChunkPos::new(0, 0),
1328 0,
1329 16,
1330 Weak::new(),
1331 );
1332 let pos = BlockPos::new(3, 4, 5);
1333 let cave_air = vanilla_blocks::CAVE_AIR.default_state();
1334
1335 proto.set_block_state_for_generation(
1336 ChunkStatus::Empty,
1337 pos,
1338 cave_air,
1339 UpdateFlags::UPDATE_CLIENTS,
1340 );
1341
1342 assert_eq!(proto.get_block_state(pos), cave_air);
1343 }
1344
1345 #[test]
1346 fn pre_light_block_writes_defer_counts_until_light_initialization() {
1347 init_vanilla_registry();
1348 init_behaviors();
1349 let proto = Chunk::new(
1350 Sections::from_owned(vec![ChunkSection::new_empty()].into_boxed_slice()),
1351 ChunkPos::new(0, 0),
1352 0,
1353 16,
1354 Weak::new(),
1355 );
1356 let pos = BlockPos::new(3, 4, 5);
1357 let stone = vanilla_blocks::STONE.default_state();
1358 let air = vanilla_blocks::AIR.default_state();
1359
1360 proto
1361 .sections
1362 .set_relative_block_for_generation(3, 4, 5, stone);
1363 assert_eq!(proto.sections.sections[0].read().non_empty_block_count(), 0);
1364
1365 assert_eq!(
1366 proto.set_block_state_for_generation(
1367 ChunkStatus::Empty,
1368 pos,
1369 air,
1370 UpdateFlags::UPDATE_CLIENTS,
1371 ),
1372 Some(stone)
1373 );
1374 assert_eq!(proto.get_block_state(pos), air);
1375
1376 assert_eq!(
1377 proto.set_block_state_for_generation(
1378 ChunkStatus::Empty,
1379 pos,
1380 stone,
1381 UpdateFlags::UPDATE_CLIENTS,
1382 ),
1383 Some(air)
1384 );
1385 assert_eq!(proto.sections.sections[0].read().non_empty_block_count(), 0);
1386
1387 proto.initialize_light_sources();
1388 assert_eq!(proto.sections.sections[0].read().non_empty_block_count(), 1);
1389 }
1390
1391 #[test]
1392 fn proto_mutation_defers_lifecycle_and_promotion_revalidates_concrete_entities() {
1393 init_vanilla_registry();
1394 init_behaviors();
1395 let proto = Chunk::new(
1396 Sections::from_owned(vec![ChunkSection::new_empty()].into_boxed_slice()),
1397 ChunkPos::new(0, 0),
1398 0,
1399 16,
1400 Weak::new(),
1401 );
1402 let pos = BlockPos::new(3, 4, 5);
1403 let sign = vanilla_blocks::OAK_SIGN.default_state();
1404 assert!(
1405 proto
1406 .set_block_state_for_generation(
1407 ChunkStatus::Empty,
1408 pos,
1409 sign,
1410 UpdateFlags::UPDATE_NONE,
1411 )
1412 .is_some()
1413 );
1414 let entity: SharedBlockEntity = Arc::new(SignBlockEntity::new(Weak::new(), pos, sign));
1415 assert!(proto.set_block_entity(Arc::clone(&entity)));
1416
1417 assert_eq!(
1418 proto.set_block_state_for_generation(
1419 ChunkStatus::Empty,
1420 pos,
1421 vanilla_blocks::STONE.default_state(),
1422 UpdateFlags::UPDATE_NONE,
1423 ),
1424 Some(sign)
1425 );
1426 assert!(proto.get_block_entity(pos).is_some());
1427
1428 let full = proto.promote_to_full().chunk;
1429 assert!(!entity.is_removed());
1430 assert!(full.get_block_entity(pos).is_none());
1431 }
1432
1433 #[test]
1434 fn proto_storage_preserves_removed_entries_until_full_promotion() {
1435 init_vanilla_registry();
1436 init_behaviors();
1437 let proto = Chunk::new(
1438 Sections::from_owned(vec![ChunkSection::new_empty()].into_boxed_slice()),
1439 ChunkPos::new(0, 0),
1440 0,
1441 16,
1442 Weak::new(),
1443 );
1444 let pos = BlockPos::new(3, 4, 5);
1445 let sign = vanilla_blocks::OAK_SIGN.default_state();
1446 assert!(
1447 proto
1448 .set_block_state_for_generation(
1449 ChunkStatus::Empty,
1450 pos,
1451 sign,
1452 UpdateFlags::UPDATE_NONE,
1453 )
1454 .is_some()
1455 );
1456 let entity: SharedBlockEntity = Arc::new(SignBlockEntity::new(Weak::new(), pos, sign));
1457 entity.set_removed();
1458 assert!(proto.set_block_entity(Arc::clone(&entity)));
1459
1460 assert_eq!(proto.get_block_entities().len(), 1);
1461 assert_eq!(
1462 proto
1463 .block_entities
1464 .save_snapshot_without_lifecycle_filter()
1465 .0
1466 .len(),
1467 1
1468 );
1469
1470 let promotion = proto.promote_to_full();
1471 let full = promotion.chunk;
1472 assert!(!entity.is_removed());
1473 assert!(full.get_block_entity(pos).is_some());
1474 }
1475
1476 #[test]
1477 fn pending_proto_entity_promotes_without_running_live_lifecycle() {
1478 init_vanilla_registry();
1479 init_behaviors();
1480 init_block_entities();
1481 let proto = Chunk::new(
1482 Sections::from_owned(vec![ChunkSection::new_empty()].into_boxed_slice()),
1483 ChunkPos::new(0, 0),
1484 0,
1485 16,
1486 Weak::new(),
1487 );
1488 let pos = BlockPos::new(3, 4, 5);
1489 let sign = vanilla_blocks::OAK_SIGN.default_state();
1490 assert!(
1491 proto
1492 .set_block_state_for_generation(
1493 ChunkStatus::Empty,
1494 pos,
1495 sign,
1496 UpdateFlags::UPDATE_NONE,
1497 )
1498 .is_some()
1499 );
1500 proto.set_pending_block_entity(pos);
1501
1502 assert!(proto.promote_pending_block_entity(pos).is_some());
1503 assert!(proto.pending_block_entity_positions().is_empty());
1504 }
1505
1506 #[test]
1507 fn conditional_proto_marker_mutation_rejects_stale_worldgen_state() {
1508 init_vanilla_registry();
1509 init_behaviors();
1510 let proto = Chunk::new(
1511 Sections::from_owned(vec![ChunkSection::new_empty()].into_boxed_slice()),
1512 ChunkPos::new(0, 0),
1513 0,
1514 16,
1515 Weak::new(),
1516 );
1517 let pos = BlockPos::new(3, 4, 5);
1518 let copper = vanilla_blocks::COPPER_CHEST.default_state();
1519 let exposed = vanilla_blocks::EXPOSED_COPPER_CHEST.default_state();
1520 assert!(
1521 proto
1522 .set_block_state_for_generation(
1523 ChunkStatus::Empty,
1524 pos,
1525 exposed,
1526 UpdateFlags::UPDATE_NONE,
1527 )
1528 .is_some()
1529 );
1530
1531 assert!(!proto.set_pending_block_entity_if_state(pos, copper));
1532 assert!(proto.pending_block_entity_positions().is_empty());
1533 assert!(proto.set_pending_block_entity_if_state(pos, exposed));
1534
1535 let stone = vanilla_blocks::STONE.default_state();
1536 assert_eq!(
1537 proto.set_block_state_for_generation(
1538 ChunkStatus::Empty,
1539 pos,
1540 stone,
1541 UpdateFlags::UPDATE_NONE,
1542 ),
1543 Some(exposed)
1544 );
1545 assert!(!proto.remove_block_entity_if_state(pos, exposed));
1546 assert_eq!(proto.pending_block_entity_positions(), [pos]);
1547 assert!(proto.remove_block_entity_if_state(pos, stone));
1548 assert!(proto.pending_block_entity_positions().is_empty());
1549 }
1550
1551 #[test]
1552 fn dummy_factory_outcomes_keep_proto_and_full_stage_semantics() {
1553 init_vanilla_registry();
1554 init_behaviors();
1555 let proto = Chunk::new(
1556 Sections::from_owned(vec![ChunkSection::new_empty()].into_boxed_slice()),
1557 ChunkPos::new(0, 0),
1558 0,
1559 16,
1560 Weak::new(),
1561 );
1562 let moving_pos = BlockPos::new(2, 4, 5);
1563 let chest_pos = BlockPos::new(3, 4, 5);
1564 assert!(
1565 proto
1566 .set_block_state_for_generation(
1567 ChunkStatus::Empty,
1568 moving_pos,
1569 vanilla_blocks::MOVING_PISTON.default_state(),
1570 UpdateFlags::UPDATE_NONE,
1571 )
1572 .is_some()
1573 );
1574 assert!(
1575 proto
1576 .set_block_state_for_generation(
1577 ChunkStatus::Empty,
1578 chest_pos,
1579 vanilla_blocks::CHEST.default_state(),
1580 UpdateFlags::UPDATE_NONE,
1581 )
1582 .is_some()
1583 );
1584 proto.set_pending_block_entity(moving_pos);
1585 proto.set_pending_block_entity(chest_pos);
1586
1587 assert!(proto.promote_pending_block_entity(moving_pos).is_none());
1588 assert!(proto.promote_pending_block_entity(chest_pos).is_none());
1589 let pending = proto.pending_block_entity_positions();
1590 assert!(pending.contains(&moving_pos));
1591 assert!(pending.contains(&chest_pos));
1592
1593 let full = proto.promote_to_full().chunk;
1594 assert!(full.get_block_entity(moving_pos).is_none());
1595 assert!(!full.pending_block_entity_positions().contains(&moving_pos));
1596 assert!(full.get_block_entity(chest_pos).is_none());
1597 assert!(full.pending_block_entity_positions().contains(&chest_pos));
1598 }
1599
1600 #[test]
1601 fn stale_proto_factory_cannot_consume_a_replacement_marker() {
1602 init_vanilla_registry();
1603 init_behaviors();
1604 let proto = Chunk::new(
1605 Sections::from_owned(vec![ChunkSection::new_empty()].into_boxed_slice()),
1606 ChunkPos::new(0, 0),
1607 0,
1608 16,
1609 Weak::new(),
1610 );
1611 let pos = BlockPos::new(2, 4, 5);
1612 let copper = vanilla_blocks::COPPER_CHEST.default_state();
1613 let exposed = vanilla_blocks::EXPOSED_COPPER_CHEST.default_state();
1614 assert!(
1615 proto
1616 .set_block_state_for_generation(
1617 ChunkStatus::Empty,
1618 pos,
1619 copper,
1620 UpdateFlags::UPDATE_NONE,
1621 )
1622 .is_some()
1623 );
1624 proto.set_pending_block_entity(pos);
1625 let stale_entity: SharedBlockEntity = Arc::new(RawBlockEntity::new(
1626 &vanilla_block_entity_types::CHEST,
1627 Weak::new(),
1628 pos,
1629 copper,
1630 ));
1631
1632 assert_eq!(
1633 proto.set_block_state_for_generation(
1634 ChunkStatus::Empty,
1635 pos,
1636 exposed,
1637 UpdateFlags::UPDATE_NONE,
1638 ),
1639 Some(copper)
1640 );
1641 assert!(matches!(
1642 proto.commit_pending_creation(pos, copper, BlockEntityCreation::Created(stale_entity),),
1643 PendingPromotionCommit::Retry
1644 ));
1645 assert_eq!(proto.pending_block_entity_positions(), [pos]);
1646 assert!(proto.get_block_entity(pos).is_none());
1647 }
1648}