valence_server/layer/
chunk.rs

1#[allow(clippy::module_inception)]
2mod chunk;
3pub mod loaded;
4mod paletted_container;
5pub mod unloaded;
6
7use std::borrow::Cow;
8use std::collections::hash_map::{Entry, OccupiedEntry, VacantEntry};
9use std::fmt;
10
11use bevy_app::prelude::*;
12use bevy_ecs::prelude::*;
13pub use chunk::{MAX_HEIGHT, *};
14pub use loaded::LoadedChunk;
15use rustc_hash::FxHashMap;
16pub use unloaded::UnloadedChunk;
17use valence_math::{DVec3, Vec3};
18use valence_nbt::Compound;
19use valence_protocol::encode::{PacketWriter, WritePacket};
20use valence_protocol::packets::play::particle_s2c::Particle;
21use valence_protocol::packets::play::{ParticleS2c, PlaySoundS2c};
22use valence_protocol::sound::{Sound, SoundCategory, SoundId};
23use valence_protocol::{BlockPos, ChunkPos, CompressionThreshold, Encode, Ident, Packet};
24use valence_registry::biome::{BiomeId, BiomeRegistry};
25use valence_registry::DimensionTypeRegistry;
26use valence_server_common::Server;
27
28use super::bvh::GetChunkPos;
29use super::message::Messages;
30use super::{Layer, UpdateLayersPostClientSet, UpdateLayersPreClientSet};
31
32/// A [`Component`] containing the [chunks](LoadedChunk) and [dimension
33/// information](valence_registry::dimension_type::DimensionTypeId) of a
34/// Minecraft world.
35#[derive(Component, Debug)]
36pub struct ChunkLayer {
37    messages: ChunkLayerMessages,
38    chunks: FxHashMap<ChunkPos, LoadedChunk>,
39    info: ChunkLayerInfo,
40}
41
42/// Chunk layer information.
43pub(crate) struct ChunkLayerInfo {
44    dimension_type_name: Ident<String>,
45    height: u32,
46    min_y: i32,
47    biome_registry_len: usize,
48    threshold: CompressionThreshold,
49}
50
51impl fmt::Debug for ChunkLayerInfo {
52    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
53        f.debug_struct("ChunkLayerInfo")
54            .field("dimension_type_name", &self.dimension_type_name)
55            .field("height", &self.height)
56            .field("min_y", &self.min_y)
57            .field("biome_registry_len", &self.biome_registry_len)
58            .field("threshold", &self.threshold)
59            // Ignore sky light mask and array.
60            .finish()
61    }
62}
63
64type ChunkLayerMessages = Messages<GlobalMsg, LocalMsg>;
65
66#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Debug)]
67pub(crate) enum GlobalMsg {
68    /// Send packet data to all clients viewing the layer.
69    Packet,
70    /// Send packet data to all clients viewing the layer, except the client
71    /// identified by `except`.
72    PacketExcept { except: Entity },
73}
74
75#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Debug)]
76pub(crate) enum LocalMsg {
77    /// Send packet data to all clients viewing the layer in view of `pos`.
78    PacketAt {
79        pos: ChunkPos,
80    },
81    PacketAtExcept {
82        pos: ChunkPos,
83        except: Entity,
84    },
85    RadiusAt {
86        center: BlockPos,
87        radius_squared: u32,
88    },
89    RadiusAtExcept {
90        center: BlockPos,
91        radius_squared: u32,
92        except: Entity,
93    },
94    /// Instruct clients to load or unload the chunk at `pos`. Loading and
95    /// unloading are combined into a single message so that load/unload order
96    /// is not lost when messages are sorted.
97    ///
98    /// Message content is a single byte indicating load (1) or unload (0).
99    ChangeChunkState {
100        pos: ChunkPos,
101    },
102    /// Message content is the data for a single biome in the "change biomes"
103    /// packet.
104    ChangeBiome {
105        pos: ChunkPos,
106    },
107}
108
109impl GetChunkPos for LocalMsg {
110    fn chunk_pos(&self) -> ChunkPos {
111        match *self {
112            LocalMsg::PacketAt { pos } => pos,
113            LocalMsg::PacketAtExcept { pos, .. } => pos,
114            LocalMsg::RadiusAt { center, .. } => center.to_chunk_pos(),
115            LocalMsg::RadiusAtExcept { center, .. } => center.to_chunk_pos(),
116            LocalMsg::ChangeBiome { pos } => pos,
117            LocalMsg::ChangeChunkState { pos } => pos,
118        }
119    }
120}
121
122impl ChunkLayer {
123    pub(crate) const LOAD: u8 = 0;
124    pub(crate) const UNLOAD: u8 = 1;
125    pub(crate) const OVERWRITE: u8 = 2;
126
127    /// Creates a new chunk layer.
128    #[track_caller]
129    pub fn new(
130        dimension_type_name: impl Into<Ident<String>>,
131        dimensions: &DimensionTypeRegistry,
132        biomes: &BiomeRegistry,
133        server: &Server,
134    ) -> Self {
135        let dimension_type_name = dimension_type_name.into();
136
137        let dim = &dimensions[dimension_type_name.as_str_ident()];
138
139        assert!(
140            (0..MAX_HEIGHT as i32).contains(&dim.height),
141            "invalid dimension height of {}",
142            dim.height
143        );
144
145        Self {
146            messages: Messages::new(),
147            chunks: Default::default(),
148            info: ChunkLayerInfo {
149                dimension_type_name,
150                height: dim.height as u32,
151                min_y: dim.min_y,
152                biome_registry_len: biomes.iter().len(),
153                threshold: server.compression_threshold(),
154            },
155        }
156    }
157
158    /// The name of the dimension this chunk layer is using.
159    pub fn dimension_type_name(&self) -> Ident<&str> {
160        self.info.dimension_type_name.as_str_ident()
161    }
162
163    /// The height of this instance's dimension.
164    pub fn height(&self) -> u32 {
165        self.info.height
166    }
167
168    /// The `min_y` of this instance's dimension.
169    pub fn min_y(&self) -> i32 {
170        self.info.min_y
171    }
172
173    /// Get a reference to the chunk at the given position, if it is loaded.
174    pub fn chunk(&self, pos: impl Into<ChunkPos>) -> Option<&LoadedChunk> {
175        self.chunks.get(&pos.into())
176    }
177
178    /// Get a mutable reference to the chunk at the given position, if it is
179    /// loaded.
180    pub fn chunk_mut(&mut self, pos: impl Into<ChunkPos>) -> Option<&mut LoadedChunk> {
181        self.chunks.get_mut(&pos.into())
182    }
183
184    /// Insert a chunk into the instance at the given position. The preivous
185    /// chunk data is returned.
186    pub fn insert_chunk(
187        &mut self,
188        pos: impl Into<ChunkPos>,
189        chunk: UnloadedChunk,
190    ) -> Option<UnloadedChunk> {
191        match self.chunk_entry(pos) {
192            ChunkEntry::Occupied(mut oe) => Some(oe.insert(chunk)),
193            ChunkEntry::Vacant(ve) => {
194                ve.insert(chunk);
195                None
196            }
197        }
198    }
199
200    /// Unload the chunk at the given position, if it is loaded. Returns the
201    /// chunk if it was loaded.
202    pub fn remove_chunk(&mut self, pos: impl Into<ChunkPos>) -> Option<UnloadedChunk> {
203        match self.chunk_entry(pos) {
204            ChunkEntry::Occupied(oe) => Some(oe.remove()),
205            ChunkEntry::Vacant(_) => None,
206        }
207    }
208
209    /// Unload all chunks in this instance.
210    pub fn clear_chunks(&mut self) {
211        self.retain_chunks(|_, _| false)
212    }
213
214    /// Retain only the chunks for which the given predicate returns `true`.
215    pub fn retain_chunks<F>(&mut self, mut f: F)
216    where
217        F: FnMut(ChunkPos, &mut LoadedChunk) -> bool,
218    {
219        self.chunks.retain(|pos, chunk| {
220            if !f(*pos, chunk) {
221                self.messages
222                    .send_local_infallible(LocalMsg::ChangeChunkState { pos: *pos }, |b| {
223                        b.push(Self::UNLOAD)
224                    });
225
226                false
227            } else {
228                true
229            }
230        });
231    }
232
233    /// Get a [`ChunkEntry`] for the given position.
234    pub fn chunk_entry(&mut self, pos: impl Into<ChunkPos>) -> ChunkEntry {
235        match self.chunks.entry(pos.into()) {
236            Entry::Occupied(oe) => ChunkEntry::Occupied(OccupiedChunkEntry {
237                messages: &mut self.messages,
238                entry: oe,
239            }),
240            Entry::Vacant(ve) => ChunkEntry::Vacant(VacantChunkEntry {
241                height: self.info.height,
242                messages: &mut self.messages,
243                entry: ve,
244            }),
245        }
246    }
247
248    /// Get an iterator over all loaded chunks in the instance. The order of the
249    /// chunks is undefined.
250    pub fn chunks(&self) -> impl Iterator<Item = (ChunkPos, &LoadedChunk)> + Clone + '_ {
251        self.chunks.iter().map(|(pos, chunk)| (*pos, chunk))
252    }
253
254    /// Get an iterator over all loaded chunks in the instance, mutably. The
255    /// order of the chunks is undefined.
256    pub fn chunks_mut(&mut self) -> impl Iterator<Item = (ChunkPos, &mut LoadedChunk)> + '_ {
257        self.chunks.iter_mut().map(|(pos, chunk)| (*pos, chunk))
258    }
259
260    /// Optimizes the memory usage of the instance.
261    pub fn shrink_to_fit(&mut self) {
262        for (_, chunk) in self.chunks_mut() {
263            chunk.shrink_to_fit();
264        }
265
266        self.chunks.shrink_to_fit();
267        self.messages.shrink_to_fit();
268    }
269
270    pub fn block(&self, pos: impl Into<BlockPos>) -> Option<BlockRef> {
271        let (chunk, x, y, z) = self.chunk_and_offsets(pos.into())?;
272        Some(chunk.block(x, y, z))
273    }
274
275    pub fn set_block(&mut self, pos: impl Into<BlockPos>, block: impl IntoBlock) -> Option<Block> {
276        let (chunk, x, y, z) = self.chunk_and_offsets_mut(pos.into())?;
277        Some(chunk.set_block(x, y, z, block))
278    }
279
280    pub fn block_entity_mut(&mut self, pos: impl Into<BlockPos>) -> Option<&mut Compound> {
281        let (chunk, x, y, z) = self.chunk_and_offsets_mut(pos.into())?;
282        chunk.block_entity_mut(x, y, z)
283    }
284
285    pub fn biome(&self, pos: impl Into<BlockPos>) -> Option<BiomeId> {
286        let (chunk, x, y, z) = self.chunk_and_offsets(pos.into())?;
287        Some(chunk.biome(x / 4, y / 4, z / 4))
288    }
289
290    pub fn set_biome(&mut self, pos: impl Into<BlockPos>, biome: BiomeId) -> Option<BiomeId> {
291        let (chunk, x, y, z) = self.chunk_and_offsets_mut(pos.into())?;
292        Some(chunk.set_biome(x / 4, y / 4, z / 4, biome))
293    }
294
295    #[inline]
296    fn chunk_and_offsets(&self, pos: BlockPos) -> Option<(&LoadedChunk, u32, u32, u32)> {
297        let Some(y) = pos
298            .y
299            .checked_sub(self.info.min_y)
300            .and_then(|y| y.try_into().ok())
301        else {
302            return None;
303        };
304
305        if y >= self.info.height {
306            return None;
307        }
308
309        let Some(chunk) = self.chunk(ChunkPos::from_block_pos(pos)) else {
310            return None;
311        };
312
313        let x = pos.x.rem_euclid(16) as u32;
314        let z = pos.z.rem_euclid(16) as u32;
315
316        Some((chunk, x, y, z))
317    }
318
319    #[inline]
320    fn chunk_and_offsets_mut(
321        &mut self,
322        pos: BlockPos,
323    ) -> Option<(&mut LoadedChunk, u32, u32, u32)> {
324        let Some(y) = pos
325            .y
326            .checked_sub(self.info.min_y)
327            .and_then(|y| y.try_into().ok())
328        else {
329            return None;
330        };
331
332        if y >= self.info.height {
333            return None;
334        }
335
336        let Some(chunk) = self.chunk_mut(ChunkPos::from_block_pos(pos)) else {
337            return None;
338        };
339
340        let x = pos.x.rem_euclid(16) as u32;
341        let z = pos.z.rem_euclid(16) as u32;
342
343        Some((chunk, x, y, z))
344    }
345
346    pub(crate) fn info(&self) -> &ChunkLayerInfo {
347        &self.info
348    }
349
350    pub(crate) fn messages(&self) -> &ChunkLayerMessages {
351        &self.messages
352    }
353
354    // TODO: move to `valence_particle`.
355    /// Puts a particle effect at the given position in the world. The particle
356    /// effect is visible to all players in the instance with the
357    /// appropriate chunk in view.
358    pub fn play_particle(
359        &mut self,
360        particle: &Particle,
361        long_distance: bool,
362        position: impl Into<DVec3>,
363        offset: impl Into<Vec3>,
364        max_speed: f32,
365        count: i32,
366    ) {
367        let position = position.into();
368
369        self.view_writer(ChunkPos::from_pos(position))
370            .write_packet(&ParticleS2c {
371                particle: Cow::Borrowed(particle),
372                long_distance,
373                position,
374                offset: offset.into(),
375                max_speed,
376                count,
377            });
378    }
379
380    // TODO: move to `valence_sound`.
381    /// Plays a sound effect at the given position in the world. The sound
382    /// effect is audible to all players in the instance with the
383    /// appropriate chunk in view.
384    pub fn play_sound(
385        &mut self,
386        sound: Sound,
387        category: SoundCategory,
388        position: impl Into<DVec3>,
389        volume: f32,
390        pitch: f32,
391    ) {
392        let position = position.into();
393
394        self.view_writer(ChunkPos::from_pos(position))
395            .write_packet(&PlaySoundS2c {
396                id: SoundId::Direct {
397                    id: sound.to_ident().into(),
398                    range: None,
399                },
400                category,
401                position: (position * 8.0).as_ivec3(),
402                volume,
403                pitch,
404                seed: rand::random(),
405            });
406    }
407}
408
409impl Layer for ChunkLayer {
410    type ExceptWriter<'a> = ExceptWriter<'a>;
411
412    type ViewWriter<'a> = ViewWriter<'a>;
413
414    type ViewExceptWriter<'a> = ViewExceptWriter<'a>;
415
416    type RadiusWriter<'a> = RadiusWriter<'a>;
417
418    type RadiusExceptWriter<'a> = RadiusExceptWriter<'a>;
419
420    fn except_writer(&mut self, except: Entity) -> Self::ExceptWriter<'_> {
421        ExceptWriter {
422            layer: self,
423            except,
424        }
425    }
426
427    fn view_writer(&mut self, pos: impl Into<ChunkPos>) -> Self::ViewWriter<'_> {
428        ViewWriter {
429            layer: self,
430            pos: pos.into(),
431        }
432    }
433
434    fn view_except_writer(
435        &mut self,
436        pos: impl Into<ChunkPos>,
437        except: Entity,
438    ) -> Self::ViewExceptWriter<'_> {
439        ViewExceptWriter {
440            layer: self,
441            pos: pos.into(),
442            except,
443        }
444    }
445
446    fn radius_writer(
447        &mut self,
448        center: impl Into<BlockPos>,
449        radius: u32,
450    ) -> Self::RadiusWriter<'_> {
451        RadiusWriter {
452            layer: self,
453            center: center.into(),
454            radius,
455        }
456    }
457
458    fn radius_except_writer(
459        &mut self,
460        center: impl Into<BlockPos>,
461        radius: u32,
462        except: Entity,
463    ) -> Self::RadiusExceptWriter<'_> {
464        RadiusExceptWriter {
465            layer: self,
466            center: center.into(),
467            radius,
468            except,
469        }
470    }
471}
472
473impl WritePacket for ChunkLayer {
474    fn write_packet_fallible<P>(&mut self, packet: &P) -> anyhow::Result<()>
475    where
476        P: Packet + Encode,
477    {
478        self.messages.send_global(GlobalMsg::Packet, |b| {
479            PacketWriter::new(b, self.info.threshold).write_packet_fallible(packet)
480        })
481    }
482
483    fn write_packet_bytes(&mut self, bytes: &[u8]) {
484        self.messages
485            .send_global_infallible(GlobalMsg::Packet, |b| b.extend_from_slice(bytes));
486    }
487}
488
489pub struct ExceptWriter<'a> {
490    layer: &'a mut ChunkLayer,
491    except: Entity,
492}
493
494impl WritePacket for ExceptWriter<'_> {
495    fn write_packet_fallible<P>(&mut self, packet: &P) -> anyhow::Result<()>
496    where
497        P: Packet + Encode,
498    {
499        self.layer.messages.send_global(
500            GlobalMsg::PacketExcept {
501                except: self.except,
502            },
503            |b| PacketWriter::new(b, self.layer.info.threshold).write_packet_fallible(packet),
504        )
505    }
506
507    fn write_packet_bytes(&mut self, bytes: &[u8]) {
508        self.layer.messages.send_global_infallible(
509            GlobalMsg::PacketExcept {
510                except: self.except,
511            },
512            |b| b.extend_from_slice(bytes),
513        )
514    }
515}
516
517pub struct ViewWriter<'a> {
518    layer: &'a mut ChunkLayer,
519    pos: ChunkPos,
520}
521
522impl WritePacket for ViewWriter<'_> {
523    fn write_packet_fallible<P>(&mut self, packet: &P) -> anyhow::Result<()>
524    where
525        P: Packet + Encode,
526    {
527        self.layer
528            .messages
529            .send_local(LocalMsg::PacketAt { pos: self.pos }, |b| {
530                PacketWriter::new(b, self.layer.info.threshold).write_packet_fallible(packet)
531            })
532    }
533
534    fn write_packet_bytes(&mut self, bytes: &[u8]) {
535        self.layer
536            .messages
537            .send_local_infallible(LocalMsg::PacketAt { pos: self.pos }, |b| {
538                b.extend_from_slice(bytes)
539            });
540    }
541}
542
543pub struct ViewExceptWriter<'a> {
544    layer: &'a mut ChunkLayer,
545    pos: ChunkPos,
546    except: Entity,
547}
548
549impl WritePacket for ViewExceptWriter<'_> {
550    fn write_packet_fallible<P>(&mut self, packet: &P) -> anyhow::Result<()>
551    where
552        P: Packet + Encode,
553    {
554        self.layer.messages.send_local(
555            LocalMsg::PacketAtExcept {
556                pos: self.pos,
557                except: self.except,
558            },
559            |b| PacketWriter::new(b, self.layer.info.threshold).write_packet_fallible(packet),
560        )
561    }
562
563    fn write_packet_bytes(&mut self, bytes: &[u8]) {
564        self.layer.messages.send_local_infallible(
565            LocalMsg::PacketAtExcept {
566                pos: self.pos,
567                except: self.except,
568            },
569            |b| b.extend_from_slice(bytes),
570        );
571    }
572}
573
574pub struct RadiusWriter<'a> {
575    layer: &'a mut ChunkLayer,
576    center: BlockPos,
577    radius: u32,
578}
579
580impl WritePacket for RadiusWriter<'_> {
581    fn write_packet_fallible<P>(&mut self, packet: &P) -> anyhow::Result<()>
582    where
583        P: Packet + Encode,
584    {
585        self.layer.messages.send_local(
586            LocalMsg::RadiusAt {
587                center: self.center,
588                radius_squared: self.radius,
589            },
590            |b| PacketWriter::new(b, self.layer.info.threshold).write_packet_fallible(packet),
591        )
592    }
593
594    fn write_packet_bytes(&mut self, bytes: &[u8]) {
595        self.layer.messages.send_local_infallible(
596            LocalMsg::RadiusAt {
597                center: self.center,
598                radius_squared: self.radius,
599            },
600            |b| b.extend_from_slice(bytes),
601        );
602    }
603}
604
605pub struct RadiusExceptWriter<'a> {
606    layer: &'a mut ChunkLayer,
607    center: BlockPos,
608    radius: u32,
609    except: Entity,
610}
611
612impl WritePacket for RadiusExceptWriter<'_> {
613    fn write_packet_fallible<P>(&mut self, packet: &P) -> anyhow::Result<()>
614    where
615        P: Packet + Encode,
616    {
617        self.layer.messages.send_local(
618            LocalMsg::RadiusAtExcept {
619                center: self.center,
620                radius_squared: self.radius,
621                except: self.except,
622            },
623            |b| PacketWriter::new(b, self.layer.info.threshold).write_packet_fallible(packet),
624        )
625    }
626
627    fn write_packet_bytes(&mut self, bytes: &[u8]) {
628        self.layer.messages.send_local_infallible(
629            LocalMsg::RadiusAtExcept {
630                center: self.center,
631                radius_squared: self.radius,
632                except: self.except,
633            },
634            |b| b.extend_from_slice(bytes),
635        );
636    }
637}
638
639#[derive(Debug)]
640pub enum ChunkEntry<'a> {
641    Occupied(OccupiedChunkEntry<'a>),
642    Vacant(VacantChunkEntry<'a>),
643}
644
645impl<'a> ChunkEntry<'a> {
646    pub fn or_default(self) -> &'a mut LoadedChunk {
647        match self {
648            ChunkEntry::Occupied(oe) => oe.into_mut(),
649            ChunkEntry::Vacant(ve) => ve.insert(UnloadedChunk::new()),
650        }
651    }
652}
653
654#[derive(Debug)]
655pub struct OccupiedChunkEntry<'a> {
656    messages: &'a mut ChunkLayerMessages,
657    entry: OccupiedEntry<'a, ChunkPos, LoadedChunk>,
658}
659
660impl<'a> OccupiedChunkEntry<'a> {
661    pub fn get(&self) -> &LoadedChunk {
662        self.entry.get()
663    }
664
665    pub fn get_mut(&mut self) -> &mut LoadedChunk {
666        self.entry.get_mut()
667    }
668
669    pub fn insert(&mut self, chunk: UnloadedChunk) -> UnloadedChunk {
670        self.messages.send_local_infallible(
671            LocalMsg::ChangeChunkState {
672                pos: *self.entry.key(),
673            },
674            |b| b.push(ChunkLayer::OVERWRITE),
675        );
676
677        self.entry.get_mut().insert(chunk)
678    }
679
680    pub fn into_mut(self) -> &'a mut LoadedChunk {
681        self.entry.into_mut()
682    }
683
684    pub fn key(&self) -> &ChunkPos {
685        self.entry.key()
686    }
687
688    pub fn remove(self) -> UnloadedChunk {
689        self.messages.send_local_infallible(
690            LocalMsg::ChangeChunkState {
691                pos: *self.entry.key(),
692            },
693            |b| b.push(ChunkLayer::UNLOAD),
694        );
695
696        self.entry.remove().remove()
697    }
698
699    pub fn remove_entry(mut self) -> (ChunkPos, UnloadedChunk) {
700        let pos = *self.entry.key();
701        let chunk = self.entry.get_mut().remove();
702
703        self.messages.send_local_infallible(
704            LocalMsg::ChangeChunkState {
705                pos: *self.entry.key(),
706            },
707            |b| b.push(ChunkLayer::UNLOAD),
708        );
709
710        (pos, chunk)
711    }
712}
713
714#[derive(Debug)]
715pub struct VacantChunkEntry<'a> {
716    height: u32,
717    messages: &'a mut ChunkLayerMessages,
718    entry: VacantEntry<'a, ChunkPos, LoadedChunk>,
719}
720
721impl<'a> VacantChunkEntry<'a> {
722    pub fn insert(self, chunk: UnloadedChunk) -> &'a mut LoadedChunk {
723        let mut loaded = LoadedChunk::new(self.height);
724        loaded.insert(chunk);
725
726        self.messages.send_local_infallible(
727            LocalMsg::ChangeChunkState {
728                pos: *self.entry.key(),
729            },
730            |b| b.push(ChunkLayer::LOAD),
731        );
732
733        self.entry.insert(loaded)
734    }
735
736    pub fn into_key(self) -> ChunkPos {
737        *self.entry.key()
738    }
739
740    pub fn key(&self) -> &ChunkPos {
741        self.entry.key()
742    }
743}
744
745pub(super) fn build(app: &mut App) {
746    app.add_systems(
747        PostUpdate,
748        (
749            update_chunk_layers_pre_client.in_set(UpdateLayersPreClientSet),
750            update_chunk_layers_post_client.in_set(UpdateLayersPostClientSet),
751        ),
752    );
753}
754
755fn update_chunk_layers_pre_client(mut layers: Query<&mut ChunkLayer>) {
756    for layer in &mut layers {
757        let layer = layer.into_inner();
758
759        for (&pos, chunk) in &mut layer.chunks {
760            chunk.update_pre_client(pos, &layer.info, &mut layer.messages);
761        }
762
763        layer.messages.ready();
764    }
765}
766
767fn update_chunk_layers_post_client(mut layers: Query<&mut ChunkLayer>) {
768    for mut layer in &mut layers {
769        layer.messages.unready();
770    }
771}