Skip to main content

naia_shared/world/local/
local_entity_map.rs

1use std::collections::HashMap;
2
3use crate::{
4    world::local::{
5        local_entity::{HostEntity, OwnedLocalEntity, RemoteEntity},
6        local_entity_record::LocalEntityRecord,
7    },
8    EntityDoesNotExistError, GlobalEntity, HostType, Instant, LocalEntityAndGlobalEntityConverter,
9};
10
11/// Bidirectional lookup table between [`GlobalEntity`] identifiers and their connection-local [`HostEntity`] or [`RemoteEntity`] counterparts.
12pub struct LocalEntityMap {
13    host_type: HostType,
14    global_to_local: HashMap<GlobalEntity, LocalEntityRecord>,
15    /// Keyed by `HostEntity { id, is_static }` — static and dynamic pools both
16    /// start from 0, but `HostEntity` carries `is_static` so they hash distinctly.
17    host_to_global: HashMap<HostEntity, GlobalEntity>,
18    remote_to_global: HashMap<RemoteEntity, GlobalEntity>,
19    entity_redirects: HashMap<OwnedLocalEntity, (OwnedLocalEntity, Instant)>,
20}
21
22impl LocalEntityAndGlobalEntityConverter for LocalEntityMap {
23    fn global_entity_to_host_entity(
24        &self,
25        global_entity: &GlobalEntity,
26    ) -> Result<HostEntity, EntityDoesNotExistError> {
27        if let Some(record) = self.global_to_local.get(global_entity) {
28            if record.is_host_owned() {
29                return Ok(record.host_entity());
30            }
31        }
32        Err(EntityDoesNotExistError)
33    }
34
35    fn global_entity_to_remote_entity(
36        &self,
37        global_entity: &GlobalEntity,
38    ) -> Result<RemoteEntity, EntityDoesNotExistError> {
39        if let Some(record) = self.global_to_local.get(global_entity) {
40            if record.is_remote_owned() {
41                return Ok(record.remote_entity());
42            }
43        }
44        Err(EntityDoesNotExistError)
45    }
46
47    fn global_entity_to_owned_entity(
48        &self,
49        global_entity: &GlobalEntity,
50    ) -> Result<OwnedLocalEntity, EntityDoesNotExistError> {
51        if let Some(record) = self.global_to_local.get(global_entity) {
52            // info!("global_entity_to_owned_entity(). Found record for global entity {:?}: {:?}", global_entity, record);
53            return Ok(record.owned_entity());
54        }
55        Err(EntityDoesNotExistError)
56    }
57
58    fn host_entity_to_global_entity(
59        &self,
60        host_entity: &HostEntity,
61    ) -> Result<GlobalEntity, EntityDoesNotExistError> {
62        if let Some(global_entity) = self.host_to_global.get(host_entity) {
63            return Ok(*global_entity);
64        }
65        Err(EntityDoesNotExistError)
66    }
67
68    fn static_host_entity_to_global_entity(
69        &self,
70        host_entity: &HostEntity,
71    ) -> Result<GlobalEntity, EntityDoesNotExistError> {
72        if let Some(global_entity) = self.host_to_global.get(host_entity) {
73            return Ok(*global_entity);
74        }
75        Err(EntityDoesNotExistError)
76    }
77
78    fn remote_entity_to_global_entity(
79        &self,
80        remote_entity: &RemoteEntity,
81    ) -> Result<GlobalEntity, EntityDoesNotExistError> {
82        if let Some(global_entity) = self.remote_to_global.get(remote_entity) {
83            return Ok(*global_entity);
84        }
85        Err(EntityDoesNotExistError)
86    }
87
88    fn apply_entity_redirect(&self, entity: &OwnedLocalEntity) -> OwnedLocalEntity {
89        if let Some((new_entity, _timestamp)) = self.entity_redirects.get(entity) {
90            *new_entity
91        } else {
92            *entity
93        }
94    }
95}
96
97impl LocalEntityMap {
98    /// Creates an empty map for the given `host_type` side of a connection.
99    pub fn new(host_type: HostType) -> Self {
100        Self {
101            host_type,
102            global_to_local: HashMap::new(),
103            host_to_global: HashMap::new(),
104            remote_to_global: HashMap::new(),
105            entity_redirects: HashMap::new(),
106        }
107    }
108
109    /// Returns whether this map belongs to a server or client side.
110    pub fn host_type(&self) -> HostType {
111        self.host_type
112    }
113
114    /// Registers a host-owned mapping from `global_entity` to `host_entity`, panicking on duplicate keys.
115    pub fn insert_with_host_entity(
116        &mut self,
117        global_entity: GlobalEntity,
118        host_entity: HostEntity,
119    ) {
120        if self.global_to_local.contains_key(&global_entity) {
121            panic!(
122                "Cannot overwrite inserted global entity: {:?}",
123                global_entity
124            );
125        }
126        if self.host_to_global.contains_key(&host_entity) {
127            panic!("Cannot overwrite inserted host entity {:?}", host_entity);
128        }
129
130        self.global_to_local.insert(
131            global_entity,
132            LocalEntityRecord::new_host_owned_entity(host_entity),
133        );
134
135        self.host_to_global.insert(host_entity, global_entity);
136    }
137
138    /// Registers a static host-owned mapping from `global_entity` to `host_entity`, panicking on duplicate keys.
139    pub fn insert_with_static_host_entity(
140        &mut self,
141        global_entity: GlobalEntity,
142        host_entity: HostEntity,
143    ) {
144        if self.global_to_local.contains_key(&global_entity) {
145            panic!(
146                "Cannot overwrite inserted global entity: {:?}",
147                global_entity
148            );
149        }
150        if self.host_to_global.contains_key(&host_entity) {
151            panic!("Cannot overwrite inserted static host entity {:?}", host_entity);
152        }
153
154        self.global_to_local.insert(
155            global_entity,
156            LocalEntityRecord::new_static_host_owned_entity(host_entity),
157        );
158
159        self.host_to_global.insert(host_entity, global_entity);
160    }
161
162    /// Registers a remote-owned mapping from `global_entity` to `remote_entity`, panicking on duplicate keys.
163    pub fn insert_with_remote_entity(
164        &mut self,
165        global_entity: GlobalEntity,
166        remote_entity: RemoteEntity,
167    ) {
168        if self.global_to_local.contains_key(&global_entity) {
169            panic!(
170                "Cannot overwrite inserted global entity: {:?}",
171                global_entity
172            );
173        }
174        if self.remote_to_global.contains_key(&remote_entity) {
175            panic!(
176                "Cannot overwrite inserted remote entity {:?}",
177                remote_entity
178            );
179        }
180
181        self.global_to_local.insert(
182            global_entity,
183            LocalEntityRecord::new_remote_owned_entity(remote_entity),
184        );
185        self.remote_to_global.insert(remote_entity, global_entity);
186    }
187
188    /// Returns the [`GlobalEntity`] mapped from `remote_entity`, if one exists.
189    pub fn global_entity_from_remote(&self, remote_entity: &RemoteEntity) -> Option<&GlobalEntity> {
190        self.remote_to_global.get(remote_entity)
191    }
192
193    /// Returns the [`GlobalEntity`] mapped from `host_entity`, if one exists.
194    pub fn global_entity_from_host(&self, host_entity: &HostEntity) -> Option<&GlobalEntity> {
195        self.host_to_global.get(host_entity)
196    }
197
198/// Removes the record for `global_entity` and cleans up the reverse index, returning the record if it existed.
199    pub fn remove_by_global_entity(
200        &mut self,
201        global_entity: &GlobalEntity,
202    ) -> Option<LocalEntityRecord> {
203        // info!("Removing global entity: {:?}", global_entity);
204        let record_opt = self.global_to_local.remove(global_entity);
205        if let Some(record) = &record_opt {
206            if record.is_host_owned() {
207                let host_entity = record.host_entity();
208                self.host_to_global.remove(&host_entity);
209            } else {
210                let remote_entity = record.remote_entity();
211                self.remote_to_global.remove(&remote_entity);
212            }
213        }
214        record_opt
215    }
216
217    pub(crate) fn remove_by_remote_entity(&mut self, remote_entity: &RemoteEntity) -> GlobalEntity {
218        let global_entity = self.remote_to_global.remove(remote_entity);
219        let Some(global_entity) = global_entity else {
220            panic!(
221                "Attempting to remove remote entity which does not exist: {:?}",
222                remote_entity
223            );
224        };
225        self.remove_by_global_entity(&global_entity);
226        global_entity
227    }
228
229    /// Remove remote mapping if it exists (idempotent, used during migration cleanup)
230    /// This ensures that after migration, global_entity_to_remote_entity() will fail
231    pub(crate) fn remove_remote_mapping_if_exists(&mut self, remote_entity: &RemoteEntity) {
232        // Remove from remote_to_global map - this is the key that global_entity_to_remote_entity uses
233        // via remote_entity_to_global_entity lookup, but more importantly, we need to ensure
234        // that global_to_local doesn't have a remote-owned record for the same global_entity
235        if let Some(_global_entity) = self.remote_to_global.remove(remote_entity) {
236            // Double-check: if global_to_local still has this global_entity marked as remote-owned,
237            // that's a bug - it should have been removed by remove_by_global_entity
238            // But we can't fix it here without knowing the new state, so we just remove the mapping
239        }
240    }
241
242    /// Returns `true` if `global_entity` is currently registered in the map.
243    pub fn contains_global_entity(&self, global_entity: &GlobalEntity) -> bool {
244        self.global_to_local.contains_key(global_entity)
245    }
246
247    /// Returns `true` if `host_entity` is currently registered in the map.
248    pub fn contains_host_entity(&self, host_entity: &HostEntity) -> bool {
249        self.host_to_global.contains_key(host_entity)
250    }
251
252    /// Returns `true` if `remote_entity` is currently registered in the map.
253    pub fn contains_remote_entity(&self, remote_entity: &RemoteEntity) -> bool {
254        self.remote_to_global.contains_key(remote_entity)
255    }
256
257    /// Iterates over all `(GlobalEntity, LocalEntityRecord)` pairs currently in the map.
258    pub fn iter(&self) -> impl Iterator<Item = (&GlobalEntity, &LocalEntityRecord)> {
259        self.global_to_local.iter()
260    }
261
262    pub(crate) fn remote_entities(&self) -> Vec<GlobalEntity> {
263        self.iter()
264            .filter(|(_, record)| record.is_remote_owned())
265            .map(|(global_entity, _)| *global_entity)
266            .collect::<Vec<GlobalEntity>>()
267    }
268
269    // pub(crate) fn global_entity_is_delegated(&self, global_entity: &GlobalEntity) -> bool {
270    //     if let Some(record) = self.global_to_local.get(global_entity) {
271    //         return record.is_delegated();
272    //     }
273    //     false
274    // }
275
276    /// Returns `self` as a read-only [`LocalEntityAndGlobalEntityConverter`] reference.
277    pub fn entity_converter(&self) -> &dyn LocalEntityAndGlobalEntityConverter {
278        self
279    }
280
281    /// Installs a redirect so that lookups of `old_entity` transparently return `new_entity` for a TTL period.
282    pub fn install_entity_redirect(
283        &mut self,
284        old_entity: OwnedLocalEntity,
285        new_entity: OwnedLocalEntity,
286    ) {
287        let now = Instant::now();
288        self.entity_redirects.insert(old_entity, (new_entity, now));
289    }
290
291    pub(crate) fn apply_entity_redirect(&self, entity: &OwnedLocalEntity) -> OwnedLocalEntity {
292        self.entity_redirects
293            .get(entity)
294            .map(|(new_entity, _)| *new_entity)
295            .unwrap_or(*entity)
296    }
297
298    pub(crate) fn cleanup_old_redirects(&mut self, now: &Instant, ttl_seconds: u64) {
299        use std::time::Duration;
300        let ttl_duration = Duration::from_secs(ttl_seconds);
301        self.entity_redirects
302            .retain(|_, (_, timestamp)| timestamp.elapsed(now) < ttl_duration);
303    }
304}