Skip to main content

moirai/event/
registry.rs

1//! Checked event registration table and retention policy for one [`crate::world::World`].
2//!
3//! Ordinary events are looked up by payload type. Lifecycle channels are registered alongside
4//! component indices and are excluded from ordinary type lookup.
5
6use alloc::collections::BTreeMap;
7use alloc::string::{String, ToString};
8use alloc::vec::Vec;
9use core::any::{type_name, TypeId};
10
11use crate::operation::StageOperation;
12use crate::world::WorldOwner;
13
14/// Dense registry-local event handle scoped to one world owner.
15#[derive(Clone, Debug, Eq, PartialEq, Hash)]
16pub struct EventId {
17    owner: WorldOwner,
18    index: u32,
19}
20
21/// How long event payloads remain readable before pruning or frame cleanup.
22#[derive(Copy, Clone, Debug, Eq, PartialEq)]
23pub enum EventRetention {
24    /// Cleared at the end of one [`crate::operation::StageOperation`] pass.
25    Frame(StageOperation),
26    /// Retained until explicitly pruned by bounded policy or channel closure.
27    Manual,
28    /// Retains at most `capacity` newest payloads; slow readers may lag.
29    Bounded(usize),
30}
31
32/// Registration-time event retention and source policy.
33#[derive(Copy, Clone, Debug, Eq, PartialEq)]
34pub struct EventOptions {
35    retention: EventRetention,
36    external_source: bool,
37}
38
39impl EventOptions {
40    /// Frame-scoped retention cleared after the given host operation finishes.
41    pub fn frame(operation: StageOperation) -> Self {
42        Self {
43            retention: EventRetention::Frame(operation),
44            external_source: false,
45        }
46    }
47
48    /// Manual retention until bounded pruning or channel closure.
49    pub fn manual() -> Self {
50        Self {
51            retention: EventRetention::Manual,
52            external_source: false,
53        }
54    }
55
56    /// Bounded ring retention keeping the newest `capacity` payloads.
57    pub fn bounded(capacity: usize) -> Result<Self, EventRegistrationError> {
58        if capacity == 0 {
59            return Err(EventRegistrationError::InvalidCapacity);
60        }
61        Ok(Self {
62            retention: EventRetention::Bounded(capacity),
63            external_source: false,
64        })
65    }
66
67    /// Marks the event as originating outside in-world producers for schedule validation.
68    pub fn external_source(mut self) -> Self {
69        self.external_source = true;
70        self
71    }
72
73    pub(crate) fn retention(self) -> EventRetention {
74        self.retention
75    }
76
77    #[allow(dead_code)]
78    pub(crate) fn is_external_source(self) -> bool {
79        self.external_source
80    }
81}
82
83/// Event registration conflict or invalid retention input.
84#[non_exhaustive]
85#[derive(Clone, Debug, Eq, PartialEq)]
86pub enum EventRegistrationError {
87    /// The same payload type or name was registered with incompatible options.
88    TypeConflict {
89        /// Registration name associated with the conflict.
90        name: String,
91        /// Existing entry name.
92        existing: String,
93        /// Requested entry name.
94        requested: String,
95    },
96    /// Bounded retention capacity must be nonzero.
97    InvalidCapacity,
98}
99
100struct EventEntry {
101    name: String,
102    type_id: TypeId,
103    options: EventOptions,
104    #[allow(dead_code)]
105    lifecycle_component_index: Option<usize>,
106}
107
108pub(crate) struct EventRegistry {
109    entries: Vec<EventEntry>,
110    ordinary_by_type: BTreeMap<TypeId, u32>,
111    ordinary_count: usize,
112}
113
114const LINEAR_TYPE_LOOKUP_PREFIX: usize = 16;
115
116impl EventId {
117    pub(crate) fn new(owner: WorldOwner, index: u32) -> Self {
118        Self { owner, index }
119    }
120
121    /// Dense registry index for diagnostics and channel lookup.
122    pub fn index(&self) -> usize {
123        self.index as usize
124    }
125
126    pub(crate) fn validate_owner(&self, owner: &WorldOwner) -> Result<(), EventRegistrationError> {
127        if self.owner.same(owner) {
128            Ok(())
129        } else {
130            Err(EventRegistrationError::TypeConflict {
131                name: String::from("<event>"),
132                existing: String::from("different world"),
133                requested: String::from("different world"),
134            })
135        }
136    }
137}
138
139impl EventRegistry {
140    pub fn new() -> Self {
141        Self {
142            entries: Vec::new(),
143            ordinary_by_type: BTreeMap::new(),
144            ordinary_count: 0,
145        }
146    }
147
148    pub fn len(&self) -> usize {
149        self.entries.len()
150    }
151
152    pub fn register<E: Clone + 'static>(
153        &mut self,
154        owner: &WorldOwner,
155        options: EventOptions,
156    ) -> Result<EventId, EventRegistrationError> {
157        let name = type_name::<E>().to_string();
158        let type_id = TypeId::of::<E>();
159        if let Some(index) = self.ordinary_index_of_type_id(type_id) {
160            let index = index as usize;
161            let entry = &self.entries[index];
162            if entry.options == options {
163                return Ok(EventId::new(owner.clone(), index as u32));
164            }
165            return Err(EventRegistrationError::TypeConflict {
166                name: name.clone(),
167                existing: entry.name.clone(),
168                requested: name,
169            });
170        }
171        if let Some((_index, entry)) = self
172            .entries
173            .iter()
174            .enumerate()
175            .find(|(_, entry)| entry.name == name)
176        {
177            return Err(EventRegistrationError::TypeConflict {
178                name: name.clone(),
179                existing: entry.name.clone(),
180                requested: name,
181            });
182        }
183        let index = self.entries.len() as u32;
184        self.entries.push(EventEntry {
185            name,
186            type_id,
187            options,
188            lifecycle_component_index: None,
189        });
190        if self.ordinary_count >= LINEAR_TYPE_LOOKUP_PREFIX {
191            self.ordinary_by_type.insert(type_id, index);
192        }
193        self.ordinary_count += 1;
194        Ok(EventId::new(owner.clone(), index))
195    }
196
197    pub(crate) fn register_lifecycle<E: Clone + 'static>(
198        &mut self,
199        owner: &WorldOwner,
200        component_index: usize,
201        kind: crate::event::component::LifecycleKind,
202        options: EventOptions,
203    ) -> Result<EventId, EventRegistrationError> {
204        let name = alloc::format!("__lifecycle_{kind:?}_{component_index}");
205        let type_id = TypeId::of::<E>();
206        let index = self.entries.len() as u32;
207        self.entries.push(EventEntry {
208            name,
209            type_id,
210            options,
211            lifecycle_component_index: Some(component_index),
212        });
213        Ok(EventId::new(owner.clone(), index))
214    }
215
216    pub fn options(&self, id: &EventId) -> Option<EventOptions> {
217        self.entries.get(id.index()).map(|entry| entry.options)
218    }
219
220    #[allow(dead_code)]
221    pub fn type_id(&self, id: &EventId) -> Option<TypeId> {
222        self.entries.get(id.index()).map(|entry| entry.type_id)
223    }
224
225    pub fn id_of<E: Clone + 'static>(&self, owner: &WorldOwner) -> Option<EventId> {
226        self.id_of_type_id(owner, TypeId::of::<E>())
227    }
228
229    pub(crate) fn id_of_type_id(&self, owner: &WorldOwner, type_id: TypeId) -> Option<EventId> {
230        self.ordinary_index_of_type_id(type_id)
231            .map(|index| EventId::new(owner.clone(), index))
232    }
233
234    fn ordinary_index_of_type_id(&self, type_id: TypeId) -> Option<u32> {
235        let mut ordinary_entries = 0;
236        for (index, entry) in self.entries.iter().enumerate() {
237            if entry.lifecycle_component_index.is_some() {
238                continue;
239            }
240            ordinary_entries += 1;
241            if entry.type_id == type_id {
242                return Some(index as u32);
243            }
244            if ordinary_entries == LINEAR_TYPE_LOOKUP_PREFIX {
245                break;
246            }
247        }
248        self.ordinary_by_type.get(&type_id).copied()
249    }
250}
251
252impl Default for EventRegistry {
253    fn default() -> Self {
254        Self::new()
255    }
256}
257
258#[cfg(feature = "std")]
259impl core::fmt::Display for EventRegistrationError {
260    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
261        match self {
262            Self::TypeConflict {
263                name,
264                existing,
265                requested,
266            } => write!(
267                f,
268                "event registration conflict for {name}: existing={existing}, requested={requested}"
269            ),
270            Self::InvalidCapacity => f.write_str("event retention capacity must be nonzero"),
271        }
272    }
273}
274
275#[cfg(feature = "std")]
276impl std::error::Error for EventRegistrationError {}
277
278#[cfg(test)]
279mod tests {
280    use super::*;
281
282    #[derive(Clone, Copy)]
283    struct Damage(#[allow(dead_code)] u32);
284
285    #[derive(Clone, Copy)]
286    struct Heal(#[allow(dead_code)] u32);
287
288    #[derive(Clone, Copy)]
289    struct Indexed<const N: usize>;
290
291    #[test]
292    fn external_source_flag_round_trip() {
293        let options = EventOptions::manual().external_source();
294        assert!(options.is_external_source());
295        assert_eq!(options.retention(), EventRetention::Manual);
296    }
297
298    #[test]
299    fn default_registry_is_empty() {
300        let registry = EventRegistry::default();
301        assert_eq!(registry.len(), 0);
302    }
303
304    #[test]
305    fn duplicate_registration_is_idempotent() {
306        let owner = WorldOwner::new();
307        let mut registry = EventRegistry::new();
308        let options = EventOptions::manual();
309        let first = registry.register::<Damage>(&owner, options).expect("first");
310        let second = registry
311            .register::<Damage>(&owner, options)
312            .expect("repeat");
313        assert_eq!(first, second);
314    }
315
316    #[test]
317    fn same_name_different_type_is_rejected() {
318        let owner = WorldOwner::new();
319        let mut registry = EventRegistry::new();
320        let name = type_name::<Damage>().to_string();
321        registry.entries.push(EventEntry {
322            name: name.clone(),
323            type_id: TypeId::of::<Heal>(),
324            options: EventOptions::manual(),
325            lifecycle_component_index: None,
326        });
327        let err = registry
328            .register::<Damage>(&owner, EventOptions::manual())
329            .expect_err("conflict");
330        assert!(matches!(err, EventRegistrationError::TypeConflict { .. }));
331    }
332
333    #[test]
334    fn event_id_validate_owner_rejects_foreign_world() {
335        let owner_a = WorldOwner::new();
336        let owner_b = WorldOwner::new();
337        let id = EventId::new(owner_a, 0);
338        assert!(registry_owner_check(&id, &owner_b).is_err());
339    }
340
341    #[test]
342    fn type_id_accessor_returns_registered_type() {
343        let owner = WorldOwner::new();
344        let mut registry = EventRegistry::new();
345        let id = registry
346            .register::<Damage>(&owner, EventOptions::manual())
347            .expect("register");
348        assert_eq!(registry.type_id(&id), Some(TypeId::of::<Damage>()));
349    }
350
351    #[test]
352    fn ordinary_type_index_ignores_lifecycle_entries_of_the_same_payload_type() {
353        use crate::event::component::LifecycleKind;
354
355        let owner = WorldOwner::new();
356        let mut registry = EventRegistry::new();
357        let lifecycle = registry
358            .register_lifecycle::<Damage>(
359                &owner,
360                0,
361                LifecycleKind::Added,
362                EventOptions::frame(StageOperation::Update),
363            )
364            .expect("lifecycle");
365        assert!(registry.id_of::<Damage>(&owner).is_none());
366
367        let ordinary = registry
368            .register::<Damage>(&owner, EventOptions::manual())
369            .expect("ordinary");
370        assert_ne!(ordinary, lifecycle);
371        assert_eq!(registry.id_of::<Damage>(&owner), Some(ordinary));
372    }
373
374    #[test]
375    fn adaptive_type_lookup_resolves_prefix_and_tree_entries() {
376        let owner = WorldOwner::new();
377        let mut registry = EventRegistry::new();
378        macro_rules! register {
379            ($index:literal) => {
380                registry
381                    .register::<Indexed<$index>>(&owner, EventOptions::manual())
382                    .expect("register");
383            };
384        }
385        register!(0);
386        register!(1);
387        register!(2);
388        register!(3);
389        register!(4);
390        register!(5);
391        register!(6);
392        register!(7);
393        register!(8);
394        register!(9);
395        register!(10);
396        register!(11);
397        register!(12);
398        register!(13);
399        register!(14);
400        register!(15);
401        register!(16);
402
403        assert_eq!(registry.ordinary_count, 17);
404        assert_eq!(registry.ordinary_by_type.len(), 1);
405
406        assert_eq!(
407            registry.id_of::<Indexed<0>>(&owner).map(|id| id.index()),
408            Some(0)
409        );
410        assert_eq!(
411            registry.id_of::<Indexed<16>>(&owner).map(|id| id.index()),
412            Some(16)
413        );
414        assert_eq!(
415            registry
416                .register::<Indexed<16>>(&owner, EventOptions::manual())
417                .expect("duplicate fallback entry")
418                .index(),
419            16
420        );
421    }
422
423    fn registry_owner_check(
424        id: &EventId,
425        owner: &WorldOwner,
426    ) -> Result<(), EventRegistrationError> {
427        id.validate_owner(owner)
428    }
429}