Skip to main content

canwu_event/
lib.rs

1//! Inspectable, serializable events with compact causal provenance.
2
3use canwu_core::{ArmyId, BoundaryId, CommandId, EntityRef, EventId, PersonId, TerritoryId};
4use canwu_time::SimTime;
5use serde::{Deserialize, Serialize};
6
7#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
8#[serde(tag = "type", content = "id", rename_all = "snake_case")]
9pub enum CauseRef {
10    Boundary(BoundaryId),
11    Command(CommandId),
12    Event(EventId),
13    System(String),
14}
15
16#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
17#[serde(tag = "type", rename_all = "snake_case")]
18pub enum EventKind {
19    MoveOrdered {
20        army: ArmyId,
21        from: TerritoryId,
22        to: TerritoryId,
23        arrival_at: SimTime,
24    },
25    ArmyArrived {
26        army: ArmyId,
27        territory: TerritoryId,
28    },
29    ReportDispatched {
30        recipient: PersonId,
31        army: ArmyId,
32        arrives_at: SimTime,
33    },
34    KnowledgeUpdated {
35        recipient: PersonId,
36        army: ArmyId,
37        known_location: TerritoryId,
38    },
39    DebugFieldChanged {
40        entity: EntityRef,
41        field: String,
42        old_value: String,
43        new_value: String,
44    },
45    Plugin {
46        plugin: String,
47        event_type: String,
48    },
49}
50
51/// Declarative audience for a persisted event projection.
52///
53/// This is intentionally separate from plugin-to-plugin dispatch permissions:
54/// it controls only whether a trusted viewer may receive the event through a
55/// player-facing observation projection. `Private` is the safe default for
56/// plugin events that do not declare an audience.
57#[derive(Clone, Debug, Default, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
58#[serde(tag = "type", content = "value", rename_all = "snake_case")]
59pub enum EventAudience {
60    Public,
61    Actor(PersonId),
62    Actors(Vec<PersonId>),
63    /// The event is visible to actors represented by `EntityRef::Person` in
64    /// the event's affected-entity list.
65    AffectedActors,
66    #[default]
67    Private,
68}
69
70impl EventKind {
71    #[must_use]
72    pub const fn event_type(&self) -> &'static str {
73        match self {
74            Self::MoveOrdered { .. } => "move_ordered",
75            Self::ArmyArrived { .. } => "army_arrived",
76            Self::ReportDispatched { .. } => "report_dispatched",
77            Self::KnowledgeUpdated { .. } => "knowledge_updated",
78            Self::DebugFieldChanged { .. } => "debug_field_changed",
79            Self::Plugin { .. } => "plugin",
80        }
81    }
82
83    /// Returns the stable, display-oriented type identity for this event.
84    ///
85    /// Built-in event kinds retain the same snake-case labels returned by
86    /// [`Self::event_type`]. Plugin events are qualified with their registered
87    /// plugin name and event type, separated by a dot. The two plugin-provided
88    /// components are preserved as registered; no case folding or additional
89    /// normalization is applied.
90    #[must_use]
91    pub fn qualified_event_type(&self) -> String {
92        match self {
93            Self::Plugin { plugin, event_type } => format!("{plugin}.{event_type}"),
94            _ => self.event_type().to_owned(),
95        }
96    }
97}
98
99#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
100pub struct SimEvent {
101    pub id: EventId,
102    pub timestamp: SimTime,
103    pub kind: EventKind,
104    pub affected_entities: Vec<EntityRef>,
105    pub summary: String,
106    pub cause: Option<CauseRef>,
107    pub correlation_id: u64,
108}
109
110#[cfg(test)]
111mod tests {
112    use super::EventKind;
113    use canwu_core::{ArmyId, TerritoryId};
114
115    #[test]
116    fn qualified_event_type_preserves_legacy_labels_and_disambiguates_plugins() {
117        let built_in = EventKind::ArmyArrived {
118            army: ArmyId::new(1),
119            territory: TerritoryId::new(2),
120        };
121        assert_eq!(built_in.event_type(), "army_arrived");
122        assert_eq!(built_in.qualified_event_type(), "army_arrived");
123
124        let supply = EventKind::Plugin {
125            plugin: "example-supply".to_owned(),
126            event_type: "grain_allocated".to_owned(),
127        };
128        let demand = EventKind::Plugin {
129            plugin: "example-demand".to_owned(),
130            event_type: "grain_allocated".to_owned(),
131        };
132
133        assert_eq!(supply.event_type(), "plugin");
134        assert_eq!(
135            supply.qualified_event_type(),
136            "example-supply.grain_allocated"
137        );
138        assert_eq!(
139            demand.qualified_event_type(),
140            "example-demand.grain_allocated"
141        );
142        assert_ne!(supply.qualified_event_type(), demand.qualified_event_type());
143    }
144}