1use std::fmt;
4use std::num::ParseIntError;
5
6use crate::authoring::primitives::rng::mix_seed;
7use crate::model::SimState;
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13pub struct ActionDescriptor {
14 pub id: &'static str,
16 pub label: &'static str,
18}
19
20impl ActionDescriptor {
21 pub const fn new(id: &'static str, label: &'static str) -> Self {
23 Self { id, label }
24 }
25}
26
27const ACTION_SALT: u64 = 0x00AC_7104_5EED_0001;
29
30pub fn action_seed(seed: Option<u64>) -> u64 {
35 mix_seed(seed.unwrap_or(0) ^ ACTION_SALT)
36}
37
38#[macro_export]
50macro_rules! actions {
51 ($($(#[$meta:meta])* $vis:vis const $name:ident = $descriptor:expr;)+) => {
52 $crate::__indices!(0usize, $([$(#[$meta])* $vis $name],)+);
53
54 const ACTION_SPECS: &[$crate::__macro_support::ActionDescriptor] = &[$($descriptor),+];
56 };
57}
58
59#[derive(Debug, Clone, PartialEq, Eq)]
61pub struct Scheduled {
62 pub index: usize,
64 pub id: String,
66 pub tick: u64,
68}
69
70#[derive(Clone, Copy, Debug, PartialEq, Eq)]
75pub enum Fire {
76 BeforeStep,
79 AfterStep,
82}
83
84pub type RefusedActions<'a> = Vec<&'a Scheduled>;
86
87#[derive(Debug, Clone, PartialEq, Eq)]
89pub enum ScheduleError {
90 BadEntry {
92 raw: String,
94 },
95 BadTick {
97 raw: String,
99 source: ParseIntError,
101 },
102 NoActions {
104 model: String,
106 },
107 UnknownAction {
109 id: String,
111 model: String,
113 known: Vec<&'static str>,
115 },
116}
117
118impl fmt::Display for ScheduleError {
119 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
120 match self {
121 Self::BadEntry { raw } => write!(f, "bad --act '{raw}', expected ID@TICK"),
122 Self::BadTick { raw, .. } => write!(f, "bad tick in --act '{raw}'"),
123 Self::NoActions { model } => write!(f, "'{model}' declares no actions"),
124 Self::UnknownAction { id, model, known } => {
125 write!(f, "unknown action '{id}' for '{model}' (has {})", known.join(", "))
126 }
127 }
128 }
129}
130
131impl std::error::Error for ScheduleError {
132 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
133 match self {
134 Self::BadTick { source, .. } => Some(source),
135 _ => None,
136 }
137 }
138}
139
140#[derive(Debug, Clone, Default, PartialEq, Eq)]
144pub struct Schedule {
145 entries: Vec<Scheduled>,
146}
147
148impl Schedule {
149 pub fn parse(raw: &[String], model_id: &str, actions: &[ActionDescriptor]) -> Result<Self, ScheduleError> {
158 let mut entries = Vec::with_capacity(raw.len());
159 for spec in raw {
160 let (id, tick) = spec
161 .rsplit_once('@')
162 .ok_or_else(|| ScheduleError::BadEntry { raw: spec.clone() })?;
163 let tick = tick.parse::<u64>().map_err(|source| ScheduleError::BadTick {
164 raw: spec.clone(),
165 source,
166 })?;
167 let Some(index) = actions.iter().position(|action| action.id == id) else {
168 let known: Vec<&'static str> = actions.iter().map(|action| action.id).collect();
169 if known.is_empty() {
170 return Err(ScheduleError::NoActions {
171 model: model_id.to_owned(),
172 });
173 }
174 return Err(ScheduleError::UnknownAction {
175 id: id.to_owned(),
176 model: model_id.to_owned(),
177 known,
178 });
179 };
180 entries.push(Scheduled {
181 index,
182 id: id.to_owned(),
183 tick,
184 });
185 }
186 Ok(Self { entries })
187 }
188
189 pub fn from_entries(entries: Vec<Scheduled>) -> Self {
191 Self { entries }
192 }
193
194 pub fn entries(&self) -> &[Scheduled] {
196 &self.entries
197 }
198
199 pub fn is_empty(&self) -> bool {
201 self.entries.is_empty()
202 }
203
204 pub fn last_tick(&self) -> Option<u64> {
206 self.entries.iter().map(|a| a.tick).max()
207 }
208
209 pub fn next_due_after(&self, tick: u64) -> Option<u64> {
211 self.entries
212 .iter()
213 .map(|entry| entry.tick)
214 .filter(|&due| due > tick)
215 .min()
216 }
217
218 pub fn due(&self, tick: u64) -> impl Iterator<Item = &Scheduled> {
220 self.entries.iter().filter(move |a| a.tick == tick)
221 }
222
223 pub fn fire_ticks(&self, start: u64, count: u64, fire: Fire) -> Vec<u64> {
229 let window = match fire {
230 Fire::BeforeStep => start..start.saturating_add(count),
231 Fire::AfterStep => start.saturating_add(1)..start.saturating_add(count).saturating_add(1),
232 };
233 let mut ticks: Vec<u64> = self
234 .entries
235 .iter()
236 .map(|a| a.tick)
237 .filter(|t| window.contains(t))
238 .collect();
239 ticks.sort_unstable();
240 ticks.dedup();
241 ticks
242 }
243
244 #[inline]
249 #[must_use = "a refused action is reported only through the returned entries"]
250 pub fn run_due(&self, state: &mut dyn SimState) -> RefusedActions<'_> {
251 if self.entries.is_empty() {
252 return Vec::new();
253 }
254 let tick = state.tick();
255 let mut refused = Vec::new();
256 for action in self.due(tick) {
257 if !state.act(action.index) {
258 refused.push(action);
259 }
260 }
261 refused
262 }
263}
264
265#[cfg(test)]
266mod tests {
267 use super::{ActionDescriptor, Fire, Schedule, Scheduled};
268
269 #[test]
271 fn actions_macro_numbers_entries_in_declaration_order() {
272 crate::actions! {
273 const RANDOMISE = crate::action::ActionDescriptor::new("randomise", "Randomise");
274 const CLEAR = crate::action::ActionDescriptor::new("clear", "Clear");
276 }
277 assert_eq!((RANDOMISE, CLEAR), (0, 1), "indices follow declaration order");
278 assert_eq!(ACTION_SPECS.len(), 2);
279 assert_eq!(ACTION_SPECS[CLEAR].label, "Clear");
280 }
281
282 #[test]
284 fn the_action_stream_is_seeded_and_apart_from_the_tick_stream() {
285 use crate::action::action_seed;
286 assert_eq!(action_seed(Some(7)), action_seed(Some(7)));
287 assert_ne!(action_seed(Some(7)), action_seed(Some(8)));
288 assert_ne!(action_seed(Some(7)), crate::authoring::primitives::rng::mix_seed(7));
289 assert_ne!(action_seed(None), 0, "an unseeded state still needs a usable state");
290 }
291
292 fn schedule_at(ticks: &[u64]) -> Schedule {
293 let entries = ticks
294 .iter()
295 .map(|&tick| Scheduled {
296 index: 0,
297 id: "act".to_owned(),
298 tick,
299 })
300 .collect();
301 Schedule::from_entries(entries)
302 }
303
304 fn fire_runs(schedule: &Schedule, counts: &[u64], fire: Fire) -> Vec<Vec<u64>> {
306 let mut start = 0;
307 counts
308 .iter()
309 .map(|&count| {
310 let fired = schedule.fire_ticks(start, count, fire);
311 start += count;
312 fired
313 })
314 .collect()
315 }
316
317 #[test]
321 fn back_to_back_runs_fire_each_tick_once() {
322 let schedule = schedule_at(&[0, 7, 10, 10, 14, 39, 40, 41]);
324 let splits: [&[u64]; 4] = [&[40], &[10, 10, 10, 10], &[7, 7, 7, 7, 7, 5], &[0, 13, 0, 27, 0]];
325 for split in splits {
326 let before = fire_runs(&schedule, split, Fire::BeforeStep).concat();
327 assert_eq!(before, [0, 7, 10, 14, 39], "before the step, runs of {split:?}");
328 let after = fire_runs(&schedule, split, Fire::AfterStep).concat();
329 assert_eq!(after, [7, 10, 14, 39, 40], "after the step, runs of {split:?}");
330 }
331 }
332
333 #[test]
334 fn the_next_due_tick_is_strictly_after_the_one_given() {
335 let schedule = schedule_at(&[40, 7, 10, 10]);
336 assert_eq!(schedule.next_due_after(0), Some(7));
337 assert_eq!(schedule.next_due_after(7), Some(10), "a tick is not due after itself");
338 assert_eq!(schedule.next_due_after(10), Some(40));
339 assert_eq!(schedule.next_due_after(40), None);
340 assert_eq!(Schedule::default().next_due_after(0), None);
341 assert!(Schedule::default().is_empty());
342 }
343
344 #[test]
345 fn an_entry_resolves_to_its_declared_action() {
346 let actions = [
347 ActionDescriptor::new("randomise", "Randomise"),
348 ActionDescriptor::new("clear", "Clear"),
349 ];
350 let raw = ["clear@5", "randomise@2", "clear@5"].map(str::to_owned);
351 let schedule = Schedule::parse(&raw, "life", &actions).expect("declared actions");
352 let resolved: Vec<(usize, u64)> = schedule
353 .entries()
354 .iter()
355 .map(|entry| (entry.index, entry.tick))
356 .collect();
357 assert_eq!(resolved, [(1, 5), (0, 2), (1, 5)], "in the order given");
358
359 let refuse = |raw: &str, actions: &[ActionDescriptor]| {
360 Schedule::parse(&[raw.to_owned()], "life", actions)
361 .expect_err("refused")
362 .to_string()
363 };
364 assert_eq!(refuse("clear", &actions), "bad --act 'clear', expected ID@TICK");
365 assert_eq!(refuse("clear@soon", &actions), "bad tick in --act 'clear@soon'");
366 let bad_tick = Schedule::parse(&["clear@soon".to_owned()], "life", &actions).expect_err("refused");
367 let source = std::error::Error::source(&bad_tick).map(ToString::to_string);
368 assert_eq!(
369 source.as_deref(),
370 Some("invalid digit found in string"),
371 "the tick's parse error is the source"
372 );
373 assert_eq!(refuse("clear@5", &[]), "'life' declares no actions");
374 assert_eq!(
375 refuse("reset@5", &actions),
376 "unknown action 'reset' for 'life' (has randomise, clear)"
377 );
378 }
379
380 #[test]
382 fn an_id_holding_an_at_sign_resolves() {
383 let actions = [ActionDescriptor::new("spawn@centre", "Spawn at centre")];
384 let schedule = Schedule::parse(&["spawn@centre@100".to_owned()], "life", &actions).expect("a declared action");
385 assert_eq!(
386 schedule.entries(),
387 [Scheduled {
388 index: 0,
389 id: "spawn@centre".to_owned(),
390 tick: 100,
391 }]
392 );
393 }
394}