Skip to main content

steel_registry/data_components/components/
use_cooldown.rs

1//! Vanilla `minecraft:use_cooldown` item component.
2
3use std::io::{Cursor, Result, Write};
4use std::str::FromStr;
5
6use simdnbt::owned::{NbtCompound, NbtTag};
7use simdnbt::{FromNbtTag, ToNbtTag};
8use steel_utils::hash::{ComponentHasher, HashComponent, HashEntry, sort_map_entries};
9use steel_utils::serial::{ReadFrom, WriteTo};
10use steel_utils::{Identifier, nbt::NbtNumeric as _};
11
12/// Vanilla `UseCooldown`: seconds plus an optional shared cooldown group.
13#[derive(Debug, Clone, PartialEq)]
14pub struct UseCooldown {
15    pub seconds: f32,
16    pub cooldown_group: Option<Identifier>,
17}
18
19impl UseCooldown {
20    #[must_use]
21    pub const fn new(seconds: f32, cooldown_group: Option<Identifier>) -> Self {
22        Self {
23            seconds,
24            cooldown_group,
25        }
26    }
27
28    #[must_use]
29    pub fn ticks(&self) -> i32 {
30        (self.seconds * 20.0) as i32
31    }
32}
33
34impl WriteTo for UseCooldown {
35    fn write(&self, writer: &mut impl Write) -> Result<()> {
36        self.seconds.write(writer)?;
37        self.cooldown_group.write(writer)
38    }
39}
40
41impl ReadFrom for UseCooldown {
42    fn read(data: &mut Cursor<&[u8]>) -> Result<Self> {
43        Ok(Self {
44            seconds: f32::read(data)?,
45            cooldown_group: Option::<Identifier>::read(data)?,
46        })
47    }
48}
49
50impl ToNbtTag for UseCooldown {
51    fn to_nbt_tag(self) -> NbtTag {
52        let mut compound = NbtCompound::new();
53        compound.insert("seconds", self.seconds);
54        if let Some(group) = self.cooldown_group {
55            compound.insert("cooldown_group", group.to_string());
56        }
57        NbtTag::Compound(compound)
58    }
59}
60
61impl FromNbtTag for UseCooldown {
62    fn from_nbt_tag(tag: simdnbt::borrow::NbtTag) -> Option<Self> {
63        let compound = tag.compound()?;
64        let seconds = compound.get("seconds")?.codec_f32()?;
65        if !seconds.is_finite() || seconds <= 0.0 {
66            return None;
67        }
68        let cooldown_group = match compound.get("cooldown_group") {
69            Some(tag) => Some(Identifier::from_str(&tag.string()?.to_str()).ok()?),
70            None => None,
71        };
72        Some(Self {
73            seconds,
74            cooldown_group,
75        })
76    }
77}
78
79impl HashComponent for UseCooldown {
80    fn hash_component(&self, hasher: &mut ComponentHasher) {
81        let mut entries = Vec::new();
82        push_hash_entry(&mut entries, "seconds", &self.seconds);
83        if let Some(group) = &self.cooldown_group {
84            push_hash_entry(&mut entries, "cooldown_group", &group.to_string());
85        }
86        sort_map_entries(&mut entries);
87        hasher.start_map();
88        for entry in entries {
89            hasher.put_raw_bytes(&entry.key_bytes);
90            hasher.put_raw_bytes(&entry.value_bytes);
91        }
92        hasher.end_map();
93    }
94}
95
96fn push_hash_entry<T: HashComponent + ?Sized>(entries: &mut Vec<HashEntry>, key: &str, value: &T) {
97    let mut key_hasher = ComponentHasher::new();
98    key_hasher.put_string(key);
99    let mut value_hasher = ComponentHasher::new();
100    value.hash_component(&mut value_hasher);
101    entries.push(HashEntry::new(key_hasher, value_hasher));
102}
103
104#[cfg(test)]
105mod tests {
106    use std::io::Cursor;
107
108    use simdnbt::FromNbtTag;
109    use simdnbt::borrow::{NbtTag as BorrowedNbtTag, read_tag};
110    use simdnbt::owned::{NbtCompound, NbtTag};
111    use steel_utils::Identifier;
112
113    use super::UseCooldown;
114
115    fn with_borrowed_tag<R>(tag: NbtTag, visitor: impl FnOnce(BorrowedNbtTag<'_, '_>) -> R) -> R {
116        let mut bytes = Vec::new();
117        tag.write(&mut bytes);
118        let borrowed =
119            read_tag(&mut Cursor::new(bytes.as_slice())).expect("owned test tag should parse");
120        visitor(borrowed.as_tag())
121    }
122
123    fn parse_use_cooldown(tag: NbtTag) -> Option<UseCooldown> {
124        with_borrowed_tag(tag, UseCooldown::from_nbt_tag)
125    }
126
127    #[test]
128    fn nbt_accepts_positive_seconds_and_optional_group() {
129        let mut compound = NbtCompound::new();
130        compound.insert("seconds", 5.5_f64);
131        compound.insert("cooldown_group", "minecraft:test_group");
132
133        let parsed = parse_use_cooldown(NbtTag::Compound(compound))
134            .expect("valid use_cooldown should parse");
135
136        assert_eq!(parsed.seconds, 5.5);
137        assert_eq!(
138            parsed.cooldown_group,
139            Some(Identifier::vanilla_static("test_group"))
140        );
141    }
142
143    #[test]
144    fn nbt_rejects_non_positive_seconds() {
145        for seconds in [0.0_f32, -1.0] {
146            let mut compound = NbtCompound::new();
147            compound.insert("seconds", seconds);
148
149            assert!(parse_use_cooldown(NbtTag::Compound(compound)).is_none());
150        }
151    }
152
153    #[test]
154    fn nbt_rejects_invalid_cooldown_group() {
155        let mut compound = NbtCompound::new();
156        compound.insert("seconds", 1.0_f32);
157        compound.insert("cooldown_group", "not valid");
158
159        assert!(parse_use_cooldown(NbtTag::Compound(compound)).is_none());
160    }
161}