steel_protocol/packets/game/
c_player_info_update.rs1use steel_macros::ClientPacket;
2use steel_registry::GameProfile;
3use steel_registry::packets::play::C_PLAYER_INFO_UPDATE;
4use steel_utils::codec::VarInt;
5use steel_utils::serial::PrefixedWrite;
6use text_components::TextComponent;
7use uuid::Uuid;
8
9use super::ProtocolRemoteChatSessionData;
11
12#[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
27pub 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#[derive(Debug, Clone)]
40pub enum PlayerDisplayName {
41 Reset,
43 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)]
58pub enum PlayerInfoTarget {
59 Uuid(Uuid),
61 Profile(GameProfile),
63}
64
65impl PlayerInfoTarget {
66 #[must_use]
68 pub const fn uuid(&self) -> Uuid {
69 match self {
70 Self::Uuid(uuid) => *uuid,
71 Self::Profile(profile) => profile.id(),
72 }
73 }
74}
75
76#[derive(Debug, Clone)]
77pub struct PlayerInfoEntry {
78 pub target: PlayerInfoTarget,
79 pub chat_session: Option<ProtocolRemoteChatSessionData>,
80 pub game_mode: Option<VarInt>,
81 pub listed: Option<bool>,
82 pub latency: Option<VarInt>,
83 pub display_name: Option<PlayerDisplayName>,
84 pub list_order: Option<VarInt>,
85 pub show_hat: Option<bool>,
86}
87
88impl PlayerInfoEntry {
89 #[must_use]
91 pub const fn new(uuid: Uuid) -> Self {
92 Self {
93 target: PlayerInfoTarget::Uuid(uuid),
94 chat_session: None,
95 game_mode: None,
96 listed: None,
97 latency: None,
98 display_name: None,
99 list_order: None,
100 show_hat: None,
101 }
102 }
103
104 #[must_use]
106 pub const fn with_latency(mut self, latency: i32) -> Self {
107 self.latency = Some(VarInt(latency));
108 self
109 }
110
111 #[must_use]
113 pub const fn with_game_mode(mut self, game_mode: i32) -> Self {
114 self.game_mode = Some(VarInt(game_mode));
115 self
116 }
117
118 #[must_use]
120 pub const fn with_listed(mut self, listed: bool) -> Self {
121 self.listed = Some(listed);
122 self
123 }
124
125 #[must_use]
127 pub fn with_display_name(mut self, display_name: impl Into<PlayerDisplayName>) -> Self {
128 self.display_name = Some(display_name.into());
129 self
130 }
131
132 #[must_use]
134 pub const fn with_show_hat(mut self, show_hat: bool) -> Self {
135 self.show_hat = Some(show_hat);
136 self
137 }
138
139 #[must_use]
141 pub const fn with_list_order(mut self, list_order: i32) -> Self {
142 self.list_order = Some(VarInt(list_order));
143 self
144 }
145
146 #[must_use]
148 pub fn with_chat_session(mut self, chat_session: ProtocolRemoteChatSessionData) -> Self {
149 self.chat_session = Some(chat_session);
150 self
151 }
152}
153
154#[derive(ClientPacket, Debug, Clone)]
155#[packet_id(Play = C_PLAYER_INFO_UPDATE)]
156pub struct CPlayerInfoUpdate {
157 pub actions: u8, pub entries: Vec<PlayerInfoEntry>,
159}
160
161impl CPlayerInfoUpdate {
162 #[must_use]
164 pub fn create_player_initializing_entry(
165 profile: GameProfile,
166 game_mode: i32,
167 latency: i32,
168 display_name: Option<TextComponent>,
169 show_hat: bool,
170 ) -> PlayerInfoEntry {
171 PlayerInfoEntry {
172 target: PlayerInfoTarget::Profile(profile),
173 chat_session: None,
174 game_mode: Some(VarInt(game_mode)),
175 listed: Some(true),
176 latency: Some(VarInt(latency)),
177 display_name: Some(display_name.into()),
178 list_order: Some(VarInt(0)),
179 show_hat: Some(show_hat),
180 }
181 }
182
183 #[must_use]
186 pub fn create_player_initializing(
187 profile: GameProfile,
188 game_mode: i32,
189 latency: i32,
190 display_name: Option<TextComponent>,
191 show_hat: bool,
192 ) -> Self {
193 Self {
194 actions: PLAYER_INFO_INIT_ACTIONS,
195 entries: vec![Self::create_player_initializing_entry(
196 profile,
197 game_mode,
198 latency,
199 display_name,
200 show_hat,
201 )],
202 }
203 }
204
205 #[must_use]
207 pub fn update_chat_session(uuid: Uuid, chat_session: ProtocolRemoteChatSessionData) -> Self {
208 Self {
209 actions: PlayerInfoAction::InitializeChat as u8,
210 entries: vec![PlayerInfoEntry::new(uuid).with_chat_session(chat_session)],
211 }
212 }
213
214 #[must_use]
217 pub fn update_latency(entries: Vec<(Uuid, i32)>) -> Self {
218 Self {
219 actions: PlayerInfoAction::UpdateLatency as u8,
220 entries: entries
221 .into_iter()
222 .map(|(uuid, latency)| PlayerInfoEntry::new(uuid).with_latency(latency))
223 .collect(),
224 }
225 }
226
227 #[must_use]
229 pub fn update_game_mode(uuid: Uuid, game_mode: i32) -> Self {
230 Self {
231 actions: PlayerInfoAction::UpdateGameMode as u8,
232 entries: vec![PlayerInfoEntry::new(uuid).with_game_mode(game_mode)],
233 }
234 }
235
236 #[must_use]
238 pub fn update_listed(uuid: Uuid, listed: bool) -> Self {
239 Self {
240 actions: PlayerInfoAction::UpdateListed as u8,
241 entries: vec![PlayerInfoEntry::new(uuid).with_listed(listed)],
242 }
243 }
244
245 #[must_use]
247 pub fn update_display_name(uuid: Uuid, display_name: Option<TextComponent>) -> Self {
248 Self {
249 actions: PlayerInfoAction::UpdateDisplayName as u8,
250 entries: vec![PlayerInfoEntry::new(uuid).with_display_name(display_name)],
251 }
252 }
253
254 #[must_use]
256 pub fn update_hat(uuid: Uuid, show_hat: bool) -> Self {
257 Self {
258 actions: PlayerInfoAction::UpdateHat as u8,
259 entries: vec![PlayerInfoEntry::new(uuid).with_show_hat(show_hat)],
260 }
261 }
262
263 #[must_use]
265 pub fn update_list_order(uuid: Uuid, list_order: i32) -> Self {
266 Self {
267 actions: PlayerInfoAction::UpdateListOrder as u8,
268 entries: vec![PlayerInfoEntry::new(uuid).with_list_order(list_order)],
269 }
270 }
271}
272
273impl steel_utils::serial::WriteTo for CPlayerInfoUpdate {
274 fn write(&self, writer: &mut impl std::io::Write) -> std::io::Result<()> {
275 self.actions.write(writer)?;
276 VarInt(self.entries.len() as i32).write(writer)?;
277
278 for entry in &self.entries {
279 entry.target.uuid().write(writer)?;
280
281 if self.actions & (PlayerInfoAction::AddPlayer as u8) != 0 {
282 let PlayerInfoTarget::Profile(profile) = &entry.target else {
283 return Err(std::io::Error::other(
284 "AddPlayer entry requires a game profile",
285 ));
286 };
287 profile.name().write_prefixed::<VarInt>(writer)?;
288 profile.write_properties(writer)?;
289 }
290
291 if self.actions & (PlayerInfoAction::InitializeChat as u8) != 0 {
292 if let Some(session_data) = &entry.chat_session {
294 true.write(writer)?;
295 session_data.write(writer)?;
296 } else {
297 false.write(writer)?;
298 }
299 }
300
301 if self.actions & (PlayerInfoAction::UpdateGameMode as u8) != 0 {
302 let game_mode = entry.game_mode.unwrap_or(VarInt(0));
303 game_mode.write(writer)?;
304 }
305
306 if self.actions & (PlayerInfoAction::UpdateListed as u8) != 0 {
307 let listed = entry.listed.unwrap_or(true);
308 listed.write(writer)?;
309 }
310
311 if self.actions & (PlayerInfoAction::UpdateLatency as u8) != 0 {
312 let latency = entry.latency.unwrap_or(VarInt(0));
313 latency.write(writer)?;
314 }
315
316 if self.actions & (PlayerInfoAction::UpdateDisplayName as u8) != 0 {
317 match &entry.display_name {
319 Some(PlayerDisplayName::Custom(display_name)) => {
320 true.write(writer)?;
321 display_name.write(writer)?;
322 }
323 Some(PlayerDisplayName::Reset) | None => {
324 false.write(writer)?;
325 }
326 }
327 }
328
329 if self.actions & (PlayerInfoAction::UpdateListOrder as u8) != 0 {
330 let list_order = entry.list_order.unwrap_or(VarInt(0));
331 list_order.write(writer)?;
332 }
333
334 if self.actions & (PlayerInfoAction::UpdateHat as u8) != 0 {
335 let show_hat = entry.show_hat.unwrap_or(true);
336 show_hat.write(writer)?;
337 }
338 }
339
340 Ok(())
341 }
342}
343
344#[cfg(test)]
345mod tests {
346 use std::io::Cursor;
347
348 use steel_utils::{
349 codec::VarInt,
350 serial::{PrefixedRead as _, ReadFrom as _, WriteTo as _},
351 };
352 use uuid::Uuid;
353
354 use steel_registry::{GameProfile, ProfileProperty};
355
356 use super::{CPlayerInfoUpdate, PLAYER_INFO_INIT_ACTIONS};
357
358 #[test]
359 fn initial_player_info_preserves_signed_texture_property() {
360 let uuid = Uuid::from_u128(1);
361 let profile = GameProfile::new(
362 uuid,
363 "CapePlayer".to_owned(),
364 vec![
365 ProfileProperty::new(
366 "textures".to_owned(),
367 "signed-texture-payload".to_owned(),
368 Some("texture-signature".to_owned()),
369 )
370 .expect("valid property"),
371 ],
372 )
373 .expect("valid profile");
374 let packet = CPlayerInfoUpdate::create_player_initializing(profile, 0, 0, None, true);
375 let mut bytes = Vec::new();
376 packet.write(&mut bytes).expect("player info should encode");
377
378 let mut cursor = Cursor::new(bytes.as_slice());
379 assert_eq!(
380 u8::read(&mut cursor).expect("actions should decode"),
381 PLAYER_INFO_INIT_ACTIONS
382 );
383 assert_eq!(
384 VarInt::read(&mut cursor)
385 .expect("entry count should decode")
386 .0,
387 1
388 );
389 assert_eq!(
390 Uuid::read(&mut cursor).expect("entry UUID should decode"),
391 uuid
392 );
393 assert_eq!(
394 String::read_prefixed::<VarInt>(&mut cursor).expect("profile name should decode"),
395 "CapePlayer"
396 );
397 assert_eq!(
398 VarInt::read(&mut cursor)
399 .expect("property count should decode")
400 .0,
401 1
402 );
403 assert_eq!(
404 String::read_prefixed::<VarInt>(&mut cursor).expect("property name should decode"),
405 "textures"
406 );
407 assert_eq!(
408 String::read_prefixed::<VarInt>(&mut cursor).expect("property value should decode"),
409 "signed-texture-payload"
410 );
411 assert!(bool::read(&mut cursor).expect("signature presence should decode"));
412 assert_eq!(
413 String::read_prefixed::<VarInt>(&mut cursor).expect("signature should decode"),
414 "texture-signature"
415 );
416 }
417}