Skip to main content

ghast_inventory/
stack.rs

1//! Item stacks: one version-independent type over the 1.8 (`slot::legacy`) and 1.16.5
2//! (`slot::modern`) wire formats.
3//!
4//! NBT stays raw ([`RawNbt`]): converting a `window_items` packet into stacks only moves the
5//! reference-counted bytes. Comparisons compare the raw bytes first and parse only when the bytes
6//! differ (vanilla compares NBT trees, which ignore compound key order and the root name).
7
8use ghast_protocol::nbt::{RawNbt, Tag};
9use ghast_protocol::types::slot::{legacy, modern};
10
11/// A non-empty item stack as the client holds it.
12///
13/// `count` is a Java `int` in vanilla (sent as a byte). 1.8 keeps stacks with a count of zero
14/// or less as real stacks (they render as a red "0"); 1.16.5 treats them as empty, so a 1.16.5
15/// [`Inventory`](crate::Inventory) never stores one.
16#[derive(Debug, Clone, PartialEq, Eq, Hash)]
17pub struct Stack {
18    /// Numeric item id (1.8: legacy id; 1.16.5: item registry id).
19    pub id: i32,
20    /// Stack size.
21    pub count: i32,
22    /// 1.8 damage / metadata. Always 0 on 1.16.5 (damage lives in the `Damage` NBT tag).
23    pub damage: i16,
24    /// Item NBT exactly as received, if any.
25    pub nbt: Option<RawNbt>,
26}
27
28impl Stack {
29    /// A stack without NBT.
30    #[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    /// The same item (id, damage, NBT) with another count.
41    #[must_use]
42    pub fn with_count(&self, count: i32) -> Stack {
43        Stack {
44            count,
45            ..self.clone()
46        }
47    }
48
49    /// Converts a 1.8 wire stack.
50    #[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    /// Converts a 1.16.5 wire stack.
61    #[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    /// The 1.8 wire form (count truncated to a byte like Java's `writeByte`).
72    #[must_use]
73    pub fn to_legacy(&self) -> legacy::ItemStack {
74        legacy::ItemStack {
75            // An id outside i16 cannot come from a 1.8 server; -1 would encode "empty".
76            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    /// The 1.16.5 wire form.
87    #[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    /// Same item, damage and NBT (vanilla's item + damage check and its NBT check); counts are
97    /// not compared.
98    #[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    /// Vanilla's tag comparison: both absent, or both present and equal as trees.
104    #[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    /// Exact equality as the server checks the clicked item (count, id, damage, NBT).
110    #[must_use]
111    pub fn equals_exact(&self, other: &Stack) -> bool {
112        self.count == other.count && self.same_item(other)
113    }
114
115    /// `getBoolean(key)` of the root compound: a numeric tag whose byte value is not zero.
116    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    /// `getInt(key)` of the root compound (0 when absent or not numeric).
121    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    /// 1.16.5 `EnchantmentHelper.hasBindingCurse`: an `Enchantments` entry with id
132    /// `minecraft:binding_curse` and `lvl` > 0.
133    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                // getLevel returns the first matching entry, clamped to 0..=255.
149                return last(e, "lvl").and_then(number).unwrap_or(0) as i32 > 0;
150            }
151        }
152        false
153    }
154}
155
156/// The value of the last entry named `key` (a Java `HashMap` keeps the last duplicate).
157fn 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        // NbtFloat/NbtDouble int and byte values are `MathHelper.floor(value)` (then `& 0xFF`).
168        Tag::Float(v) => {
169            let i = v as i32; // Java (int): saturating, NaN -> 0
170            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/// Vanilla tag equality on raw NBT: equal bytes are equal; otherwise both trees are parsed and
181/// compared (compounds as maps, root names ignored). Unparsable NBT only equals itself.
182#[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            // Map semantics: the last value of each key, same key set.
204            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; // a later duplicate decides
216                }
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        // Java `==` on float/double values: NaN != NaN, 0.0 == -0.0.
226        (Tag::Float(x), Tag::Float(y)) => x == y,
227        (Tag::Double(x), Tag::Double(y)) => x == y,
228        _ => a == b,
229    }
230}
231
232/// `Option<&Stack>` exact equality (the server's `ItemStack.equalsAll`).
233#[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/// Converts a 1.8 wire slot.
243#[must_use]
244pub fn from_legacy_slot(s: legacy::Slot) -> Option<Stack> {
245    s.0.map(Stack::from_legacy)
246}
247
248/// Converts a 1.16.5 wire slot; stacks that vanilla treats as empty (air, count <= 0) are `None`.
249#[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/// The 1.8 wire slot of an optional stack.
256#[must_use]
257pub fn to_legacy_slot(s: Option<&Stack>) -> legacy::Slot {
258    legacy::Slot(s.map(Stack::to_legacy))
259}
260
261/// The 1.16.5 wire slot of an optional stack.
262#[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        // Java float `==`: NaN is never equal (only reached when the bytes differ).
305        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        // Garbage NBT never panics.
347        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}