steel_registry/data_components/components/
tool.rs1use 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
18pub type ToolRuleBlocks = RegistryHolderSet<Block>;
20
21#[derive(Debug, Clone, PartialEq)]
23pub struct ToolRule {
24 pub blocks: ToolRuleBlocks,
26 pub speed: Option<f32>,
28 pub correct_for_drops: Option<bool>,
30}
31
32impl ToolRule {
33 #[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 #[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 #[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 #[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#[derive(Debug, Clone, PartialEq)]
75pub struct Tool {
76 pub rules: Vec<ToolRule>,
78 pub default_mining_speed: f32,
80 pub damage_per_block: i32,
82 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 #[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 #[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}