deepslate-protocol 0.1.0

Minecraft protocol primitives for the Deepslate proxy.
Documentation
//! PLAY and CONFIG state packets needed for server switching and command interception.
//!
//! Most PLAY packets are relayed as opaque frames. Only the packets needed for
//! `/server` command interception and the CONFIG state transition are defined here.

use crate::types;
use crate::version::ProtocolVersion;

/// Version-specific PLAY packet IDs.
///
/// Minecraft remaps packet IDs between minor versions, so we need a lookup
/// per protocol version for the handful of packets the proxy inspects.
pub struct PlayPacketIds {
    /// Serverbound: unsigned player command (command string without `/`).
    pub unsigned_command: i32,
    /// Serverbound: signed player command (command string + crypto fields).
    pub signed_command: i32,
    /// Clientbound: start configuration (tells client to enter CONFIG).
    pub start_configuration: i32,
    /// Serverbound: acknowledge configuration (client confirms CONFIG switch).
    pub acknowledge_configuration: i32,
    /// Clientbound: keep alive.
    pub keep_alive: i32,
    /// Clientbound: system chat message.
    pub system_chat: i32,
}

impl PlayPacketIds {
    /// Get the PLAY packet IDs for a given protocol version.
    #[must_use]
    pub const fn for_version(version: ProtocolVersion) -> Self {
        match version {
            // 1.21, 1.21.1 (protocol 767)
            ProtocolVersion::V1_21 => Self {
                unsigned_command: 0x04,
                signed_command: 0x05,
                start_configuration: 0x69,
                acknowledge_configuration: 0x0C,
                keep_alive: 0x26,
                system_chat: 0x6C,
            },
            // 1.21.2, 1.21.3 (protocol 768) and 1.21.4 (protocol 769)
            ProtocolVersion::V1_21_2 | ProtocolVersion::V1_21_4 => Self {
                unsigned_command: 0x05,
                signed_command: 0x06,
                start_configuration: 0x70,
                acknowledge_configuration: 0x0E,
                keep_alive: 0x27,
                system_chat: 0x73,
            },
            // 1.21.5 (protocol 770), 1.21.6 (771), 1.21.7-1.21.8 (772)
            ProtocolVersion::V1_21_5 | ProtocolVersion::V1_21_6 | ProtocolVersion::V1_21_7 => {
                Self {
                    unsigned_command: 0x05,
                    signed_command: 0x06,
                    start_configuration: 0x6F,
                    acknowledge_configuration: 0x0E,
                    keep_alive: 0x26,
                    system_chat: 0x72,
                }
            }
            // 1.21.9-1.21.10 (773), 1.21.11 (774)
            ProtocolVersion::V1_21_9 | ProtocolVersion::V1_21_11 => Self {
                unsigned_command: 0x06,
                signed_command: 0x07,
                start_configuration: 0x74,
                acknowledge_configuration: 0x0F,
                keep_alive: 0x2B,
                system_chat: 0x77,
            },
        }
    }
}

/// CONFIG state packet IDs (stable across all 1.21+ versions).
pub struct ConfigPacketIds;

impl ConfigPacketIds {
    /// Clientbound: finished configuration (tells client to enter PLAY).
    /// Packet ID 0x03 for all 1.21+ versions.
    pub const FINISHED_CONFIGURATION_CLIENTBOUND: i32 = 0x03;

    /// Serverbound: finished configuration (client confirms PLAY switch).
    /// Packet ID 0x03 for all 1.21+ versions.
    pub const FINISHED_CONFIGURATION_SERVERBOUND: i32 = 0x03;

    /// Clientbound: disconnect during configuration.
    /// Packet ID 0x02 for all 1.21+ versions.
    pub const DISCONNECT: i32 = 0x02;
}

/// Parse the command string from an unsigned player command packet.
///
/// The wire format is simply a `VarInt`-prefixed UTF-8 string (the command
/// without the leading `/`). This is the first (and only meaningful) field.
///
/// # Errors
///
/// Returns `None` if the data is too short to contain a valid string.
#[must_use]
pub fn parse_command_from_unsigned(payload: &[u8]) -> Option<String> {
    let mut cursor = payload;
    types::read_string(&mut cursor).ok()
}

/// Parse the command string from a signed player command packet.
///
/// The command string is the first field, same as unsigned. The remaining
/// fields (timestamp, salt, signatures, last-seen) are ignored.
///
/// # Errors
///
/// Returns `None` if the data is too short to contain a valid string.
#[must_use]
pub fn parse_command_from_signed(payload: &[u8]) -> Option<String> {
    // Same wire position as unsigned — command string is the first field
    parse_command_from_unsigned(payload)
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_packet_ids_differ_per_version() {
        let v121 = PlayPacketIds::for_version(ProtocolVersion::V1_21);
        let v12111 = PlayPacketIds::for_version(ProtocolVersion::V1_21_11);

        // IDs should differ between 1.21 and 1.21.11
        assert_ne!(v121.start_configuration, v12111.start_configuration);
        assert_ne!(v121.system_chat, v12111.system_chat);
    }

    #[test]
    fn test_1_21_6_inherits_from_1_21_5() {
        let v1215 = PlayPacketIds::for_version(ProtocolVersion::V1_21_5);
        let v1216 = PlayPacketIds::for_version(ProtocolVersion::V1_21_6);

        assert_eq!(v1215.start_configuration, v1216.start_configuration);
        assert_eq!(v1215.keep_alive, v1216.keep_alive);
        assert_eq!(v1215.system_chat, v1216.system_chat);
    }

    #[test]
    fn test_1_21_11_inherits_from_1_21_9() {
        let v1219 = PlayPacketIds::for_version(ProtocolVersion::V1_21_9);
        let v12111 = PlayPacketIds::for_version(ProtocolVersion::V1_21_11);

        assert_eq!(v1219.unsigned_command, v12111.unsigned_command);
        assert_eq!(v1219.start_configuration, v12111.start_configuration);
        assert_eq!(v1219.keep_alive, v12111.keep_alive);
        assert_eq!(v1219.system_chat, v12111.system_chat);
    }

    #[test]
    fn test_parse_command_from_unsigned() {
        let mut buf = Vec::new();
        crate::types::write_string(&mut buf, "server lobby");
        let cmd = parse_command_from_unsigned(&buf).unwrap();
        assert_eq!(cmd, "server lobby");
    }

    #[test]
    fn test_parse_command_from_signed() {
        let mut buf = Vec::new();
        crate::types::write_string(&mut buf, "server survival");
        buf.extend_from_slice(&[0u8; 32]);
        let cmd = parse_command_from_signed(&buf).unwrap();
        assert_eq!(cmd, "server survival");
    }
}