chunkedge_server/layer/chunk/
loaded.rs

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; // Using nonstandard mutex to avoid poisoning API.
23
24use 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    /// A count of the clients viewing this chunk. Useful for knowing if it's
35    /// necessary to record changes, since no client would be in view to receive
36    /// the changes if this were zero.
37    viewer_count: AtomicU32,
38    /// Block and biome data for the chunk.
39    sections: Box<[Section]>,
40    /// Sky light data for the chunk. Light sections have one extra section at
41    /// the top and bottom to account for skylight changes above and below the
42    /// chunk.
43    sky_light_sections: Box<[LightSection]>,
44    /// Block light data for the chunk. Light sections have one extra section at
45    /// the top and bottom to account for light changes above and below the
46    /// chunk.
47    block_light_sections: Box<[LightSection]>,
48    /// The block entities in this chunk.
49    block_entities: BTreeMap<u32, Compound>,
50    /// The set of block entities that have been modified this tick.
51    changed_block_entities: BTreeSet<u32>,
52    /// If any biomes in this chunk have been modified this tick.
53    changed_biomes: bool,
54    /// Cached bytes of the chunk initialization packet. The cache is considered
55    /// invalidated if empty. This should be cleared whenever the chunk is
56    /// modified in an observable way, even if the chunk is not viewed.
57    cached_init_packets: Mutex<Vec<u8>>,
58}
59
60#[derive(Clone, Debug, Default)]
61pub struct Section {
62    block_states: BlockStateContainer,
63    biomes: BiomeContainer,
64    /// Contains modifications for the update section packet. (Or the regular
65    /// block update packet if len == 1).
66    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/// Enum describing the light contents of a data section.
99///
100/// We need to differentiate between [`LightSection::NotSet`] and
101/// [`LightSection::FullyDark`]. This is because, for sky light,
102/// [`LightSection::NotSet`] could mean the section is either fully lit or fully
103/// dark, and the client should deduce that from the sky light data that is
104/// included.
105#[derive(Clone, Debug, Default)]
106pub enum LightSection {
107    #[default]
108    NotSet,
109    FullyDark,
110    FullData(Box<[u8; 2048]>),
111}
112
113impl LightSection {
114    /// Create a new section of light data with the given raw byte array
115    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            // We don't have a full lighting engine implemented so we set all sky light to be
128            // NotSet so that no light data is sent to the client and we rely
129            // on a hack that sets ambient light to full brightness for all dimensions
130            // to make the chunks appear fully lit. We don't send a full light section filled with
131            // 0xFF here, instead of the ambient light hack, because this is extremely unoptimized
132            // in terms of memory consumption and crashes the many_players_spread_out
133            // benchmark/test.
134            sky_light_sections: vec![LightSection::NotSet; light_section_count].into(),
135            // We don't have a full lighting engine implemented so we set all block light to be
136            // fully dark.
137            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    /// Sets the content of this chunk to the supplied [`UnloadedChunk`]. The
146    /// given unloaded chunk is [resized] to match the height of this loaded
147    /// chunk prior to insertion.
148    ///
149    /// The previous chunk data is returned.
150    ///
151    /// [resized]: UnloadedChunk::set_height
152    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    /// Returns the number of clients in view of this chunk.
207    pub fn viewer_count(&self) -> u32 {
208        self.viewer_count.load(Ordering::Relaxed)
209    }
210
211    /// Like [`Self::viewer_count`], but avoids an atomic operation.
212    pub fn viewer_count_mut(&mut self) -> u32 {
213        *self.viewer_count.get_mut()
214    }
215
216    /// Increments the viewer count.
217    pub(crate) fn inc_viewer_count(&self) {
218        self.viewer_count.fetch_add(1, Ordering::Relaxed);
219    }
220
221    /// Decrements the viewer count.
222    #[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    /// Performs the changes necessary to prepare this chunk for client updates.
229    /// - Chunk change messages are written to the layer.
230    /// - Recorded changes are cleared.
231    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            // Nobody is viewing the chunk, so no need to send any update packets. There
239            // also shouldn't be any changes that need to be cleared.
240            self.assert_no_changes();
241
242            return;
243        }
244
245        // Block states
246        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        // Block entities
285        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        // Biomes
320        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        // All changes should be cleared.
339        self.assert_no_changes();
340    }
341
342    /// Generates the `MOTION_BLOCKING` heightmap for this chunk, which stores
343    /// the height of the highest non motion-blocking block in each column.
344    ///
345    /// `MOTION_BLOCKING` considers:
346    ///
347    /// "Solid" blocks, except bamboo saplings and cactuses; fluids. To
348    /// determine where to display rain and snow.
349    ///
350    /// [Minecraft Wiki `MOTION_BLOCKING`](https://minecraft.wiki/w/Java_Edition_protocol/Chunk_format#Heightmap_structure:~:text=MOTION%5FBLOCKING)
351    pub(crate) fn motion_blocking(&self) -> [u32; 16 * 16] {
352        self.build_heightmap(Self::is_motion_blocking_occupied)
353    }
354
355    /// Generates the `MOTION_BLOCKING_NO_LEAVES` heightmap for this chunk,
356    /// which stores the height of the highest non motion-blocking and non-leaf
357    /// block in each column.
358    ///
359    /// `MOTION_BLOCKING_NO_LEAVES` is the same as `MOTION_BLOCKING`, but also
360    /// considers leaf blocks to be non-blocking.
361    ///
362    /// [Minecraft Wiki `MOTION_BLOCKING_NO_LEAVES`](https://minecraft.wiki/w/Java_Edition_protocol/Chunk_format#Heightmap_structure:~:text=MOTION%5FBLOCKING%5FNO%5FLEAVES)
363    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    /// Generates the `WORLD_SURFACE` heightmap for this chunk, which stores the
368    /// height of the highest non-air block in each column.
369    ///
370    /// `WORLD_SURFACE` cosiders:
371    ///
372    /// All blocks other than air, cave air and void air. To determine if a
373    /// beacon beam is obstructed.
374    ///
375    /// [Minecraft Wiki `WORLD_SURFACE`](https://minecraft.wiki/w/Java_Edition_protocol/Chunk_format#Heightmap_structure:~:text=WORLD%5FSURFACE)
376    pub(crate) fn world_surface(&self) -> [u32; 16 * 16] {
377        self.build_heightmap(|state| !state.is_air())
378    }
379
380    /// Generates a heightmap for this chunk using the provided predicate.
381    ///
382    /// The lowest value of the heightmap is 0, which means that there are no
383    /// blocks matching the predicate in the column. Since 0 is reserved for
384    /// this case, the heightmap values are 1-indexed. A value of 1 means that
385    /// the heightmap has the lowest possible height given by
386    /// [`DimensionType::min_y`]. Note that blocks cannot be placed at `min_y -
387    /// 1`.
388    ///
389    /// [`DimensionType::min_y`]: chunkedge_registry::dimension_type::DimensionType::min_y
390    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 values are 1-indexed local Y coordinates, where 0
401                        // means "no occupied block in this column".
402                        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    /// Encodes a given heightmap into the packed long-array format used in
437    /// `LevelChunkWithLightS2c`.
438    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                // For sky light, the client will deduce this section to be either fully lit or
468                // fully dark based on the presence of light data in other light sections in the
469                // chunk.
470                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    /// Writes the packet data needed to initialize this chunk.
485    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    /// Asserts that no changes to this chunk are currently recorded.
616    #[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) = &sect.block_states {
676            if *b != block {
677                self.cached_init_packets.get_mut().clear();
678
679                if *self.viewer_count.get_mut() > 0 {
680                    // The whole section is being modified, so any previous modifications would
681                    // be overwritten.
682                    sect.updates.clear();
683
684                    // Push section updates for all the blocks in the section.
685                    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) = &sect.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            // Rebuild cache.
913            chunk.write_init_packets(&mut writer, ChunkPos::new(3, 4), &info);
914
915            // Check that the cache is built.
916            assert!(!chunk.cached_init_packets.get_mut().is_empty());
917
918            // Making a change should clear the cache.
919            change(chunk);
920            assert!(chunk.cached_init_packets.get_mut().is_empty());
921
922            // Rebuild cache again.
923            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        // Old block state is the same as new block state, so the cache should still be
944        // intact.
945        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        // Based on: https://minecraft.wiki/w/Java_Edition_protocol/Chunk_format#Heightmap_structure
956        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        // 512 world height => ceil(log2(512 + 1)) = 10 bits, so 64/10 = 6 entries per
1002        // long.
1003        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}