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}