valence_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 parking_lot::Mutex; // Using nonstandard mutex to avoid poisoning API.
7use valence_generated::block::{PropName, PropValue};
8use valence_nbt::{compound, Compound, Value};
9use valence_protocol::encode::{PacketWriter, WritePacket};
10use valence_protocol::packets::play::chunk_data_s2c::ChunkDataBlockEntity;
11use valence_protocol::packets::play::chunk_delta_update_s2c::ChunkDeltaUpdateEntry;
12use valence_protocol::packets::play::{
13    BlockEntityUpdateS2c, BlockUpdateS2c, ChunkDataS2c, ChunkDeltaUpdateS2c,
14};
15use valence_protocol::{BlockPos, BlockState, ChunkPos, ChunkSectionPos, Encode};
16use valence_registry::biome::BiomeId;
17use valence_registry::RegistryIdx;
18
19use super::chunk::{
20    bit_width, check_biome_oob, check_block_oob, check_section_oob, BiomeContainer,
21    BlockStateContainer, Chunk, SECTION_BLOCK_COUNT,
22};
23use super::paletted_container::PalettedContainer;
24use super::unloaded::{self, UnloadedChunk};
25use super::{ChunkLayerInfo, ChunkLayerMessages, LocalMsg};
26
27#[derive(Debug)]
28pub struct LoadedChunk {
29    /// A count of the clients viewing this chunk. Useful for knowing if it's
30    /// necessary to record changes, since no client would be in view to receive
31    /// the changes if this were zero.
32    viewer_count: AtomicU32,
33    /// Block and biome data for the chunk.
34    sections: Box<[Section]>,
35    /// The block entities in this chunk.
36    block_entities: BTreeMap<u32, Compound>,
37    /// The set of block entities that have been modified this tick.
38    changed_block_entities: BTreeSet<u32>,
39    /// If any biomes in this chunk have been modified this tick.
40    changed_biomes: bool,
41    /// Cached bytes of the chunk initialization packet. The cache is considered
42    /// invalidated if empty. This should be cleared whenever the chunk is
43    /// modified in an observable way, even if the chunk is not viewed.
44    cached_init_packets: Mutex<Vec<u8>>,
45}
46
47#[derive(Clone, Default, Debug)]
48struct Section {
49    block_states: BlockStateContainer,
50    biomes: BiomeContainer,
51    /// Contains modifications for the update section packet. (Or the regular
52    /// block update packet if len == 1).
53    updates: Vec<ChunkDeltaUpdateEntry>,
54}
55
56impl Section {
57    fn count_non_air_blocks(&self) -> u16 {
58        let mut count = 0;
59
60        match &self.block_states {
61            PalettedContainer::Single(s) => {
62                if !s.is_air() {
63                    count += SECTION_BLOCK_COUNT as u16;
64                }
65            }
66            PalettedContainer::Indirect(ind) => {
67                for i in 0..SECTION_BLOCK_COUNT {
68                    if !ind.get(i).is_air() {
69                        count += 1;
70                    }
71                }
72            }
73            PalettedContainer::Direct(dir) => {
74                for s in dir.as_ref() {
75                    if !s.is_air() {
76                        count += 1;
77                    }
78                }
79            }
80        }
81        count
82    }
83}
84
85impl LoadedChunk {
86    pub(crate) fn new(height: u32) -> Self {
87        Self {
88            viewer_count: AtomicU32::new(0),
89            sections: vec![Section::default(); height as usize / 16].into(),
90            block_entities: BTreeMap::new(),
91            changed_block_entities: BTreeSet::new(),
92            changed_biomes: false,
93            cached_init_packets: Mutex::new(vec![]),
94        }
95    }
96
97    /// Sets the content of this chunk to the supplied [`UnloadedChunk`]. The
98    /// given unloaded chunk is [resized] to match the height of this loaded
99    /// chunk prior to insertion.
100    ///
101    /// The previous chunk data is returned.
102    ///
103    /// [resized]: UnloadedChunk::set_height
104    pub(crate) fn insert(&mut self, mut chunk: UnloadedChunk) -> UnloadedChunk {
105        chunk.set_height(self.height());
106
107        let old_sections = self
108            .sections
109            .iter_mut()
110            .zip(chunk.sections)
111            .map(|(sect, other_sect)| {
112                sect.updates.clear();
113
114                unloaded::Section {
115                    block_states: mem::replace(&mut sect.block_states, other_sect.block_states),
116                    biomes: mem::replace(&mut sect.biomes, other_sect.biomes),
117                }
118            })
119            .collect();
120        let old_block_entities = mem::replace(&mut self.block_entities, chunk.block_entities);
121        self.changed_block_entities.clear();
122        self.changed_biomes = false;
123        self.cached_init_packets.get_mut().clear();
124        self.assert_no_changes();
125
126        UnloadedChunk {
127            sections: old_sections,
128            block_entities: old_block_entities,
129        }
130    }
131
132    pub(crate) fn remove(&mut self) -> UnloadedChunk {
133        let old_sections = self
134            .sections
135            .iter_mut()
136            .map(|sect| {
137                sect.updates.clear();
138
139                unloaded::Section {
140                    block_states: mem::take(&mut sect.block_states),
141                    biomes: mem::take(&mut sect.biomes),
142                }
143            })
144            .collect();
145        let old_block_entities = mem::take(&mut self.block_entities);
146        self.changed_block_entities.clear();
147        self.changed_biomes = false;
148        self.cached_init_packets.get_mut().clear();
149
150        self.assert_no_changes();
151
152        UnloadedChunk {
153            sections: old_sections,
154            block_entities: old_block_entities,
155        }
156    }
157
158    /// Returns the number of clients in view of this chunk.
159    pub fn viewer_count(&self) -> u32 {
160        self.viewer_count.load(Ordering::Relaxed)
161    }
162
163    /// Like [`Self::viewer_count`], but avoids an atomic operation.
164    pub fn viewer_count_mut(&mut self) -> u32 {
165        *self.viewer_count.get_mut()
166    }
167
168    /// Increments the viewer count.
169    pub(crate) fn inc_viewer_count(&self) {
170        self.viewer_count.fetch_add(1, Ordering::Relaxed);
171    }
172
173    /// Decrements the viewer count.
174    #[track_caller]
175    pub(crate) fn dec_viewer_count(&self) {
176        let old = self.viewer_count.fetch_sub(1, Ordering::Relaxed);
177        debug_assert_ne!(old, 0, "viewer count underflow!");
178    }
179
180    /// Performs the changes necessary to prepare this chunk for client updates.
181    /// - Chunk change messages are written to the layer.
182    /// - Recorded changes are cleared.
183    pub(crate) fn update_pre_client(
184        &mut self,
185        pos: ChunkPos,
186        info: &ChunkLayerInfo,
187        messages: &mut ChunkLayerMessages,
188    ) {
189        if *self.viewer_count.get_mut() == 0 {
190            // Nobody is viewing the chunk, so no need to send any update packets. There
191            // also shouldn't be any changes that need to be cleared.
192            self.assert_no_changes();
193
194            return;
195        }
196
197        // Block states
198        for (sect_y, sect) in self.sections.iter_mut().enumerate() {
199            match sect.updates.as_slice() {
200                &[] => {}
201                &[entry] => {
202                    let global_x = pos.x * 16 + i32::from(entry.off_x());
203                    let global_y = info.min_y + sect_y as i32 * 16 + i32::from(entry.off_y());
204                    let global_z = pos.z * 16 + i32::from(entry.off_z());
205
206                    messages.send_local_infallible(LocalMsg::PacketAt { pos }, |buf| {
207                        let mut writer = PacketWriter::new(buf, info.threshold);
208
209                        writer.write_packet(&BlockUpdateS2c {
210                            position: BlockPos::new(global_x, global_y, global_z),
211                            block_id: BlockState::from_raw(entry.block_state() as u16).unwrap(),
212                        });
213                    });
214                }
215                entries => {
216                    let chunk_sect_pos = ChunkSectionPos {
217                        x: pos.x,
218                        y: sect_y as i32 + info.min_y.div_euclid(16),
219                        z: pos.z,
220                    };
221
222                    messages.send_local_infallible(LocalMsg::PacketAt { pos }, |buf| {
223                        let mut writer = PacketWriter::new(buf, info.threshold);
224
225                        writer.write_packet(&ChunkDeltaUpdateS2c {
226                            chunk_sect_pos,
227                            blocks: Cow::Borrowed(entries),
228                        });
229                    });
230                }
231            }
232
233            sect.updates.clear();
234        }
235
236        // Block entities
237        for &idx in &self.changed_block_entities {
238            let Some(nbt) = self.block_entities.get(&idx) else {
239                continue;
240            };
241
242            let x = idx % 16;
243            let z = (idx / 16) % 16;
244            let y = idx / 16 / 16;
245
246            let state = self.sections[y as usize / 16]
247                .block_states
248                .get(idx as usize % SECTION_BLOCK_COUNT);
249
250            let Some(kind) = state.block_entity_kind() else {
251                continue;
252            };
253
254            let global_x = pos.x * 16 + x as i32;
255            let global_y = info.min_y + y as i32;
256            let global_z = pos.z * 16 + z as i32;
257
258            messages.send_local_infallible(LocalMsg::PacketAt { pos }, |buf| {
259                let mut writer = PacketWriter::new(buf, info.threshold);
260
261                writer.write_packet(&BlockEntityUpdateS2c {
262                    position: BlockPos::new(global_x, global_y, global_z),
263                    kind,
264                    data: Cow::Borrowed(nbt),
265                });
266            });
267        }
268
269        self.changed_block_entities.clear();
270
271        // Biomes
272        if self.changed_biomes {
273            self.changed_biomes = false;
274
275            messages.send_local_infallible(LocalMsg::ChangeBiome { pos }, |buf| {
276                for sect in &self.sections {
277                    sect.biomes
278                        .encode_mc_format(
279                            &mut *buf,
280                            |b| b.to_index() as u64,
281                            0,
282                            3,
283                            bit_width(info.biome_registry_len - 1),
284                        )
285                        .expect("paletted container encode should always succeed");
286                }
287            });
288        }
289
290        // All changes should be cleared.
291        self.assert_no_changes();
292    }
293
294    /// Generates the `MOTION_BLOCKING` heightmap for this chunk, which stores
295    /// the height of the highest non motion-blocking block in each column.
296    ///
297    /// The lowest value of the heightmap is 0, which means that there are no
298    /// motion-blocking blocks in the column. In this case, rain will fall
299    /// through the void and there will be no rain particles.
300    ///
301    /// A value of 1 means that rain particles will appear at the lowest
302    /// possible height given by [`DimensionType::min_y`]. Note that
303    /// blocks cannot be placed at `min_y - 1`.
304    ///
305    /// We take these two special cases into account by adding a value of 2 to
306    /// our heightmap if we find a motion-blocking block, since
307    /// `self.block_state(x, 0, z)` corresponds to the block at `(x, min_y, z)`
308    /// ingame.
309    ///
310    /// [`DimensionType::min_y`]: valence_registry::dimension_type::DimensionType::min_y
311    #[allow(clippy::needless_range_loop)]
312    fn motion_blocking(&self) -> Vec<Vec<u32>> {
313        let mut heightmap: Vec<Vec<u32>> = vec![vec![0; 16]; 16];
314
315        for z in 0..16 {
316            for x in 0..16 {
317                for y in (0..self.height()).rev() {
318                    let state = self.block_state(x as u32, y, z as u32);
319                    if state.blocks_motion()
320                        || state.is_liquid()
321                        || state.get(PropName::Waterlogged) == Some(PropValue::True)
322                    {
323                        heightmap[z][x] = y + 2;
324                        break;
325                    }
326                }
327            }
328        }
329
330        heightmap
331    }
332
333    /// Encodes a given heightmap into the correct format of the
334    /// `ChunkDataS2c` packet.
335    ///
336    /// The heightmap values are stored in a long array. Each value is encoded
337    /// as a 9-bit unsigned integer, so every long with 64 bits can hold at
338    /// most seven values. The long is padded at the left side with a single
339    /// zero. Since there are 256 values for 256 columns in a chunk, there
340    /// will be 36 fully filled longs and one half-filled long with four
341    /// values. The remaining three values in the last long are left unused.
342    ///
343    /// For example, the `MOTION_BLOCKING` heightmap in an empty superflat
344    /// world is always 4. The first 36 long values will then be
345    ///
346    /// 0 000000100 000000100 000000100 000000100 000000100 000000100 000000100,
347    ///
348    /// and the last long will be
349    ///
350    /// 0 000000000 000000000 000000000 000000100 000000100 000000100 000000100.
351    fn encode_heightmap(heightmap: Vec<Vec<u32>>) -> Value {
352        const BITS_PER_ENTRY: u32 = 9;
353        const ENTRIES_PER_LONG: u32 = i64::BITS / BITS_PER_ENTRY;
354
355        // Unless `ENTRIES_PER_LONG` is a power of 2 and therefore evenly divides 16*16,
356        // we need to add one extra long to fit all values in the packet.
357        const LONGS_PER_PACKET: u32 =
358            16 * 16 / ENTRIES_PER_LONG + (16 * 16 % ENTRIES_PER_LONG != 0) as u32;
359
360        let mut encoded: Vec<i64> = vec![0; LONGS_PER_PACKET as usize];
361        let mut iter = heightmap.into_iter().flatten();
362
363        for long in &mut encoded {
364            for j in 0..ENTRIES_PER_LONG {
365                match iter.next() {
366                    None => break,
367                    Some(y) => *long += i64::from(y) << (BITS_PER_ENTRY * j),
368                }
369            }
370        }
371
372        Value::LongArray(encoded)
373    }
374
375    /// Writes the packet data needed to initialize this chunk.
376    pub(crate) fn write_init_packets(
377        &self,
378        mut writer: impl WritePacket,
379        pos: ChunkPos,
380        info: &ChunkLayerInfo,
381    ) {
382        let mut init_packets = self.cached_init_packets.lock();
383
384        if init_packets.is_empty() {
385            let heightmaps = compound! {
386                "MOTION_BLOCKING" => LoadedChunk::encode_heightmap(self.motion_blocking()),
387                // TODO Implement `WORLD_SURFACE` (or explain why we don't need it)
388                // "WORLD_SURFACE" => self.encode_heightmap(self.world_surface()),
389            };
390
391            let mut blocks_and_biomes: Vec<u8> = vec![];
392
393            for sect in &self.sections {
394                sect.count_non_air_blocks()
395                    .encode(&mut blocks_and_biomes)
396                    .unwrap();
397
398                sect.block_states
399                    .encode_mc_format(
400                        &mut blocks_and_biomes,
401                        |b| b.to_raw().into(),
402                        4,
403                        8,
404                        bit_width(BlockState::max_raw().into()),
405                    )
406                    .expect("paletted container encode should always succeed");
407
408                sect.biomes
409                    .encode_mc_format(
410                        &mut blocks_and_biomes,
411                        |b| b.to_index() as u64,
412                        0,
413                        3,
414                        bit_width(info.biome_registry_len - 1),
415                    )
416                    .expect("paletted container encode should always succeed");
417            }
418
419            let block_entities: Vec<_> = self
420                .block_entities
421                .iter()
422                .filter_map(|(&idx, nbt)| {
423                    let x = idx % 16;
424                    let z = idx / 16 % 16;
425                    let y = idx / 16 / 16;
426
427                    let kind = self.sections[y as usize / 16]
428                        .block_states
429                        .get(idx as usize % SECTION_BLOCK_COUNT)
430                        .block_entity_kind();
431
432                    kind.map(|kind| ChunkDataBlockEntity {
433                        packed_xz: ((x << 4) | z) as i8,
434                        y: y as i16 + info.min_y as i16,
435                        kind,
436                        data: Cow::Borrowed(nbt),
437                    })
438                })
439                .collect();
440
441            PacketWriter::new(&mut init_packets, info.threshold).write_packet(&ChunkDataS2c {
442                pos,
443                heightmaps: Cow::Owned(heightmaps),
444                blocks_and_biomes: &blocks_and_biomes,
445                block_entities: Cow::Owned(block_entities),
446                sky_light_mask: Cow::Borrowed(&[]),
447                block_light_mask: Cow::Borrowed(&[]),
448                empty_sky_light_mask: Cow::Borrowed(&[]),
449                empty_block_light_mask: Cow::Borrowed(&[]),
450                sky_light_arrays: Cow::Borrowed(&[]),
451                block_light_arrays: Cow::Borrowed(&[]),
452            })
453        }
454
455        writer.write_packet_bytes(&init_packets);
456    }
457
458    /// Asserts that no changes to this chunk are currently recorded.
459    #[track_caller]
460    fn assert_no_changes(&self) {
461        #[cfg(debug_assertions)]
462        {
463            assert!(!self.changed_biomes);
464            assert!(self.changed_block_entities.is_empty());
465
466            for sect in &self.sections {
467                assert!(sect.updates.is_empty());
468            }
469        }
470    }
471}
472
473impl Chunk for LoadedChunk {
474    fn height(&self) -> u32 {
475        self.sections.len() as u32 * 16
476    }
477
478    fn block_state(&self, x: u32, y: u32, z: u32) -> BlockState {
479        check_block_oob(self, x, y, z);
480
481        let idx = x + z * 16 + y % 16 * 16 * 16;
482        self.sections[y as usize / 16]
483            .block_states
484            .get(idx as usize)
485    }
486
487    fn set_block_state(&mut self, x: u32, y: u32, z: u32, block: BlockState) -> BlockState {
488        check_block_oob(self, x, y, z);
489
490        let sect_y = y / 16;
491        let sect = &mut self.sections[sect_y as usize];
492        let idx = x + z * 16 + y % 16 * 16 * 16;
493
494        let old_block = sect.block_states.set(idx as usize, block);
495
496        if block != old_block {
497            self.cached_init_packets.get_mut().clear();
498
499            if *self.viewer_count.get_mut() > 0 {
500                sect.updates.push(
501                    ChunkDeltaUpdateEntry::new()
502                        .with_off_x(x as u8)
503                        .with_off_y((y % 16) as u8)
504                        .with_off_z(z as u8)
505                        .with_block_state(block.to_raw().into()),
506                );
507            }
508        }
509
510        old_block
511    }
512
513    fn fill_block_state_section(&mut self, sect_y: u32, block: BlockState) {
514        check_section_oob(self, sect_y);
515
516        let sect = &mut self.sections[sect_y as usize];
517
518        if let PalettedContainer::Single(b) = &sect.block_states {
519            if *b != block {
520                self.cached_init_packets.get_mut().clear();
521
522                if *self.viewer_count.get_mut() > 0 {
523                    // The whole section is being modified, so any previous modifications would
524                    // be overwritten.
525                    sect.updates.clear();
526
527                    // Push section updates for all the blocks in the section.
528                    sect.updates.reserve_exact(SECTION_BLOCK_COUNT);
529                    for z in 0..16 {
530                        for x in 0..16 {
531                            for y in 0..16 {
532                                sect.updates.push(
533                                    ChunkDeltaUpdateEntry::new()
534                                        .with_off_x(x)
535                                        .with_off_y(y)
536                                        .with_off_z(z)
537                                        .with_block_state(block.to_raw().into()),
538                                );
539                            }
540                        }
541                    }
542                }
543            }
544        } else {
545            for z in 0..16 {
546                for x in 0..16 {
547                    for y in 0..16 {
548                        let idx = x + z * 16 + (sect_y * 16 + y) * (16 * 16);
549
550                        if block != sect.block_states.get(idx as usize) {
551                            self.cached_init_packets.get_mut().clear();
552
553                            if *self.viewer_count.get_mut() > 0 {
554                                sect.updates.push(
555                                    ChunkDeltaUpdateEntry::new()
556                                        .with_off_x(x as u8)
557                                        .with_off_y(y as u8)
558                                        .with_off_z(z as u8)
559                                        .with_block_state(block.to_raw().into()),
560                                );
561                            }
562                        }
563                    }
564                }
565            }
566        }
567
568        sect.block_states.fill(block);
569    }
570
571    fn block_entity(&self, x: u32, y: u32, z: u32) -> Option<&Compound> {
572        check_block_oob(self, x, y, z);
573
574        let idx = x + z * 16 + y * 16 * 16;
575        self.block_entities.get(&idx)
576    }
577
578    fn block_entity_mut(&mut self, x: u32, y: u32, z: u32) -> Option<&mut Compound> {
579        check_block_oob(self, x, y, z);
580
581        let idx = x + z * 16 + y * 16 * 16;
582
583        if let Some(be) = self.block_entities.get_mut(&idx) {
584            if *self.viewer_count.get_mut() > 0 {
585                self.changed_block_entities.insert(idx);
586            }
587            self.cached_init_packets.get_mut().clear();
588
589            Some(be)
590        } else {
591            None
592        }
593    }
594
595    fn set_block_entity(
596        &mut self,
597        x: u32,
598        y: u32,
599        z: u32,
600        block_entity: Option<Compound>,
601    ) -> Option<Compound> {
602        check_block_oob(self, x, y, z);
603
604        let idx = x + z * 16 + y * 16 * 16;
605
606        match block_entity {
607            Some(nbt) => {
608                if *self.viewer_count.get_mut() > 0 {
609                    self.changed_block_entities.insert(idx);
610                }
611                self.cached_init_packets.get_mut().clear();
612
613                self.block_entities.insert(idx, nbt)
614            }
615            None => {
616                let res = self.block_entities.remove(&idx);
617
618                if res.is_some() {
619                    self.cached_init_packets.get_mut().clear();
620                }
621
622                res
623            }
624        }
625    }
626
627    fn clear_block_entities(&mut self) {
628        if self.block_entities.is_empty() {
629            return;
630        }
631
632        self.cached_init_packets.get_mut().clear();
633
634        if *self.viewer_count.get_mut() > 0 {
635            self.changed_block_entities
636                .extend(mem::take(&mut self.block_entities).into_keys());
637        } else {
638            self.block_entities.clear();
639        }
640    }
641
642    fn biome(&self, x: u32, y: u32, z: u32) -> BiomeId {
643        check_biome_oob(self, x, y, z);
644
645        let idx = x + z * 4 + y % 4 * 4 * 4;
646        self.sections[y as usize / 4].biomes.get(idx as usize)
647    }
648
649    fn set_biome(&mut self, x: u32, y: u32, z: u32, biome: BiomeId) -> BiomeId {
650        check_biome_oob(self, x, y, z);
651
652        let idx = x + z * 4 + y % 4 * 4 * 4;
653        let old_biome = self.sections[y as usize / 4]
654            .biomes
655            .set(idx as usize, biome);
656
657        if biome != old_biome {
658            self.cached_init_packets.get_mut().clear();
659
660            if *self.viewer_count.get_mut() > 0 {
661                self.changed_biomes = true;
662            }
663        }
664
665        old_biome
666    }
667
668    fn fill_biome_section(&mut self, sect_y: u32, biome: BiomeId) {
669        check_section_oob(self, sect_y);
670
671        let sect = &mut self.sections[sect_y as usize];
672
673        if let PalettedContainer::Single(b) = &sect.biomes {
674            if *b != biome {
675                self.cached_init_packets.get_mut().clear();
676                self.changed_biomes = *self.viewer_count.get_mut() > 0;
677            }
678        } else {
679            self.cached_init_packets.get_mut().clear();
680            self.changed_biomes = *self.viewer_count.get_mut() > 0;
681        }
682
683        sect.biomes.fill(biome);
684    }
685
686    fn shrink_to_fit(&mut self) {
687        self.cached_init_packets.get_mut().shrink_to_fit();
688
689        for sect in &mut self.sections {
690            sect.block_states.shrink_to_fit();
691            sect.biomes.shrink_to_fit();
692            sect.updates.shrink_to_fit();
693        }
694    }
695}
696
697#[cfg(test)]
698mod tests {
699    use valence_protocol::{ident, CompressionThreshold};
700
701    use super::*;
702
703    #[test]
704    fn loaded_chunk_unviewed_no_changes() {
705        let mut chunk = LoadedChunk::new(512);
706
707        chunk.set_block(0, 10, 0, BlockState::MAGMA_BLOCK);
708        chunk.assert_no_changes();
709
710        chunk.set_biome(0, 0, 0, BiomeId::from_index(5));
711        chunk.assert_no_changes();
712
713        chunk.fill_block_states(BlockState::ACACIA_BUTTON);
714        chunk.assert_no_changes();
715
716        chunk.fill_biomes(BiomeId::from_index(42));
717        chunk.assert_no_changes();
718    }
719
720    #[test]
721    fn loaded_chunk_changes_clear_packet_cache() {
722        #[track_caller]
723        fn check<T>(chunk: &mut LoadedChunk, change: impl FnOnce(&mut LoadedChunk) -> T) {
724            let info = ChunkLayerInfo {
725                dimension_type_name: ident!("whatever").into(),
726                height: 512,
727                min_y: -16,
728                biome_registry_len: 200,
729                threshold: CompressionThreshold(-1),
730            };
731
732            let mut buf = vec![];
733            let mut writer = PacketWriter::new(&mut buf, CompressionThreshold(-1));
734
735            // Rebuild cache.
736            chunk.write_init_packets(&mut writer, ChunkPos::new(3, 4), &info);
737
738            // Check that the cache is built.
739            assert!(!chunk.cached_init_packets.get_mut().is_empty());
740
741            // Making a change should clear the cache.
742            change(chunk);
743            assert!(chunk.cached_init_packets.get_mut().is_empty());
744
745            // Rebuild cache again.
746            chunk.write_init_packets(&mut writer, ChunkPos::new(3, 4), &info);
747            assert!(!chunk.cached_init_packets.get_mut().is_empty());
748        }
749
750        let mut chunk = LoadedChunk::new(512);
751
752        check(&mut chunk, |c| {
753            c.set_block_state(0, 4, 0, BlockState::ACACIA_WOOD)
754        });
755        check(&mut chunk, |c| c.set_biome(1, 2, 3, BiomeId::from_index(4)));
756        check(&mut chunk, |c| c.fill_biomes(BiomeId::DEFAULT));
757        check(&mut chunk, |c| c.fill_block_states(BlockState::WET_SPONGE));
758        check(&mut chunk, |c| {
759            c.set_block_entity(3, 40, 5, Some(compound! {}))
760        });
761        check(&mut chunk, |c| {
762            c.block_entity_mut(3, 40, 5).unwrap();
763        });
764        check(&mut chunk, |c| c.set_block_entity(3, 40, 5, None));
765
766        // Old block state is the same as new block state, so the cache should still be
767        // intact.
768        assert_eq!(
769            chunk.set_block_state(0, 0, 0, BlockState::WET_SPONGE),
770            BlockState::WET_SPONGE
771        );
772
773        assert!(!chunk.cached_init_packets.get_mut().is_empty());
774    }
775}