use ghast_protocol::nbt::{RawNbt, Tag};
use ghast_protocol::types::slot::{legacy, modern};
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct Stack {
pub id: i32,
pub count: i32,
pub damage: i16,
pub nbt: Option<RawNbt>,
}
impl Stack {
#[must_use]
pub fn new(id: i32, count: i32, damage: i16) -> Stack {
Stack {
id,
count,
damage,
nbt: None,
}
}
#[must_use]
pub fn with_count(&self, count: i32) -> Stack {
Stack {
count,
..self.clone()
}
}
#[must_use]
pub fn from_legacy(s: legacy::ItemStack) -> Stack {
Stack {
id: i32::from(s.id),
count: i32::from(s.count),
damage: s.damage,
nbt: s.nbt,
}
}
#[must_use]
pub fn from_modern(s: modern::ItemStack) -> Stack {
Stack {
id: s.id,
count: i32::from(s.count),
damage: 0,
nbt: s.nbt,
}
}
#[must_use]
pub fn to_legacy(&self) -> legacy::ItemStack {
legacy::ItemStack {
id: i16::try_from(self.id)
.ok()
.filter(|&i| i != -1)
.unwrap_or(0),
count: self.count as i8,
damage: self.damage,
nbt: self.nbt.clone(),
}
}
#[must_use]
pub fn to_modern(&self) -> modern::ItemStack {
modern::ItemStack {
id: self.id,
count: self.count as i8,
nbt: self.nbt.clone(),
}
}
#[must_use]
pub fn same_item(&self, other: &Stack) -> bool {
self.id == other.id && self.damage == other.damage && self.nbt_eq(other)
}
#[must_use]
pub fn nbt_eq(&self, other: &Stack) -> bool {
nbt_eq(self.nbt.as_ref(), other.nbt.as_ref())
}
#[must_use]
pub fn equals_exact(&self, other: &Stack) -> bool {
self.count == other.count && self.same_item(other)
}
pub(crate) fn nbt_bool(&self, key: &str) -> bool {
self.nbt_number(key).is_some_and(|v| v as i8 != 0)
}
pub(crate) fn nbt_int(&self, key: &str) -> i32 {
self.nbt_number(key).unwrap_or(0) as i32
}
fn nbt_number(&self, key: &str) -> Option<i64> {
let tree = self.nbt.as_ref()?.parse().ok()??;
let c = tree.tag.as_compound()?;
number(last(c, key)?)
}
pub(crate) fn has_binding_curse(&self) -> bool {
let Some(Ok(Some(tree))) = self.nbt.as_ref().map(RawNbt::parse) else {
return false;
};
let Some(Tag::List(list)) = tree.tag.as_compound().and_then(|c| last(c, "Enchantments"))
else {
return false;
};
for e in &list.items {
let Tag::Compound(e) = e else { return false };
let id = match last(e, "id") {
Some(Tag::String(s)) => s.as_str(),
_ => "",
};
if id == "minecraft:binding_curse" || id == "binding_curse" {
return last(e, "lvl").and_then(number).unwrap_or(0) as i32 > 0;
}
}
false
}
}
fn last<'a>(c: &'a ghast_protocol::nbt::Compound, key: &str) -> Option<&'a Tag> {
c.0.iter().rev().find(|(k, _)| k == key).map(|(_, v)| v)
}
fn number(t: &Tag) -> Option<i64> {
Some(match *t {
Tag::Byte(v) => i64::from(v),
Tag::Short(v) => i64::from(v),
Tag::Int(v) => i64::from(v),
Tag::Long(v) => v,
Tag::Float(v) => {
let i = v as i32; i64::from(if v < i as f32 { i - 1 } else { i })
}
Tag::Double(v) => {
let i = v as i32;
i64::from(if v < f64::from(i) { i - 1 } else { i })
}
_ => return None,
})
}
#[must_use]
pub fn nbt_eq(a: Option<&RawNbt>, b: Option<&RawNbt>) -> bool {
match (a, b) {
(None, None) => true,
(Some(a), Some(b)) => {
if a.as_bytes() == b.as_bytes() {
return true;
}
match (a.parse(), b.parse()) {
(Ok(Some(x)), Ok(Some(y))) => tag_eq(&x.tag, &y.tag),
(Ok(None), Ok(None)) => true,
_ => false,
}
}
_ => false,
}
}
fn tag_eq(a: &Tag, b: &Tag) -> bool {
match (a, b) {
(Tag::Compound(x), Tag::Compound(y)) => {
let distinct = |c: &ghast_protocol::nbt::Compound| {
c.0.iter()
.enumerate()
.filter(|(i, (k, _))| !c.0[i + 1..].iter().any(|(k2, _)| k2 == k))
.count()
};
if distinct(x) != distinct(y) {
return false;
}
x.0.iter().enumerate().all(|(i, (k, v))| {
if x.0[i + 1..].iter().any(|(k2, _)| k2 == k) {
return true; }
last(y, k).is_some_and(|w| tag_eq(v, w))
})
}
(Tag::List(x), Tag::List(y)) => {
x.elem_type == y.elem_type
&& x.items.len() == y.items.len()
&& x.items.iter().zip(&y.items).all(|(v, w)| tag_eq(v, w))
}
(Tag::Float(x), Tag::Float(y)) => x == y,
(Tag::Double(x), Tag::Double(y)) => x == y,
_ => a == b,
}
}
#[must_use]
pub fn stacks_equal(a: Option<&Stack>, b: Option<&Stack>) -> bool {
match (a, b) {
(None, None) => true,
(Some(a), Some(b)) => a.equals_exact(b),
_ => false,
}
}
#[must_use]
pub fn from_legacy_slot(s: legacy::Slot) -> Option<Stack> {
s.0.map(Stack::from_legacy)
}
#[must_use]
pub fn from_modern_slot(s: modern::Slot) -> Option<Stack> {
s.0.map(Stack::from_modern)
.filter(|s| s.id != 0 && s.count > 0)
}
#[must_use]
pub fn to_legacy_slot(s: Option<&Stack>) -> legacy::Slot {
legacy::Slot(s.map(Stack::to_legacy))
}
#[must_use]
pub fn to_modern_slot(s: Option<&Stack>) -> modern::Slot {
modern::Slot(s.map(Stack::to_modern))
}
#[cfg(test)]
mod tests {
use super::*;
use bytes::Bytes;
use ghast_protocol::nbt::{Compound, NamedTag};
fn raw(entries: Vec<(&str, Tag)>, root: &str) -> RawNbt {
RawNbt::from_named(&NamedTag {
name: root.into(),
tag: Tag::Compound(Compound(
entries
.into_iter()
.map(|(k, v)| (k.to_string(), v))
.collect(),
)),
})
.unwrap()
}
#[test]
fn nbt_equality_is_tree_equality() {
let a = raw(vec![("a", Tag::Int(1)), ("b", Tag::Byte(2))], "");
let b = raw(vec![("b", Tag::Byte(2)), ("a", Tag::Int(1))], "");
let c = raw(vec![("a", Tag::Int(1)), ("b", Tag::Byte(3))], "");
let d = raw(vec![("a", Tag::Int(1)), ("b", Tag::Byte(2))], "root");
assert!(nbt_eq(Some(&a), Some(&a)));
assert!(nbt_eq(Some(&a), Some(&b)));
assert!(!nbt_eq(Some(&a), Some(&c)));
assert!(nbt_eq(Some(&a), Some(&d)));
assert!(!nbt_eq(Some(&a), None));
let empty = raw(vec![], "");
assert!(!nbt_eq(Some(&empty), None));
let dup = raw(
vec![("a", Tag::Int(5)), ("a", Tag::Int(1)), ("b", Tag::Byte(2))],
"",
);
assert!(nbt_eq(Some(&a), Some(&dup)));
let nan = raw(vec![("f", Tag::Float(f32::NAN)), ("g", Tag::Byte(0))], "");
let nan_reordered = raw(vec![("g", Tag::Byte(0)), ("f", Tag::Float(f32::NAN))], "");
assert!(!nbt_eq(Some(&nan), Some(&nan_reordered)));
}
#[test]
fn nbt_helpers() {
let mut s = Stack::new(1, 1, 0);
assert!(!s.nbt_bool("Unbreakable"));
s.nbt = Some(raw(
vec![("Unbreakable", Tag::Byte(1)), ("Damage", Tag::Int(7))],
"",
));
assert!(s.nbt_bool("Unbreakable"));
assert_eq!(s.nbt_int("Damage"), 7);
s.nbt = Some(raw(vec![("Unbreakable", Tag::Int(256))], ""));
assert!(!s.nbt_bool("Unbreakable"), "(byte) 256 == 0");
let ench = |id: &str, lvl: i16| {
Tag::List(ghast_protocol::nbt::List {
elem_type: 10,
items: vec![Tag::Compound(Compound(vec![
("id".into(), Tag::String(id.into())),
("lvl".into(), Tag::Short(lvl)),
]))],
})
};
s.nbt = Some(raw(
vec![("Enchantments", ench("minecraft:binding_curse", 1))],
"",
));
assert!(s.has_binding_curse());
s.nbt = Some(raw(
vec![("Enchantments", ench("minecraft:binding_curse", 0))],
"",
));
assert!(!s.has_binding_curse());
s.nbt = Some(raw(
vec![("StoredEnchantments", ench("minecraft:binding_curse", 1))],
"",
));
assert!(!s.has_binding_curse());
s.nbt = RawNbt::from_bytes(Bytes::from_static(&[0x0a, 0, 0, 0])).ok();
assert!(!s.has_binding_curse());
}
#[test]
fn wire_round_trip() {
let s = Stack::new(276, 1, 5);
assert_eq!(Stack::from_legacy(s.to_legacy()), s);
let m = Stack::new(600, 64, 0);
assert_eq!(Stack::from_modern(m.to_modern()), m);
assert_eq!(
from_modern_slot(modern::Slot(Some(modern::ItemStack {
id: 1,
count: 0,
nbt: None
}))),
None
);
assert_eq!(
from_modern_slot(modern::Slot(Some(modern::ItemStack {
id: 0,
count: 5,
nbt: None
}))),
None
);
assert!(
from_legacy_slot(legacy::Slot(Some(legacy::ItemStack {
id: 1,
count: 0,
damage: 0,
nbt: None
})))
.is_some()
);
assert_eq!(Stack::new(1, 300, 0).to_legacy().count, 44);
}
}