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}