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    /// Clientbound: disconnect (play).
27    pub disconnect: i32,
28}
29
30impl PlayPacketIds {
31    /// Get the PLAY packet IDs for a given protocol version.
32    #[must_use]
33    pub const fn for_version(version: ProtocolVersion) -> Self {
34        match version {
35            // 1.21, 1.21.1 (protocol 767)
36            ProtocolVersion::V1_21 => Self {
37                unsigned_command: 0x04,
38                signed_command: 0x05,
39                start_configuration: 0x69,
40                acknowledge_configuration: 0x0C,
41                keep_alive: 0x26,
42                system_chat: 0x6C,
43                disconnect: 0x1D,
44            },
45            // 1.21.2, 1.21.3 (protocol 768) and 1.21.4 (protocol 769)
46            ProtocolVersion::V1_21_2 | ProtocolVersion::V1_21_4 => Self {
47                unsigned_command: 0x05,
48                signed_command: 0x06,
49                start_configuration: 0x70,
50                acknowledge_configuration: 0x0E,
51                keep_alive: 0x27,
52                system_chat: 0x73,
53                disconnect: 0x1D,
54            },
55            // 1.21.5 (protocol 770), 1.21.6 (771), 1.21.7-1.21.8 (772)
56            ProtocolVersion::V1_21_5 | ProtocolVersion::V1_21_6 | ProtocolVersion::V1_21_7 => {
57                Self {
58                    unsigned_command: 0x05,
59                    signed_command: 0x06,
60                    start_configuration: 0x6F,
61                    acknowledge_configuration: 0x0E,
62                    keep_alive: 0x26,
63                    system_chat: 0x72,
64                    disconnect: 0x1C,
65                }
66            }
67            // 1.21.9-1.21.10 (773), 1.21.11 (774)
68            ProtocolVersion::V1_21_9 | ProtocolVersion::V1_21_11 => Self {
69                unsigned_command: 0x06,
70                signed_command: 0x07,
71                start_configuration: 0x74,
72                acknowledge_configuration: 0x0F,
73                keep_alive: 0x2B,
74                system_chat: 0x77,
75                disconnect: 0x20,
76            },
77        }
78    }
79}
80
81/// CONFIG state packet IDs (stable across all 1.21+ versions).
82pub struct ConfigPacketIds;
83
84impl ConfigPacketIds {
85    /// Clientbound: finished configuration (tells client to enter PLAY).
86    /// Packet ID 0x03 for all 1.21+ versions.
87    pub const FINISHED_CONFIGURATION_CLIENTBOUND: i32 = 0x03;
88
89    /// Serverbound: finished configuration (client confirms PLAY switch).
90    /// Packet ID 0x03 for all 1.21+ versions.
91    pub const FINISHED_CONFIGURATION_SERVERBOUND: i32 = 0x03;
92
93    /// Clientbound: disconnect during configuration.
94    /// Packet ID 0x02 for all 1.21+ versions.
95    pub const DISCONNECT: i32 = 0x02;
96}
97
98/// Parse the command string from an unsigned player command packet.
99///
100/// The wire format is simply a `VarInt`-prefixed UTF-8 string (the command
101/// without the leading `/`). This is the first (and only meaningful) field.
102///
103/// # Errors
104///
105/// Returns `None` if the data is too short to contain a valid string.
106#[must_use]
107pub fn parse_command_from_unsigned(payload: &[u8]) -> Option<String> {
108    let mut cursor = payload;
109    types::read_string(&mut cursor).ok()
110}
111
112/// Parse the command string from a signed player command packet.
113///
114/// The command string is the first field, same as unsigned. The remaining
115/// fields (timestamp, salt, signatures, last-seen) are ignored.
116///
117/// # Errors
118///
119/// Returns `None` if the data is too short to contain a valid string.
120#[must_use]
121pub fn parse_command_from_signed(payload: &[u8]) -> Option<String> {
122    // Same wire position as unsigned — command string is the first field
123    parse_command_from_unsigned(payload)
124}
125
126#[cfg(test)]
127mod tests {
128    use super::*;
129
130    #[test]
131    fn test_packet_ids_differ_per_version() {
132        let v121 = PlayPacketIds::for_version(ProtocolVersion::V1_21);
133        let v12111 = PlayPacketIds::for_version(ProtocolVersion::V1_21_11);
134
135        // IDs should differ between 1.21 and 1.21.11
136        assert_ne!(v121.start_configuration, v12111.start_configuration);
137        assert_ne!(v121.system_chat, v12111.system_chat);
138        assert_ne!(v121.disconnect, v12111.disconnect);
139    }
140
141    #[test]
142    fn test_disconnect_ids_per_version() {
143        assert_eq!(
144            PlayPacketIds::for_version(ProtocolVersion::V1_21).disconnect,
145            0x1D
146        );
147        assert_eq!(
148            PlayPacketIds::for_version(ProtocolVersion::V1_21_2).disconnect,
149            0x1D
150        );
151        assert_eq!(
152            PlayPacketIds::for_version(ProtocolVersion::V1_21_5).disconnect,
153            0x1C
154        );
155        assert_eq!(
156            PlayPacketIds::for_version(ProtocolVersion::V1_21_9).disconnect,
157            0x20
158        );
159    }
160
161    #[test]
162    fn test_1_21_6_inherits_from_1_21_5() {
163        let v1215 = PlayPacketIds::for_version(ProtocolVersion::V1_21_5);
164        let v1216 = PlayPacketIds::for_version(ProtocolVersion::V1_21_6);
165
166        assert_eq!(v1215.start_configuration, v1216.start_configuration);
167        assert_eq!(v1215.keep_alive, v1216.keep_alive);
168        assert_eq!(v1215.system_chat, v1216.system_chat);
169    }
170
171    #[test]
172    fn test_1_21_11_inherits_from_1_21_9() {
173        let v1219 = PlayPacketIds::for_version(ProtocolVersion::V1_21_9);
174        let v12111 = PlayPacketIds::for_version(ProtocolVersion::V1_21_11);
175
176        assert_eq!(v1219.unsigned_command, v12111.unsigned_command);
177        assert_eq!(v1219.start_configuration, v12111.start_configuration);
178        assert_eq!(v1219.keep_alive, v12111.keep_alive);
179        assert_eq!(v1219.system_chat, v12111.system_chat);
180    }
181
182    #[test]
183    fn test_parse_command_from_unsigned() {
184        let mut buf = Vec::new();
185        crate::types::write_string(&mut buf, "server lobby");
186        let cmd = parse_command_from_unsigned(&buf).unwrap();
187        assert_eq!(cmd, "server lobby");
188    }
189
190    #[test]
191    fn test_parse_command_from_signed() {
192        let mut buf = Vec::new();
193        crate::types::write_string(&mut buf, "server survival");
194        buf.extend_from_slice(&[0u8; 32]);
195        let cmd = parse_command_from_signed(&buf).unwrap();
196        assert_eq!(cmd, "server survival");
197    }
198}