openrtc-netcode-core 1.0.0

Shared protocol types for OpenRTC netcode clients.
Documentation
use serde::{Deserialize, Serialize};
use serde_json::Value;

pub const NETCODE_PROTOCOL: &str = "openrtc-netcode";
pub const NETCODE_PROTOCOL_VERSION: u8 = 1;

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum EnvelopeKind {
    Hello,
    Peer,
    User,
    #[serde(rename = "state.snapshot")]
    StateSnapshot,
    #[serde(rename = "state.patch")]
    StatePatch,
    #[serde(rename = "lobby.meta")]
    LobbyMeta,
    #[serde(rename = "member.meta")]
    MemberMeta,
    Ping,
    Pong,
}

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct NetcodeEnvelope<T = Value> {
    pub protocol: String,
    pub v: u8,
    pub kind: EnvelopeKind,
    #[serde(rename = "lobbyId")]
    pub lobby_id: String,
    pub from: String,
    pub seq: u64,
    #[serde(rename = "sentAt")]
    pub sent_at: u64,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub target: Option<String>,
    pub body: T,
}

impl<T> NetcodeEnvelope<T> {
    pub fn new(
        kind: EnvelopeKind,
        lobby_id: impl Into<String>,
        from: impl Into<String>,
        seq: u64,
        sent_at: u64,
        body: T,
    ) -> Self {
        Self {
            protocol: NETCODE_PROTOCOL.to_string(),
            v: NETCODE_PROTOCOL_VERSION,
            kind,
            lobby_id: lobby_id.into(),
            from: from.into(),
            seq,
            sent_at,
            target: None,
            body,
        }
    }

    pub fn with_target(mut self, target: impl Into<String>) -> Self {
        self.target = Some(target.into());
        self
    }

    pub fn validate(&self) -> Result<(), NetcodeProtocolError> {
        if self.protocol != NETCODE_PROTOCOL {
            return Err(NetcodeProtocolError::ProtocolMismatch);
        }
        if self.v != NETCODE_PROTOCOL_VERSION {
            return Err(NetcodeProtocolError::VersionMismatch(self.v));
        }
        if self.lobby_id.is_empty() {
            return Err(NetcodeProtocolError::MissingLobbyId);
        }
        if self.from.is_empty() {
            return Err(NetcodeProtocolError::MissingSender);
        }
        Ok(())
    }
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum NetcodeProtocolError {
    ProtocolMismatch,
    VersionMismatch(u8),
    MissingLobbyId,
    MissingSender,
}

impl std::fmt::Display for NetcodeProtocolError {
    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::ProtocolMismatch => write!(formatter, "netcode protocol mismatch"),
            Self::VersionMismatch(version) => {
                write!(formatter, "unsupported netcode protocol version {version}")
            }
            Self::MissingLobbyId => write!(formatter, "netcode envelope missing lobby id"),
            Self::MissingSender => write!(formatter, "netcode envelope missing sender"),
        }
    }
}

impl std::error::Error for NetcodeProtocolError {}

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct HelloBody {
    #[serde(rename = "peerId")]
    pub peer_id: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    #[serde(rename = "displayName")]
    pub display_name: Option<String>,
}

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct UserMessageBody {
    #[serde(rename = "type")]
    pub message_type: String,
    pub payload: Value,
}

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct StateSnapshotBody {
    pub values: std::collections::BTreeMap<String, Value>,
    pub owners: std::collections::BTreeMap<String, String>,
}

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct StatePatchBody {
    pub key: String,
    pub value: Value,
    pub owner: String,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct MetadataBody {
    pub values: std::collections::BTreeMap<String, String>,
}

pub fn encode_envelope<T: Serialize>(envelope: &NetcodeEnvelope<T>) -> serde_json::Result<String> {
    serde_json::to_string(envelope)
}

pub fn decode_envelope(json: &str) -> serde_json::Result<NetcodeEnvelope<Value>> {
    serde_json::from_str(json)
}