diff --git a/crates/shared/src/protocol.rs b/crates/shared/src/protocol.rs index 4b68e06..13a2945 100644 --- a/crates/shared/src/protocol.rs +++ b/crates/shared/src/protocol.rs @@ -2,8 +2,9 @@ //! Network protocol types and constants. //! -//! The module is split by stream purpose, starting with the [`control`]-stream handshake and disconnect messages. Control-stream types are re-exported here so callers continue to refer to `shared::protocol::` regardless of the internal layout. +//! The module is split by stream purpose: [`control`]-stream handshake and disconnect messages, and the [`chunk`]-sync request/delivery messages. Control-stream types are re-exported here so callers continue to refer to `shared::protocol::` regardless of the internal layout, while the chunk types stay namespaced under `shared::protocol::chunk` to keep the two protocols visually distinct. +pub mod chunk; mod control; pub use control::{ diff --git a/crates/shared/src/protocol/chunk.rs b/crates/shared/src/protocol/chunk.rs new file mode 100644 index 0000000..236443f --- /dev/null +++ b/crates/shared/src/protocol/chunk.rs @@ -0,0 +1,38 @@ +// SPDX-License-Identifier: AGPL-3.0-only + +//! Chunk-sync message types carried between server and client. + +use crate::world::{ChunkData, ChunkPos}; +use serde::{Deserialize, Serialize}; + +/// A client's request for the chunks it wants resident, expressed as a center and radius. +// TODO: per-chunk request/ack + flow control for the fuller protocol. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct ChunkSubscribe { + /// Chunk-space center the client wants chunks around (derived from its camera/player). + pub center: ChunkPos, + /// Load radius in chunks. The server clamps this to a server-side maximum. + pub radius: u16, +} + +/// One chunk delivered to the client. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub enum ChunkMessage { + /// Full chunk payload at a position. + // TODO: LOD tier selection so a chunk can be delivered at a coarser stream tier. + Chunk { + /// Position of the delivered chunk. + pos: ChunkPos, + /// Serializable chunk contents (reuses the save/diff representation). + data: ChunkData, + }, + /// The server has dropped this chunk from the client's set; the client should unload it. This is the server-authoritative counterpart to the client's own radius-based unload: the server can force a discard even when the chunk is still within the client's radius. + Drop { + /// Position the client should discard. + pos: ChunkPos, + }, +} + +#[cfg(test)] +#[path = "../tests/protocol_chunk.rs"] +mod tests; diff --git a/crates/shared/src/tests/protocol_chunk.rs b/crates/shared/src/tests/protocol_chunk.rs new file mode 100644 index 0000000..07b6de7 --- /dev/null +++ b/crates/shared/src/tests/protocol_chunk.rs @@ -0,0 +1,51 @@ +// SPDX-License-Identifier: AGPL-3.0-only + +use super::*; +use crate::world::BlockId; + +/// Round-trips a `ChunkSubscribe` through postcard and asserts the decoded value is identical. +fn roundtrip_subscribe(msg: &ChunkSubscribe) -> Result<(), postcard::Error> { + let bytes = postcard::to_stdvec(msg)?; + let decoded: ChunkSubscribe = postcard::from_bytes(&bytes)?; + assert_eq!(msg, &decoded); + Ok(()) +} + +/// Round-trips a `ChunkMessage` through postcard and asserts the decoded value is identical. +fn roundtrip_message(msg: &ChunkMessage) -> Result<(), postcard::Error> { + let bytes = postcard::to_stdvec(msg)?; + let decoded: ChunkMessage = postcard::from_bytes(&bytes)?; + assert_eq!(msg, &decoded); + Ok(()) +} + +#[test] +fn roundtrip_chunk_subscribe() -> Result<(), postcard::Error> { + // Negative coordinates exercise the signed ChunkPos fields under serialization. + let msg = ChunkSubscribe { + center: ChunkPos::new(-4, 0, 7), + radius: 12, + }; + roundtrip_subscribe(&msg) +} + +#[test] +fn roundtrip_chunk_message_chunk() -> Result<(), postcard::Error> { + // A non-empty ChunkData: private fields force construction via new + set. + let mut data = ChunkData::new(ChunkPos::new(1, 2, 3), 0); + data.set(0, BlockId(1)); + data.set(42, BlockId(1)); + let msg = ChunkMessage::Chunk { + pos: ChunkPos::new(1, 2, 3), + data, + }; + roundtrip_message(&msg) +} + +#[test] +fn roundtrip_chunk_message_drop() -> Result<(), postcard::Error> { + let msg = ChunkMessage::Drop { + pos: ChunkPos::new(0, -1, 0), + }; + roundtrip_message(&msg) +}