1use ghast_protocol::nbt::{RawNbt, Tag};
9use ghast_protocol::types::slot::{legacy, modern};
10
11#[derive(Debug, Clone, PartialEq, Eq, Hash)]
17pub struct Stack {
18 pub id: i32,
20 pub count: i32,
22 pub damage: i16,
24 pub nbt: Option<RawNbt>,
26}
27
28impl Stack {
29 #[must_use]
31 pub fn new(id: i32, count: i32, damage: i16) -> Stack {
32 Stack {
33 id,
34 count,
35 damage,
36 nbt: None,
37 }
38 }
39
40 #[must_use]
42 pub fn with_count(&self, count: i32) -> Stack {
43 Stack {
44 count,
45 ..self.clone()
46 }
47 }
48
49 #[must_use]
51 pub fn from_legacy(s: legacy::ItemStack) -> Stack {
52 Stack {
53 id: i32::from(s.id),
54 count: i32::from(s.count),
55 damage: s.damage,
56 nbt: s.nbt,
57 }
58 }
59
60 #[must_use]
62 pub fn from_modern(s: modern::ItemStack) -> Stack {
63 Stack {
64 id: s.id,
65 count: i32::from(s.count),
66 damage: 0,
67 nbt: s.nbt,
68 }
69 }
70
71 #[must_use]
73 pub fn to_legacy(&self) -> legacy::ItemStack {
74 legacy::ItemStack {
75 id: i16::try_from(self.id)
77 .ok()
78 .filter(|&i| i != -1)
79 .unwrap_or(0),
80 count: self.count as i8,
81 damage: self.damage,
82 nbt: self.nbt.clone(),
83 }
84 }
85
86 #[must_use]
88 pub fn to_modern(&self) -> modern::ItemStack {
89 modern::ItemStack {
90 id: self.id,
91 count: self.count as i8,
92 nbt: self.nbt.clone(),
93 }
94 }
95
96 #[must_use]
99 pub fn same_item(&self, other: &Stack) -> bool {
100 self.id == other.id && self.damage == other.damage && self.nbt_eq(other)
101 }
102
103 #[must_use]
105 pub fn nbt_eq(&self, other: &Stack) -> bool {
106 nbt_eq(self.nbt.as_ref(), other.nbt.as_ref())
107 }
108
109 #[must_use]
111 pub fn equals_exact(&self, other: &Stack) -> bool {
112 self.count == other.count && self.same_item(other)
113 }
114
115 pub(crate) fn nbt_bool(&self, key: &str) -> bool {
117 self.nbt_number(key).is_some_and(|v| v as i8 != 0)
118 }
119
120 pub(crate) fn nbt_int(&self, key: &str) -> i32 {
122 self.nbt_number(key).unwrap_or(0) as i32
123 }
124
125 fn nbt_number(&self, key: &str) -> Option<i64> {
126 let tree = self.nbt.as_ref()?.parse().ok()??;
127 let c = tree.tag.as_compound()?;
128 number(last(c, key)?)
129 }
130
131 pub(crate) fn has_binding_curse(&self) -> bool {
134 let Some(Ok(Some(tree))) = self.nbt.as_ref().map(RawNbt::parse) else {
135 return false;
136 };
137 let Some(Tag::List(list)) = tree.tag.as_compound().and_then(|c| last(c, "Enchantments"))
138 else {
139 return false;
140 };
141 for e in &list.items {
142 let Tag::Compound(e) = e else { return false };
143 let id = match last(e, "id") {
144 Some(Tag::String(s)) => s.as_str(),
145 _ => "",
146 };
147 if id == "minecraft:binding_curse" || id == "binding_curse" {
148 return last(e, "lvl").and_then(number).unwrap_or(0) as i32 > 0;
150 }
151 }
152 false
153 }
154}
155
156fn last<'a>(c: &'a ghast_protocol::nbt::Compound, key: &str) -> Option<&'a Tag> {
158 c.0.iter().rev().find(|(k, _)| k == key).map(|(_, v)| v)
159}
160
161fn number(t: &Tag) -> Option<i64> {
162 Some(match *t {
163 Tag::Byte(v) => i64::from(v),
164 Tag::Short(v) => i64::from(v),
165 Tag::Int(v) => i64::from(v),
166 Tag::Long(v) => v,
167 Tag::Float(v) => {
169 let i = v as i32; i64::from(if v < i as f32 { i - 1 } else { i })
171 }
172 Tag::Double(v) => {
173 let i = v as i32;
174 i64::from(if v < f64::from(i) { i - 1 } else { i })
175 }
176 _ => return None,
177 })
178}
179
180#[must_use]
183pub fn nbt_eq(a: Option<&RawNbt>, b: Option<&RawNbt>) -> bool {
184 match (a, b) {
185 (None, None) => true,
186 (Some(a), Some(b)) => {
187 if a.as_bytes() == b.as_bytes() {
188 return true;
189 }
190 match (a.parse(), b.parse()) {
191 (Ok(Some(x)), Ok(Some(y))) => tag_eq(&x.tag, &y.tag),
192 (Ok(None), Ok(None)) => true,
193 _ => false,
194 }
195 }
196 _ => false,
197 }
198}
199
200fn tag_eq(a: &Tag, b: &Tag) -> bool {
201 match (a, b) {
202 (Tag::Compound(x), Tag::Compound(y)) => {
203 let distinct = |c: &ghast_protocol::nbt::Compound| {
205 c.0.iter()
206 .enumerate()
207 .filter(|(i, (k, _))| !c.0[i + 1..].iter().any(|(k2, _)| k2 == k))
208 .count()
209 };
210 if distinct(x) != distinct(y) {
211 return false;
212 }
213 x.0.iter().enumerate().all(|(i, (k, v))| {
214 if x.0[i + 1..].iter().any(|(k2, _)| k2 == k) {
215 return true; }
217 last(y, k).is_some_and(|w| tag_eq(v, w))
218 })
219 }
220 (Tag::List(x), Tag::List(y)) => {
221 x.elem_type == y.elem_type
222 && x.items.len() == y.items.len()
223 && x.items.iter().zip(&y.items).all(|(v, w)| tag_eq(v, w))
224 }
225 (Tag::Float(x), Tag::Float(y)) => x == y,
227 (Tag::Double(x), Tag::Double(y)) => x == y,
228 _ => a == b,
229 }
230}
231
232#[must_use]
234pub fn stacks_equal(a: Option<&Stack>, b: Option<&Stack>) -> bool {
235 match (a, b) {
236 (None, None) => true,
237 (Some(a), Some(b)) => a.equals_exact(b),
238 _ => false,
239 }
240}
241
242#[must_use]
244pub fn from_legacy_slot(s: legacy::Slot) -> Option<Stack> {
245 s.0.map(Stack::from_legacy)
246}
247
248#[must_use]
250pub fn from_modern_slot(s: modern::Slot) -> Option<Stack> {
251 s.0.map(Stack::from_modern)
252 .filter(|s| s.id != 0 && s.count > 0)
253}
254
255#[must_use]
257pub fn to_legacy_slot(s: Option<&Stack>) -> legacy::Slot {
258 legacy::Slot(s.map(Stack::to_legacy))
259}
260
261#[must_use]
263pub fn to_modern_slot(s: Option<&Stack>) -> modern::Slot {
264 modern::Slot(s.map(Stack::to_modern))
265}
266
267#[cfg(test)]
268mod tests {
269 use super::*;
270 use bytes::Bytes;
271 use ghast_protocol::nbt::{Compound, NamedTag};
272
273 fn raw(entries: Vec<(&str, Tag)>, root: &str) -> RawNbt {
274 RawNbt::from_named(&NamedTag {
275 name: root.into(),
276 tag: Tag::Compound(Compound(
277 entries
278 .into_iter()
279 .map(|(k, v)| (k.to_string(), v))
280 .collect(),
281 )),
282 })
283 .unwrap()
284 }
285
286 #[test]
287 fn nbt_equality_is_tree_equality() {
288 let a = raw(vec![("a", Tag::Int(1)), ("b", Tag::Byte(2))], "");
289 let b = raw(vec![("b", Tag::Byte(2)), ("a", Tag::Int(1))], "");
290 let c = raw(vec![("a", Tag::Int(1)), ("b", Tag::Byte(3))], "");
291 let d = raw(vec![("a", Tag::Int(1)), ("b", Tag::Byte(2))], "root");
292 assert!(nbt_eq(Some(&a), Some(&a)));
293 assert!(nbt_eq(Some(&a), Some(&b)));
294 assert!(!nbt_eq(Some(&a), Some(&c)));
295 assert!(nbt_eq(Some(&a), Some(&d)));
296 assert!(!nbt_eq(Some(&a), None));
297 let empty = raw(vec![], "");
298 assert!(!nbt_eq(Some(&empty), None));
299 let dup = raw(
300 vec![("a", Tag::Int(5)), ("a", Tag::Int(1)), ("b", Tag::Byte(2))],
301 "",
302 );
303 assert!(nbt_eq(Some(&a), Some(&dup)));
304 let nan = raw(vec![("f", Tag::Float(f32::NAN)), ("g", Tag::Byte(0))], "");
306 let nan_reordered = raw(vec![("g", Tag::Byte(0)), ("f", Tag::Float(f32::NAN))], "");
307 assert!(!nbt_eq(Some(&nan), Some(&nan_reordered)));
308 }
309
310 #[test]
311 fn nbt_helpers() {
312 let mut s = Stack::new(1, 1, 0);
313 assert!(!s.nbt_bool("Unbreakable"));
314 s.nbt = Some(raw(
315 vec![("Unbreakable", Tag::Byte(1)), ("Damage", Tag::Int(7))],
316 "",
317 ));
318 assert!(s.nbt_bool("Unbreakable"));
319 assert_eq!(s.nbt_int("Damage"), 7);
320 s.nbt = Some(raw(vec![("Unbreakable", Tag::Int(256))], ""));
321 assert!(!s.nbt_bool("Unbreakable"), "(byte) 256 == 0");
322 let ench = |id: &str, lvl: i16| {
323 Tag::List(ghast_protocol::nbt::List {
324 elem_type: 10,
325 items: vec![Tag::Compound(Compound(vec![
326 ("id".into(), Tag::String(id.into())),
327 ("lvl".into(), Tag::Short(lvl)),
328 ]))],
329 })
330 };
331 s.nbt = Some(raw(
332 vec![("Enchantments", ench("minecraft:binding_curse", 1))],
333 "",
334 ));
335 assert!(s.has_binding_curse());
336 s.nbt = Some(raw(
337 vec![("Enchantments", ench("minecraft:binding_curse", 0))],
338 "",
339 ));
340 assert!(!s.has_binding_curse());
341 s.nbt = Some(raw(
342 vec![("StoredEnchantments", ench("minecraft:binding_curse", 1))],
343 "",
344 ));
345 assert!(!s.has_binding_curse());
346 s.nbt = RawNbt::from_bytes(Bytes::from_static(&[0x0a, 0, 0, 0])).ok();
348 assert!(!s.has_binding_curse());
349 }
350
351 #[test]
352 fn wire_round_trip() {
353 let s = Stack::new(276, 1, 5);
354 assert_eq!(Stack::from_legacy(s.to_legacy()), s);
355 let m = Stack::new(600, 64, 0);
356 assert_eq!(Stack::from_modern(m.to_modern()), m);
357 assert_eq!(
358 from_modern_slot(modern::Slot(Some(modern::ItemStack {
359 id: 1,
360 count: 0,
361 nbt: None
362 }))),
363 None
364 );
365 assert_eq!(
366 from_modern_slot(modern::Slot(Some(modern::ItemStack {
367 id: 0,
368 count: 5,
369 nbt: None
370 }))),
371 None
372 );
373 assert!(
374 from_legacy_slot(legacy::Slot(Some(legacy::ItemStack {
375 id: 1,
376 count: 0,
377 damage: 0,
378 nbt: None
379 })))
380 .is_some()
381 );
382 assert_eq!(Stack::new(1, 300, 0).to_legacy().count, 44);
383 }
384}