steel_core/player/
item_cooldowns.rs1use rustc_hash::FxHashMap;
2use steel_protocol::packets::game::CCooldown;
3use steel_registry::data_components::vanilla_components::USE_COOLDOWN;
4use steel_registry::item_stack::ItemStack;
5use steel_utils::Identifier;
6
7use super::Player;
8
9#[derive(Clone, Copy)]
10struct CooldownInstance {
11 end_time: i32,
12}
13
14#[derive(Default)]
15pub(super) struct ItemCooldowns {
16 cooldowns: FxHashMap<Identifier, CooldownInstance>,
17 tick_count: i32,
18}
19
20impl ItemCooldowns {
21 #[must_use]
22 pub(super) fn is_on_cooldown(&self, stack: &ItemStack) -> bool {
23 let group = cooldown_group(stack);
24 self.cooldowns
25 .get(&group)
26 .is_some_and(|cooldown| cooldown.end_time > self.tick_count)
27 }
28
29 pub(super) fn tick(&mut self) -> Vec<Identifier> {
30 self.tick_count = self.tick_count.wrapping_add(1);
31 let mut ended = Vec::new();
32 self.cooldowns.retain(|group, cooldown| {
33 if cooldown.end_time <= self.tick_count {
34 ended.push(group.clone());
35 false
36 } else {
37 true
38 }
39 });
40 ended
41 }
42
43 pub(super) fn add_from_stack(&mut self, stack: &ItemStack) -> Option<(Identifier, i32)> {
44 let cooldown = stack.get(USE_COOLDOWN)?;
45 let duration = cooldown.ticks();
46 if duration <= 0 {
47 return None;
48 }
49 let group = cooldown_group(stack);
50 self.cooldowns.insert(
51 group.clone(),
52 CooldownInstance {
53 end_time: self.tick_count + duration,
54 },
55 );
56 Some((group, duration))
57 }
58}
59
60fn cooldown_group(stack: &ItemStack) -> Identifier {
61 stack
62 .get(USE_COOLDOWN)
63 .and_then(|cooldown| cooldown.cooldown_group.clone())
64 .unwrap_or_else(|| stack.item().key.clone())
65}
66
67impl Player {
68 pub fn is_item_on_cooldown(&self, stack: &ItemStack) -> bool {
70 self.item_cooldowns.lock().is_on_cooldown(stack)
71 }
72
73 pub fn apply_item_use_cooldown(&self, stack: &ItemStack) {
75 let cooldown = self.item_cooldowns.lock().add_from_stack(stack);
76 if let Some((cooldown_group, duration)) = cooldown {
77 self.send_packet(CCooldown {
78 cooldown_group,
79 duration,
80 });
81 }
82 }
83
84 pub(super) fn tick_item_cooldowns(&self) {
85 let ended = self.item_cooldowns.lock().tick();
86 for cooldown_group in ended {
87 self.send_packet(CCooldown {
88 cooldown_group,
89 duration: 0,
90 });
91 }
92 }
93}
94
95#[cfg(test)]
96mod tests {
97 use steel_registry::data_components::vanilla_components::{USE_COOLDOWN, UseCooldown};
98 use steel_registry::item_stack::ItemStack;
99 use steel_registry::{init_vanilla_registry, vanilla_items};
100
101 use super::ItemCooldowns;
102
103 #[test]
104 fn cooldown_blocks_until_duration_ticks_pass() {
105 init_vanilla_registry();
106
107 let stack = ItemStack::with_count(&vanilla_items::ENDER_PEARL, 1);
108 let mut cooldowns = ItemCooldowns::default();
109
110 let Some((group, duration)) = cooldowns.add_from_stack(&stack) else {
111 panic!("ender pearl should have a use cooldown");
112 };
113
114 assert_eq!(group, vanilla_items::ENDER_PEARL.key);
115 assert_eq!(duration, 20);
116 assert!(cooldowns.is_on_cooldown(&stack));
117
118 for _ in 0..19 {
119 assert!(cooldowns.tick().is_empty());
120 assert!(cooldowns.is_on_cooldown(&stack));
121 }
122
123 assert_eq!(
124 cooldowns.tick(),
125 vec![vanilla_items::ENDER_PEARL.key.clone()]
126 );
127 assert!(!cooldowns.is_on_cooldown(&stack));
128 }
129
130 #[test]
131 fn explicit_group_is_shared_between_items() {
132 init_vanilla_registry();
133
134 let group = steel_utils::Identifier::vanilla_static("test_group");
135 let mut stack = ItemStack::with_count(&vanilla_items::ENDER_PEARL, 1);
136 stack.set(USE_COOLDOWN, UseCooldown::new(0.5, Some(group.clone())));
137 let mut other = ItemStack::with_count(&vanilla_items::CHORUS_FRUIT, 1);
138 other.set(USE_COOLDOWN, UseCooldown::new(1.0, Some(group.clone())));
139 let unrelated = ItemStack::with_count(&vanilla_items::WIND_CHARGE, 1);
140 let mut cooldowns = ItemCooldowns::default();
141
142 let Some((started_group, duration)) = cooldowns.add_from_stack(&stack) else {
143 panic!("stack should have a use cooldown");
144 };
145
146 assert_eq!(started_group, group);
147 assert_eq!(duration, 10);
148 assert!(cooldowns.is_on_cooldown(&stack));
149 assert!(cooldowns.is_on_cooldown(&other));
150 assert!(!cooldowns.is_on_cooldown(&unrelated));
151 }
152}