Skip to main content

deepslate_protocol/packet/
play.rs

1//! PLAY and CONFIG state packets needed for server switching and command interception.
2//!
3//! Most PLAY packets are relayed as opaque frames. Only the packets needed for
4//! `/server` command interception and the CONFIG state transition are defined here.
5
6use crate::types;
7use crate::version::ProtocolVersion;
8
9/// Version-specific PLAY packet IDs.
10///
11/// Minecraft remaps packet IDs between minor versions, so we need a lookup
12/// per protocol version for the handful of packets the proxy inspects.
13pub struct PlayPacketIds {
14    /// Serverbound: unsigned player command (command string without `/`).
15    pub unsigned_command: i32,
16    /// Serverbound: signed player command (command string + crypto fields).
17    pub signed_command: i32,
18    /// Clientbound: start configuration (tells client to enter CONFIG).
19    pub start_configuration: i32,
20    /// Serverbound: acknowledge configuration (client confirms CONFIG switch).
21    pub acknowledge_configuration: i32,
22    /// Clientbound: keep alive.
23    pub keep_alive: i32,
24    /// Clientbound: system chat message.
25    pub system_chat: i32,
26}
27
28impl PlayPacketIds {
29    /// Get the PLAY packet IDs for a given protocol version.
30    #[must_use]
31    pub const fn for_version(version: ProtocolVersion) -> Self {
32        match version {
33            // 1.21, 1.21.1 (protocol 767)
34            ProtocolVersion::V1_21 => Self {
35                unsigned_command: 0x04,
36                signed_command: 0x05,
37                start_configuration: 0x69,
38                acknowledge_configuration: 0x0C,
39                keep_alive: 0x26,
40                system_chat: 0x6C,
41            },
42            // 1.21.2, 1.21.3 (protocol 768) and 1.21.4 (protocol 769)
43            ProtocolVersion::V1_21_2 | ProtocolVersion::V1_21_4 => Self {
44                unsigned_command: 0x05,
45                signed_command: 0x06,
46                start_configuration: 0x70,
47                acknowledge_configuration: 0x0E,
48                keep_alive: 0x27,
49                system_chat: 0x73,
50            },
51            // 1.21.5 (protocol 770), 1.21.6 (771), 1.21.7-1.21.8 (772)
52            ProtocolVersion::V1_21_5 | ProtocolVersion::V1_21_6 | ProtocolVersion::V1_21_7 => {
53                Self {
54                    unsigned_command: 0x05,
55                    signed_command: 0x06,
56                    start_configuration: 0x6F,
57                    acknowledge_configuration: 0x0E,
58                    keep_alive: 0x26,
59                    system_chat: 0x72,
60                }
61            }
62            // 1.21.9-1.21.10 (773), 1.21.11 (774)
63            ProtocolVersion::V1_21_9 | ProtocolVersion::V1_21_11 => Self {
64                unsigned_command: 0x06,
65                signed_command: 0x07,
66                start_configuration: 0x74,
67                acknowledge_configuration: 0x0F,
68                keep_alive: 0x2B,
69                system_chat: 0x77,
70            },
71        }
72    }
73}
74
75/// CONFIG state packet IDs (stable across all 1.21+ versions).
76pub struct ConfigPacketIds;
77
78impl ConfigPacketIds {
79    /// Clientbound: finished configuration (tells client to enter PLAY).
80    /// Packet ID 0x03 for all 1.21+ versions.
81    pub const FINISHED_CONFIGURATION_CLIENTBOUND: i32 = 0x03;
82
83    /// Serverbound: finished configuration (client confirms PLAY switch).
84    /// Packet ID 0x03 for all 1.21+ versions.
85    pub const FINISHED_CONFIGURATION_SERVERBOUND: i32 = 0x03;
86
87    /// Clientbound: disconnect during configuration.
88    /// Packet ID 0x02 for all 1.21+ versions.
89    pub const DISCONNECT: i32 = 0x02;
90}
91
92/// Parse the command string from an unsigned player command packet.
93///
94/// The wire format is simply a `VarInt`-prefixed UTF-8 string (the command
95/// without the leading `/`). This is the first (and only meaningful) field.
96///
97/// # Errors
98///
99/// Returns `None` if the data is too short to contain a valid string.
100#[must_use]
101pub fn parse_command_from_unsigned(payload: &[u8]) -> Option<String> {
102    let mut cursor = payload;
103    types::read_string(&mut cursor).ok()
104}
105
106/// Parse the command string from a signed player command packet.
107///
108/// The command string is the first field, same as unsigned. The remaining
109/// fields (timestamp, salt, signatures, last-seen) are ignored.
110///
111/// # Errors
112///
113/// Returns `None` if the data is too short to contain a valid string.
114#[must_use]
115pub fn parse_command_from_signed(payload: &[u8]) -> Option<String> {
116    // Same wire position as unsigned — command string is the first field
117    parse_command_from_unsigned(payload)
118}
119
120#[cfg(test)]
121mod tests {
122    use super::*;
123
124    #[test]
125    fn test_packet_ids_differ_per_version() {
126        let v121 = PlayPacketIds::for_version(ProtocolVersion::V1_21);
127        let v12111 = PlayPacketIds::for_version(ProtocolVersion::V1_21_11);
128
129        // IDs should differ between 1.21 and 1.21.11
130        assert_ne!(v121.start_configuration, v12111.start_configuration);
131        assert_ne!(v121.system_chat, v12111.system_chat);
132    }
133
134    #[test]
135    fn test_1_21_6_inherits_from_1_21_5() {
136        let v1215 = PlayPacketIds::for_version(ProtocolVersion::V1_21_5);
137        let v1216 = PlayPacketIds::for_version(ProtocolVersion::V1_21_6);
138
139        assert_eq!(v1215.start_configuration, v1216.start_configuration);
140        assert_eq!(v1215.keep_alive, v1216.keep_alive);
141        assert_eq!(v1215.system_chat, v1216.system_chat);
142    }
143
144    #[test]
145    fn test_1_21_11_inherits_from_1_21_9() {
146        let v1219 = PlayPacketIds::for_version(ProtocolVersion::V1_21_9);
147        let v12111 = PlayPacketIds::for_version(ProtocolVersion::V1_21_11);
148
149        assert_eq!(v1219.unsigned_command, v12111.unsigned_command);
150        assert_eq!(v1219.start_configuration, v12111.start_configuration);
151        assert_eq!(v1219.keep_alive, v12111.keep_alive);
152        assert_eq!(v1219.system_chat, v12111.system_chat);
153    }
154
155    #[test]
156    fn test_parse_command_from_unsigned() {
157        let mut buf = Vec::new();
158        crate::types::write_string(&mut buf, "server lobby");
159        let cmd = parse_command_from_unsigned(&buf).unwrap();
160        assert_eq!(cmd, "server lobby");
161    }
162
163    #[test]
164    fn test_parse_command_from_signed() {
165        let mut buf = Vec::new();
166        crate::types::write_string(&mut buf, "server survival");
167        buf.extend_from_slice(&[0u8; 32]);
168        let cmd = parse_command_from_signed(&buf).unwrap();
169        assert_eq!(cmd, "server survival");
170    }
171}