Skip to main content

steel_core/entity/damage/
history.rs

1use std::mem;
2use std::sync::{Arc, Weak};
3
4use rustc_hash::FxHashMap;
5use steel_utils::locks::SyncMutex;
6
7use crate::entity::{EntityBase, EntityGeneration};
8use crate::level_data::GameTime;
9
10use super::DamageSource;
11
12mod binding;
13mod source;
14
15pub(crate) use binding::DamageHistoryBinding;
16use binding::DamageHistoryVictim;
17pub use source::RecentDamageSource;
18use source::WeakDamageSource;
19
20/// Maximum retained age from vanilla `LivingEntity.getLastDamageSource`.
21const MAX_DAMAGE_SOURCE_AGE_TICKS: i64 = 40;
22
23struct DamageRecord {
24    victim_lifetime: Weak<DamageHistoryVictim>,
25    // Gameplay membership can end while damage sources still retain the entity.
26    owner: Weak<()>,
27    source: WeakDamageSource,
28    retained: Option<DamageSource>,
29    timestamp: i64,
30    clock: Arc<GameTime>,
31}
32
33impl DamageRecord {
34    fn is_expired(&self) -> bool {
35        self.victim_lifetime.strong_count() == 0
36            || self.clock.ticks().wrapping_sub(self.timestamp) > MAX_DAMAGE_SOURCE_AGE_TICKS
37    }
38}
39
40/// Server-owned equivalent of vanilla's per-entity `lastDamageSource` fields.
41///
42/// Entities and worlds link back weakly. Records retain the victim's clock,
43/// which has no world back-reference, to avoid cycles through the victim or world.
44/// Only gameplay-owned victims retain strong sources. Removal downgrades their
45/// own record; an ownership sweep also covers disconnects and manager teardown.
46#[derive(Default)]
47pub struct DamageHistory {
48    records: SyncMutex<FxHashMap<EntityGeneration, DamageRecord>>,
49}
50
51impl DamageHistory {
52    pub(crate) fn record(
53        self: &Arc<Self>,
54        victim: &EntityBase,
55        source: &DamageSource,
56        clock: &Arc<GameTime>,
57    ) {
58        victim
59            .damage_history()
60            .record(self, victim.generation(), source, clock);
61    }
62
63    pub(crate) fn last_damage_source(
64        &self,
65        victim: EntityGeneration,
66    ) -> Option<RecentDamageSource> {
67        let (source, expired) = {
68            let mut records = self.records.lock();
69            if records.get(&victim).is_some_and(DamageRecord::is_expired) {
70                (None, records.remove(&victim))
71            } else {
72                (
73                    records.get(&victim).map(|record| record.source.resolve()),
74                    None,
75                )
76            }
77        };
78        drop(expired);
79        source
80    }
81
82    pub(crate) fn clear_victim(&self, victim: EntityGeneration) {
83        let removed = self.records.lock().remove(&victim);
84        drop(removed);
85    }
86
87    /// Releases expired records and strong sources with no gameplay owner.
88    /// Runs while frozen too; expiry still uses the unchanged gameplay clock.
89    pub(crate) fn expire(&self) {
90        // Entity destructors can re-enter history; release the mutex before dropping sources.
91        let (expired, released) = {
92            let mut records = self.records.lock();
93            let expired: Vec<_> = records
94                .extract_if(|_, record| record.is_expired())
95                .map(|(_, record)| record)
96                .collect();
97            let released: Vec<_> = records
98                .values_mut()
99                .filter(|record| record.owner.strong_count() == 0)
100                .filter_map(|record| record.retained.take())
101                .collect();
102            (expired, released)
103        };
104        drop((expired, released));
105    }
106
107    /// Releases history ownership after gameplay work has stopped.
108    pub(crate) fn clear(&self) {
109        let records = mem::take(&mut *self.records.lock());
110        drop(records);
111    }
112}
113
114#[cfg(test)]
115mod tests;