refactor(shared): split protocol into stub and control submodule
This commit is contained in:
parent
8cfee29b53
commit
ce79449a6c
|
|
@ -1,198 +1,11 @@
|
|||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
//! 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::<Type>` regardless of the internal layout.
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
mod control;
|
||||
|
||||
/// 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;
|
||||
pub use control::{
|
||||
ClientHello, ControlMessage, Disconnect, FeatureFlags, HandshakeAck, HandshakeReject, PROTOCOL_VERSION, PackRef, PackTier, PlayerIdentity, RejectReason, RequiredPack, StreamLayout,
|
||||
};
|
||||
|
|
|
|||
200
crates/shared/src/protocol/control.rs
Normal file
200
crates/shared/src/protocol/control.rs
Normal file
|
|
@ -0,0 +1,200 @@
|
|||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
//! Control-stream messages: handshake negotiation and orderly disconnect.
|
||||
|
||||
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;
|
||||
Loading…
Reference in a new issue