Skip to main content

pfx_core/sim/
director.rs

1use std::fmt;
2
3use super::rng::{Rng, STATE_BYTES, mix64};
4
5pub const ONE: i32 = 1 << 16;
6pub const UNCAPPED: u32 = u32::MAX;
7pub const NO_SPOT: u32 = u32::MAX;
8pub const MAX_ORDERS_PER_TICK: usize = 16;
9pub const DIRECTOR_VERSION: u32 = 3;
10pub const DIRECTOR_PLAIN_VERSION: u32 = 2;
11
12pub const TIMING: u64 = 1;
13pub const PICKS: u64 = 2;
14pub const POSITIONS: u64 = 3;
15pub const FORMS: u64 = 4;
16pub const SPREADS: u64 = 5;
17pub const JITTERS: u64 = 6;
18
19#[derive(Clone, Copy, Debug, PartialEq, Eq)]
20pub enum Shape {
21    Linear,
22    Stepped(u32),
23}
24
25#[derive(Clone, Copy, Debug, PartialEq, Eq)]
26pub struct Ramp {
27    pub from: i32,
28    pub to: i32,
29    pub shape: Shape,
30}
31
32impl Ramp {
33    pub const fn flat(value: i32) -> Self {
34        Self {
35            from: value,
36            to: value,
37            shape: Shape::Linear,
38        }
39    }
40
41    pub const fn linear(from: i32, to: i32) -> Self {
42        Self {
43            from,
44            to,
45            shape: Shape::Linear,
46        }
47    }
48
49    pub const fn stepped(from: i32, to: i32, steps: u32) -> Self {
50        Self {
51            from,
52            to,
53            shape: Shape::Stepped(steps),
54        }
55    }
56
57    #[must_use]
58    pub fn at(&self, elapsed: u64, duration: u64) -> i32 {
59        let duration = duration.max(1);
60        let elapsed = elapsed.min(duration);
61        let span = self.to as i128 - self.from as i128;
62        let value = match self.shape {
63            Shape::Linear => self.from as i128 + span * elapsed as i128 / duration as i128,
64            Shape::Stepped(steps) => {
65                let steps = steps.max(2) as u128;
66                let level = (elapsed as u128 * steps / duration as u128).min(steps - 1);
67                self.from as i128 + span * level as i128 / (steps as i128 - 1)
68            }
69        };
70        value as i32
71    }
72
73    fn top(&self) -> i32 {
74        self.from.max(self.to)
75    }
76
77    fn lowest(&self) -> i32 {
78        self.from.min(self.to)
79    }
80}
81
82#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
83pub struct Lull {
84    pub min: u32,
85    pub max: u32,
86}
87
88impl Lull {
89    pub const fn fixed(ticks: u32) -> Self {
90        Self {
91            min: ticks,
92            max: ticks,
93        }
94    }
95
96    pub const fn between(min: u32, max: u32) -> Self {
97        Self { min, max }
98    }
99}
100
101#[derive(Clone, Copy, Debug, PartialEq, Eq)]
102pub struct SpawnLull {
103    pub spawns: u32,
104    pub lull: Lull,
105}
106
107#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
108pub struct Window {
109    pub start: u64,
110    pub end: u64,
111}
112
113#[derive(Clone, Copy, Debug, PartialEq, Eq)]
114pub struct Cycle {
115    pub start: u64,
116    pub every: u64,
117    pub length: u64,
118    pub end: u64,
119}
120
121impl Cycle {
122    pub const fn new(start: u64, every: u64, length: u64) -> Self {
123        Self {
124            start,
125            every,
126            length,
127            end: u64::MAX,
128        }
129    }
130
131    pub const fn until(mut self, end: u64) -> Self {
132        self.end = end;
133        self
134    }
135}
136
137#[derive(Clone, Copy, Debug, PartialEq, Eq)]
138pub struct Entry {
139    pub id: u32,
140    pub weight: u32,
141    pub tags: u32,
142    pub cost: u32,
143    pub from: u64,
144    pub line: Option<Line>,
145    pub spots: Option<u32>,
146}
147
148impl Entry {
149    pub const fn new(id: u32, weight: u32) -> Self {
150        Self {
151            id,
152            weight,
153            tags: 0,
154            cost: 1,
155            from: 0,
156            line: None,
157            spots: None,
158        }
159    }
160
161    pub const fn tagged(mut self, tags: u32) -> Self {
162        self.tags = tags;
163        self
164    }
165
166    pub const fn costing(mut self, cost: u32) -> Self {
167        self.cost = cost;
168        self
169    }
170
171    pub const fn from_tick(mut self, tick: u64) -> Self {
172        self.from = tick;
173        self
174    }
175
176    pub const fn in_line(mut self, line: Line) -> Self {
177        self.line = Some(line);
178        self
179    }
180
181    pub const fn at_spots(mut self, set: u32) -> Self {
182        self.spots = Some(set);
183        self
184    }
185
186    const fn least_members(&self) -> u32 {
187        match self.line {
188            Some(line) => line.min,
189            None => 1,
190        }
191    }
192}
193
194#[derive(Clone, Copy, Debug, PartialEq, Eq)]
195pub enum Anchor {
196    Start,
197    Centre,
198    End,
199}
200
201#[derive(Clone, Copy, Debug, PartialEq, Eq)]
202pub struct Line {
203    pub min: u32,
204    pub max: u32,
205    pub direction: [i32; 3],
206    pub gap: i32,
207    pub spread: [i32; 3],
208    pub anchor: Anchor,
209}
210
211impl Line {
212    pub const fn new(min: u32, max: u32, direction: [i32; 3], gap: i32) -> Self {
213        Self {
214            min,
215            max,
216            direction,
217            gap,
218            spread: [0; 3],
219            anchor: Anchor::Start,
220        }
221    }
222
223    pub const fn spread(mut self, spread: [i32; 3]) -> Self {
224        self.spread = spread;
225        self
226    }
227
228    pub const fn anchored(mut self, anchor: Anchor) -> Self {
229        self.anchor = anchor;
230        self
231    }
232
233    fn step(&self, axis: usize) -> i64 {
234        self.direction[axis] as i64 * self.gap as i64
235    }
236}
237
238#[derive(Clone, Debug, PartialEq, Eq)]
239pub struct Table {
240    pub entries: Vec<Entry>,
241    pub cap: u32,
242}
243
244impl Table {
245    pub fn new(entries: Vec<Entry>) -> Self {
246        Self {
247            entries,
248            cap: UNCAPPED,
249        }
250    }
251}
252
253#[derive(Clone, Debug, PartialEq, Eq)]
254pub struct Phase {
255    pub start: u64,
256    pub end: u64,
257    pub table: u32,
258    pub tag_filter: u32,
259    pub rate: Ramp,
260    pub rate_jitter: i32,
261    pub group: Ramp,
262    pub speed: Ramp,
263    pub after_spawns: Option<SpawnLull>,
264    pub on_clear: Option<Lull>,
265}
266
267impl Phase {
268    pub const fn new(start: u64, end: u64, table: u32) -> Self {
269        Self {
270            start,
271            end,
272            table,
273            tag_filter: 0,
274            rate: Ramp::flat(0),
275            rate_jitter: 0,
276            group: Ramp::flat(ONE),
277            speed: Ramp::flat(ONE),
278            after_spawns: None,
279            on_clear: None,
280        }
281    }
282}
283
284#[derive(Clone, Debug, PartialEq, Eq)]
285pub struct Schedule {
286    pub tick_rate: u32,
287    pub cap: u32,
288    pub allow_overlap: bool,
289    pub tables: Vec<Table>,
290    pub phases: Vec<Phase>,
291    pub lulls: Vec<Window>,
292    pub cycles: Vec<Cycle>,
293    pub spot_sets: Vec<Vec<u32>>,
294}
295
296impl Default for Schedule {
297    fn default() -> Self {
298        Self {
299            tick_rate: 60,
300            cap: UNCAPPED,
301            allow_overlap: false,
302            tables: Vec::new(),
303            phases: Vec::new(),
304            lulls: Vec::new(),
305            cycles: Vec::new(),
306            spot_sets: Vec::new(),
307        }
308    }
309}
310
311#[derive(Clone, Copy, Debug, PartialEq, Eq)]
312pub enum DirectorError {
313    TickRate,
314    NoTables,
315    ZeroWeight { table: u32 },
316    DuplicateEntry { id: u32 },
317    BadPhase { phase: u32 },
318    UnknownTable { phase: u32, table: u32 },
319    Overlap { first: u32, second: u32 },
320    ZeroCap { phase: u32 },
321    Unspawnable { phase: u32 },
322    BadRamp { phase: u32 },
323    BadLull { phase: u32 },
324    BadLine { id: u32 },
325    BadWindow { window: u32 },
326    BadCycle { cycle: u32 },
327    BadJitter { phase: u32 },
328    UnknownSpots { id: u32, set: u32 },
329    EmptySpots { set: u32 },
330    Length { found: usize, expected: usize },
331    Version { found: u32 },
332    Mismatch,
333    Corrupt,
334}
335
336impl fmt::Display for DirectorError {
337    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
338        match self {
339            Self::TickRate => write!(f, "the schedule's tick rate is zero"),
340            Self::NoTables => write!(f, "the schedule has phases but no tables"),
341            Self::ZeroWeight { table } => {
342                write!(f, "table {table} has no entry with a weight above zero")
343            }
344            Self::DuplicateEntry { id } => {
345                write!(f, "entry id {id} appears more than once across the tables")
346            }
347            Self::BadPhase { phase } => write!(f, "phase {phase} ends at or before its start"),
348            Self::UnknownTable { phase, table } => {
349                write!(
350                    f,
351                    "phase {phase} draws from table {table}, which does not exist"
352                )
353            }
354            Self::Overlap { first, second } => write!(
355                f,
356                "phases {first} and {second} overlap and the schedule does not allow it"
357            ),
358            Self::ZeroCap { phase } => write!(
359                f,
360                "phase {phase} schedules spawns but its table or the overall cap is zero"
361            ),
362            Self::Unspawnable { phase } => write!(
363                f,
364                "phase {phase} schedules spawns but no entry of its table passes its tag filter and fits the cap"
365            ),
366            Self::BadRamp { phase } => write!(
367                f,
368                "phase {phase} has a ramp with a negative value or fewer than two steps"
369            ),
370            Self::BadLull { phase } => write!(
371                f,
372                "phase {phase} has a lull whose minimum exceeds its maximum"
373            ),
374            Self::BadLine { id } => write!(
375                f,
376                "entry {id} has a line with no members, a minimum above its maximum or a negative spread"
377            ),
378            Self::BadWindow { window } => {
379                write!(f, "quiet window {window} ends at or before its start")
380            }
381            Self::BadCycle { cycle } => write!(
382                f,
383                "quiet cycle {cycle} has a zero period, a length of zero or of at least its period, or ends at or before its start"
384            ),
385            Self::BadJitter { phase } => write!(
386                f,
387                "phase {phase} has a rate jitter below zero or of at least ONE"
388            ),
389            Self::UnknownSpots { id, set } => {
390                write!(
391                    f,
392                    "entry {id} spawns at spot set {set}, which does not exist"
393                )
394            }
395            Self::EmptySpots { set } => write!(f, "spot set {set} lists no spots"),
396            Self::Length { found, expected } => {
397                write!(
398                    f,
399                    "saved director state is {found} bytes, expected {expected}"
400                )
401            }
402            Self::Version { found } => write!(
403                f,
404                "saved director state is version {found}, this build reads {DIRECTOR_PLAIN_VERSION} and {DIRECTOR_VERSION}"
405            ),
406            Self::Mismatch => write!(f, "saved director state belongs to a different schedule"),
407            Self::Corrupt => write!(f, "saved director state holds an invalid generator state"),
408        }
409    }
410}
411
412impl std::error::Error for DirectorError {}
413
414#[derive(Clone, Copy, Debug, PartialEq, Eq)]
415pub struct Region {
416    pub min: [i32; 3],
417    pub max: [i32; 3],
418}
419
420impl Region {
421    pub const fn new(min: [i32; 3], max: [i32; 3]) -> Self {
422        Self { min, max }
423    }
424
425    pub const fn flat(min: [i32; 2], max: [i32; 2]) -> Self {
426        Self {
427            min: [min[0], min[1], 0],
428            max: [max[0], max[1], 0],
429        }
430    }
431}
432
433#[derive(Clone, Copy, Debug, Default)]
434pub struct Spots<'a> {
435    pub points: &'a [[i32; 3]],
436    pub regions: &'a [Region],
437}
438
439impl<'a> Spots<'a> {
440    pub const EMPTY: Spots<'static> = Spots {
441        points: &[],
442        regions: &[],
443    };
444
445    pub const fn points(points: &'a [[i32; 3]]) -> Self {
446        Self {
447            points,
448            regions: &[],
449        }
450    }
451
452    pub const fn regions(regions: &'a [Region]) -> Self {
453        Self {
454            points: &[],
455            regions,
456        }
457    }
458}
459
460#[derive(Clone, Copy, Debug, PartialEq, Eq)]
461pub struct SpawnOrder {
462    pub entry: u32,
463    pub count: u32,
464    pub speed: i32,
465    pub position: [i32; 3],
466    pub spot: u32,
467    pub group: u32,
468    pub member: u32,
469    pub members: u32,
470}
471
472#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
473struct PhaseState {
474    charge: u64,
475    spawned: u32,
476    wave_open: bool,
477    lull_until: u64,
478    need: u64,
479}
480
481const PHASE_BYTES: usize = 8 + 4 + 1 + 8;
482
483#[derive(Clone, Debug)]
484pub struct Director {
485    schedule: Schedule,
486    fingerprint: u64,
487    ids: Vec<(u32, u32, u32)>,
488    bases: Vec<u32>,
489    now: u64,
490    states: Vec<PhaseState>,
491    alive: Vec<u32>,
492    table_load: Vec<u64>,
493    load: u64,
494    timing: Rng,
495    picks: Rng,
496    positions: Rng,
497    forms: Rng,
498    spreads: Rng,
499    jitters: Rng,
500    next_group: u32,
501    weights: Vec<u32>,
502    jittered: bool,
503    windows: Vec<Window>,
504    quiet_now: bool,
505    quiet_next: u64,
506}
507
508impl Schedule {
509    pub fn check(&self) -> Result<(), DirectorError> {
510        if self.tick_rate == 0 {
511            return Err(DirectorError::TickRate);
512        }
513        if !self.phases.is_empty() && self.tables.is_empty() {
514            return Err(DirectorError::NoTables);
515        }
516        let mut ids: Vec<u32> = Vec::new();
517        for (t, table) in self.tables.iter().enumerate() {
518            let total: u64 = table.entries.iter().map(|e| e.weight as u64).sum();
519            if total == 0 {
520                return Err(DirectorError::ZeroWeight { table: t as u32 });
521            }
522            for e in &table.entries {
523                if let Some(set) = e.spots
524                    && set as usize >= self.spot_sets.len()
525                {
526                    return Err(DirectorError::UnknownSpots { id: e.id, set });
527                }
528                if let Some(line) = e.line
529                    && (line.min == 0 || line.min > line.max || line.spread.iter().any(|&v| v < 0))
530                {
531                    return Err(DirectorError::BadLine { id: e.id });
532                }
533            }
534            ids.extend(table.entries.iter().map(|e| e.id));
535        }
536        ids.sort_unstable();
537        if let Some(pair) = ids.windows(2).find(|w| w[0] == w[1]) {
538            return Err(DirectorError::DuplicateEntry { id: pair[0] });
539        }
540        for (window, w) in self.lulls.iter().enumerate() {
541            if w.end <= w.start {
542                return Err(DirectorError::BadWindow {
543                    window: window as u32,
544                });
545            }
546        }
547        for (cycle, c) in self.cycles.iter().enumerate() {
548            if c.every == 0 || c.length == 0 || c.length >= c.every || c.end <= c.start {
549                return Err(DirectorError::BadCycle {
550                    cycle: cycle as u32,
551                });
552            }
553        }
554        if let Some(set) = self.spot_sets.iter().position(Vec::is_empty) {
555            return Err(DirectorError::EmptySpots { set: set as u32 });
556        }
557        for (p, phase) in self.phases.iter().enumerate() {
558            self.check_phase(p as u32, phase)?;
559        }
560        if !self.allow_overlap {
561            for (a, first) in self.phases.iter().enumerate() {
562                for (b, second) in self.phases.iter().enumerate().skip(a + 1) {
563                    if first.start < second.end && second.start < first.end {
564                        return Err(DirectorError::Overlap {
565                            first: a as u32,
566                            second: b as u32,
567                        });
568                    }
569                }
570            }
571        }
572        Ok(())
573    }
574
575    fn check_phase(&self, p: u32, phase: &Phase) -> Result<(), DirectorError> {
576        if phase.end <= phase.start {
577            return Err(DirectorError::BadPhase { phase: p });
578        }
579        let Some(table) = self.tables.get(phase.table as usize) else {
580            return Err(DirectorError::UnknownTable {
581                phase: p,
582                table: phase.table,
583            });
584        };
585        for ramp in [&phase.rate, &phase.group, &phase.speed] {
586            let steps_ok = match ramp.shape {
587                Shape::Linear => true,
588                Shape::Stepped(steps) => steps >= 2,
589            };
590            if ramp.lowest() < 0 || !steps_ok {
591                return Err(DirectorError::BadRamp { phase: p });
592            }
593        }
594        let lulls = phase
595            .after_spawns
596            .map(|s| s.lull)
597            .into_iter()
598            .chain(phase.on_clear);
599        for lull in lulls {
600            if lull.min > lull.max {
601                return Err(DirectorError::BadLull { phase: p });
602            }
603        }
604        if !(0..ONE).contains(&phase.rate_jitter) {
605            return Err(DirectorError::BadJitter { phase: p });
606        }
607        if phase.rate.top() == 0 {
608            return Ok(());
609        }
610        if table.cap == 0 || self.cap == 0 {
611            return Err(DirectorError::ZeroCap { phase: p });
612        }
613        let room = table.cap.min(self.cap);
614        let fits = table.entries.iter().any(|e| {
615            e.weight > 0
616                && (phase.tag_filter == 0 || e.tags & phase.tag_filter != 0)
617                && e.from < phase.end
618                && e.cost as u64 * e.least_members() as u64 <= room as u64
619        });
620        if !fits {
621            return Err(DirectorError::Unspawnable { phase: p });
622        }
623        Ok(())
624    }
625
626    fn fingerprint(&self) -> u64 {
627        let mut hash = mix64(DIRECTOR_PLAIN_VERSION as u64);
628        let mut put = |value: u64| hash = mix64(hash ^ value).wrapping_add(value);
629        put(self.tick_rate as u64);
630        put(self.cap as u64);
631        put(self.allow_overlap as u64);
632        put(self.tables.len() as u64);
633        for table in &self.tables {
634            put(table.cap as u64);
635            put(table.entries.len() as u64);
636            for e in &table.entries {
637                put(e.id as u64);
638                put(e.weight as u64);
639                put(e.tags as u64);
640                put(e.cost as u64);
641                put(e.from);
642                match e.line {
643                    Some(l) => {
644                        put(1 + l.min as u64);
645                        put(l.max as u64);
646                        for axis in 0..3 {
647                            put(l.direction[axis] as u32 as u64);
648                            put(l.spread[axis] as u32 as u64);
649                        }
650                        put(l.gap as u32 as u64);
651                        put(l.anchor as u64);
652                    }
653                    None => put(0),
654                }
655            }
656        }
657        put(self.phases.len() as u64);
658        for phase in &self.phases {
659            put(phase.start);
660            put(phase.end);
661            put(phase.table as u64);
662            put(phase.tag_filter as u64);
663            for ramp in [&phase.rate, &phase.group, &phase.speed] {
664                put(ramp.from as u32 as u64);
665                put(ramp.to as u32 as u64);
666                put(match ramp.shape {
667                    Shape::Linear => 0,
668                    Shape::Stepped(n) => 1 + n as u64,
669                });
670            }
671            match phase.after_spawns {
672                Some(s) => {
673                    put(1 + s.spawns as u64);
674                    put(s.lull.min as u64);
675                    put(s.lull.max as u64);
676                }
677                None => put(0),
678            }
679            match phase.on_clear {
680                Some(l) => {
681                    put(1 + l.min as u64);
682                    put(l.max as u64);
683                }
684                None => put(0),
685            }
686        }
687        put(self.lulls.len() as u64);
688        for w in &self.lulls {
689            put(w.start);
690            put(w.end);
691        }
692        if self.extended() {
693            put(DIRECTOR_VERSION as u64);
694            put(self.cycles.len() as u64);
695            for c in &self.cycles {
696                put(c.start);
697                put(c.every);
698                put(c.length);
699                put(c.end);
700            }
701            put(self.spot_sets.len() as u64);
702            for set in &self.spot_sets {
703                put(set.len() as u64);
704                for &spot in set {
705                    put(spot as u64);
706                }
707            }
708            for e in self.tables.iter().flat_map(|t| &t.entries) {
709                put(e.spots.map_or(0, |s| 1 + s as u64));
710            }
711            for phase in &self.phases {
712                put(phase.rate_jitter as u32 as u64);
713            }
714        }
715        hash
716    }
717
718    fn extended(&self) -> bool {
719        !self.cycles.is_empty()
720            || !self.spot_sets.is_empty()
721            || self.jittered()
722            || self
723                .tables
724                .iter()
725                .flat_map(|t| &t.entries)
726                .any(|e| e.spots.is_some())
727    }
728
729    fn jittered(&self) -> bool {
730        self.phases.iter().any(|p| p.rate_jitter != 0)
731    }
732
733    fn weights(&self) -> Vec<u32> {
734        self.tables
735            .iter()
736            .flat_map(|t| &t.entries)
737            .map(|e| e.weight)
738            .collect()
739    }
740}
741
742impl Director {
743    pub fn new(schedule: Schedule, seed: u64) -> Result<Self, DirectorError> {
744        schedule.check()?;
745        let root = Rng::new(seed);
746        let mut ids = Vec::new();
747        let mut bases = Vec::with_capacity(schedule.tables.len());
748        let mut flat = 0u32;
749        for (t, table) in schedule.tables.iter().enumerate() {
750            bases.push(flat);
751            for (i, e) in table.entries.iter().enumerate() {
752                ids.push((e.id, flat + i as u32, t as u32));
753            }
754            flat += table.entries.len() as u32;
755        }
756        ids.sort_unstable();
757        let mut sorted = schedule.lulls.clone();
758        sorted.sort_unstable_by_key(|w| w.start);
759        let mut windows: Vec<Window> = Vec::with_capacity(sorted.len());
760        for w in sorted {
761            match windows.last_mut() {
762                Some(last) if w.start <= last.end => last.end = last.end.max(w.end),
763                _ => windows.push(w),
764            }
765        }
766        Ok(Self {
767            fingerprint: schedule.fingerprint(),
768            ids,
769            bases,
770            now: 0,
771            states: vec![PhaseState::default(); schedule.phases.len()],
772            alive: vec![0; flat as usize],
773            table_load: vec![0; schedule.tables.len()],
774            load: 0,
775            timing: root.fork(TIMING),
776            picks: root.fork(PICKS),
777            positions: root.fork(POSITIONS),
778            forms: root.fork(FORMS),
779            spreads: root.fork(SPREADS),
780            jitters: root.fork(JITTERS),
781            next_group: 0,
782            weights: schedule.weights(),
783            jittered: schedule.jittered(),
784            windows,
785            quiet_now: false,
786            quiet_next: 0,
787            schedule,
788        })
789    }
790
791    #[must_use]
792    pub fn schedule(&self) -> &Schedule {
793        &self.schedule
794    }
795
796    #[must_use]
797    pub fn now(&self) -> u64 {
798        self.now
799    }
800
801    #[must_use]
802    pub fn load(&self) -> u64 {
803        self.load
804    }
805
806    #[must_use]
807    pub fn table_load(&self, table: u32) -> u64 {
808        self.table_load.get(table as usize).copied().unwrap_or(0)
809    }
810
811    #[must_use]
812    pub fn alive(&self, id: u32) -> u32 {
813        self.slot(id)
814            .map_or(0, |(flat, _)| self.alive[flat as usize])
815    }
816
817    #[must_use]
818    pub fn is_quiet(&self) -> bool {
819        self.quiet_span(self.now).0
820    }
821
822    #[must_use]
823    pub fn weight(&self, id: u32) -> u32 {
824        self.slot(id)
825            .map_or(0, |(flat, _)| self.weights[flat as usize])
826    }
827
828    pub fn set_weight(&mut self, id: u32, weight: u32) -> Option<u32> {
829        let (flat, _) = self.slot(id)?;
830        Some(std::mem::replace(&mut self.weights[flat as usize], weight))
831    }
832
833    fn slot(&self, id: u32) -> Option<(u32, u32)> {
834        self.ids
835            .binary_search_by_key(&id, |s| s.0)
836            .ok()
837            .map(|i| (self.ids[i].1, self.ids[i].2))
838    }
839
840    fn quiet_span(&self, now: u64) -> (bool, u64) {
841        let i = self.windows.partition_point(|w| w.end <= now);
842        let (mut quiet, mut next) = match self.windows.get(i) {
843            Some(w) if w.start <= now => (true, w.end),
844            Some(w) => (false, w.start),
845            None => (false, u64::MAX),
846        };
847        for c in &self.schedule.cycles {
848            if now < c.start {
849                next = next.min(c.start);
850                continue;
851            }
852            if now >= c.end {
853                continue;
854            }
855            let into = (now - c.start) % c.every;
856            let edge = if into < c.length {
857                quiet = true;
858                c.length - into
859            } else {
860                c.every - into
861            };
862            next = next.min(now.saturating_add(edge)).min(c.end);
863        }
864        (quiet, next)
865    }
866
867    fn draw_need(&mut self, threshold: u64, jitter: i32) -> u64 {
868        let jitter = jitter as i64;
869        let scale = (ONE as i64 + self.jitters.range_i64(-jitter, jitter + 1)) as u128;
870        ((threshold as u128 * scale / ONE as u128) as u64).max(1)
871    }
872
873    pub fn died(&mut self, id: u32, count: u32) -> u32 {
874        let Some((flat, table)) = self.slot(id) else {
875            return 0;
876        };
877        let removed = count.min(self.alive[flat as usize]);
878        self.alive[flat as usize] -= removed;
879        let cost = self.schedule.tables[table as usize].entries
880            [(flat - self.bases[table as usize]) as usize]
881            .cost as u64;
882        let freed = removed as u64 * cost;
883        self.table_load[table as usize] -= freed;
884        self.load -= freed;
885        removed
886    }
887
888    fn draw_lull(&mut self, lull: Lull) -> u64 {
889        if lull.max <= lull.min {
890            lull.min as u64
891        } else {
892            self.timing.range_u64(lull.min as u64, lull.max as u64 + 1)
893        }
894    }
895
896    pub fn tick(&mut self, spots: &Spots, out: &mut Vec<SpawnOrder>) {
897        out.clear();
898        let now = self.now;
899        self.now += 1;
900        if now >= self.quiet_next {
901            (self.quiet_now, self.quiet_next) = self.quiet_span(now);
902        }
903        let quiet = self.quiet_now;
904        for p in 0..self.schedule.phases.len() {
905            let phase = &self.schedule.phases[p];
906            if now < phase.start || now >= phase.end {
907                self.states[p] = PhaseState::default();
908                continue;
909            }
910            if quiet {
911                continue;
912            }
913            let (start, end, table) = (phase.start, phase.end, phase.table as usize);
914            let (tag_filter, on_clear, after) =
915                (phase.tag_filter, phase.on_clear, phase.after_spawns);
916            let (rate, group, speed) = (phase.rate, phase.group, phase.speed);
917            let jitter = phase.rate_jitter;
918            if let Some(lull) = on_clear
919                && self.load == 0
920                && self.states[p].wave_open
921            {
922                self.states[p].wave_open = false;
923                self.states[p].spawned = 0;
924                let ticks = self.draw_lull(lull);
925                self.states[p].lull_until = now + ticks;
926            }
927            if now < self.states[p].lull_until {
928                continue;
929            }
930            let duration = end - start;
931            let elapsed = now - start;
932            let rate_now = rate.at(elapsed, duration).max(0) as u64;
933            let threshold = 60 * self.schedule.tick_rate as u64 * ONE as u64;
934            if jitter > 0 && self.states[p].need == 0 {
935                self.states[p].need = self.draw_need(threshold, jitter);
936            }
937            let mut need = if jitter > 0 {
938                self.states[p].need
939            } else {
940                threshold
941            };
942            self.states[p].charge += rate_now;
943            let mut fired = 0;
944            while self.states[p].charge >= need && fired < MAX_ORDERS_PER_TICK {
945                let group_now = (group.at(elapsed, duration) >> 16).max(1) as u32;
946                let speed_now = speed.at(elapsed, duration).max(0);
947                let Some(members) =
948                    self.build(now, table, tag_filter, group_now, speed_now, spots, out)
949                else {
950                    break;
951                };
952                self.states[p].charge -= need;
953                if jitter > 0 {
954                    need = self.draw_need(threshold, jitter);
955                    self.states[p].need = need;
956                }
957                self.states[p].wave_open = true;
958                self.states[p].spawned = self.states[p].spawned.saturating_add(members);
959                fired += 1;
960                if let Some(after) = after
961                    && after.spawns > 0
962                    && self.states[p].spawned >= after.spawns
963                {
964                    self.states[p].spawned = 0;
965                    let ticks = self.draw_lull(after.lull);
966                    self.states[p].lull_until = now + 1 + ticks;
967                    break;
968                }
969            }
970            self.states[p].charge = self.states[p].charge.min(need);
971        }
972    }
973
974    #[allow(clippy::too_many_arguments)]
975    fn build(
976        &mut self,
977        now: u64,
978        table: usize,
979        tag_filter: u32,
980        group: u32,
981        speed: i32,
982        spots: &Spots,
983        out: &mut Vec<SpawnOrder>,
984    ) -> Option<u32> {
985        let tbl = &self.schedule.tables[table];
986        let room_table = (tbl.cap as u64).saturating_sub(self.table_load[table]);
987        let room_all = (self.schedule.cap as u64).saturating_sub(self.load);
988        let room = room_table.min(room_all);
989        let base = self.bases[table] as usize;
990        let weights = &self.weights[base..base + tbl.entries.len()];
991        let eligible = |e: &Entry, weight: u32| {
992            weight > 0
993                && now >= e.from
994                && (tag_filter == 0 || e.tags & tag_filter != 0)
995                && e.cost as u64 * e.least_members() as u64 <= room
996        };
997        let total: u64 = tbl
998            .entries
999            .iter()
1000            .zip(weights)
1001            .filter(|(e, w)| eligible(e, **w))
1002            .map(|(_, &w)| w as u64)
1003            .sum();
1004        if total == 0 {
1005            return None;
1006        }
1007        let mut target = self.picks.below(total);
1008        let mut chosen = None;
1009        for (i, (e, &w)) in tbl.entries.iter().zip(weights).enumerate() {
1010            if !eligible(e, w) {
1011                continue;
1012            }
1013            if target < w as u64 {
1014                chosen = Some((i, *e));
1015                break;
1016            }
1017            target -= w as u64;
1018        }
1019        let (index, entry) = chosen?;
1020        let fit = if entry.cost == 0 {
1021            u32::MAX
1022        } else {
1023            (room / entry.cost as u64).min(u32::MAX as u64) as u32
1024        };
1025        let (position, spot) = self.pick_position(spots, entry.spots);
1026        let id = self.next_group;
1027        self.next_group = self.next_group.wrapping_add(1);
1028        let members = match entry.line {
1029            None => group.min(fit),
1030            Some(line) => {
1031                let wanted = if line.max > line.min {
1032                    self.forms.range_u64(line.min as u64, line.max as u64 + 1) as u32
1033                } else {
1034                    line.min
1035                };
1036                wanted.min(fit)
1037            }
1038        };
1039        let cost = members as u64 * entry.cost as u64;
1040        self.alive[base + index] += members;
1041        self.table_load[table] += cost;
1042        self.load += cost;
1043        match entry.line {
1044            None => out.push(SpawnOrder {
1045                entry: entry.id,
1046                count: members,
1047                speed,
1048                position,
1049                spot,
1050                group: id,
1051                member: 0,
1052                members,
1053            }),
1054            Some(line) => {
1055                for member in 0..members {
1056                    let at = self.place(&line, position, member, members);
1057                    out.push(SpawnOrder {
1058                        entry: entry.id,
1059                        count: 1,
1060                        speed,
1061                        position: at,
1062                        spot,
1063                        group: id,
1064                        member,
1065                        members,
1066                    });
1067                }
1068            }
1069        }
1070        Some(members)
1071    }
1072
1073    fn place(&mut self, line: &Line, base: [i32; 3], member: u32, members: u32) -> [i32; 3] {
1074        let mut at = base;
1075        for (axis, slot) in at.iter_mut().enumerate() {
1076            let step = line.step(axis);
1077            let along = match line.anchor {
1078                Anchor::Start => step * member as i64,
1079                Anchor::End => step * (member as i64 - (members as i64 - 1)),
1080                Anchor::Centre => step * (2 * member as i64 - (members as i64 - 1)) / 2,
1081            };
1082            let jitter = if line.spread[axis] > 0 {
1083                self.spreads
1084                    .range_i64(-(line.spread[axis] as i64), line.spread[axis] as i64 + 1)
1085            } else {
1086                0
1087            };
1088            *slot =
1089                (base[axis] as i64 + along + jitter).clamp(i32::MIN as i64, i32::MAX as i64) as i32;
1090        }
1091        at
1092    }
1093
1094    fn pick_position(&mut self, spots: &Spots, set: Option<u32>) -> ([i32; 3], u32) {
1095        let total = spots.points.len() + spots.regions.len();
1096        let pick = match set {
1097            None if total == 0 => return ([0; 3], NO_SPOT),
1098            None => self.positions.index(total),
1099            Some(set) => {
1100                let members = &self.schedule.spot_sets[set as usize];
1101                let open = members.iter().filter(|&&m| (m as usize) < total);
1102                let count = open.clone().count();
1103                if count == 0 {
1104                    return ([0; 3], NO_SPOT);
1105                }
1106                let nth = self.positions.index(count);
1107                open.copied().nth(nth).unwrap_or(NO_SPOT) as usize
1108            }
1109        };
1110        if pick < spots.points.len() {
1111            return (spots.points[pick], pick as u32);
1112        }
1113        let region = spots.regions[pick - spots.points.len()];
1114        let mut at = [0; 3];
1115        for (axis, slot) in at.iter_mut().enumerate() {
1116            *slot = if region.max[axis] > region.min[axis] {
1117                self.positions.range_i32(region.min[axis], region.max[axis])
1118            } else {
1119                region.min[axis]
1120            };
1121        }
1122        (at, pick as u32)
1123    }
1124
1125    fn state_len(&self, version: u32) -> usize {
1126        let plain = 4
1127            + 8
1128            + 8
1129            + 4
1130            + self.states.len() * PHASE_BYTES
1131            + self.alive.len() * 4
1132            + 5 * STATE_BYTES;
1133        if version == DIRECTOR_PLAIN_VERSION {
1134            return plain;
1135        }
1136        let jitter = if self.jittered {
1137            self.states.len() * 8 + STATE_BYTES
1138        } else {
1139            0
1140        };
1141        plain + self.weights.len() * 4 + jitter
1142    }
1143
1144    fn save_version(&self) -> u32 {
1145        if self.jittered || self.weights != self.schedule.weights() {
1146            DIRECTOR_VERSION
1147        } else {
1148            DIRECTOR_PLAIN_VERSION
1149        }
1150    }
1151
1152    #[must_use]
1153    pub fn save(&self) -> Vec<u8> {
1154        let version = self.save_version();
1155        let mut out = Vec::with_capacity(self.state_len(version));
1156        out.extend_from_slice(&version.to_le_bytes());
1157        out.extend_from_slice(&self.fingerprint.to_le_bytes());
1158        out.extend_from_slice(&self.now.to_le_bytes());
1159        out.extend_from_slice(&self.next_group.to_le_bytes());
1160        for s in &self.states {
1161            out.extend_from_slice(&s.charge.to_le_bytes());
1162            out.extend_from_slice(&s.spawned.to_le_bytes());
1163            out.push(s.wave_open as u8);
1164            out.extend_from_slice(&s.lull_until.to_le_bytes());
1165        }
1166        for a in &self.alive {
1167            out.extend_from_slice(&a.to_le_bytes());
1168        }
1169        out.extend_from_slice(&self.timing.save());
1170        out.extend_from_slice(&self.picks.save());
1171        out.extend_from_slice(&self.positions.save());
1172        out.extend_from_slice(&self.forms.save());
1173        out.extend_from_slice(&self.spreads.save());
1174        if version == DIRECTOR_VERSION {
1175            for w in &self.weights {
1176                out.extend_from_slice(&w.to_le_bytes());
1177            }
1178            if self.jittered {
1179                for s in &self.states {
1180                    out.extend_from_slice(&s.need.to_le_bytes());
1181                }
1182                out.extend_from_slice(&self.jitters.save());
1183            }
1184        }
1185        out
1186    }
1187
1188    pub fn restore(&mut self, bytes: &[u8]) -> Result<(), DirectorError> {
1189        let Some(head) = bytes.get(..4) else {
1190            return Err(DirectorError::Length {
1191                found: bytes.len(),
1192                expected: self.state_len(self.save_version()),
1193            });
1194        };
1195        let version = u32::from_le_bytes(head.try_into().unwrap());
1196        if version != DIRECTOR_PLAIN_VERSION && version != DIRECTOR_VERSION {
1197            return Err(DirectorError::Version { found: version });
1198        }
1199        let layout = if self.jittered {
1200            DIRECTOR_VERSION
1201        } else {
1202            version
1203        };
1204        let expected = self.state_len(layout);
1205        if bytes.len() != expected {
1206            return Err(DirectorError::Length {
1207                found: bytes.len(),
1208                expected,
1209            });
1210        }
1211        let u32_at = |at: usize| u32::from_le_bytes(bytes[at..at + 4].try_into().unwrap());
1212        let u64_at = |at: usize| u64::from_le_bytes(bytes[at..at + 8].try_into().unwrap());
1213        if u64_at(4) != self.fingerprint {
1214            return Err(DirectorError::Mismatch);
1215        }
1216        let mut at = 24;
1217        let mut states = Vec::with_capacity(self.states.len());
1218        for _ in 0..self.states.len() {
1219            states.push(PhaseState {
1220                charge: u64_at(at),
1221                spawned: u32_at(at + 8),
1222                wave_open: bytes[at + 12] != 0,
1223                lull_until: u64_at(at + 13),
1224                need: 0,
1225            });
1226            at += PHASE_BYTES;
1227        }
1228        let mut alive = Vec::with_capacity(self.alive.len());
1229        for _ in 0..self.alive.len() {
1230            alive.push(u32_at(at));
1231            at += 4;
1232        }
1233        let rng = |at: usize| {
1234            Rng::restore(&bytes[at..at + STATE_BYTES]).map_err(|_| DirectorError::Corrupt)
1235        };
1236        let timing = rng(at)?;
1237        let picks = rng(at + STATE_BYTES)?;
1238        let positions = rng(at + 2 * STATE_BYTES)?;
1239        let forms = rng(at + 3 * STATE_BYTES)?;
1240        let spreads = rng(at + 4 * STATE_BYTES)?;
1241        at += 5 * STATE_BYTES;
1242        let mut weights = self.schedule.weights();
1243        let mut jitters = self.jitters.clone();
1244        if layout == DIRECTOR_VERSION {
1245            for w in &mut weights {
1246                *w = u32_at(at);
1247                at += 4;
1248            }
1249            if self.jittered {
1250                for s in &mut states {
1251                    s.need = u64_at(at);
1252                    at += 8;
1253                }
1254                jitters = rng(at)?;
1255            }
1256        }
1257        self.now = u64_at(12);
1258        self.next_group = u32_at(20);
1259        self.states = states;
1260        self.alive = alive;
1261        self.timing = timing;
1262        self.picks = picks;
1263        self.positions = positions;
1264        self.forms = forms;
1265        self.spreads = spreads;
1266        self.jitters = jitters;
1267        self.weights = weights;
1268        self.quiet_now = false;
1269        self.quiet_next = 0;
1270        self.table_load.iter_mut().for_each(|l| *l = 0);
1271        self.load = 0;
1272        for (t, table) in self.schedule.tables.iter().enumerate() {
1273            let base = self.bases[t] as usize;
1274            for (i, e) in table.entries.iter().enumerate() {
1275                let cost = self.alive[base + i] as u64 * e.cost as u64;
1276                self.table_load[t] += cost;
1277                self.load += cost;
1278            }
1279        }
1280        Ok(())
1281    }
1282}