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::{BiomePos, 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.into(),
115            LocalMsg::RadiusAtExcept { center, .. } => center.into(),
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<N: Into<Ident<String>>>(
130        dimension_type_name: N,
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<P: Into<ChunkPos>>(&self, pos: P) -> 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<P: Into<ChunkPos>>(&mut self, pos: P) -> Option<&mut LoadedChunk> {
181        self.chunks.get_mut(&pos.into())
182    }
183
184    /// Insert a chunk into the instance at the given position. The previous
185    /// chunk data is returned.
186    pub fn insert_chunk<P: Into<ChunkPos>>(
187        &mut self,
188        pos: P,
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<P: Into<ChunkPos>>(&mut self, pos: P) -> 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<P: Into<ChunkPos>>(&mut self, pos: P) -> 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<P: Into<BlockPos>>(&self, pos: P) -> Option<BlockRef> {
271        let pos = pos.into();
272
273        let y = pos
274            .y
275            .checked_sub(self.info.min_y)
276            .and_then(|y| y.try_into().ok())?;
277
278        if y >= self.info.height {
279            return None;
280        }
281
282        let chunk = self.chunk(pos)?;
283
284        let x = pos.x.rem_euclid(16) as u32;
285        let z = pos.z.rem_euclid(16) as u32;
286
287        Some(chunk.block(x, y, z))
288    }
289
290    pub fn set_block<P, B>(&mut self, pos: P, block: B) -> Option<Block>
291    where
292        P: Into<BlockPos>,
293        B: IntoBlock,
294    {
295        let pos = pos.into();
296
297        let y = pos
298            .y
299            .checked_sub(self.info.min_y)
300            .and_then(|y| y.try_into().ok())?;
301
302        if y >= self.info.height {
303            return None;
304        }
305
306        let chunk = self.chunk_mut(pos)?;
307
308        let x = pos.x.rem_euclid(16) as u32;
309        let z = pos.z.rem_euclid(16) as u32;
310
311        Some(chunk.set_block(x, y, z, block))
312    }
313
314    pub fn block_entity_mut<P: Into<BlockPos>>(&mut self, pos: P) -> Option<&mut Compound> {
315        let pos = pos.into();
316
317        let y = pos
318            .y
319            .checked_sub(self.info.min_y)
320            .and_then(|y| y.try_into().ok())?;
321
322        if y >= self.info.height {
323            return None;
324        }
325
326        let chunk = self.chunk_mut(pos)?;
327
328        let x = pos.x.rem_euclid(16) as u32;
329        let z = pos.z.rem_euclid(16) as u32;
330
331        chunk.block_entity_mut(x, y, z)
332    }
333
334    pub fn biome<P: Into<BiomePos>>(&self, pos: P) -> Option<BiomeId> {
335        let pos = pos.into();
336
337        let y = pos
338            .y
339            .checked_sub(self.info.min_y / 4)
340            .and_then(|y| y.try_into().ok())?;
341
342        if y >= self.info.height / 4 {
343            return None;
344        }
345
346        let chunk = self.chunk(pos)?;
347
348        let x = pos.x.rem_euclid(4) as u32;
349        let z = pos.z.rem_euclid(4) as u32;
350
351        Some(chunk.biome(x, y, z))
352    }
353
354    pub fn set_biome<P: Into<BiomePos>>(&mut self, pos: P, biome: BiomeId) -> Option<BiomeId> {
355        let pos = pos.into();
356
357        let y = pos
358            .y
359            .checked_sub(self.info.min_y / 4)
360            .and_then(|y| y.try_into().ok())?;
361
362        if y >= self.info.height / 4 {
363            return None;
364        }
365
366        let chunk = self.chunk_mut(pos)?;
367
368        let x = pos.x.rem_euclid(4) as u32;
369        let z = pos.z.rem_euclid(4) as u32;
370
371        Some(chunk.set_biome(x, y, z, biome))
372    }
373
374    pub(crate) fn info(&self) -> &ChunkLayerInfo {
375        &self.info
376    }
377
378    pub(crate) fn messages(&self) -> &ChunkLayerMessages {
379        &self.messages
380    }
381
382    // TODO: move to `valence_particle`.
383    /// Puts a particle effect at the given position in the world. The particle
384    /// effect is visible to all players in the instance with the
385    /// appropriate chunk in view.
386    pub fn play_particle<P, O>(
387        &mut self,
388        particle: &Particle,
389        long_distance: bool,
390        position: P,
391        offset: O,
392        max_speed: f32,
393        count: i32,
394    ) where
395        P: Into<DVec3>,
396        O: Into<Vec3>,
397    {
398        let position = position.into();
399
400        self.view_writer(position).write_packet(&ParticleS2c {
401            particle: Cow::Borrowed(particle),
402            long_distance,
403            position,
404            offset: offset.into(),
405            max_speed,
406            count,
407        });
408    }
409
410    // TODO: move to `valence_sound`.
411    /// Plays a sound effect at the given position in the world. The sound
412    /// effect is audible to all players in the instance with the
413    /// appropriate chunk in view.
414    pub fn play_sound<P: Into<DVec3>>(
415        &mut self,
416        sound: Sound,
417        category: SoundCategory,
418        position: P,
419        volume: f32,
420        pitch: f32,
421    ) {
422        let position = position.into();
423
424        self.view_writer(position).write_packet(&PlaySoundS2c {
425            id: SoundId::Direct {
426                id: sound.to_ident().into(),
427                range: None,
428            },
429            category,
430            position: (position * 8.0).as_ivec3(),
431            volume,
432            pitch,
433            seed: rand::random(),
434        });
435    }
436}
437
438impl Layer for ChunkLayer {
439    type ExceptWriter<'a> = ExceptWriter<'a>;
440
441    type ViewWriter<'a> = ViewWriter<'a>;
442
443    type ViewExceptWriter<'a> = ViewExceptWriter<'a>;
444
445    type RadiusWriter<'a> = RadiusWriter<'a>;
446
447    type RadiusExceptWriter<'a> = RadiusExceptWriter<'a>;
448
449    fn except_writer(&mut self, except: Entity) -> Self::ExceptWriter<'_> {
450        ExceptWriter {
451            layer: self,
452            except,
453        }
454    }
455
456    fn view_writer(&mut self, pos: impl Into<ChunkPos>) -> Self::ViewWriter<'_> {
457        ViewWriter {
458            layer: self,
459            pos: pos.into(),
460        }
461    }
462
463    fn view_except_writer(
464        &mut self,
465        pos: impl Into<ChunkPos>,
466        except: Entity,
467    ) -> Self::ViewExceptWriter<'_> {
468        ViewExceptWriter {
469            layer: self,
470            pos: pos.into(),
471            except,
472        }
473    }
474
475    fn radius_writer(
476        &mut self,
477        center: impl Into<BlockPos>,
478        radius: u32,
479    ) -> Self::RadiusWriter<'_> {
480        RadiusWriter {
481            layer: self,
482            center: center.into(),
483            radius,
484        }
485    }
486
487    fn radius_except_writer(
488        &mut self,
489        center: impl Into<BlockPos>,
490        radius: u32,
491        except: Entity,
492    ) -> Self::RadiusExceptWriter<'_> {
493        RadiusExceptWriter {
494            layer: self,
495            center: center.into(),
496            radius,
497            except,
498        }
499    }
500}
501
502impl WritePacket for ChunkLayer {
503    fn write_packet_fallible<P>(&mut self, packet: &P) -> anyhow::Result<()>
504    where
505        P: Packet + Encode,
506    {
507        self.messages.send_global(GlobalMsg::Packet, |b| {
508            PacketWriter::new(b, self.info.threshold).write_packet_fallible(packet)
509        })
510    }
511
512    fn write_packet_bytes(&mut self, bytes: &[u8]) {
513        self.messages
514            .send_global_infallible(GlobalMsg::Packet, |b| b.extend_from_slice(bytes));
515    }
516}
517
518pub struct ExceptWriter<'a> {
519    layer: &'a mut ChunkLayer,
520    except: Entity,
521}
522
523impl WritePacket for ExceptWriter<'_> {
524    fn write_packet_fallible<P>(&mut self, packet: &P) -> anyhow::Result<()>
525    where
526        P: Packet + Encode,
527    {
528        self.layer.messages.send_global(
529            GlobalMsg::PacketExcept {
530                except: self.except,
531            },
532            |b| PacketWriter::new(b, self.layer.info.threshold).write_packet_fallible(packet),
533        )
534    }
535
536    fn write_packet_bytes(&mut self, bytes: &[u8]) {
537        self.layer.messages.send_global_infallible(
538            GlobalMsg::PacketExcept {
539                except: self.except,
540            },
541            |b| b.extend_from_slice(bytes),
542        )
543    }
544}
545
546pub struct ViewWriter<'a> {
547    layer: &'a mut ChunkLayer,
548    pos: ChunkPos,
549}
550
551impl WritePacket for ViewWriter<'_> {
552    fn write_packet_fallible<P>(&mut self, packet: &P) -> anyhow::Result<()>
553    where
554        P: Packet + Encode,
555    {
556        self.layer
557            .messages
558            .send_local(LocalMsg::PacketAt { pos: self.pos }, |b| {
559                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
565            .messages
566            .send_local_infallible(LocalMsg::PacketAt { pos: self.pos }, |b| {
567                b.extend_from_slice(bytes)
568            });
569    }
570}
571
572pub struct ViewExceptWriter<'a> {
573    layer: &'a mut ChunkLayer,
574    pos: ChunkPos,
575    except: Entity,
576}
577
578impl WritePacket for ViewExceptWriter<'_> {
579    fn write_packet_fallible<P>(&mut self, packet: &P) -> anyhow::Result<()>
580    where
581        P: Packet + Encode,
582    {
583        self.layer.messages.send_local(
584            LocalMsg::PacketAtExcept {
585                pos: self.pos,
586                except: self.except,
587            },
588            |b| PacketWriter::new(b, self.layer.info.threshold).write_packet_fallible(packet),
589        )
590    }
591
592    fn write_packet_bytes(&mut self, bytes: &[u8]) {
593        self.layer.messages.send_local_infallible(
594            LocalMsg::PacketAtExcept {
595                pos: self.pos,
596                except: self.except,
597            },
598            |b| b.extend_from_slice(bytes),
599        );
600    }
601}
602
603pub struct RadiusWriter<'a> {
604    layer: &'a mut ChunkLayer,
605    center: BlockPos,
606    radius: u32,
607}
608
609impl WritePacket for RadiusWriter<'_> {
610    fn write_packet_fallible<P>(&mut self, packet: &P) -> anyhow::Result<()>
611    where
612        P: Packet + Encode,
613    {
614        self.layer.messages.send_local(
615            LocalMsg::RadiusAt {
616                center: self.center,
617                radius_squared: self.radius,
618            },
619            |b| PacketWriter::new(b, self.layer.info.threshold).write_packet_fallible(packet),
620        )
621    }
622
623    fn write_packet_bytes(&mut self, bytes: &[u8]) {
624        self.layer.messages.send_local_infallible(
625            LocalMsg::RadiusAt {
626                center: self.center,
627                radius_squared: self.radius,
628            },
629            |b| b.extend_from_slice(bytes),
630        );
631    }
632}
633
634pub struct RadiusExceptWriter<'a> {
635    layer: &'a mut ChunkLayer,
636    center: BlockPos,
637    radius: u32,
638    except: Entity,
639}
640
641impl WritePacket for RadiusExceptWriter<'_> {
642    fn write_packet_fallible<P>(&mut self, packet: &P) -> anyhow::Result<()>
643    where
644        P: Packet + Encode,
645    {
646        self.layer.messages.send_local(
647            LocalMsg::RadiusAtExcept {
648                center: self.center,
649                radius_squared: self.radius,
650                except: self.except,
651            },
652            |b| PacketWriter::new(b, self.layer.info.threshold).write_packet_fallible(packet),
653        )
654    }
655
656    fn write_packet_bytes(&mut self, bytes: &[u8]) {
657        self.layer.messages.send_local_infallible(
658            LocalMsg::RadiusAtExcept {
659                center: self.center,
660                radius_squared: self.radius,
661                except: self.except,
662            },
663            |b| b.extend_from_slice(bytes),
664        );
665    }
666}
667
668#[derive(Debug)]
669pub enum ChunkEntry<'a> {
670    Occupied(OccupiedChunkEntry<'a>),
671    Vacant(VacantChunkEntry<'a>),
672}
673
674impl<'a> ChunkEntry<'a> {
675    pub fn or_default(self) -> &'a mut LoadedChunk {
676        match self {
677            ChunkEntry::Occupied(oe) => oe.into_mut(),
678            ChunkEntry::Vacant(ve) => ve.insert(UnloadedChunk::new()),
679        }
680    }
681}
682
683#[derive(Debug)]
684pub struct OccupiedChunkEntry<'a> {
685    messages: &'a mut ChunkLayerMessages,
686    entry: OccupiedEntry<'a, ChunkPos, LoadedChunk>,
687}
688
689impl<'a> OccupiedChunkEntry<'a> {
690    pub fn get(&self) -> &LoadedChunk {
691        self.entry.get()
692    }
693
694    pub fn get_mut(&mut self) -> &mut LoadedChunk {
695        self.entry.get_mut()
696    }
697
698    pub fn insert(&mut self, chunk: UnloadedChunk) -> UnloadedChunk {
699        self.messages.send_local_infallible(
700            LocalMsg::ChangeChunkState {
701                pos: *self.entry.key(),
702            },
703            |b| b.push(ChunkLayer::OVERWRITE),
704        );
705
706        self.entry.get_mut().insert(chunk)
707    }
708
709    pub fn into_mut(self) -> &'a mut LoadedChunk {
710        self.entry.into_mut()
711    }
712
713    pub fn key(&self) -> &ChunkPos {
714        self.entry.key()
715    }
716
717    pub fn remove(self) -> UnloadedChunk {
718        self.messages.send_local_infallible(
719            LocalMsg::ChangeChunkState {
720                pos: *self.entry.key(),
721            },
722            |b| b.push(ChunkLayer::UNLOAD),
723        );
724
725        self.entry.remove().remove()
726    }
727
728    pub fn remove_entry(mut self) -> (ChunkPos, UnloadedChunk) {
729        let pos = *self.entry.key();
730        let chunk = self.entry.get_mut().remove();
731
732        self.messages.send_local_infallible(
733            LocalMsg::ChangeChunkState {
734                pos: *self.entry.key(),
735            },
736            |b| b.push(ChunkLayer::UNLOAD),
737        );
738
739        (pos, chunk)
740    }
741}
742
743#[derive(Debug)]
744pub struct VacantChunkEntry<'a> {
745    height: u32,
746    messages: &'a mut ChunkLayerMessages,
747    entry: VacantEntry<'a, ChunkPos, LoadedChunk>,
748}
749
750impl<'a> VacantChunkEntry<'a> {
751    pub fn insert(self, chunk: UnloadedChunk) -> &'a mut LoadedChunk {
752        let mut loaded = LoadedChunk::new(self.height);
753        loaded.insert(chunk);
754
755        self.messages.send_local_infallible(
756            LocalMsg::ChangeChunkState {
757                pos: *self.entry.key(),
758            },
759            |b| b.push(ChunkLayer::LOAD),
760        );
761
762        self.entry.insert(loaded)
763    }
764
765    pub fn into_key(self) -> ChunkPos {
766        *self.entry.key()
767    }
768
769    pub fn key(&self) -> &ChunkPos {
770        self.entry.key()
771    }
772}
773
774pub(super) fn build(app: &mut App) {
775    app.add_systems(
776        PostUpdate,
777        (
778            update_chunk_layers_pre_client.in_set(UpdateLayersPreClientSet),
779            update_chunk_layers_post_client.in_set(UpdateLayersPostClientSet),
780        ),
781    );
782}
783
784fn update_chunk_layers_pre_client(mut layers: Query<&mut ChunkLayer>) {
785    for layer in &mut layers {
786        let layer = layer.into_inner();
787
788        for (&pos, chunk) in &mut layer.chunks {
789            chunk.update_pre_client(pos, &layer.info, &mut layer.messages);
790        }
791
792        layer.messages.ready();
793    }
794}
795
796fn update_chunk_layers_post_client(mut layers: Query<&mut ChunkLayer>) {
797    for mut layer in &mut layers {
798        layer.messages.unready();
799    }
800}