Skip to main content

murk_core/
command.rs

1//! Command, command payload, and receipt types for the ingress pipeline.
2
3use crate::error::IngressError;
4use crate::id::{Coord, EntityId, FieldId, ParameterKey, PropertyIndex, TickId};
5
6/// A command submitted to the simulation via the ingress pipeline.
7///
8/// Commands are ordered by `priority_class` (lower = higher priority),
9/// then by `source_id` for disambiguation, then by `source_seq` for
10/// per-source sequencing, then by `arrival_seq` as a final tiebreaker.
11///
12/// # Examples
13///
14/// ```
15/// use murk_core::{Command, CommandPayload, FieldId, TickId, ParameterKey};
16///
17/// // A command that sets a global parameter.
18/// let cmd = Command {
19///     payload: CommandPayload::SetParameter {
20///         key: ParameterKey(0),
21///         value: 2.5,
22///     },
23///     expires_after_tick: TickId(100),
24///     source_id: Some(1),
25///     source_seq: Some(0),
26///     priority_class: 1,
27///     arrival_seq: 0,
28/// };
29///
30/// assert_eq!(cmd.priority_class, 1);
31/// assert_eq!(cmd.expires_after_tick, TickId(100));
32/// ```
33#[derive(Clone, Debug, PartialEq)]
34pub struct Command {
35    /// The operation to perform.
36    pub payload: CommandPayload,
37    /// The command expires if not applied by this tick.
38    pub expires_after_tick: TickId,
39    /// Optional source identifier for deduplication and ordering.
40    pub source_id: Option<u64>,
41    /// Optional per-source sequence number for ordering.
42    pub source_seq: Option<u64>,
43    /// Priority class. Lower values = higher priority.
44    /// 0 = system, 1 = user default.
45    pub priority_class: u8,
46    /// Monotonic arrival sequence number, set by the ingress pipeline.
47    pub arrival_seq: u64,
48}
49
50/// All command payloads.
51///
52/// `WorldEvent` variants affect per-cell state; `GlobalParameter` variants
53/// affect simulation-wide scalar parameters.
54///
55/// # Examples
56///
57/// ```
58/// use murk_core::{CommandPayload, FieldId, ParameterKey};
59///
60/// // Set a single field value at a coordinate.
61/// let coord: murk_core::Coord = vec![3i32, 7].into();
62/// let payload = CommandPayload::SetField {
63///     coord,
64///     field_id: FieldId(0),
65///     value: 42.0,
66/// };
67///
68/// // Batch-set multiple global parameters atomically.
69/// let batch = CommandPayload::SetParameterBatch {
70///     params: vec![(ParameterKey(0), 1.0), (ParameterKey(1), 0.5)],
71/// };
72///
73/// assert!(matches!(payload, CommandPayload::SetField { .. }));
74/// assert!(matches!(batch, CommandPayload::SetParameterBatch { .. }));
75/// ```
76#[derive(Clone, Debug, PartialEq)]
77pub enum CommandPayload {
78    // --- WorldEvent variants ---
79    /// Move an entity to a target coordinate.
80    ///
81    /// Rejected if `entity_id` is unknown or `target_coord` is out of bounds.
82    Move {
83        /// The entity to move.
84        entity_id: EntityId,
85        /// The destination coordinate.
86        target_coord: Coord,
87    },
88    /// Spawn a new entity at a coordinate with initial property values.
89    Spawn {
90        /// The spawn location.
91        coord: Coord,
92        /// User-defined entity type tag.
93        entity_type: u32,
94        /// Initial property overrides for the new entity.
95        property_overrides: Vec<(PropertyIndex, f32)>,
96    },
97    /// Remove an entity. Associated properties are cleared at the next tick.
98    Despawn {
99        /// The entity to remove.
100        entity_id: EntityId,
101    },
102    /// Set a single field value at a coordinate. Primarily for `Sparse` fields.
103    SetField {
104        /// The target cell coordinate.
105        coord: Coord,
106        /// The field to modify.
107        field_id: FieldId,
108        /// The new value.
109        value: f32,
110    },
111    /// Extension point for domain-specific commands.
112    Custom {
113        /// User-registered type identifier.
114        type_id: u32,
115        /// Opaque payload data.
116        data: Vec<u8>,
117    },
118
119    // --- GlobalParameter variants ---
120    /// Set a single global parameter. Takes effect at the next tick boundary.
121    SetParameter {
122        /// The parameter to set.
123        key: ParameterKey,
124        /// The new value.
125        value: f64,
126    },
127    /// Batch-set multiple parameters atomically.
128    SetParameterBatch {
129        /// The parameters to set.
130        params: Vec<(ParameterKey, f64)>,
131    },
132}
133
134/// Receipt returned for each command in a submitted batch.
135///
136/// Indicates whether the command was accepted and, if applied,
137/// which tick it was applied in.
138///
139/// # Examples
140///
141/// ```
142/// use murk_core::command::Receipt;
143/// use murk_core::TickId;
144///
145/// let receipt = Receipt {
146///     accepted: true,
147///     applied_tick_id: Some(TickId(5)),
148///     reason_code: None,
149///     command_index: 0,
150///     spawned_entity_id: None,
151/// };
152///
153/// assert!(receipt.accepted);
154/// assert_eq!(receipt.applied_tick_id, Some(TickId(5)));
155/// ```
156#[derive(Clone, Debug, PartialEq, Eq)]
157pub struct Receipt {
158    /// Whether the command was accepted by the ingress pipeline.
159    pub accepted: bool,
160    /// The tick at which the command was applied, if applicable.
161    pub applied_tick_id: Option<TickId>,
162    /// The reason the command was rejected, if applicable.
163    pub reason_code: Option<IngressError>,
164    /// Index of this command within the submitted batch.
165    pub command_index: usize,
166    /// Entity ID allocated by a spawn command.
167    pub spawned_entity_id: Option<EntityId>,
168}
169
170#[cfg(test)]
171mod entity_command_tests {
172    use super::*;
173
174    #[test]
175    fn move_and_despawn_use_entity_id() {
176        let id = EntityId::new(12, 3);
177        let move_payload = CommandPayload::Move {
178            entity_id: id,
179            target_coord: vec![1, 2].into(),
180        };
181        let despawn_payload = CommandPayload::Despawn { entity_id: id };
182
183        assert!(matches!(
184            move_payload,
185            CommandPayload::Move {
186                entity_id,
187                ..
188            } if entity_id == id
189        ));
190        assert_eq!(despawn_payload, CommandPayload::Despawn { entity_id: id });
191    }
192
193    #[test]
194    fn spawn_carries_entity_type_and_property_overrides() {
195        let payload = CommandPayload::Spawn {
196            coord: vec![3, 4].into(),
197            entity_type: 7,
198            property_overrides: vec![(PropertyIndex(1), 42.0)],
199        };
200
201        match payload {
202            CommandPayload::Spawn {
203                entity_type,
204                property_overrides,
205                ..
206            } => {
207                assert_eq!(entity_type, 7);
208                assert_eq!(property_overrides, vec![(PropertyIndex(1), 42.0)]);
209            }
210            _ => panic!("expected spawn"),
211        }
212    }
213
214    #[test]
215    fn receipt_can_carry_spawned_entity_id() {
216        let id = EntityId::new(2, 1);
217        let receipt = Receipt {
218            accepted: true,
219            applied_tick_id: Some(TickId(4)),
220            reason_code: None,
221            command_index: 0,
222            spawned_entity_id: Some(id),
223        };
224
225        assert_eq!(receipt.spawned_entity_id, Some(id));
226    }
227}