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