//! Network protocol types and constants. use serde::{Deserialize, Serialize}; /// Wire-protocol version. Incremented on any breaking change to the message layout below. pub const PROTOCOL_VERSION: u32 = 1; /// Messages carried on the control stream (stream 0): handshake and disconnect. #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] pub enum ControlMessage { /// First message a client sends after the QUIC/TLS handshake. ClientHello(ClientHello), /// Server acceptance carrying negotiated session parameters. HandshakeAck(HandshakeAck), /// Server refusal with a machine-readable reason. HandshakeReject(HandshakeReject), /// Orderly session teardown initiated by either side. Disconnect(Disconnect), } /// First message a client sends after the QUIC/TLS handshake. #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] pub struct ClientHello { /// Protocol version the client was built against; compared to `PROTOCOL_VERSION`. pub protocol_version: u32, /// Human-readable client build string (e.g. crate version + git hash). pub client_build: String, /// Identity the player presents. Minimal for M1. pub player_identity: PlayerIdentity, /// Content packs the client has installed. Empty in M1; validated later. pub installed_packs: Vec, /// Optional protocol feature bits the client requests. Zero in M1. pub requested_features: FeatureFlags, } /// Server acceptance carrying negotiated session parameters. #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] pub struct HandshakeAck { /// Server's protocol version (equal to the client's on success). pub protocol_version: u32, /// Human-readable server build string. pub server_build: String, /// Packs the world requires, each with an optional download source. May include `PackTier::Resource` entries (a server resource pack), which are delivered one-way and applied client-side rather than strict-matched; a consumer must branch on tier (or `PackTier::requires_strict_match`) before treating an entry as a match requirement. pub world_packs: Vec, /// Packs the client is missing relative to the server, each with an optional download source. As with `world_packs`, `PackTier::Resource` entries are delivered, not matched. pub missing_packs: Vec, /// Which stream carries which purpose for this session. pub stream_layout: StreamLayout, /// Advisory server tick rate in Hz, for client clock setup. pub tick_rate_hint: u16, } /// Server refusal with a machine-readable reason. #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] pub struct HandshakeReject { /// Machine-readable rejection category. pub reason: RejectReason, /// Human-readable detail for logs and UI. pub detail: String, /// Optional URL directing the user to a compatible build or pack, when the rejection is recoverable (e.g. `ProtocolMismatch`, `PackMismatch`). pub upgrade_url: Option, } /// Orderly session teardown initiated by either side. #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] pub struct Disconnect { /// Human-readable reason shown to the peer and logged. pub reason: String, } /// Machine-readable categories for handshake rejection. #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] pub enum RejectReason { /// Client protocol version does not match the server's. ProtocolMismatch, /// Client is missing required packs or has incompatible versions. PackMismatch, /// Client declined or failed to fetch a server resource pack the server marked required. ResourcePackDeclined, /// Client failed to authenticate. AuthFailed, /// Client is banned from the server. Banned, /// Server is full. Full, /// Server encountered an internal error during handshake. ServerError, } /// Identity presented by the player to the server. // TODO: use authenticated identity once the Account system exists. #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] pub struct PlayerIdentity { /// Human-readable display name. pub display_name: String, } /// Reference to a content pack (resource pack, data pack, or Lua mod) as it appears in a modlist exchanged during the handshake. #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] pub struct PackRef { /// Namespaced content identifier of the pack (`namespace:id`). Charset validation is deferred to the modlist-matching concept (out of M1 scope). pub id: String, /// Human-readable semantic version. Informational only; not the match key. pub version: String, /// Canonical hash of the pack contents; the authoritative match key. // TODO: pin the canonical hashing procedure (traversal order, newline normalization) so independent builds of one pack hash identically. pub content_hash: [u8; 32], /// Tier the pack was classified into, which governs whether a client/server mismatch on this pack is fatal or tolerated. Inferred by the owner from the pack's folder contents (see Load order), never self-declared. pub tier: PackTier, } /// Classification of a content pack, determining the handshake matching rule applied to it. Inferred from folder contents, not self-declared: `assets/`-only is a resource pack, `data/`-only is a data pack, presence of `scripts/` is a Lua mod. #[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] pub enum PackTier { /// Client-side asset overlay (`assets/` only). Never strict-matched between peers. A server may push one server resource pack of its own, delivered one-way and applied on top of the client's local pack stack; enforcement of a `required` server pack is apply-or-reject at the client, not a peer hash-match. Resource, /// Declarative content (`data/` only). Must match exactly between peers. Data, /// Lua mod (`scripts/`, optionally `data/` and `assets/`); full API access. Mod { /// Set when the mod ships no `data/` and every system is `scope = "client"`, so a client/server mismatch on it cannot desync authoritative state and is therefore tolerated. Not trusted blindly by the server for packs carrying data or server-scoped systems. client_only: bool, }, } impl PackTier { /// Returns whether a pack of this tier must match byte-for-byte between client and server for the connection to be accepted. Resource packs are never matched; data packs and non-`client_only` mods must match exactly. A `false` here does not imply the server never sends the pack, a server resource pack is delivered one-way despite not being part of bidirectional matching. #[must_use] pub fn requires_strict_match(self) -> bool { match self { PackTier::Resource => false, PackTier::Data => true, PackTier::Mod { client_only } => !client_only, } } } /// A pack the server's world requires, paired with an optional out-of-band download source. Sent server → client in the handshake; the client fetches any it lacks via the URL when present, otherwise over the QUIC asset stream. #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] pub struct RequiredPack { /// Identity and tier of the required pack. pub pack: PackRef, /// Optional HTTP(S) URL to fetch the pack from, bypassing the QUIC asset stream for large downloads. `None` means fetch over the asset stream. pub download_url: Option, /// Whether the connection is rejected if the client cannot obtain and apply this pack. For data/mod tiers this is always `true` (they are mandatory for a correct session). For a `PackTier::Resource` entry (a server resource pack) it distinguishes an *optional* overlay the client may decline and keep playing (`false`) from a *required* one whose decline or fetch failure rejects the connection (`true`). pub required: bool, } /// Optional protocol feature bits. #[repr(transparent)] #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)] pub struct FeatureFlags(pub u32); /// Mapping of logical purposes to QUIC stream IDs. #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] pub struct StreamLayout { /// Stream ID for control messages (handshake, disconnect). pub control: u8, /// Stream ID for client input to server. pub input: u8, /// Stream ID for server authoritative state updates. pub authority: u8, /// Stream ID for highest detail chunk updates (LOD0). pub chunk_lod0: u8, /// Stream ID for chunk updates (LOD1). pub chunk_lod1: u8, /// Stream ID for chunk updates (LOD2). pub chunk_lod2: u8, /// Stream ID for chunk updates (LOD3). pub chunk_lod3: u8, /// Stream ID for lowest detail chunk updates (LOD4). pub chunk_lod4: u8, /// Stream ID for downloading assets. pub asset: u8, /// Stream ID for downloading mod scripts. pub mod_data: u8, } impl Default for StreamLayout { fn default() -> Self { Self { control: 0, input: 1, authority: 2, chunk_lod0: 3, chunk_lod1: 4, chunk_lod2: 5, chunk_lod3: 6, chunk_lod4: 7, asset: 8, mod_data: 9, } } } #[cfg(test)] #[path = "tests/protocol.rs"] mod tests;