Skip to main content

steel_core/player/
stats_counter.rs

1//! This module provides the [`StatsCounter`], which keeps track of stats with their counters, and
2//! implements some stat-related functions for the player.
3
4use 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/// This enum is used to decide if a stat is dirty or not, and whether it should be serialized or not.
12///
13/// This is done so that during a domain transfer, stats reset to zero from a previous domain
14/// to update the client are not serialized to the next.
15#[derive(Debug, Copy, Clone, PartialEq, Eq)]
16pub(crate) enum StatState {
17    /// The stat is clean (not dirty), so no update will be sent from this stat until the counter
18    /// changes value.
19    /// The stat will be persisted.
20    Clean,
21
22    /// This stat is dirty, so the next time stats are queried by the
23    /// client, its update will get sent to the client.
24    /// The stat will be persisted.
25    Dirty,
26
27    /// This stat was reset upon transferring domains, so its update will get sent to the client.
28    /// The stat will not be persisted.
29    Reset,
30}
31
32/// Manages the counters for every statistic for a particular player.
33/// Analogous to Vanilla's `ServerStatsCounter.java`.
34pub struct StatsCounter {
35    /// The map of each stat currently being tracked to its value and state.
36    // Vanilla uses a map and set separately for the counters and dirty flag respectively,
37    // but it is faster to just use one map to store both the count and state in the same map.
38    pub(super) stats: FxHashMap<Stat, (i32, StatState)>,
39}
40
41impl StatsCounter {
42    /// Creates a new, empty [`StatsCounter`].
43    #[must_use]
44    pub fn new() -> Self {
45        Self {
46            stats: FxHashMap::default(),
47        }
48    }
49
50    /// Gets the value of the counter corresponding to the given stat.
51    /// If this counter is not currently being tracked, `0` is returned instead.
52    #[must_use]
53    pub fn get(&self, stat: &Stat) -> i32 {
54        self.stats.get(stat).map_or_default(|(count, _)| *count)
55    }
56
57    /// Sets the value of the counter corresponding to the given stat to a given value.
58    pub fn set(&mut self, stat: Stat, count: i32) {
59        self.stats.insert(stat, (count, StatState::Dirty));
60    }
61
62    /// Increments the value of the counter corresponding to the given stat by a given value.
63    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    /// Marks all the stat counters of this player to be dirty. This means that the next time
70    /// statistics are requested, all tracked stat counters will be sent to the client.
71    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    /// Gets all the counters of stats that are marked dirty and clears their dirty flag as
80    /// well.
81    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    /// Sets the counters of all stats in this counter to zero,
104    /// and marks them as reset. The stats will not be persisted, but will be sent to the client
105    /// the next time they are queried.
106    pub fn reset(&mut self) {
107        for tuple in self.stats.values_mut() {
108            *tuple = (0, StatState::Reset);
109        }
110    }
111
112    /// Returns the number of stats currently being tracked for this player.
113    #[must_use]
114    pub fn len(&self) -> usize {
115        self.stats
116            .iter()
117            .filter(|(_, (_, state))| *state != StatState::Reset)
118            .count()
119    }
120
121    /// Returns whether there are no stats are currently being tracked or not.
122    #[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    /// Awards one count of a particular stat to this player.
138    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    /// Awards a given amount of a particular stat to this player.
146    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    /// Awards a given amount of a custom stat to this player.
158    pub fn award_custom_stat(&self, stat: CustomStatRef) {
159        self.award_stat(&vanilla_stat_types::CUSTOM, stat);
160    }
161
162    /// Awards a given amount of a custom stat to this player.
163    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    /// Awards one count of a particular stat to this player.
168    pub(crate) fn award_erased_stat(&self, stat: Stat) {
169        self.award_erased_stat_with_count(stat, 1);
170    }
171
172    /// Awards a given amount of a particular stat to this player.
173    pub(crate) fn award_erased_stat_with_count(&self, stat: Stat, count: i32) {
174        self.stats.lock().increment(stat, count);
175        // TODO: Add score to the objectives having the criterion of this stat for the player.
176    }
177
178    /// Resets the counter of a stat from this player to zero.
179    pub fn reset_stat(&self, stat: Stat) {
180        self.stats.lock().set(stat, 0);
181        // TODO: Reset score of the objectives having the criterion of this stat for the player.
182    }
183
184    /// Resets the counter of a custom stat from this player to zero.
185    pub fn reset_custom_stat(&self, stat: CustomStatRef) {
186        self.reset_stat(vanilla_stat_types::CUSTOM.get(stat));
187    }
188
189    /// Marks all the stat counters of this player to be dirty. This means that the next time
190    /// statistics are requested, all tracked stat counters will be sent to the client.
191    pub fn mark_all_stats_dirty(&self) {
192        self.stats.lock().mark_all_dirty();
193    }
194
195    /// Sends all the dirty stats of this player to their client, and removes
196    /// the dirty flag from all of them.
197    pub fn send_stats(&self) {
198        let stats = self.stats.lock().get_dirty_and_clear();
199        self.send_packet(CAwardStats { stats });
200    }
201
202    /// Returns the player's currently tracked stats and their counters.
203    /// This excludes stats marked as reset (from transferring domains).
204    #[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        // No need to sort because only one stat is used.
325        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        // Set the stat to 5 before resetting it so that we can verify that an incremented stat
364        // after a reset does not get removed.
365        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}