steel_protocol/packets/game/
s_container_click.rs1use std::io::{Cursor, Error, ErrorKind, Result};
2
3use rustc_hash::FxHashMap;
4use steel_macros::ServerPacket;
5use steel_utils::{codec::VarInt, serial::ReadFrom};
6
7#[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#[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 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 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#[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 let present = bool::read(data)?;
97 if !present {
98 return Ok(HashedStack::Empty);
99 }
100
101 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#[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 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(); VarInt(0).write(&mut data).unwrap(); 0_i16.write(&mut data).unwrap(); 0_i8.write(&mut data).unwrap(); VarInt(0).write(&mut data).unwrap(); VarInt(129).write(&mut data).unwrap(); let err = SContainerClick::read(&mut Cursor::new(&data)).unwrap_err();
183 assert_eq!(err.kind(), ErrorKind::InvalidData);
184 }
185}