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