1use crate::player::Player;
5use rustc_hash::FxHashMap;
6use steel_protocol::packets::game::CAwardStats;
7use steel_registry::RegistryExt;
8use steel_registry::stat::custom::CustomStatRef;
9use steel_registry::stat::{Stat, StatTypeRef, vanilla_stat_types};
10
11#[derive(Debug, Copy, Clone, PartialEq, Eq)]
16pub(crate) enum StatState {
17 Clean,
21
22 Dirty,
26
27 Reset,
30}
31
32pub struct StatsCounter {
35 pub(super) stats: FxHashMap<Stat, (i32, StatState)>,
39}
40
41impl StatsCounter {
42 #[must_use]
44 pub fn new() -> Self {
45 Self {
46 stats: FxHashMap::default(),
47 }
48 }
49
50 #[must_use]
53 pub fn get(&self, stat: &Stat) -> i32 {
54 self.stats.get(stat).map_or_default(|(count, _)| *count)
55 }
56
57 pub fn set(&mut self, stat: Stat, count: i32) {
59 self.stats.insert(stat, (count, StatState::Dirty));
60 }
61
62 pub fn increment(&mut self, stat: Stat, count: i32) {
64 let entry = self.stats.entry(stat).or_insert((0, StatState::Dirty));
65 let sum = (i64::from(entry.0) + i64::from(count)).min(i64::from(i32::MAX));
66 *entry = (sum as i32, StatState::Dirty);
67 }
68
69 pub fn mark_all_dirty(&mut self) {
72 for (_, dirty_flag) in self.stats.values_mut() {
73 if *dirty_flag == StatState::Clean {
74 *dirty_flag = StatState::Dirty;
75 }
76 }
77 }
78
79 pub(crate) fn get_dirty_and_clear(&mut self) -> Vec<(Stat, i32)> {
82 let mut dirty_stats = Vec::new();
83 let mut stats_to_remove = Vec::new();
84 for (&stat, (count, state)) in &mut self.stats {
85 match *state {
86 StatState::Dirty => {
87 dirty_stats.push((stat, *count));
88 *state = StatState::Clean;
89 }
90 StatState::Reset => {
91 dirty_stats.push((stat, *count));
92 stats_to_remove.push(stat);
93 }
94 StatState::Clean => {}
95 }
96 }
97 for stat in stats_to_remove {
98 self.stats.remove(&stat);
99 }
100 dirty_stats
101 }
102
103 pub fn reset(&mut self) {
107 for tuple in self.stats.values_mut() {
108 *tuple = (0, StatState::Reset);
109 }
110 }
111
112 #[must_use]
114 pub fn len(&self) -> usize {
115 self.stats
116 .iter()
117 .filter(|(_, (_, state))| *state != StatState::Reset)
118 .count()
119 }
120
121 #[must_use]
123 pub fn is_empty(&self) -> bool {
124 self.stats
125 .iter()
126 .all(|(_, (_, state))| *state == StatState::Reset)
127 }
128}
129
130impl Default for StatsCounter {
131 fn default() -> Self {
132 Self::new()
133 }
134}
135
136impl Player {
137 pub fn award_stat<R: RegistryExt>(&self, stat_type: StatTypeRef<R>, value: &'static R::Entry)
139 where
140 R::Entry: Send + Sync,
141 {
142 self.award_erased_stat(stat_type.get(value));
143 }
144
145 pub fn award_stat_with_count<R: RegistryExt>(
147 &self,
148 stat_type: StatTypeRef<R>,
149 value: &'static R::Entry,
150 count: i32,
151 ) where
152 R::Entry: Send + Sync,
153 {
154 self.award_erased_stat_with_count(stat_type.get(value), count);
155 }
156
157 pub fn award_custom_stat(&self, stat: CustomStatRef) {
159 self.award_stat(&vanilla_stat_types::CUSTOM, stat);
160 }
161
162 pub fn award_custom_stat_with_count(&self, stat: CustomStatRef, count: i32) {
164 self.award_stat_with_count(&vanilla_stat_types::CUSTOM, stat, count);
165 }
166
167 pub(crate) fn award_erased_stat(&self, stat: Stat) {
169 self.award_erased_stat_with_count(stat, 1);
170 }
171
172 pub(crate) fn award_erased_stat_with_count(&self, stat: Stat, count: i32) {
174 self.stats.lock().increment(stat, count);
175 }
177
178 pub fn reset_stat(&self, stat: Stat) {
180 self.stats.lock().set(stat, 0);
181 }
183
184 pub fn reset_custom_stat(&self, stat: CustomStatRef) {
186 self.reset_stat(vanilla_stat_types::CUSTOM.get(stat));
187 }
188
189 pub fn mark_all_stats_dirty(&self) {
192 self.stats.lock().mark_all_dirty();
193 }
194
195 pub fn send_stats(&self) {
198 let stats = self.stats.lock().get_dirty_and_clear();
199 self.send_packet(CAwardStats { stats });
200 }
201
202 #[must_use]
205 pub fn stats(&self) -> Vec<(Stat, i32)> {
206 self.stats
207 .lock()
208 .stats
209 .iter()
210 .filter(|(_, (_, state))| *state != StatState::Reset)
211 .map(|(&stat, &(count, _))| (stat, count))
212 .collect()
213 }
214}
215
216#[cfg(test)]
217mod tests {
218 use crate::player::stats_counter::StatsCounter;
219 use steel_registry::stat::{Stat, vanilla_stat_types};
220 use steel_registry::{init_vanilla_registry, vanilla_custom_stats};
221
222 fn deterministic_dirty_and_clear(counter: &mut StatsCounter) -> Vec<(Stat, i32)> {
223 let mut dirty = counter.get_dirty_and_clear();
224 dirty.sort_by_key(|(stat, _)| stat.stat_value_key().clone());
225
226 dirty
227 }
228
229 #[test]
230 fn stat_counter_query_dirty_and_modifications() {
231 init_vanilla_registry();
232
233 let mut stats_counter = StatsCounter::new();
234
235 let jump_stat = vanilla_stat_types::CUSTOM.get(&vanilla_custom_stats::JUMP);
236 let deaths_stat = vanilla_stat_types::CUSTOM.get(&vanilla_custom_stats::DEATHS);
237
238 stats_counter.increment(jump_stat, 9);
239 stats_counter.increment(jump_stat, 4);
240
241 assert_eq!(stats_counter.get(&jump_stat), 13);
242 assert_eq!(stats_counter.get(&deaths_stat), 0);
243
244 stats_counter.increment(deaths_stat, 1);
245 assert_eq!(
246 deterministic_dirty_and_clear(&mut stats_counter),
247 vec![(deaths_stat, 1), (jump_stat, 13)]
248 );
249
250 stats_counter.increment(deaths_stat, 1);
251 assert_eq!(
252 deterministic_dirty_and_clear(&mut stats_counter),
253 vec![(deaths_stat, 2)]
254 );
255
256 stats_counter.mark_all_dirty();
257 assert_eq!(
258 deterministic_dirty_and_clear(&mut stats_counter),
259 vec![(deaths_stat, 2), (jump_stat, 13)]
260 );
261
262 assert_eq!(deterministic_dirty_and_clear(&mut stats_counter), vec![]);
263
264 stats_counter.set(deaths_stat, 7);
265 assert_eq!(
266 deterministic_dirty_and_clear(&mut stats_counter),
267 vec![(deaths_stat, 7)]
268 );
269
270 assert_eq!(stats_counter.get(&jump_stat), 13);
271 }
272
273 #[test]
274 fn overflow_cap() {
275 init_vanilla_registry();
276
277 let mut stats_counter = StatsCounter::new();
278 let jump_stat = vanilla_stat_types::CUSTOM.get(&vanilla_custom_stats::JUMP);
279
280 stats_counter.set(jump_stat, i32::MAX - 1);
281
282 stats_counter.increment(jump_stat, 1);
283 assert_eq!(stats_counter.get(&jump_stat), i32::MAX);
284
285 stats_counter.increment(jump_stat, 1);
286 assert_eq!(stats_counter.get(&jump_stat), i32::MAX);
287
288 stats_counter.increment(jump_stat, 1000);
289 assert_eq!(stats_counter.get(&jump_stat), i32::MAX);
290
291 stats_counter.increment(jump_stat, i32::MAX);
292 assert_eq!(stats_counter.get(&jump_stat), i32::MAX);
293
294 stats_counter.increment(jump_stat, i32::MIN + 1);
295 assert_eq!(stats_counter.get(&jump_stat), 0);
296 }
297
298 #[test]
299 fn no_underflow_cap() {
300 init_vanilla_registry();
301
302 let mut stats_counter = StatsCounter::new();
303 let jump_stat = vanilla_stat_types::CUSTOM.get(&vanilla_custom_stats::JUMP);
304
305 stats_counter.set(jump_stat, i32::MIN + 1);
306
307 stats_counter.increment(jump_stat, -1);
308 assert_eq!(stats_counter.get(&jump_stat), i32::MIN);
309
310 stats_counter.increment(jump_stat, -1);
311 assert_eq!(stats_counter.get(&jump_stat), i32::MAX);
312
313 stats_counter.increment(jump_stat, i32::MAX);
314 assert_eq!(stats_counter.get(&jump_stat), i32::MAX);
315 }
316
317 #[test]
318 fn reset_stats() {
319 let mut stats_counter = StatsCounter::new();
320
321 let jump_stat = vanilla_stat_types::CUSTOM.get(&vanilla_custom_stats::JUMP);
322 stats_counter.set(jump_stat, 17);
323
324 assert_eq!(
326 stats_counter.get_dirty_and_clear(),
327 [(jump_stat, 17)],
328 "stat did not set value"
329 );
330 assert_eq!(
331 stats_counter.get_dirty_and_clear(),
332 [],
333 "stat should not send the value again after being cleared"
334 );
335
336 stats_counter.reset();
337
338 assert_eq!(
339 stats_counter.len(),
340 0,
341 "reset stat entries should not be counted"
342 );
343 assert!(
344 stats_counter.is_empty(),
345 "reset stat entries should not be counted"
346 );
347
348 assert_eq!(
349 stats_counter.get_dirty_and_clear(),
350 [(jump_stat, 0)],
351 "reset stat should update the client with zero"
352 );
353 assert_eq!(
354 stats_counter.get_dirty_and_clear(),
355 [],
356 "reset stat should not update the client again with zero after being removed"
357 );
358 assert!(
359 stats_counter.stats.is_empty(),
360 "stale stat counter was not removed"
361 );
362
363 stats_counter.set(jump_stat, 5);
366 stats_counter.reset();
367 stats_counter.increment(jump_stat, 3);
368 assert_eq!(
369 stats_counter.get_dirty_and_clear(),
370 [(jump_stat, 3)],
371 "stat counter should have updated with a new value after incrementing from a reset"
372 );
373 assert!(
374 !stats_counter.stats.is_empty(),
375 "stat counter should not have been removed after increment"
376 );
377 }
378}