Skip to main content

steel_registry/data_components/components/
tool.rs

1//! Tool component for mining speed and drop behavior.
2
3use std::io::{Cursor, Error, Result, Write};
4
5use simdnbt::owned::{NbtCompound, NbtList, NbtTag};
6use simdnbt::{FromNbtTag, ToNbtTag};
7use steel_utils::{
8    BlockStateId,
9    codec::VarInt,
10    hash::{ComponentHasher, HashComponent, HashEntry, sort_map_entries},
11    nbt::NbtNumeric as _,
12    serial::{ReadFrom, WriteTo},
13};
14
15use crate::blocks::Block;
16use crate::{REGISTRY, RegistryHolderSet};
17
18/// The block holder set used by a tool rule.
19pub type ToolRuleBlocks = RegistryHolderSet<Block>;
20
21/// A single rule within a tool component.
22#[derive(Debug, Clone, PartialEq)]
23pub struct ToolRule {
24    /// Blocks to which this rule applies.
25    pub blocks: ToolRuleBlocks,
26    /// Mining speed, or the tool's default speed when absent.
27    pub speed: Option<f32>,
28    /// Whether matching blocks drop items, when explicitly specified.
29    pub correct_for_drops: Option<bool>,
30}
31
32impl ToolRule {
33    /// Creates a rule that sets both mining speed and correct-tool behavior.
34    #[must_use]
35    pub const fn mines_and_drops(blocks: ToolRuleBlocks, speed: f32) -> Self {
36        Self {
37            blocks,
38            speed: Some(speed),
39            correct_for_drops: Some(true),
40        }
41    }
42
43    /// Creates a rule that explicitly denies drops.
44    #[must_use]
45    pub const fn denies_drops(blocks: ToolRuleBlocks) -> Self {
46        Self {
47            blocks,
48            speed: None,
49            correct_for_drops: Some(false),
50        }
51    }
52
53    /// Creates a rule that only overrides mining speed.
54    #[must_use]
55    pub const fn override_speed(blocks: ToolRuleBlocks, speed: f32) -> Self {
56        Self {
57            blocks,
58            speed: Some(speed),
59            correct_for_drops: None,
60        }
61    }
62
63    /// Returns whether this rule matches a block state.
64    #[must_use]
65    pub fn matches_block(&self, block_state_id: BlockStateId) -> bool {
66        REGISTRY
67            .blocks
68            .by_state_id(block_state_id)
69            .is_some_and(|block| self.blocks.contains(block))
70    }
71}
72
73/// Mining speed and drop behavior for an item.
74#[derive(Debug, Clone, PartialEq)]
75pub struct Tool {
76    /// Rules evaluated in order.
77    pub rules: Vec<ToolRule>,
78    /// Mining speed when no rule with a speed matches.
79    pub default_mining_speed: f32,
80    /// Item damage applied per block mined.
81    pub damage_per_block: i32,
82    /// Whether the tool may destroy blocks in creative mode.
83    pub can_destroy_blocks_in_creative: bool,
84}
85
86impl Default for Tool {
87    fn default() -> Self {
88        Self {
89            rules: Vec::new(),
90            default_mining_speed: 1.0,
91            damage_per_block: 1,
92            can_destroy_blocks_in_creative: true,
93        }
94    }
95}
96
97impl Tool {
98    /// Returns the first matching rule speed, or the default speed.
99    #[must_use]
100    pub fn get_mining_speed(&self, block_state_id: BlockStateId) -> f32 {
101        for rule in &self.rules {
102            if let Some(speed) = rule.speed
103                && rule.matches_block(block_state_id)
104            {
105                return speed;
106            }
107        }
108        self.default_mining_speed
109    }
110
111    /// Returns the first explicitly specified correct-tool result.
112    #[must_use]
113    pub fn is_correct_for_drops(&self, block_state_id: BlockStateId) -> bool {
114        for rule in &self.rules {
115            if let Some(correct) = rule.correct_for_drops
116                && rule.matches_block(block_state_id)
117            {
118                return correct;
119            }
120        }
121        false
122    }
123}
124
125impl WriteTo for ToolRule {
126    fn write(&self, writer: &mut impl Write) -> Result<()> {
127        self.blocks.write(writer)?;
128        self.speed.write(writer)?;
129        self.correct_for_drops.write(writer)
130    }
131}
132
133impl ReadFrom for ToolRule {
134    fn read(data: &mut Cursor<&[u8]>) -> Result<Self> {
135        Ok(Self {
136            blocks: ToolRuleBlocks::read(data)?,
137            speed: Option::<f32>::read(data)?,
138            correct_for_drops: Option::<bool>::read(data)?,
139        })
140    }
141}
142
143impl WriteTo for Tool {
144    fn write(&self, writer: &mut impl Write) -> Result<()> {
145        let count = i32::try_from(self.rules.len())
146            .map_err(|_| Error::other(format!("Tool rule list too large: {}", self.rules.len())))?;
147        VarInt(count).write(writer)?;
148        for rule in &self.rules {
149            rule.write(writer)?;
150        }
151        self.default_mining_speed.write(writer)?;
152        VarInt(self.damage_per_block).write(writer)?;
153        self.can_destroy_blocks_in_creative.write(writer)
154    }
155}
156
157impl ReadFrom for Tool {
158    fn read(data: &mut Cursor<&[u8]>) -> Result<Self> {
159        let count = VarInt::read(data)?.0;
160        let count = usize::try_from(count)
161            .map_err(|_| Error::other(format!("Negative tool rule count: {count}")))?;
162        let mut rules = Vec::with_capacity(count.min(65_536));
163        for _ in 0..count {
164            rules.push(ToolRule::read(data)?);
165        }
166        Ok(Self {
167            rules,
168            default_mining_speed: f32::read(data)?,
169            damage_per_block: VarInt::read(data)?.0,
170            can_destroy_blocks_in_creative: bool::read(data)?,
171        })
172    }
173}
174
175impl ToNbtTag for ToolRule {
176    fn to_nbt_tag(self) -> NbtTag {
177        NbtTag::Compound(self.into_nbt_compound())
178    }
179}
180
181impl ToolRule {
182    fn into_nbt_compound(self) -> NbtCompound {
183        let mut compound = NbtCompound::new();
184        compound.insert("blocks", self.blocks.to_nbt_tag());
185        if let Some(speed) = self.speed {
186            compound.insert("speed", speed);
187        }
188        if let Some(correct_for_drops) = self.correct_for_drops {
189            compound.insert("correct_for_drops", i8::from(correct_for_drops));
190        }
191        compound
192    }
193
194    fn from_nbt_compound(compound: simdnbt::borrow::NbtCompound<'_, '_>) -> Option<Self> {
195        let blocks = ToolRuleBlocks::from_nbt_tag(compound.get("blocks")?)?;
196        let speed = match compound.get("speed") {
197            Some(tag) => {
198                let speed = tag.codec_f32()?;
199                if !speed.is_finite() || speed <= 0.0 {
200                    return None;
201                }
202                Some(speed)
203            }
204            None => None,
205        };
206        let correct_for_drops = match compound.get("correct_for_drops") {
207            Some(tag) => Some(tag.codec_bool()?),
208            None => None,
209        };
210        Some(Self {
211            blocks,
212            speed,
213            correct_for_drops,
214        })
215    }
216}
217
218impl FromNbtTag for ToolRule {
219    fn from_nbt_tag(tag: simdnbt::borrow::NbtTag) -> Option<Self> {
220        Self::from_nbt_compound(tag.compound()?)
221    }
222}
223
224impl ToNbtTag for Tool {
225    fn to_nbt_tag(self) -> NbtTag {
226        let mut compound = NbtCompound::new();
227        compound.insert(
228            "rules",
229            NbtList::Compound(
230                self.rules
231                    .into_iter()
232                    .map(ToolRule::into_nbt_compound)
233                    .collect(),
234            ),
235        );
236        if self.default_mining_speed.to_bits() != 1.0_f32.to_bits() {
237            compound.insert("default_mining_speed", self.default_mining_speed);
238        }
239        if self.damage_per_block != 1 {
240            compound.insert("damage_per_block", self.damage_per_block);
241        }
242        if !self.can_destroy_blocks_in_creative {
243            compound.insert(
244                "can_destroy_blocks_in_creative",
245                i8::from(self.can_destroy_blocks_in_creative),
246            );
247        }
248        NbtTag::Compound(compound)
249    }
250}
251
252impl FromNbtTag for Tool {
253    fn from_nbt_tag(tag: simdnbt::borrow::NbtTag) -> Option<Self> {
254        let compound = tag.compound()?;
255        let rule_compounds = compound.get("rules")?.list()?.compounds()?;
256        let mut rules = Vec::with_capacity(rule_compounds.len());
257        for rule in rule_compounds {
258            rules.push(ToolRule::from_nbt_compound(rule)?);
259        }
260        let default_mining_speed = match compound.get("default_mining_speed") {
261            Some(tag) => tag.codec_f32()?,
262            None => 1.0,
263        };
264        let damage_per_block = match compound.get("damage_per_block") {
265            Some(tag) => tag.codec_i32()?,
266            None => 1,
267        };
268        if damage_per_block < 0 {
269            return None;
270        }
271        let can_destroy_blocks_in_creative = match compound.get("can_destroy_blocks_in_creative") {
272            Some(tag) => tag.codec_bool()?,
273            None => true,
274        };
275        Some(Self {
276            rules,
277            default_mining_speed,
278            damage_per_block,
279            can_destroy_blocks_in_creative,
280        })
281    }
282}
283
284impl HashComponent for ToolRule {
285    fn hash_component(&self, hasher: &mut ComponentHasher) {
286        let mut entries = Vec::new();
287        push_hash_entry(&mut entries, "blocks", &self.blocks);
288        if let Some(speed) = self.speed {
289            push_hash_entry(&mut entries, "speed", &speed);
290        }
291        if let Some(correct_for_drops) = self.correct_for_drops {
292            push_hash_entry(&mut entries, "correct_for_drops", &correct_for_drops);
293        }
294        hash_entries(hasher, &mut entries);
295    }
296}
297
298impl HashComponent for Tool {
299    fn hash_component(&self, hasher: &mut ComponentHasher) {
300        let mut entries = Vec::new();
301        push_hash_list_entry(&mut entries, "rules", &self.rules);
302        if self.default_mining_speed.to_bits() != 1.0_f32.to_bits() {
303            push_hash_entry(
304                &mut entries,
305                "default_mining_speed",
306                &self.default_mining_speed,
307            );
308        }
309        if self.damage_per_block != 1 {
310            push_hash_entry(&mut entries, "damage_per_block", &self.damage_per_block);
311        }
312        if !self.can_destroy_blocks_in_creative {
313            push_hash_entry(
314                &mut entries,
315                "can_destroy_blocks_in_creative",
316                &self.can_destroy_blocks_in_creative,
317            );
318        }
319        hash_entries(hasher, &mut entries);
320    }
321}
322
323fn hash_entries(hasher: &mut ComponentHasher, entries: &mut [HashEntry]) {
324    sort_map_entries(entries);
325    hasher.start_map();
326    for entry in entries {
327        hasher.put_raw_bytes(&entry.key_bytes);
328        hasher.put_raw_bytes(&entry.value_bytes);
329    }
330    hasher.end_map();
331}
332
333fn push_hash_entry<T: HashComponent + ?Sized>(entries: &mut Vec<HashEntry>, key: &str, value: &T) {
334    let mut key_hasher = ComponentHasher::new();
335    key_hasher.put_string(key);
336    let mut value_hasher = ComponentHasher::new();
337    value.hash_component(&mut value_hasher);
338    entries.push(HashEntry::new(key_hasher, value_hasher));
339}
340
341fn push_hash_list_entry(entries: &mut Vec<HashEntry>, key: &str, values: &[ToolRule]) {
342    let mut key_hasher = ComponentHasher::new();
343    key_hasher.put_string(key);
344    let mut value_hasher = ComponentHasher::new();
345    value_hasher.start_list();
346    for value in values {
347        value_hasher.put_component_hash(value);
348    }
349    value_hasher.end_list();
350    entries.push(HashEntry::new(key_hasher, value_hasher));
351}
352
353#[cfg(test)]
354mod tests {
355    use std::io::Cursor;
356
357    use simdnbt::borrow::{NbtTag as BorrowedNbtTag, read_tag};
358    use simdnbt::owned::{NbtCompound, NbtList, NbtTag};
359    use simdnbt::{FromNbtTag, ToNbtTag};
360    use steel_utils::Identifier;
361    use steel_utils::hash::HashComponent;
362    use steel_utils::serial::{ReadFrom, WriteTo};
363
364    use super::{Tool, ToolRule, ToolRuleBlocks};
365    use crate::init_vanilla_registry;
366    use crate::vanilla_blocks::{COBWEB, STONE};
367
368    fn with_borrowed_tag<R>(tag: NbtTag, visitor: impl FnOnce(BorrowedNbtTag<'_, '_>) -> R) -> R {
369        let mut bytes = Vec::new();
370        tag.write(&mut bytes);
371        let borrowed =
372            read_tag(&mut Cursor::new(bytes.as_slice())).expect("owned test tag should parse");
373        visitor(borrowed.as_tag())
374    }
375
376    fn parse_tool(tag: NbtTag) -> Option<Tool> {
377        with_borrowed_tag(tag, Tool::from_nbt_tag)
378    }
379
380    fn sample_tool() -> Tool {
381        Tool {
382            rules: vec![
383                ToolRule {
384                    blocks: ToolRuleBlocks::Tag(Identifier::vanilla_static("mineable/pickaxe")),
385                    speed: Some(4.0),
386                    correct_for_drops: Some(true),
387                },
388                ToolRule {
389                    blocks: ToolRuleBlocks::Direct(vec![&COBWEB, &STONE]),
390                    speed: Some(15.0),
391                    correct_for_drops: None,
392                },
393            ],
394            default_mining_speed: 1.5,
395            damage_per_block: 2,
396            can_destroy_blocks_in_creative: false,
397        }
398    }
399
400    #[test]
401    fn tool_network_round_trips_tag_and_direct_holder_sets() {
402        init_vanilla_registry();
403        let tool = sample_tool();
404        let mut bytes = Vec::new();
405        tool.write(&mut bytes).expect("tool should serialize");
406
407        let decoded =
408            Tool::read(&mut Cursor::new(bytes.as_slice())).expect("tool should deserialize");
409
410        assert_eq!(decoded, tool);
411    }
412
413    #[test]
414    fn tool_nbt_uses_compact_holder_sets_and_numeric_coercion() {
415        init_vanilla_registry();
416        let mut rule = NbtCompound::new();
417        rule.insert("blocks", "minecraft:cobweb");
418        rule.insert("speed", 5.5_f64);
419        rule.insert("correct_for_drops", 1_i32);
420        let mut compound = NbtCompound::new();
421        compound.insert("rules", NbtList::Compound(vec![rule]));
422        compound.insert("damage_per_block", 2_i8);
423
424        let parsed = parse_tool(NbtTag::Compound(compound)).expect("valid tool should parse");
425
426        assert_eq!(
427            parsed.rules[0].blocks,
428            ToolRuleBlocks::Direct(vec![&COBWEB])
429        );
430        assert_eq!(parsed.rules[0].speed, Some(5.5));
431        assert_eq!(parsed.rules[0].correct_for_drops, Some(true));
432        assert_eq!(parsed.damage_per_block, 2);
433
434        let NbtTag::Compound(encoded) = parsed.to_nbt_tag() else {
435            panic!("tool should encode as a compound");
436        };
437        let rules = encoded
438            .get("rules")
439            .and_then(|tag| match tag {
440                NbtTag::List(NbtList::Compound(rules)) => Some(rules),
441                _ => None,
442            })
443            .expect("tool rules should encode as a compound list");
444        assert_eq!(
445            rules[0].get("blocks"),
446            Some(&NbtTag::String("minecraft:cobweb".into()))
447        );
448    }
449
450    #[test]
451    fn malformed_present_tool_fields_fail_the_codec() {
452        init_vanilla_registry();
453
454        assert!(parse_tool(NbtTag::Compound(NbtCompound::new())).is_none());
455
456        let mut bad_rule = NbtCompound::new();
457        bad_rule.insert("blocks", "minecraft:cobweb");
458        bad_rule.insert("speed", "fast");
459        let mut compound = NbtCompound::new();
460        compound.insert("rules", NbtList::Compound(vec![bad_rule]));
461        assert!(parse_tool(NbtTag::Compound(compound)).is_none());
462
463        let mut bad_rule = NbtCompound::new();
464        bad_rule.insert("blocks", "minecraft:not_a_block");
465        let mut compound = NbtCompound::new();
466        compound.insert("rules", NbtList::Compound(vec![bad_rule]));
467        assert!(parse_tool(NbtTag::Compound(compound)).is_none());
468    }
469
470    #[test]
471    fn tool_hash_matches_its_persistent_codec_shape() {
472        init_vanilla_registry();
473        let tool = Tool {
474            rules: vec![ToolRule {
475                blocks: ToolRuleBlocks::Direct(vec![&COBWEB]),
476                speed: Some(4.0),
477                correct_for_drops: None,
478            }],
479            ..Tool::default()
480        };
481        let expected = tool.clone().to_nbt_tag().compute_hash();
482
483        assert_eq!(tool.compute_hash(), expected);
484
485        let mut with_correct_for_drops = tool.clone();
486        with_correct_for_drops.rules[0].correct_for_drops = Some(true);
487        assert_ne!(tool.compute_hash(), with_correct_for_drops.compute_hash());
488    }
489}