Skip to main content

steel_protocol/packets/game/
s_container_click.rs

1use std::io::{Cursor, Error, ErrorKind, Result};
2
3use rustc_hash::FxHashMap;
4use steel_macros::ServerPacket;
5use steel_utils::{codec::VarInt, serial::ReadFrom};
6
7/// The type of click action performed on a container slot.
8#[derive(Debug, Clone, Copy, PartialEq, Eq)]
9#[repr(u8)]
10pub enum ClickType {
11    Pickup = 0,
12    QuickMove = 1,
13    Swap = 2,
14    Clone = 3,
15    Throw = 4,
16    QuickCraft = 5,
17    PickupAll = 6,
18}
19
20impl ReadFrom for ClickType {
21    fn read(data: &mut Cursor<&[u8]>) -> Result<Self> {
22        let id = VarInt::read(data)?.0;
23        Ok(match id {
24            0 => ClickType::Pickup,
25            1 => ClickType::QuickMove,
26            2 => ClickType::Swap,
27            3 => ClickType::Clone,
28            4 => ClickType::Throw,
29            5 => ClickType::QuickCraft,
30            6 => ClickType::PickupAll,
31            _ => ClickType::Pickup,
32        })
33    }
34}
35
36fn read_bounded_len(data: &mut Cursor<&[u8]>, max: usize, field: &str) -> Result<usize> {
37    let len = VarInt::read(data)?.0;
38    if len < 0 || len as usize > max {
39        return Err(Error::new(
40            ErrorKind::InvalidData,
41            format!("{field} length {len} exceeds max {max}"),
42        ));
43    }
44    Ok(len as usize)
45}
46
47/// A hashed representation of component patches for verification.
48/// Maps data component type IDs to their hash values.
49#[derive(Debug, Clone, Default)]
50pub struct HashedPatchMap {
51    pub added_components: FxHashMap<i32, i32>,
52    pub removed_components: Vec<i32>,
53}
54
55impl ReadFrom for HashedPatchMap {
56    fn read(data: &mut Cursor<&[u8]>) -> Result<Self> {
57        // Read added components map: Map<VarInt, Int>
58        let added_count = read_bounded_len(data, 256, "hashed patch added components")?;
59        let mut added_components = FxHashMap::default();
60        for _ in 0..added_count {
61            let type_id = VarInt::read(data)?.0;
62            let hash = i32::read(data)?;
63            added_components.insert(type_id, hash);
64        }
65
66        // Read removed components set: Collection<VarInt>
67        let removed_count = read_bounded_len(data, 256, "hashed patch removed components")?;
68        let mut removed_components = Vec::with_capacity(removed_count);
69        for _ in 0..removed_count {
70            let type_id = VarInt::read(data)?.0;
71            removed_components.push(type_id);
72        }
73
74        Ok(Self {
75            added_components,
76            removed_components,
77        })
78    }
79}
80
81/// A hashed representation of an `ItemStack` sent from client to server.
82/// Used for verification without trusting client data.
83#[derive(Debug, Clone)]
84pub enum HashedStack {
85    Empty,
86    Item {
87        item_id: i32,
88        count: i32,
89        components: HashedPatchMap,
90    },
91}
92
93impl ReadFrom for HashedStack {
94    fn read(data: &mut Cursor<&[u8]>) -> Result<Self> {
95        // Optional<ActualItem> - bool prefix
96        let present = bool::read(data)?;
97        if !present {
98            return Ok(HashedStack::Empty);
99        }
100
101        // ActualItem: Holder<Item> (VarInt), count (VarInt), HashedPatchMap
102        let item_id = VarInt::read(data)?.0;
103        let count = VarInt::read(data)?.0;
104        let components = HashedPatchMap::read(data)?;
105
106        Ok(HashedStack::Item {
107            item_id,
108            count,
109            components,
110        })
111    }
112}
113
114/// Serverbound packet sent when a player clicks in a container.
115#[derive(ServerPacket, Debug, Clone)]
116pub struct SContainerClick {
117    pub container_id: i32,
118    pub state_id: i32,
119    pub slot_num: i16,
120    pub button_num: i8,
121    pub click_type: ClickType,
122    pub changed_slots: FxHashMap<i16, HashedStack>,
123    pub carried_item: HashedStack,
124}
125
126impl ReadFrom for SContainerClick {
127    fn read(data: &mut Cursor<&[u8]>) -> Result<Self> {
128        let container_id = VarInt::read(data)?.0;
129        let state_id = VarInt::read(data)?.0;
130        let slot_num = i16::read(data)?;
131        let button_num = i8::read(data)?;
132        let click_type = ClickType::read(data)?;
133
134        // Read changed slots map with max 128 entries
135        let slot_count = read_bounded_len(data, 128, "changed slots")?;
136        let mut changed_slots = FxHashMap::default();
137        for _ in 0..slot_count {
138            let slot = i16::read(data)?;
139            let stack = HashedStack::read(data)?;
140            changed_slots.insert(slot, stack);
141        }
142
143        let carried_item = HashedStack::read(data)?;
144
145        Ok(Self {
146            container_id,
147            state_id,
148            slot_num,
149            button_num,
150            click_type,
151            changed_slots,
152            carried_item,
153        })
154    }
155}
156
157#[cfg(test)]
158mod tests {
159    use steel_utils::serial::WriteTo;
160
161    use super::*;
162
163    #[test]
164    fn unknown_click_type_falls_back_to_pickup() {
165        let mut data = Vec::new();
166        VarInt(7).write(&mut data).unwrap();
167
168        let click_type = ClickType::read(&mut Cursor::new(&data)).unwrap();
169        assert_eq!(click_type, ClickType::Pickup);
170    }
171
172    #[test]
173    fn rejects_changed_slot_count_above_vanilla_limit() {
174        let mut data = Vec::new();
175        VarInt(0).write(&mut data).unwrap(); // container id
176        VarInt(0).write(&mut data).unwrap(); // state id
177        0_i16.write(&mut data).unwrap(); // slot num
178        0_i8.write(&mut data).unwrap(); // button num
179        VarInt(0).write(&mut data).unwrap(); // pickup click type
180        VarInt(129).write(&mut data).unwrap(); // changed slots count
181
182        let err = SContainerClick::read(&mut Cursor::new(&data)).unwrap_err();
183        assert_eq!(err.kind(), ErrorKind::InvalidData);
184    }
185}