Skip to main content

steel_protocol/packets/game/chat/
player.rs

1use steel_macros::{ClientPacket, WriteTo};
2use steel_registry::packets::play::{C_DISGUISED_CHAT, C_PLAYER_CHAT};
3use steel_utils::{
4    codec::{BitSet, VarInt},
5    serial::PrefixedWrite,
6};
7use text_components::{TextComponent, resolving::TextResolutor};
8use uuid::Uuid;
9
10/// Represents Minecraft's ChatType.Bound structure
11/// Contains a registry holder + sender name + optional target name
12#[derive(Clone, Debug, WriteTo)]
13pub struct ChatTypeBound {
14    /// Registry holder ID - written as (id + 1) per Minecraft's holder format
15    #[write(as = RegistryHolder)]
16    pub registry_id: i32,
17    /// Sender name as NBT Component
18    pub sender_name: TextComponent,
19    /// Optional target name as NBT Component (bool-prefixed)
20    pub target_name: Option<TextComponent>,
21}
22
23#[derive(ClientPacket, Clone, Debug)]
24#[packet_id(Play = C_PLAYER_CHAT)]
25pub struct CPlayerChat {
26    pub global_index: i32,
27    pub sender: Uuid,
28    pub index: i32,
29    pub message_signature: Option<Box<[u8]>>,
30    pub message: String,
31    pub timestamp: i64,
32    pub salt: i64,
33    pub previous_messages: Box<[PreviousMessage]>,
34    pub unsigned_content: Option<TextComponent>,
35    pub filter_type: FilterType,
36    pub chat_type: ChatTypeBound,
37}
38
39impl CPlayerChat {
40    #[expect(clippy::too_many_arguments)]
41    #[must_use]
42    pub const fn new(
43        global_index: i32,
44        sender: Uuid,
45        index: i32,
46        message_signature: Option<Box<[u8]>>,
47        message: String,
48        timestamp: i64,
49        salt: i64,
50        previous_messages: Box<[PreviousMessage]>,
51        unsigned_content: Option<TextComponent>,
52        filter_type: FilterType,
53        chat_type: ChatTypeBound,
54    ) -> Self {
55        Self {
56            global_index,
57            sender,
58            index,
59            message_signature,
60            message,
61            timestamp,
62            salt,
63            previous_messages,
64            unsigned_content,
65            filter_type,
66            chat_type,
67        }
68    }
69}
70
71impl steel_utils::serial::WriteTo for CPlayerChat {
72    fn write(&self, writer: &mut impl std::io::Write) -> std::io::Result<()> {
73        VarInt(self.global_index).write(writer)?;
74        self.sender.write(writer)?;
75        VarInt(self.index).write(writer)?;
76
77        match &self.message_signature {
78            Some(sig) => {
79                true.write(writer)?;
80                writer.write_all(sig)?;
81            }
82            None => false.write(writer)?,
83        }
84
85        self.message.write_prefixed::<VarInt>(writer)?;
86        self.timestamp.write(writer)?;
87        self.salt.write(writer)?;
88
89        VarInt(self.previous_messages.len() as i32).write(writer)?;
90        for msg in &self.previous_messages {
91            // Write ID. In Minecraft's packed format:
92            // - If id is 0: write 0 (VarInt(0)), then write full signature (256 bytes)
93            // - If id is N > 0: write N (VarInt(N)), no signature bytes
94            // Our id field already contains the correct value (0 for full, cache_index+1 for referenced)
95            VarInt(msg.id).write(writer)?;
96            // Only write signature if id is 0 (full signature)
97            if msg.id == 0 {
98                if let Some(sig) = &msg.signature {
99                    writer.write_all(sig)?;
100                } else {
101                    // This should never happen - id=0 means full signature must be present
102                    return Err(std::io::Error::new(
103                        std::io::ErrorKind::InvalidData,
104                        "PreviousMessage with id=0 must have signature",
105                    ));
106                }
107            }
108        }
109
110        match &self.unsigned_content {
111            Some(content) => {
112                true.write(writer)?;
113                content.write(writer)?;
114            }
115            None => false.write(writer)?,
116        }
117
118        VarInt(match self.filter_type {
119            FilterType::PassThrough => 0,
120            FilterType::FullyFiltered => 1,
121            FilterType::PartiallyFiltered(_) => 2,
122        })
123        .write(writer)?;
124
125        self.chat_type.write(writer)?;
126        Ok(())
127    }
128}
129
130#[derive(Clone, Debug)]
131pub struct PreviousMessage {
132    pub id: i32,
133    pub signature: Option<Box<[u8]>>,
134}
135
136#[derive(Clone, Debug)]
137pub enum FilterType {
138    PassThrough,
139    FullyFiltered,
140    PartiallyFiltered(BitSet),
141}
142
143/// Clientbound packet for unsigned/disguised chat messages
144/// This is sent when the server doesn't have a signed message
145#[derive(ClientPacket, WriteTo, Clone, Debug)]
146#[packet_id(Play = C_DISGUISED_CHAT)]
147pub struct CDisguisedChat {
148    pub message: TextComponent,
149    pub chat_type: ChatTypeBound,
150}
151
152impl CDisguisedChat {
153    pub fn new<T: TextResolutor>(
154        message: &TextComponent,
155        chat_type: ChatTypeBound,
156        player: &T,
157    ) -> Self {
158        Self {
159            message: message.resolve(player),
160            chat_type,
161        }
162    }
163}