synvael/crates/shared/src/protocol.rs

199 lines
9.2 KiB
Rust

//! 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<PackRef>,
/// 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<RequiredPack>,
/// 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<RequiredPack>,
/// 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<String>,
}
/// 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<String>,
/// 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;