1use std::borrow::Cow;
2use std::collections::{BTreeMap, BTreeSet};
3use std::mem;
4use std::sync::atomic::{AtomicU32, Ordering};
5
6use chunkedge_binary::Encode;
7use chunkedge_generated::block::{BlockKind, PropName, PropValue};
8use chunkedge_nbt::Compound;
9use chunkedge_protocol::encode::{PacketWriter, WritePacket};
10use chunkedge_protocol::packets::play::level_chunk_with_light_s2c::{
11 ChunkDataBlockEntity, HeightMap, HeightMapKind,
12};
13use chunkedge_protocol::packets::play::section_blocks_update_s2c::ChunkDeltaUpdateEntry;
14use chunkedge_protocol::packets::play::{
15 BlockEntityDataS2c, BlockUpdateS2c, LevelChunkWithLightS2c, SectionBlocksUpdateS2c,
16};
17use chunkedge_protocol::{
18 BlockPos, BlockState, ChunkPos, ChunkSectionPos, FixedArray, VariableBitSet,
19};
20use chunkedge_registry::biome::BiomeId;
21use chunkedge_registry::RegistryIdx;
22use parking_lot::Mutex; use super::chunk::{
25 bit_width, check_biome_oob, check_block_oob, check_section_oob, BiomeContainer,
26 BlockStateContainer, Chunk, SECTION_BLOCK_COUNT,
27};
28use super::paletted_container::PalettedContainer;
29use super::unloaded::{self, UnloadedChunk};
30use super::{ChunkLayerInfo, ChunkLayerMessages, LocalMsg};
31
32#[derive(Debug)]
33pub struct LoadedChunk {
34 viewer_count: AtomicU32,
38 sections: Box<[Section]>,
40 sky_light_sections: Box<[LightSection]>,
44 block_light_sections: Box<[LightSection]>,
48 block_entities: BTreeMap<u32, Compound>,
50 changed_block_entities: BTreeSet<u32>,
52 changed_biomes: bool,
54 cached_init_packets: Mutex<Vec<u8>>,
58}
59
60#[derive(Clone, Debug, Default)]
61pub struct Section {
62 block_states: BlockStateContainer,
63 biomes: BiomeContainer,
64 updates: Vec<ChunkDeltaUpdateEntry>,
67}
68
69impl Section {
70 fn count_non_air_blocks(&self) -> u16 {
71 let mut count = 0;
72
73 match &self.block_states {
74 PalettedContainer::Single(s) => {
75 if !s.is_air() {
76 count += SECTION_BLOCK_COUNT as u16;
77 }
78 }
79 PalettedContainer::Indirect(ind) => {
80 for i in 0..SECTION_BLOCK_COUNT {
81 if !ind.get(i).is_air() {
82 count += 1;
83 }
84 }
85 }
86 PalettedContainer::Direct(dir) => {
87 for s in dir.as_ref() {
88 if !s.is_air() {
89 count += 1;
90 }
91 }
92 }
93 }
94 count
95 }
96}
97
98#[derive(Clone, Debug, Default)]
106pub enum LightSection {
107 #[default]
108 NotSet,
109 FullyDark,
110 FullData(Box<[u8; 2048]>),
111}
112
113impl LightSection {
114 pub fn from_data(data: [u8; 2048]) -> Self {
116 Self::FullData(Box::new(data))
117 }
118}
119
120impl LoadedChunk {
121 pub(crate) fn new(height: u32) -> Self {
122 let section_count = height as usize / 16;
123 let light_section_count = section_count + 2;
124 Self {
125 viewer_count: AtomicU32::new(0),
126 sections: vec![Section::default(); section_count].into(),
127 sky_light_sections: vec![LightSection::NotSet; light_section_count].into(),
135 block_light_sections: vec![LightSection::FullyDark; light_section_count].into(),
138 block_entities: BTreeMap::new(),
139 changed_block_entities: BTreeSet::new(),
140 changed_biomes: false,
141 cached_init_packets: Mutex::new(vec![]),
142 }
143 }
144
145 pub(crate) fn insert(&mut self, mut chunk: UnloadedChunk) -> UnloadedChunk {
153 chunk.set_height(self.height());
154
155 let old_sections = self
156 .sections
157 .iter_mut()
158 .zip(chunk.sections)
159 .map(|(sect, other_sect)| {
160 sect.updates.clear();
161
162 unloaded::Section {
163 block_states: mem::replace(&mut sect.block_states, other_sect.block_states),
164 biomes: mem::replace(&mut sect.biomes, other_sect.biomes),
165 }
166 })
167 .collect();
168 let old_block_entities = mem::replace(&mut self.block_entities, chunk.block_entities);
169 self.changed_block_entities.clear();
170 self.changed_biomes = false;
171 self.cached_init_packets.get_mut().clear();
172 self.assert_no_changes();
173
174 UnloadedChunk {
175 sections: old_sections,
176 block_entities: old_block_entities,
177 }
178 }
179
180 pub(crate) fn remove(&mut self) -> UnloadedChunk {
181 let old_sections = self
182 .sections
183 .iter_mut()
184 .map(|sect| {
185 sect.updates.clear();
186
187 unloaded::Section {
188 block_states: mem::take(&mut sect.block_states),
189 biomes: mem::take(&mut sect.biomes),
190 }
191 })
192 .collect();
193 let old_block_entities = mem::take(&mut self.block_entities);
194 self.changed_block_entities.clear();
195 self.changed_biomes = false;
196 self.cached_init_packets.get_mut().clear();
197
198 self.assert_no_changes();
199
200 UnloadedChunk {
201 sections: old_sections,
202 block_entities: old_block_entities,
203 }
204 }
205
206 pub fn viewer_count(&self) -> u32 {
208 self.viewer_count.load(Ordering::Relaxed)
209 }
210
211 pub fn viewer_count_mut(&mut self) -> u32 {
213 *self.viewer_count.get_mut()
214 }
215
216 pub(crate) fn inc_viewer_count(&self) {
218 self.viewer_count.fetch_add(1, Ordering::Relaxed);
219 }
220
221 #[track_caller]
223 pub(crate) fn dec_viewer_count(&self) {
224 let old = self.viewer_count.fetch_sub(1, Ordering::Relaxed);
225 debug_assert_ne!(old, 0, "viewer count underflow!");
226 }
227
228 pub(crate) fn update_pre_client(
232 &mut self,
233 pos: ChunkPos,
234 info: &ChunkLayerInfo,
235 messages: &mut ChunkLayerMessages,
236 ) {
237 if *self.viewer_count.get_mut() == 0 {
238 self.assert_no_changes();
241
242 return;
243 }
244
245 for (sect_y, sect) in self.sections.iter_mut().enumerate() {
247 match sect.updates.as_slice() {
248 &[] => {}
249 &[entry] => {
250 let global_x = pos.x * 16 + i32::from(entry.off_x());
251 let global_y = info.min_y + sect_y as i32 * 16 + i32::from(entry.off_y());
252 let global_z = pos.z * 16 + i32::from(entry.off_z());
253
254 messages.send_local_infallible(LocalMsg::PacketAt { pos }, |buf| {
255 let mut writer = PacketWriter::new(buf, info.threshold);
256
257 writer.write_packet(&BlockUpdateS2c {
258 position: BlockPos::new(global_x, global_y, global_z),
259 block_id: BlockState::from_raw(entry.block_state() as u16).unwrap(),
260 });
261 });
262 }
263 entries => {
264 let chunk_sect_pos = ChunkSectionPos {
265 x: pos.x,
266 y: sect_y as i32 + info.min_y.div_euclid(16),
267 z: pos.z,
268 };
269
270 messages.send_local_infallible(LocalMsg::PacketAt { pos }, |buf| {
271 let mut writer = PacketWriter::new(buf, info.threshold);
272
273 writer.write_packet(&SectionBlocksUpdateS2c {
274 chunk_sect_pos,
275 blocks: Cow::Borrowed(entries),
276 });
277 });
278 }
279 }
280
281 sect.updates.clear();
282 }
283
284 for &idx in &self.changed_block_entities {
286 let Some(nbt) = self.block_entities.get(&idx) else {
287 continue;
288 };
289
290 let x = idx % 16;
291 let z = (idx / 16) % 16;
292 let y = idx / 16 / 16;
293
294 let state = self.sections[y as usize / 16]
295 .block_states
296 .get(idx as usize % SECTION_BLOCK_COUNT);
297
298 let Some(kind) = state.block_entity_kind() else {
299 continue;
300 };
301
302 let global_x = pos.x * 16 + x as i32;
303 let global_y = info.min_y + y as i32;
304 let global_z = pos.z * 16 + z as i32;
305
306 messages.send_local_infallible(LocalMsg::PacketAt { pos }, |buf| {
307 let mut writer = PacketWriter::new(buf, info.threshold);
308
309 writer.write_packet(&BlockEntityDataS2c {
310 location: BlockPos::new(global_x, global_y, global_z),
311 kind,
312 data: Cow::Borrowed(nbt),
313 });
314 });
315 }
316
317 self.changed_block_entities.clear();
318
319 if self.changed_biomes {
321 self.changed_biomes = false;
322
323 messages.send_local_infallible(LocalMsg::ChangeBiome { pos }, |buf| {
324 for sect in &self.sections {
325 sect.biomes
326 .encode_mc_format(
327 &mut *buf,
328 |b| b.to_index() as u64,
329 0,
330 3,
331 bit_width(info.biome_registry_len - 1),
332 )
333 .expect("paletted container encode should always succeed");
334 }
335 });
336 }
337
338 self.assert_no_changes();
340 }
341
342 pub(crate) fn motion_blocking(&self) -> [u32; 16 * 16] {
352 self.build_heightmap(Self::is_motion_blocking_occupied)
353 }
354
355 pub(crate) fn motion_blocking_no_leaves(&self) -> [u32; 16 * 16] {
364 self.build_heightmap(Self::is_motion_blocking_no_leaves_occupied)
365 }
366
367 pub(crate) fn world_surface(&self) -> [u32; 16 * 16] {
377 self.build_heightmap(|state| !state.is_air())
378 }
379
380 pub(crate) fn build_heightmap(
391 &self,
392 mut is_occupied: impl FnMut(BlockState) -> bool,
393 ) -> [u32; 16 * 16] {
394 let mut heightmap = [0; 16 * 16];
395
396 for z in 0_u32..16 {
397 for x in 0_u32..16 {
398 for y in (0..self.height()).rev() {
399 if is_occupied(self.block_state(x, y, z)) {
400 heightmap[(z as usize) * 16 + (x as usize)] = y + 1;
403 break;
404 }
405 }
406 }
407 }
408
409 heightmap
410 }
411
412 fn is_motion_blocking_occupied(state: BlockState) -> bool {
413 let kind = state.to_kind();
414
415 if matches!(kind, BlockKind::BambooSapling | BlockKind::Cactus) {
416 return false;
417 }
418
419 state.blocks_motion()
420 || state.is_liquid()
421 || state.get(PropName::Waterlogged) == Some(PropValue::True)
422 }
423
424 fn is_motion_blocking_no_leaves_occupied(state: BlockState) -> bool {
425 if Self::is_leaf_block(state) {
426 return false;
427 }
428
429 Self::is_motion_blocking_occupied(state)
430 }
431
432 fn is_leaf_block(state: BlockState) -> bool {
433 state.to_kind().to_str().ends_with("_leaves")
434 }
435
436 fn encode_heightmap(heightmap: &[u32; 16 * 16], world_height: u32) -> Vec<i64> {
439 let bits_per_entry = (u32::BITS - world_height.leading_zeros()).max(1);
440 let entries_per_long = i64::BITS / bits_per_entry;
441 let longs_per_packet =
442 (16 * 16) / entries_per_long + u32::from((16 * 16) % entries_per_long != 0);
443
444 let mut data: Vec<i64> = vec![0; longs_per_packet as usize];
445
446 for (idx, y) in heightmap.iter().enumerate() {
447 debug_assert!(*y <= world_height);
448
449 let long_idx = idx / entries_per_long as usize;
450 let bit_offset = (idx % entries_per_long as usize) as u32 * bits_per_entry;
451 data[long_idx] |= i64::from(*y) << bit_offset;
452 }
453
454 data
455 }
456
457 fn fill_light_data(
458 light: &LightSection,
459 light_arrays: &mut Vec<FixedArray<u8, 2048>>,
460 light_mask: &mut VariableBitSet,
461 empty_light_mask: &mut VariableBitSet,
462 i: usize,
463 is_block_light: bool,
464 ) {
465 match light {
466 LightSection::NotSet => {
467 if is_block_light {
471 empty_light_mask.set(i);
472 }
473 }
474 LightSection::FullyDark => {
475 empty_light_mask.set(i);
476 }
477 LightSection::FullData(data) => {
478 light_arrays.push(FixedArray(**data));
479 light_mask.set(i);
480 }
481 }
482 }
483
484 pub(crate) fn write_init_packets(
486 &self,
487 mut writer: impl WritePacket,
488 pos: ChunkPos,
489 info: &ChunkLayerInfo,
490 ) {
491 let mut init_packets = self.cached_init_packets.lock();
492
493 if init_packets.is_empty() {
494 let world_surface = self.world_surface();
495 let motion_blocking = self.motion_blocking();
496 let motion_blocking_no_leaves = self.motion_blocking_no_leaves();
497 let world_height = self.height();
498
499 let heightmaps = vec![
500 HeightMap {
501 kind: HeightMapKind::WorldSurface,
502 data: LoadedChunk::encode_heightmap(&world_surface, world_height),
503 },
504 HeightMap {
505 kind: HeightMapKind::MotionBlocking,
506 data: LoadedChunk::encode_heightmap(&motion_blocking, world_height),
507 },
508 HeightMap {
509 kind: HeightMapKind::MotionBlockingNoLeaves,
510 data: LoadedChunk::encode_heightmap(&motion_blocking_no_leaves, world_height),
511 },
512 ];
513
514 let mut blocks_and_biomes: Vec<u8> = vec![];
515
516 let light_section_count = self.sections.len() + 2;
517
518 let mut sky_light_mask = VariableBitSet::default();
519 let mut empty_sky_light_mask = VariableBitSet::default();
520 let mut block_light_mask = VariableBitSet::default();
521 let mut empty_block_light_mask = VariableBitSet::default();
522
523 let mut sky_light_arrays = Vec::with_capacity(light_section_count);
524 let mut block_light_arrays = Vec::with_capacity(light_section_count);
525
526 for (i, sky_light) in self.sky_light_sections.iter().enumerate() {
527 LoadedChunk::fill_light_data(
528 sky_light,
529 &mut sky_light_arrays,
530 &mut sky_light_mask,
531 &mut empty_sky_light_mask,
532 i,
533 false,
534 );
535 }
536
537 for (i, block_light) in self.block_light_sections.iter().enumerate() {
538 LoadedChunk::fill_light_data(
539 block_light,
540 &mut block_light_arrays,
541 &mut block_light_mask,
542 &mut empty_block_light_mask,
543 i,
544 true,
545 );
546 }
547
548 for sect in &self.sections {
549 sect.count_non_air_blocks()
550 .encode(&mut blocks_and_biomes)
551 .unwrap();
552
553 sect.block_states
554 .encode_mc_format(
555 &mut blocks_and_biomes,
556 |b| b.to_raw().into(),
557 4,
558 8,
559 bit_width(BlockState::max_raw().into()),
560 )
561 .expect("paletted container encode should always succeed");
562
563 sect.biomes
564 .encode_mc_format(
565 &mut blocks_and_biomes,
566 |b| b.to_index() as u64,
567 0,
568 3,
569 bit_width(info.biome_registry_len - 1),
570 )
571 .expect("paletted container encode should always succeed");
572 }
573
574 let block_entities: Vec<_> = self
575 .block_entities
576 .iter()
577 .filter_map(|(&idx, nbt)| {
578 let x = idx % 16;
579 let z = idx / 16 % 16;
580 let y = idx / 16 / 16;
581
582 let kind = self.sections[y as usize / 16]
583 .block_states
584 .get(idx as usize % SECTION_BLOCK_COUNT)
585 .block_entity_kind();
586
587 kind.map(|kind| ChunkDataBlockEntity {
588 packed_xz: ((x << 4) | z) as i8,
589 y: y as i16 + info.min_y as i16,
590 kind,
591 data: Cow::Borrowed(nbt),
592 })
593 })
594 .collect();
595
596 PacketWriter::new(&mut init_packets, info.threshold).write_packet(
597 &LevelChunkWithLightS2c {
598 pos,
599 heightmaps: Cow::Owned(heightmaps),
600 blocks_and_biomes: &blocks_and_biomes,
601 block_entities: Cow::Owned(block_entities),
602 sky_light_mask: Cow::Borrowed(&sky_light_mask),
603 block_light_mask: Cow::Borrowed(&block_light_mask),
604 empty_sky_light_mask: Cow::Borrowed(&empty_sky_light_mask),
605 empty_block_light_mask: Cow::Borrowed(&empty_block_light_mask),
606 sky_light_arrays: Cow::Borrowed(&sky_light_arrays),
607 block_light_arrays: Cow::Borrowed(&block_light_arrays),
608 },
609 )
610 }
611
612 writer.write_packet_bytes(&init_packets);
613 }
614
615 #[track_caller]
617 fn assert_no_changes(&self) {
618 #[cfg(debug_assertions)]
619 {
620 assert!(!self.changed_biomes);
621 assert!(self.changed_block_entities.is_empty());
622
623 for sect in &self.sections {
624 assert!(sect.updates.is_empty());
625 }
626 }
627 }
628}
629
630impl Chunk for LoadedChunk {
631 fn height(&self) -> u32 {
632 self.sections.len() as u32 * 16
633 }
634
635 fn block_state(&self, x: u32, y: u32, z: u32) -> BlockState {
636 check_block_oob(self, x, y, z);
637
638 let idx = x + z * 16 + y % 16 * 16 * 16;
639 self.sections[y as usize / 16]
640 .block_states
641 .get(idx as usize)
642 }
643
644 fn set_block_state(&mut self, x: u32, y: u32, z: u32, block: BlockState) -> BlockState {
645 check_block_oob(self, x, y, z);
646
647 let sect_y = y / 16;
648 let sect = &mut self.sections[sect_y as usize];
649 let idx = x + z * 16 + y % 16 * 16 * 16;
650
651 let old_block = sect.block_states.set(idx as usize, block);
652
653 if block != old_block {
654 self.cached_init_packets.get_mut().clear();
655
656 if *self.viewer_count.get_mut() > 0 {
657 sect.updates.push(
658 ChunkDeltaUpdateEntry::new()
659 .with_off_x(x as u8)
660 .with_off_y((y % 16) as u8)
661 .with_off_z(z as u8)
662 .with_block_state(block.to_raw().into()),
663 );
664 }
665 }
666
667 old_block
668 }
669
670 fn fill_block_state_section(&mut self, sect_y: u32, block: BlockState) {
671 check_section_oob(self, sect_y);
672
673 let sect = &mut self.sections[sect_y as usize];
674
675 if let PalettedContainer::Single(b) = §.block_states {
676 if *b != block {
677 self.cached_init_packets.get_mut().clear();
678
679 if *self.viewer_count.get_mut() > 0 {
680 sect.updates.clear();
683
684 sect.updates.reserve_exact(SECTION_BLOCK_COUNT);
686 for z in 0..16 {
687 for x in 0..16 {
688 for y in 0..16 {
689 sect.updates.push(
690 ChunkDeltaUpdateEntry::new()
691 .with_off_x(x)
692 .with_off_y(y)
693 .with_off_z(z)
694 .with_block_state(block.to_raw().into()),
695 );
696 }
697 }
698 }
699 }
700 }
701 } else {
702 for z in 0..16 {
703 for x in 0..16 {
704 for y in 0..16 {
705 let idx = x + z * 16 + (sect_y * 16 + y) * (16 * 16);
706
707 if block != sect.block_states.get(idx as usize) {
708 self.cached_init_packets.get_mut().clear();
709
710 if *self.viewer_count.get_mut() > 0 {
711 sect.updates.push(
712 ChunkDeltaUpdateEntry::new()
713 .with_off_x(x as u8)
714 .with_off_y(y as u8)
715 .with_off_z(z as u8)
716 .with_block_state(block.to_raw().into()),
717 );
718 }
719 }
720 }
721 }
722 }
723 }
724
725 sect.block_states.fill(block);
726 }
727
728 fn block_entity(&self, x: u32, y: u32, z: u32) -> Option<&Compound> {
729 check_block_oob(self, x, y, z);
730
731 let idx = x + z * 16 + y * 16 * 16;
732 self.block_entities.get(&idx)
733 }
734
735 fn block_entity_mut(&mut self, x: u32, y: u32, z: u32) -> Option<&mut Compound> {
736 check_block_oob(self, x, y, z);
737
738 let idx = x + z * 16 + y * 16 * 16;
739
740 if let Some(be) = self.block_entities.get_mut(&idx) {
741 if *self.viewer_count.get_mut() > 0 {
742 self.changed_block_entities.insert(idx);
743 }
744 self.cached_init_packets.get_mut().clear();
745
746 Some(be)
747 } else {
748 None
749 }
750 }
751
752 fn set_block_entity(
753 &mut self,
754 x: u32,
755 y: u32,
756 z: u32,
757 block_entity: Option<Compound>,
758 ) -> Option<Compound> {
759 check_block_oob(self, x, y, z);
760
761 let idx = x + z * 16 + y * 16 * 16;
762
763 match block_entity {
764 Some(nbt) => {
765 if *self.viewer_count.get_mut() > 0 {
766 self.changed_block_entities.insert(idx);
767 }
768 self.cached_init_packets.get_mut().clear();
769
770 self.block_entities.insert(idx, nbt)
771 }
772 None => {
773 let res = self.block_entities.remove(&idx);
774
775 if res.is_some() {
776 self.cached_init_packets.get_mut().clear();
777 }
778
779 res
780 }
781 }
782 }
783
784 fn clear_block_entities(&mut self) {
785 if self.block_entities.is_empty() {
786 return;
787 }
788
789 self.cached_init_packets.get_mut().clear();
790
791 if *self.viewer_count.get_mut() > 0 {
792 self.changed_block_entities
793 .extend(mem::take(&mut self.block_entities).into_keys());
794 } else {
795 self.block_entities.clear();
796 }
797 }
798
799 fn biome(&self, x: u32, y: u32, z: u32) -> BiomeId {
800 check_biome_oob(self, x, y, z);
801
802 let idx = x + z * 4 + y % 4 * 4 * 4;
803 self.sections[y as usize / 4].biomes.get(idx as usize)
804 }
805
806 fn set_biome(&mut self, x: u32, y: u32, z: u32, biome: BiomeId) -> BiomeId {
807 check_biome_oob(self, x, y, z);
808
809 let idx = x + z * 4 + y % 4 * 4 * 4;
810 let old_biome = self.sections[y as usize / 4]
811 .biomes
812 .set(idx as usize, biome);
813
814 if biome != old_biome {
815 self.cached_init_packets.get_mut().clear();
816
817 if *self.viewer_count.get_mut() > 0 {
818 self.changed_biomes = true;
819 }
820 }
821
822 old_biome
823 }
824
825 fn fill_biome_section(&mut self, sect_y: u32, biome: BiomeId) {
826 check_section_oob(self, sect_y);
827
828 let sect = &mut self.sections[sect_y as usize];
829
830 if let PalettedContainer::Single(b) = §.biomes {
831 if *b != biome {
832 self.cached_init_packets.get_mut().clear();
833 self.changed_biomes = *self.viewer_count.get_mut() > 0;
834 }
835 } else {
836 self.cached_init_packets.get_mut().clear();
837 self.changed_biomes = *self.viewer_count.get_mut() > 0;
838 }
839
840 sect.biomes.fill(biome);
841 }
842
843 fn shrink_to_fit(&mut self) {
844 self.cached_init_packets.get_mut().shrink_to_fit();
845
846 for sect in &mut self.sections {
847 sect.block_states.shrink_to_fit();
848 sect.biomes.shrink_to_fit();
849 sect.updates.shrink_to_fit();
850 }
851 }
852}
853
854#[cfg(test)]
855mod tests {
856 use chunkedge_nbt::compound;
857 use chunkedge_protocol::CompressionThreshold;
858 use chunkedge_registry::dimension_type::DimensionTypeId;
859
860 use super::*;
861
862 fn heightmap_idx(x: usize, z: usize) -> usize {
863 z * 16 + x
864 }
865
866 fn decode_heightmap(data: &[i64], bits_per_entry: u32) -> [u32; 16 * 16] {
867 let entries_per_long = i64::BITS / bits_per_entry;
868 let mask = (1_u64 << bits_per_entry) - 1;
869 let mut decoded = [0; 16 * 16];
870
871 for (idx, value) in decoded.iter_mut().enumerate() {
872 let long_idx = idx / entries_per_long as usize;
873 let bit_offset = (idx % entries_per_long as usize) as u32 * bits_per_entry;
874 *value = ((data[long_idx] as u64 >> bit_offset) & mask) as u32;
875 }
876
877 decoded
878 }
879
880 #[test]
881 fn loaded_chunk_unviewed_no_changes() {
882 let mut chunk = LoadedChunk::new(512);
883
884 chunk.set_block(0, 10, 0, BlockState::MAGMA_BLOCK);
885 chunk.assert_no_changes();
886
887 chunk.set_biome(0, 0, 0, BiomeId::from_index(5));
888 chunk.assert_no_changes();
889
890 chunk.fill_block_states(BlockState::ACACIA_BUTTON);
891 chunk.assert_no_changes();
892
893 chunk.fill_biomes(BiomeId::from_index(42));
894 chunk.assert_no_changes();
895 }
896
897 #[test]
898 fn loaded_chunk_changes_clear_packet_cache() {
899 #[track_caller]
900 fn check<T>(chunk: &mut LoadedChunk, change: impl FnOnce(&mut LoadedChunk) -> T) {
901 let info = ChunkLayerInfo {
902 dimension_type: DimensionTypeId::new(0),
903 height: 512,
904 min_y: -16,
905 biome_registry_len: 200,
906 threshold: CompressionThreshold(-1),
907 };
908
909 let mut buf = vec![];
910 let mut writer = PacketWriter::new(&mut buf, CompressionThreshold(-1));
911
912 chunk.write_init_packets(&mut writer, ChunkPos::new(3, 4), &info);
914
915 assert!(!chunk.cached_init_packets.get_mut().is_empty());
917
918 change(chunk);
920 assert!(chunk.cached_init_packets.get_mut().is_empty());
921
922 chunk.write_init_packets(&mut writer, ChunkPos::new(3, 4), &info);
924 assert!(!chunk.cached_init_packets.get_mut().is_empty());
925 }
926
927 let mut chunk = LoadedChunk::new(512);
928
929 check(&mut chunk, |c| {
930 c.set_block_state(0, 4, 0, BlockState::ACACIA_WOOD)
931 });
932 check(&mut chunk, |c| c.set_biome(1, 2, 3, BiomeId::from_index(4)));
933 check(&mut chunk, |c| c.fill_biomes(BiomeId::DEFAULT));
934 check(&mut chunk, |c| c.fill_block_states(BlockState::WET_SPONGE));
935 check(&mut chunk, |c| {
936 c.set_block_entity(3, 40, 5, Some(compound! {}))
937 });
938 check(&mut chunk, |c| {
939 c.block_entity_mut(3, 40, 5).unwrap();
940 });
941 check(&mut chunk, |c| c.set_block_entity(3, 40, 5, None));
942
943 assert_eq!(
946 chunk.set_block_state(0, 0, 0, BlockState::WET_SPONGE),
947 BlockState::WET_SPONGE
948 );
949
950 assert!(!chunk.cached_init_packets.get_mut().is_empty());
951 }
952
953 #[test]
954 fn heightmap_occupancy_rules() {
955 let mut chunk = LoadedChunk::new(32);
957
958 chunk.set_block_state(0, 0, 0, BlockState::STONE);
959 chunk.set_block_state(1, 5, 0, BlockState::OAK_LEAVES);
960 chunk.set_block_state(2, 6, 0, BlockState::CACTUS);
961 chunk.set_block_state(3, 7, 0, BlockState::WATER);
962 chunk.set_block_state(
963 4,
964 8,
965 0,
966 BlockState::OAK_LEAVES.set(PropName::Waterlogged, PropValue::True),
967 );
968
969 let world_surface = chunk.world_surface();
970 let motion_blocking = chunk.motion_blocking();
971 let motion_blocking_no_leaves = chunk.motion_blocking_no_leaves();
972
973 assert_eq!(world_surface[heightmap_idx(0, 0)], 1);
974 assert_eq!(world_surface[heightmap_idx(1, 0)], 6);
975 assert_eq!(world_surface[heightmap_idx(2, 0)], 7);
976 assert_eq!(world_surface[heightmap_idx(3, 0)], 8);
977 assert_eq!(world_surface[heightmap_idx(4, 0)], 9);
978
979 assert_eq!(motion_blocking[heightmap_idx(0, 0)], 1);
980 assert_eq!(motion_blocking[heightmap_idx(1, 0)], 6);
981 assert_eq!(motion_blocking[heightmap_idx(2, 0)], 0);
982 assert_eq!(motion_blocking[heightmap_idx(3, 0)], 8);
983 assert_eq!(motion_blocking[heightmap_idx(4, 0)], 9);
984
985 assert_eq!(motion_blocking_no_leaves[heightmap_idx(0, 0)], 1);
986 assert_eq!(motion_blocking_no_leaves[heightmap_idx(1, 0)], 0);
987 assert_eq!(motion_blocking_no_leaves[heightmap_idx(2, 0)], 0);
988 assert_eq!(motion_blocking_no_leaves[heightmap_idx(3, 0)], 8);
989 assert_eq!(motion_blocking_no_leaves[heightmap_idx(4, 0)], 0);
990 }
991
992 #[test]
993 fn encode_heightmap_uses_dynamic_bit_width() {
994 let mut chunk = LoadedChunk::new(512);
995 chunk.set_block_state(0, 511, 0, BlockState::STONE);
996
997 let motion_blocking = chunk.motion_blocking();
998 assert_eq!(motion_blocking[heightmap_idx(0, 0)], 512);
999
1000 let encoded = LoadedChunk::encode_heightmap(&motion_blocking, chunk.height());
1001 assert_eq!(encoded.len(), 43);
1004
1005 let decoded = decode_heightmap(&encoded, 10);
1006 assert_eq!(decoded[heightmap_idx(0, 0)], 512);
1007 assert_eq!(decoded[heightmap_idx(1, 0)], 0);
1008 }
1009}