Skip to main content

steel_protocol/packets/game/
c_player_info_update.rs

1use steel_macros::ClientPacket;
2use steel_registry::packets::play::C_PLAYER_INFO_UPDATE;
3use steel_utils::codec::VarInt;
4use steel_utils::serial::PrefixedWrite;
5use text_components::TextComponent;
6use uuid::Uuid;
7
8// Import RemoteChatSessionData for chat session transmission
9use super::ProtocolRemoteChatSessionData;
10use crate::packets::login::GameProfileProperty;
11
12/// Actions for the player info update packet.
13/// These match the vanilla Java ClientboundPlayerInfoUpdatePacket.Action enum.
14#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15#[repr(u8)]
16pub enum PlayerInfoAction {
17    AddPlayer = 0x01,
18    InitializeChat = 0x02,
19    UpdateGameMode = 0x04,
20    UpdateListed = 0x08,
21    UpdateLatency = 0x10,
22    UpdateDisplayName = 0x20,
23    UpdateListOrder = 0x40,
24    UpdateHat = 0x80,
25}
26
27/// Bitmask combining all actions needed when a player first joins.
28/// This matches vanilla's `createPlayerInitializing()` method.
29pub const PLAYER_INFO_INIT_ACTIONS: u8 = PlayerInfoAction::AddPlayer as u8
30    | PlayerInfoAction::InitializeChat as u8
31    | PlayerInfoAction::UpdateGameMode as u8
32    | PlayerInfoAction::UpdateListed as u8
33    | PlayerInfoAction::UpdateLatency as u8
34    | PlayerInfoAction::UpdateDisplayName as u8
35    | PlayerInfoAction::UpdateListOrder as u8
36    | PlayerInfoAction::UpdateHat as u8;
37
38/// Represents the display name state for a player.
39#[derive(Debug, Clone)]
40pub enum PlayerDisplayName {
41    /// Use the player's default username (no custom display name).
42    Reset,
43    /// Use a custom display name.
44    Custom(Box<TextComponent>),
45}
46
47impl From<Option<TextComponent>> for PlayerDisplayName {
48    fn from(opt: Option<TextComponent>) -> Self {
49        match opt {
50            Some(component) => Self::Custom(Box::new(component)),
51            None => Self::Reset,
52        }
53    }
54}
55
56#[derive(Debug, Clone)]
57pub struct PlayerInfoEntry {
58    pub uuid: Uuid,
59    pub name: Option<String>,
60    pub properties: Vec<GameProfileProperty>,
61    pub chat_session: Option<ProtocolRemoteChatSessionData>,
62    pub game_mode: Option<VarInt>,
63    pub listed: Option<bool>,
64    pub latency: Option<VarInt>,
65    pub display_name: Option<PlayerDisplayName>,
66    pub list_order: Option<VarInt>,
67    pub show_hat: Option<bool>,
68}
69
70impl PlayerInfoEntry {
71    /// Creates a new entry with only the UUID filled in.
72    #[must_use]
73    pub const fn new(uuid: Uuid) -> Self {
74        Self {
75            uuid,
76            name: None,
77            properties: Vec::new(),
78            chat_session: None,
79            game_mode: None,
80            listed: None,
81            latency: None,
82            display_name: None,
83            list_order: None,
84            show_hat: None,
85        }
86    }
87
88    /// Sets the latency field.
89    #[must_use]
90    pub const fn with_latency(mut self, latency: i32) -> Self {
91        self.latency = Some(VarInt(latency));
92        self
93    }
94
95    /// Sets the game mode field.
96    #[must_use]
97    pub const fn with_game_mode(mut self, game_mode: i32) -> Self {
98        self.game_mode = Some(VarInt(game_mode));
99        self
100    }
101
102    /// Sets the listed field.
103    #[must_use]
104    pub const fn with_listed(mut self, listed: bool) -> Self {
105        self.listed = Some(listed);
106        self
107    }
108
109    /// Sets the display name field.
110    #[must_use]
111    pub fn with_display_name(mut self, display_name: impl Into<PlayerDisplayName>) -> Self {
112        self.display_name = Some(display_name.into());
113        self
114    }
115
116    /// Sets the show hat field.
117    #[must_use]
118    pub const fn with_show_hat(mut self, show_hat: bool) -> Self {
119        self.show_hat = Some(show_hat);
120        self
121    }
122
123    /// Sets the list order field (controls sort order in tab list).
124    #[must_use]
125    pub const fn with_list_order(mut self, list_order: i32) -> Self {
126        self.list_order = Some(VarInt(list_order));
127        self
128    }
129
130    /// Sets the chat session field.
131    #[must_use]
132    pub fn with_chat_session(mut self, chat_session: ProtocolRemoteChatSessionData) -> Self {
133        self.chat_session = Some(chat_session);
134        self
135    }
136}
137
138#[derive(ClientPacket, Debug, Clone)]
139#[packet_id(Play = C_PLAYER_INFO_UPDATE)]
140pub struct CPlayerInfoUpdate {
141    pub actions: u8, // Bitmask of PlayerInfoAction
142    pub entries: Vec<PlayerInfoEntry>,
143}
144
145impl CPlayerInfoUpdate {
146    /// Creates a full player initializing packet with all information.
147    /// This is sent when a player joins to add them to the tab list.
148    /// Matches vanilla's `ClientboundPlayerInfoUpdatePacket.createPlayerInitializing()`
149    #[must_use]
150    pub fn create_player_initializing(
151        uuid: Uuid,
152        name: String,
153        properties: Vec<GameProfileProperty>,
154        game_mode: i32,
155        latency: i32,
156        display_name: Option<TextComponent>,
157        show_hat: bool,
158    ) -> Self {
159        Self {
160            actions: PLAYER_INFO_INIT_ACTIONS,
161            entries: vec![PlayerInfoEntry {
162                uuid,
163                name: Some(name),
164                properties,
165                chat_session: None,
166                game_mode: Some(VarInt(game_mode)),
167                listed: Some(true),
168                latency: Some(VarInt(latency)),
169                display_name: Some(display_name.into()),
170                list_order: Some(VarInt(0)),
171                show_hat: Some(show_hat),
172            }],
173        }
174    }
175
176    /// Creates a packet to update a player's chat session.
177    #[must_use]
178    pub fn update_chat_session(uuid: Uuid, chat_session: ProtocolRemoteChatSessionData) -> Self {
179        Self {
180            actions: PlayerInfoAction::InitializeChat as u8,
181            entries: vec![PlayerInfoEntry::new(uuid).with_chat_session(chat_session)],
182        }
183    }
184
185    /// Creates a packet to update latency for multiple players.
186    /// This is sent periodically (every 600 ticks) to update ping display.
187    #[must_use]
188    pub fn update_latency(entries: Vec<(Uuid, i32)>) -> Self {
189        Self {
190            actions: PlayerInfoAction::UpdateLatency as u8,
191            entries: entries
192                .into_iter()
193                .map(|(uuid, latency)| PlayerInfoEntry::new(uuid).with_latency(latency))
194                .collect(),
195        }
196    }
197
198    /// Creates a packet to update a player's game mode.
199    #[must_use]
200    pub fn update_game_mode(uuid: Uuid, game_mode: i32) -> Self {
201        Self {
202            actions: PlayerInfoAction::UpdateGameMode as u8,
203            entries: vec![PlayerInfoEntry::new(uuid).with_game_mode(game_mode)],
204        }
205    }
206
207    /// Creates a packet to update a player's listed status (show/hide in tab list).
208    #[must_use]
209    pub fn update_listed(uuid: Uuid, listed: bool) -> Self {
210        Self {
211            actions: PlayerInfoAction::UpdateListed as u8,
212            entries: vec![PlayerInfoEntry::new(uuid).with_listed(listed)],
213        }
214    }
215
216    /// Creates a packet to update a player's display name.
217    #[must_use]
218    pub fn update_display_name(uuid: Uuid, display_name: Option<TextComponent>) -> Self {
219        Self {
220            actions: PlayerInfoAction::UpdateDisplayName as u8,
221            entries: vec![PlayerInfoEntry::new(uuid).with_display_name(display_name)],
222        }
223    }
224
225    /// Creates a packet to update a player's hat visibility.
226    #[must_use]
227    pub fn update_hat(uuid: Uuid, show_hat: bool) -> Self {
228        Self {
229            actions: PlayerInfoAction::UpdateHat as u8,
230            entries: vec![PlayerInfoEntry::new(uuid).with_show_hat(show_hat)],
231        }
232    }
233
234    /// Creates a packet to update a player's list order (sort position in tab list).
235    #[must_use]
236    pub fn update_list_order(uuid: Uuid, list_order: i32) -> Self {
237        Self {
238            actions: PlayerInfoAction::UpdateListOrder as u8,
239            entries: vec![PlayerInfoEntry::new(uuid).with_list_order(list_order)],
240        }
241    }
242}
243
244impl steel_utils::serial::WriteTo for CPlayerInfoUpdate {
245    fn write(&self, writer: &mut impl std::io::Write) -> std::io::Result<()> {
246        self.actions.write(writer)?;
247        VarInt(self.entries.len() as i32).write(writer)?;
248
249        for entry in &self.entries {
250            entry.uuid.write(writer)?;
251
252            if self.actions & (PlayerInfoAction::AddPlayer as u8) != 0
253                && let Some(name) = &entry.name
254            {
255                name.write_prefixed::<VarInt>(writer)?;
256                // Write properties (including skin textures)
257                VarInt(entry.properties.len() as i32).write(writer)?;
258                for prop in &entry.properties {
259                    prop.write(writer)?;
260                }
261            }
262
263            if self.actions & (PlayerInfoAction::InitializeChat as u8) != 0 {
264                // Write nullable chat session data
265                if let Some(session_data) = &entry.chat_session {
266                    true.write(writer)?;
267                    session_data.write(writer)?;
268                } else {
269                    false.write(writer)?;
270                }
271            }
272
273            if self.actions & (PlayerInfoAction::UpdateGameMode as u8) != 0 {
274                let game_mode = entry.game_mode.unwrap_or(VarInt(0));
275                game_mode.write(writer)?;
276            }
277
278            if self.actions & (PlayerInfoAction::UpdateListed as u8) != 0 {
279                let listed = entry.listed.unwrap_or(true);
280                listed.write(writer)?;
281            }
282
283            if self.actions & (PlayerInfoAction::UpdateLatency as u8) != 0 {
284                let latency = entry.latency.unwrap_or(VarInt(0));
285                latency.write(writer)?;
286            }
287
288            if self.actions & (PlayerInfoAction::UpdateDisplayName as u8) != 0 {
289                // Write as optional TextComponent (boolean + component if present)
290                match &entry.display_name {
291                    Some(PlayerDisplayName::Custom(display_name)) => {
292                        true.write(writer)?;
293                        display_name.write(writer)?;
294                    }
295                    Some(PlayerDisplayName::Reset) | None => {
296                        false.write(writer)?;
297                    }
298                }
299            }
300
301            if self.actions & (PlayerInfoAction::UpdateListOrder as u8) != 0 {
302                let list_order = entry.list_order.unwrap_or(VarInt(0));
303                list_order.write(writer)?;
304            }
305
306            if self.actions & (PlayerInfoAction::UpdateHat as u8) != 0 {
307                let show_hat = entry.show_hat.unwrap_or(true);
308                show_hat.write(writer)?;
309            }
310        }
311
312        Ok(())
313    }
314}