Skip to main content

canwu_event/
lib.rs

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