nmarshall commited on
Commit
15d7fca
·
1 Parent(s): 447b0d2

gameplay: compose intermission state machine

Browse files
crates/urt-gameplay/src/bin/urt-intermission.rs CHANGED
@@ -11,7 +11,7 @@ fn run() -> Result<String, String> {
11
  }
12
  let n = w[14] as usize;
13
  let nr = w[15] as usize;
14
- if n > 64 || nr > 64 || w.len() != 16 + 11 * n + nr {
15
  return Err("invalid record count".into());
16
  }
17
  let mut state = State {
@@ -29,8 +29,8 @@ fn run() -> Result<String, String> {
29
  client_count: w[12] as usize,
30
  rank_count: w[13] as i32,
31
  };
32
- let mut clients = w[16..16 + 11 * n]
33
- .chunks_exact(11)
34
  .map(|v| Client {
35
  connection: v[0] as i32,
36
  id: v[1] as i32,
@@ -43,9 +43,13 @@ fn run() -> Result<String, String> {
43
  ready: v[8] as i32,
44
  entity_flags: v[9],
45
  field4324: v[10] as i32,
 
 
 
 
46
  })
47
  .collect::<Vec<_>>();
48
- let rank = w[16 + 11 * n..]
49
  .iter()
50
  .map(|v| *v as usize)
51
  .collect::<Vec<_>>();
@@ -107,6 +111,123 @@ fn run() -> Result<String, String> {
107
  state.pending as u32
108
  ))
109
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
110
  _ => Err("unknown command".into()),
111
  }
112
  }
 
11
  }
12
  let n = w[14] as usize;
13
  let nr = w[15] as usize;
14
+ if n > 64 || nr > 64 || w.len() != 16 + 15 * n + nr + if kind == "flow" { 9 } else { 0 } {
15
  return Err("invalid record count".into());
16
  }
17
  let mut state = State {
 
29
  client_count: w[12] as usize,
30
  rank_count: w[13] as i32,
31
  };
32
+ let mut clients = w[16..16 + 15 * n]
33
+ .chunks_exact(15)
34
  .map(|v| Client {
35
  connection: v[0] as i32,
36
  id: v[1] as i32,
 
43
  ready: v[8] as i32,
44
  entity_flags: v[9],
45
  field4324: v[10] as i32,
46
+ field304: v[11] as i32,
47
+ field3692: v[12] as i32,
48
+ field3700: v[13] as i32,
49
+ packed: v[14],
50
  })
51
  .collect::<Vec<_>>();
52
+ let rank = w[16 + 15 * n..16 + 15 * n + nr]
53
  .iter()
54
  .map(|v| *v as usize)
55
  .collect::<Vec<_>>();
 
111
  state.pending as u32
112
  ))
113
  }
114
+ "flow" => {
115
+ let v = &w[16 + 15 * n + nr..];
116
+ let timer = urt_gameplay::exit_rules::TimerState {
117
+ mode: state.mode,
118
+ now: state.now,
119
+ client_count: state.client_count,
120
+ round_limit: v[0] as i32,
121
+ round_active: v[1] != 0,
122
+ round_count: v[2] as i32,
123
+ countdown: v[3] as i32,
124
+ time_limit: v[4] as i32,
125
+ paused: v[5] != 0,
126
+ start: v[6] as i32,
127
+ };
128
+ let limits = urt_gameplay::exit_rules::LimitState {
129
+ mode: state.mode,
130
+ playing: state.playing,
131
+ max_clients: state.max_clients,
132
+ scores: state.scores,
133
+ score_limit: v[7] as i32,
134
+ team_limit: v[8] as i32,
135
+ };
136
+ let entities = (100..164).collect::<Vec<u32>>();
137
+ let r = advance_exit(&mut state, timer, limits, &mut clients, &rank, &entities)
138
+ .map_err(|e| format!("{e:?}"))?;
139
+ use urt_gameplay::exit_rules::{Decision, LimitHit};
140
+ let kind = match r.decision {
141
+ Decision::Blocked => "blocked",
142
+ Decision::Wait => "wait",
143
+ Decision::Transition => "transition",
144
+ Decision::Class(_) => "class",
145
+ Decision::Predicate => "predicate",
146
+ Decision::Round => "round",
147
+ Decision::Time => "time",
148
+ Decision::None => "none",
149
+ Decision::Limit(h) => match h {
150
+ LimitHit::LowFirst => "low_first",
151
+ LimitHit::LowSecond => "low_second",
152
+ LimitHit::HighFirst => "high_first",
153
+ LimitHit::HighSecond => "high_second",
154
+ LimitHit::Client(_) => "client",
155
+ },
156
+ };
157
+ let ready = r
158
+ .readiness
159
+ .map(|v| v.exit.to_string())
160
+ .unwrap_or("null".into());
161
+ let begin = r
162
+ .begin
163
+ .iter()
164
+ .map(|e| match e {
165
+ BeginEffect::Prepare => "[6717,-1]".into(),
166
+ BeginEffect::SlowCvar => "[-6,-1]".into(),
167
+ BeginEffect::SlowSetup => "[156523,-1]".into(),
168
+ BeginEffect::Respawn(j) => format!("[175123,{j}]"),
169
+ BeginEffect::Move(j) => format!("[6575,{j}]"),
170
+ BeginEffect::Publish => "[6328,-1]".into(),
171
+ })
172
+ .collect::<Vec<String>>()
173
+ .join(",");
174
+ let log = r
175
+ .log
176
+ .map(|v| {
177
+ let team = v
178
+ .team_scores
179
+ .map(|v| format!("[{},{}]", v[0], v[1]))
180
+ .unwrap_or("null".into());
181
+ let rows = v
182
+ .rows
183
+ .iter()
184
+ .map(|v| format!("[{},{},{}]", v.score, v.ping, v.client))
185
+ .collect::<Vec<_>>()
186
+ .join(",");
187
+ format!("{{\"team_scores\":{team},\"rows\":[{rows}]}}")
188
+ })
189
+ .unwrap_or("null".into());
190
+ let c = clients
191
+ .iter()
192
+ .map(|c| {
193
+ format!(
194
+ "[{}, {}, {}, {}, {}, {}, {}]",
195
+ c.id as u32,
196
+ c.field4324 as u32,
197
+ c.field304 as u32,
198
+ c.field308 as u32,
199
+ c.field3692 as u32,
200
+ c.field3700 as u32,
201
+ c.packed
202
+ )
203
+ })
204
+ .collect::<Vec<_>>()
205
+ .join(",");
206
+ let resets = r
207
+ .resets
208
+ .iter()
209
+ .map(|v| {
210
+ format!(
211
+ "[{}, {}, {}, {}, {}, {}, {}]",
212
+ v.client,
213
+ v.entity,
214
+ v.owner,
215
+ v.time as u32,
216
+ v.kind,
217
+ v.flags_or,
218
+ v.logged_value
219
+ )
220
+ })
221
+ .collect::<Vec<_>>()
222
+ .join(",");
223
+ Ok(format!(
224
+ "{{\"kind\":\"{kind}\",\"state\":[{},{},{},{}],\"clients\":[{c}],\"ready_exit\":{ready},\"begin\":[{begin}],\"log\":{log},\"resets\":[{resets}]}}",
225
+ state.blocked as u32,
226
+ state.pending as u32,
227
+ state.waiting as u32,
228
+ state.wait_start as u32
229
+ ))
230
+ }
231
  _ => Err("unknown command".into()),
232
  }
233
  }
crates/urt-gameplay/src/intermission.rs CHANGED
@@ -12,6 +12,10 @@ pub struct Client {
12
  pub ready: i32,
13
  pub entity_flags: u32,
14
  pub field4324: i32,
 
 
 
 
15
  }
16
  #[derive(Clone, Debug, Default, PartialEq, Eq)]
17
  pub struct State {
@@ -33,6 +37,8 @@ pub struct State {
33
  pub enum Error {
34
  InvalidCount,
35
  InvalidRank,
 
 
36
  }
37
  fn count(clients: &[Client], n: usize) -> Result<(), Error> {
38
  if clients.len() > 64 || n > clients.len() {
@@ -373,3 +379,179 @@ mod log_tests {
373
  assert_eq!(s, old);
374
  }
375
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
12
  pub ready: i32,
13
  pub entity_flags: u32,
14
  pub field4324: i32,
15
+ pub field304: i32,
16
+ pub field3692: i32,
17
+ pub field3700: i32,
18
+ pub packed: u32,
19
  }
20
  #[derive(Clone, Debug, Default, PartialEq, Eq)]
21
  pub struct State {
 
37
  pub enum Error {
38
  InvalidCount,
39
  InvalidRank,
40
+ InvalidState,
41
+ InvalidPolicy,
42
  }
43
  fn count(clients: &[Client], n: usize) -> Result<(), Error> {
44
  if clients.len() > 64 || n > clients.len() {
 
379
  assert_eq!(s, old);
380
  }
381
  }
382
+
383
+ #[derive(Clone, Debug, PartialEq, Eq)]
384
+ pub struct Flow {
385
+ pub decision: crate::exit_rules::Decision,
386
+ pub readiness: Option<Readiness>,
387
+ pub begin: Vec<BeginEffect>,
388
+ pub log: Option<ExitLog>,
389
+ pub resets: Vec<crate::exit_rules::Reset>,
390
+ }
391
+ /// Atomic safe composition of the recovered exit body and intermission children.
392
+ /// World transitions, camera/respawn work, allocation, publication and I/O remain intentions.
393
+ pub fn advance_exit(
394
+ state: &mut State,
395
+ timer: crate::exit_rules::TimerState,
396
+ limits: crate::exit_rules::LimitState,
397
+ clients: &mut [Client],
398
+ rank: &[usize],
399
+ entity_ids: &[u32],
400
+ ) -> Result<Flow, Error> {
401
+ if timer.mode != state.mode
402
+ || timer.now != state.now
403
+ || timer.client_count != state.client_count
404
+ || limits.mode != state.mode
405
+ || limits.playing != state.playing
406
+ || limits.max_clients != state.max_clients
407
+ || limits.scores != state.scores
408
+ {
409
+ return Err(Error::InvalidState);
410
+ }
411
+ let mut staged = state.clone();
412
+ let mut c = clients.to_vec();
413
+ let mut raw = c
414
+ .iter()
415
+ .map(|c| crate::exit_rules::Client {
416
+ connection: c.connection,
417
+ team: c.team,
418
+ class: c.class,
419
+ id: c.id,
420
+ score: c.score,
421
+ field304: c.field304,
422
+ field308: c.field308,
423
+ packed: c.packed,
424
+ field3692: c.field3692,
425
+ field3700: c.field3700,
426
+ })
427
+ .collect::<Vec<_>>();
428
+ let mut pending = staged.pending;
429
+ let prefix = crate::exit_prefix::advance(
430
+ staged.blocked != 0,
431
+ &mut pending,
432
+ staged.slow != 0,
433
+ staged.now,
434
+ );
435
+ let predicate = if prefix == crate::exit_prefix::Action::ContinueModeRules
436
+ && crate::exit_rules::class_guard(staged.mode, &raw, staged.max_clients)
437
+ .map_err(|_| Error::InvalidPolicy)?
438
+ .is_none()
439
+ {
440
+ tied(&staged, &c, rank)?
441
+ } else {
442
+ false
443
+ };
444
+ let mut rule = crate::exit_rules::State {
445
+ blocked: staged.blocked != 0,
446
+ pending: staged.pending,
447
+ slow: staged.slow != 0,
448
+ predicate,
449
+ timer,
450
+ limits,
451
+ };
452
+ let out = crate::exit_rules::advance(&mut rule, &mut raw, entity_ids)
453
+ .map_err(|_| Error::InvalidPolicy)?;
454
+ staged.pending = rule.pending;
455
+ for (c, r) in c.iter_mut().zip(raw) {
456
+ c.field304 = r.field304;
457
+ c.field308 = r.field308;
458
+ c.packed = r.packed;
459
+ c.field3692 = r.field3692;
460
+ c.field3700 = r.field3700;
461
+ }
462
+ let mut flow = Flow {
463
+ decision: out.decision,
464
+ readiness: None,
465
+ begin: vec![],
466
+ log: None,
467
+ resets: out.resets,
468
+ };
469
+ use crate::exit_rules::Decision;
470
+ match flow.decision {
471
+ Decision::Blocked => flow.readiness = Some(readiness(&mut staged, &c)?),
472
+ Decision::Transition => flow.begin = begin(&mut staged, &mut c)?,
473
+ Decision::Class(_) | Decision::Round | Decision::Time | Decision::Limit(_) => {
474
+ flow.log = Some(log_exit(&mut staged, &c, rank)?)
475
+ }
476
+ _ => {}
477
+ }
478
+ *state = staged;
479
+ clients.copy_from_slice(&c);
480
+ Ok(flow)
481
+ }
482
+ #[cfg(test)]
483
+ mod flow_tests {
484
+ use super::*;
485
+ fn timer(s: &State) -> crate::exit_rules::TimerState {
486
+ crate::exit_rules::TimerState {
487
+ mode: s.mode,
488
+ now: s.now,
489
+ client_count: s.client_count,
490
+ ..Default::default()
491
+ }
492
+ }
493
+ fn limits(s: &State) -> crate::exit_rules::LimitState {
494
+ crate::exit_rules::LimitState {
495
+ mode: s.mode,
496
+ playing: s.playing,
497
+ max_clients: s.max_clients,
498
+ scores: s.scores,
499
+ score_limit: 1,
500
+ team_limit: 1,
501
+ }
502
+ }
503
+ #[test]
504
+ fn tied_blocks_limit_and_log_updates_pending() {
505
+ let mut s = State {
506
+ now: 60000,
507
+ playing: 2,
508
+ enabled: 1,
509
+ max_clients: 2,
510
+ client_count: 2,
511
+ rank_count: 2,
512
+ scores: [2, 2],
513
+ ..State::default()
514
+ };
515
+ let mut c = [Client {
516
+ connection: 2,
517
+ score: 3,
518
+ ..Client::default()
519
+ }; 2];
520
+ let t = timer(&s);
521
+ let l = limits(&s);
522
+ let r = advance_exit(&mut s, t, l, &mut c, &[0, 1], &[100, 101]).unwrap();
523
+ assert_eq!(r.decision, crate::exit_rules::Decision::Predicate);
524
+ s.enabled = 0;
525
+ let t = timer(&s);
526
+ let l = limits(&s);
527
+ let r = advance_exit(&mut s, t, l, &mut c, &[0, 1], &[100, 101]).unwrap();
528
+ assert!(r.log.is_some());
529
+ assert_eq!(s.pending, 59950);
530
+ }
531
+ #[test]
532
+ fn invalid_composition_is_atomic() {
533
+ let mut s = State {
534
+ now: 60000,
535
+ playing: 2,
536
+ max_clients: 1,
537
+ client_count: 1,
538
+ rank_count: 1,
539
+ scores: [2, 0],
540
+ ..State::default()
541
+ };
542
+ let mut c = [Client {
543
+ connection: 2,
544
+ ..Client::default()
545
+ }];
546
+ let old = s.clone();
547
+ let before = c;
548
+ let t = timer(&s);
549
+ let l = limits(&s);
550
+ assert_eq!(
551
+ advance_exit(&mut s, t, l, &mut c, &[99], &[100]),
552
+ Err(Error::InvalidRank)
553
+ );
554
+ assert_eq!(s, old);
555
+ assert_eq!(c, before);
556
+ }
557
+ }
docs/FINDINGS.md CHANGED
@@ -1526,3 +1526,6 @@ RE-813–822 replaces the scoreboard decimal assumption with actual37914 executi
1526
 
1527
 
1528
  **RE-842:** 150 continuous 7805 fixtures now execute actual tie, readiness, begin-transition and exit-log children on shared world memory with separate child frames. Independent policy and nested ABI checks cover conflicting gates, client mutations and publication intentions. Lower-level next-map/respawn/camera/I/O and allocation still remain captured. [Evidence](research/QVM_INTERMISSION_FLOW.md).
 
 
 
 
1526
 
1527
 
1528
  **RE-842:** 150 continuous 7805 fixtures now execute actual tie, readiness, begin-transition and exit-log children on shared world memory with separate child frames. Independent policy and nested ABI checks cover conflicting gates, client mutations and publication intentions. Lower-level next-map/respawn/camera/I/O and allocation still remain captured. [Evidence](research/QVM_INTERMISSION_FLOW.md).
1529
+
1530
+
1531
+ **RE-843:** Rust composes recovered tie, readiness, begin and exit-log policies with prior exit timers/limits across150 continuous source fixtures. Atomic staged updates preserve shared pending/client state; reset and lower-level world/I/O effects remain explicit intentions. Source branch labels are independently classified by the tested composition oracle; captured requests and actual final source state supply effect parity. [Evidence](research/RUST_INTERMISSION_FLOW.md).
docs/PROVISIONAL_TODO.md CHANGED
@@ -1514,7 +1514,7 @@ RE-832: Complete7805 execution now composes prefix, class, supplied predicate, r
1514
  RE-833: Rust now composes the full recovered7805 priority and matches438 continuous source cases, including pending clears and mode9 reset/event intentions. Full runtime completion remains open. [Evidence](research/RUST_EXIT_COMPLETE.md).
1515
 
1516
 
1517
- - [ ] RE-834: Recover supplied exit predicate/transition/native effects, compose ranking→exit→scoreboard state, and validate actual runtime traces and macOS/Linux rendering. [Batch scope](research/QVM_FLOAT_EXIT_RULES_BATCH.md).
1518
 
1519
 
1520
  RE-834: Actual 7662 and child 33715 execute across 168 cases. Playing count and the enabled global gate ties; team-classified modes compare global scores, mode9 uses ranked field308, mode11 field696, and other modes field248. Raw numeric names remain conservative; ranked pointer legality and native execution remain open. Full runtime completion remains open. [Evidence](research/QVM_EXIT_PREDICATE.md).
@@ -1542,3 +1542,9 @@ RE-841: Rust matches 83 original exit-log cases with signed ping clamp, negative
1542
 
1543
 
1544
  RE-842: 150 continuous 7805 fixtures now execute actual tie, readiness, begin-transition and exit-log children on shared world memory with separate child frames. Independent policy and nested ABI checks cover conflicting gates, client mutations and publication intentions. Lower-level next-map/respawn/camera/I/O and allocation still remain captured. Full runtime completion remains open. [Evidence](research/QVM_INTERMISSION_FLOW.md).
 
 
 
 
 
 
 
1514
  RE-833: Rust now composes the full recovered7805 priority and matches438 continuous source cases, including pending clears and mode9 reset/event intentions. Full runtime completion remains open. [Evidence](research/RUST_EXIT_COMPLETE.md).
1515
 
1516
 
1517
+ - [x] RE-834–843: Recover exit predicate and intermission child policies; remaining native/runtime scope is tracked under RE-844. [Batch evidence](research/QVM_INTERMISSION_BATCH.md).
1518
 
1519
 
1520
  RE-834: Actual 7662 and child 33715 execute across 168 cases. Playing count and the enabled global gate ties; team-classified modes compare global scores, mode9 uses ranked field308, mode11 field696, and other modes field248. Raw numeric names remain conservative; ranked pointer legality and native execution remain open. Full runtime completion remains open. [Evidence](research/QVM_EXIT_PREDICATE.md).
 
1542
 
1543
 
1544
  RE-842: 150 continuous 7805 fixtures now execute actual tie, readiness, begin-transition and exit-log children on shared world memory with separate child frames. Independent policy and nested ABI checks cover conflicting gates, client mutations and publication intentions. Lower-level next-map/respawn/camera/I/O and allocation still remain captured. Full runtime completion remains open. [Evidence](research/QVM_INTERMISSION_FLOW.md).
1545
+
1546
+
1547
+ RE-843: Rust composes recovered tie, readiness, begin and exit-log policies with prior exit timers/limits across150 continuous source fixtures. Atomic staged updates preserve shared pending/client state; reset and lower-level world/I/O effects remain explicit intentions. Source branch labels are independently classified by the tested composition oracle; captured requests and actual final source state supply effect parity. Full runtime completion remains open. [Evidence](research/RUST_INTERMISSION_FLOW.md).
1548
+
1549
+
1550
+ - [ ] RE-844: Recover next-map/respawn/camera/publication/event allocation effects, broaden pointer/identity fixtures, and integrate ranking→intermission→scoreboard runtime traces. [Scope](research/QVM_INTERMISSION_BATCH.md).
docs/RESEARCH_LEDGER.md CHANGED
@@ -1884,3 +1884,12 @@ Rust matches 83 original exit-log cases with signed ping clamp, negative/zero/ov
1884
  ## 2026-10-27 — RE-842 Original exit flow composed with recovered intermission children
1885
 
1886
  150 continuous 7805 fixtures now execute actual tie, readiness, begin-transition and exit-log children on shared world memory with separate child frames. Independent policy and nested ABI checks cover conflicting gates, client mutations and publication intentions. Lower-level next-map/respawn/camera/I/O and allocation still remain captured. Focused `cargo xtask test-group intermission-flow` passes (5 Python tests); selected reports regenerate twice deterministically, with storage, documentation links and whitespace guards. Full suite deferred to task ten. Original assets unchanged; source fixtures do not establish complete native/runtime parity.
 
 
 
 
 
 
 
 
 
 
1884
  ## 2026-10-27 — RE-842 Original exit flow composed with recovered intermission children
1885
 
1886
  150 continuous 7805 fixtures now execute actual tie, readiness, begin-transition and exit-log children on shared world memory with separate child frames. Independent policy and nested ABI checks cover conflicting gates, client mutations and publication intentions. Lower-level next-map/respawn/camera/I/O and allocation still remain captured. Focused `cargo xtask test-group intermission-flow` passes (5 Python tests); selected reports regenerate twice deterministically, with storage, documentation links and whitespace guards. Full suite deferred to task ten. Original assets unchanged; source fixtures do not establish complete native/runtime parity.
1887
+
1888
+
1889
+ ## 2026-10-27 — RE-843 Rust composed exit and intermission state machine
1890
+
1891
+ Rust composes recovered tie, readiness, begin and exit-log policies with prior exit timers/limits across150 continuous source fixtures. Atomic staged updates preserve shared pending/client state; reset and lower-level world/I/O effects remain explicit intentions. Source branch labels are independently classified by the tested composition oracle; captured requests and actual final source state supply effect parity. Focused `cargo xtask test-group rust-intermission-flow` passes (9 Python tests); selected reports regenerate twice deterministically, with storage, documentation links and whitespace guards. Full suite deferred to task ten. Original assets unchanged; source fixtures do not establish complete native/runtime parity.
1892
+
1893
+ RE-843 final review: targeted xtask registry regression passes for the intermission module and all five source-to-Rust report dependencies. Added batch evidence, validation cadence and RE-844 lower-level/runtime follow-ups.
1894
+
1895
+ Batch RE-834–843 boundary: the single `cargo xtask test-full --since 5f7b2df8` run passed after task ten: 1,648 Python tests, 137 Rust tests (assets 10, gameplay 99, xtask 28), 45 deterministic report producers, storage policy, 403 documentation files and whitespace checks. No generated report drift; validation notes amended into task ten. Original assets unchanged.
docs/REVERSE_ENGINEERING_TODO.md CHANGED
@@ -1551,7 +1551,7 @@ Purpose: turn the supplied installation into an evidence-backed specification of
1551
  - [x] RE-833 Rust complete recovered exit-rule decision and mutation composition ([evidence](research/RUST_EXIT_COMPLETE.md)).
1552
 
1553
 
1554
- - [ ] RE-834: Recover supplied exit predicate/transition/native effects, compose ranking→exit→scoreboard state, and validate actual runtime traces and macOS/Linux rendering. [Batch scope](research/QVM_FLOAT_EXIT_RULES_BATCH.md).
1555
 
1556
 
1557
  - [x] RE-834 Original exit tie predicate and mode classifier ([evidence](research/QVM_EXIT_PREDICATE.md)).
@@ -1579,3 +1579,9 @@ Purpose: turn the supplied installation into an evidence-backed specification of
1579
 
1580
 
1581
  - [x] RE-842 Original exit flow composed with recovered intermission children ([evidence](research/QVM_INTERMISSION_FLOW.md)).
 
 
 
 
 
 
 
1551
  - [x] RE-833 Rust complete recovered exit-rule decision and mutation composition ([evidence](research/RUST_EXIT_COMPLETE.md)).
1552
 
1553
 
1554
+ - [x] RE-834–843: Recover exit predicate and intermission child policies; remaining native/runtime scope is tracked under RE-844. [Batch evidence](research/QVM_INTERMISSION_BATCH.md).
1555
 
1556
 
1557
  - [x] RE-834 Original exit tie predicate and mode classifier ([evidence](research/QVM_EXIT_PREDICATE.md)).
 
1579
 
1580
 
1581
  - [x] RE-842 Original exit flow composed with recovered intermission children ([evidence](research/QVM_INTERMISSION_FLOW.md)).
1582
+
1583
+
1584
+ - [x] RE-843 Rust composed exit and intermission state machine ([evidence](research/RUST_INTERMISSION_FLOW.md)).
1585
+
1586
+
1587
+ - [ ] RE-844: Recover next-map/respawn/camera/publication/event allocation effects, broaden pointer/identity fixtures, and integrate ranking→intermission→scoreboard runtime traces. [Scope](research/QVM_INTERMISSION_BATCH.md).
docs/TESTING.md CHANGED
@@ -133,3 +133,8 @@ Use `cargo xtask test-group <name>` with `format-string`, `format-integer`, `for
133
  ## Float formatter and complete exit-rule groups
134
 
135
  Use `cargo xtask test-group <name>` with `format-float`, `rust-format-float`, `exit-class`, `rust-exit-class`, `exit-limits`, `rust-exit-limits`, `exit-timers`, `rust-exit-timers`, `exit-complete` or `rust-exit-complete`. The registry regression verifies each source-report dependency selects its Rust adapter and test. Scoped gameplay checks refresh test-count metadata. Keep focused validation before each commit and one full run after ten commits. See the [batch evidence and remaining effects](research/QVM_FLOAT_EXIT_RULES_BATCH.md).
 
 
 
 
 
 
133
  ## Float formatter and complete exit-rule groups
134
 
135
  Use `cargo xtask test-group <name>` with `format-float`, `rust-format-float`, `exit-class`, `rust-exit-class`, `exit-limits`, `rust-exit-limits`, `exit-timers`, `rust-exit-timers`, `exit-complete` or `rust-exit-complete`. The registry regression verifies each source-report dependency selects its Rust adapter and test. Scoped gameplay checks refresh test-count metadata. Keep focused validation before each commit and one full run after ten commits. See the [batch evidence and remaining effects](research/QVM_FLOAT_EXIT_RULES_BATCH.md).
136
+
137
+
138
+ ## Exit predicate and intermission groups
139
+
140
+ Focused source/Rust pairs: `exit-predicate`/`rust-exit-predicate`, `intermission-ready`/`rust-intermission-ready`, `intermission-begin`/`rust-intermission-begin`, `exit-log`/`rust-exit-log`, and `intermission-flow`/`rust-intermission-flow`. Run `cargo xtask test-group <name>` before the task commit. The scoped intermission executable has its own adapter inputs; common module changes select every intermission consumer and scoped gameplay test-count metadata. The registry regression verifies source-report edges and module selection. Run one `test-full --since <batch-base>` after ten commits. See the [batch contracts and boundaries](research/QVM_INTERMISSION_BATCH.md).
docs/research/QVM_INTERMISSION_BATCH.md ADDED
@@ -0,0 +1,44 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Exit predicate and intermission composition: RE-834–843
2
+
3
+ This batch replaces four supplied children of the recovered exit body with original QVM instruction execution and matching Rust policies. It advances the bounded lifecycle model; it does not establish a running game or native runtime parity.
4
+
5
+ | Task | Original evidence or Rust port |
6
+ | --- | --- |
7
+ | RE-834 | [7662 tie predicate and 33715 classifier](QVM_EXIT_PREDICATE.md) |
8
+ | RE-835 | [Rust ranked tie predicate](RUST_EXIT_PREDICATE.md) |
9
+ | RE-836 | [7525 readiness and timeout](QVM_INTERMISSION_READY.md) |
10
+ | RE-837 | [Rust readiness policy](RUST_INTERMISSION_READY.md) |
11
+ | RE-838 | [6825 begin-intermission state](QVM_INTERMISSION_BEGIN.md) |
12
+ | RE-839 | [Rust begin state and intentions](RUST_INTERMISSION_BEGIN.md) |
13
+ | RE-840 | [7367 exit logging and publication](QVM_EXIT_LOG.md) |
14
+ | RE-841 | [Rust exit log projection](RUST_EXIT_LOG.md) |
15
+ | RE-842 | [Continuous exit body with recovered children](QVM_INTERMISSION_FLOW.md) |
16
+ | RE-843 | [Rust composed exit/intermission API](RUST_INTERMISSION_FLOW.md) |
17
+
18
+ ## Findings
19
+
20
+ The tie predicate uses signed mode classification: modes below 3 and modes 9/11 are not team-classified. It requires at least two playing clients and nonzero enabled state. Team-classified modes compare global scores; mode 9 compares ranked field 308, mode 11 field 696, and other modes field 248. There are 168 source fixtures.
21
+
22
+ Readiness scans configured slots, excludes disconnected clients and entity flag 8, counts nonzero ready fields and computes a first 16-slot mask. It uses signed comparisons against wrapped timestamp additions: blocked + 5000, then wait_start + 10000. An empty eligible set passes the all-ready test. The mask/counts are local values; this body does not publish them. There are 131 source fixtures.
23
+
24
+ Begin-intermission uses the raw blocked timestamp as a once-only guard. It stores the current clock, optionally sets ui_singlePlayerActive to 0 and requests additional setup, then visits connected collected clients. Mismatched IDs become field 6156; entity-client field 4324 clears before respawn/move intentions. Publication follows the loop. Zero current time leaves the raw guard zero. There are 39 source fixtures.
25
+
26
+ Exit logging writes pending=now−50, requests publication and configstring 22 before optional team scores and ranked rows. It caps the rank count at 32, excludes team 3 and non-state 2 records, and applies min(ping,999), preserving negative ping. The original strings identify Exit, red/blue scores, and score/ping/client/name rows. There are 83 source fixtures.
27
+
28
+ The final 150 source fixtures execute 7805 with actual 7662/33715,7525,6825 and7367, using isolated child frames and shared world memory. Independent tests check priority, exact nested request ABI/order and final world/client mutations. Rust composes these policies with the existing exit timers/limits, stages mutations atomically, and returns typed effect intentions.
29
+
30
+ ## Validation and dependency selection
31
+
32
+ Each task used a focused cargo xtask group before its own commit. Source and Rust reports regenerated twice; relevant Python tests and scoped gameplay Rust tests passed. Registry regression covers the intermission module and all five source-report-to-Rust dependencies. The single full-suite run after commit ten, `cargo xtask test-full --since 5f7b2df8`, passed: 1,648 Python tests and 137 Rust tests (assets 10, gameplay 99, xtask 28). All 45 affected report producers regenerated twice deterministically. Storage policy, links across 403 documentation files and whitespace checks passed. There was no generated report drift. Validation notes were amended into the tenth commit.
33
+
34
+ Focused group names: `exit-predicate`, `rust-exit-predicate`, `intermission-ready`, `rust-intermission-ready`, `intermission-begin`, `rust-intermission-begin`, `exit-log`, `rust-exit-log`, `intermission-flow`, `rust-intermission-flow`. Use `cargo xtask test-group <name>`.
35
+
36
+ ## Remaining boundaries and RE-844
37
+
38
+ - [ ] Recover 6939 next-map selection and command effects; readiness currently returns an exit intention.
39
+ - [ ] Recover 6717/156523/175123/6575 preparation, respawn and camera effects.
40
+ - [ ] Execute 6328 publication and lower-level logging/configstring/network work with verified formatting and state ownership.
41
+ - [ ] Recover 277695 event construction and allocation, including constructor flag ownership.
42
+ - [ ] Add pointer-alias and entity-client indirection fixtures. Current finite layouts seed entity-to-client pointers and readiness entity identities explicitly; typed Rust inputs require resolved entity flags and safe ranks/counts.
43
+ - [ ] Integrate ranking→exit/intermission→scoreboard publication into the runtime and compare actual game/native traces.
44
+ - [ ] Continue legacy trace audits and validate macOS/Linux Bevy/Solari gameplay and rendering. Neither platform runtime acceptance nor full-game completion is established by these policies.
docs/research/README.md CHANGED
@@ -1118,3 +1118,8 @@ The extension collector adds five focused tests for native tail calls, command-r
1118
 
1119
 
1120
  [RE-842: Original exit flow composed with recovered intermission children](QVM_INTERMISSION_FLOW.md).
 
 
 
 
 
 
1118
 
1119
 
1120
  [RE-842: Original exit flow composed with recovered intermission children](QVM_INTERMISSION_FLOW.md).
1121
+
1122
+
1123
+ [RE-843: Rust composed exit and intermission state machine](RUST_INTERMISSION_FLOW.md).
1124
+
1125
+ - [RE-834–843 intermission batch findings, validation and follow-up scope](QVM_INTERMISSION_BATCH.md).
docs/research/RUST_INTERMISSION_FLOW.md ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ # Rust composed exit and intermission state machine
2
+
3
+ RE-843. Reproduce with `python3 scripts/analyze_rust_intermission_flow.py`; focused validation: `cargo xtask test-group rust-intermission-flow`. [Machine report](rust-intermission-flow.json).
4
+
5
+ Rust composes recovered tie, readiness, begin and exit-log policies with prior exit timers/limits across150 continuous source fixtures. Atomic staged updates preserve shared pending/client state; reset and lower-level world/I/O effects remain explicit intentions. Source branch labels are independently classified by the tested composition oracle; captured requests and actual final source state supply effect parity.
6
+
7
+ Original executable/native runtime and complete Linux/macOS Rust/Bevy/Solari acceptance remain unverified.
docs/research/rust-health-impulse.json CHANGED
@@ -2985,7 +2985,7 @@
2985
  "exit_code": 2
2986
  }
2987
  ],
2988
- "rust_unit_tests": 97,
2989
  "source_report": "docs/research/qvm-health-impulse.json",
2990
  "source_report_sha256": "625fd8064004ea6dbdf398d51d0af844d9c635ff04c0a0790886dddb6ab81e71"
2991
  }
 
2985
  "exit_code": 2
2986
  }
2987
  ],
2988
+ "rust_unit_tests": 99,
2989
  "source_report": "docs/research/qvm-health-impulse.json",
2990
  "source_report_sha256": "625fd8064004ea6dbdf398d51d0af844d9c635ff04c0a0790886dddb6ab81e71"
2991
  }
docs/research/rust-intermission-flow.json ADDED
The diff for this file is too large to render. See raw diff
 
scripts/analyze_rust_intermission_flow.py ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ import hashlib,json
3
+ from pathlib import Path
4
+ from rust_intermission_support import binary,encode,result
5
+ from analyze_qvm import write_json
6
+ from test_qvm_intermission_flow import expected
7
+ from test_qvm_exit_complete import signed
8
+ from qvm_notification_support import POOL
9
+
10
+ def project(s):
11
+ kind,_,_,_=expected(s);children=s['children'];ready=next((bool(c['report']['requests']) for c in children if c['target']==7525),None);begin=[];log=None
12
+ for c in children:
13
+ if c['target']==6825:begin=[[r['target'],(r['arguments']['8']-POOL)//880 if r['target'] in [175123,6575] else -1] for r in c['report']['requests']]
14
+ if c['target']==7367:
15
+ req=c['report']['requests'];team=next(([signed(v['arguments']['12']),signed(v['arguments']['16'])] for v in req if v['target']==7259 and v['arguments']['8']==34691),None);rows=[[signed(v['arguments']['12']),signed(v['arguments']['16']),v['arguments']['20']] for v in req if v['target']==7259 and v['arguments']['8']==34655];log={'team_scores':team,'rows':rows}
16
+ req=s['requests'];resets=[]
17
+ for call,event,r in zip([v for v in req if v['target']==277695],s['events'],[v for v in req if v['target']==7259]):
18
+ entity=(call['arguments']['8']-POOL-24)//880;slot=next(j for j,c in enumerate(s['clients_before']) if c['140']==entity);resets.append([slot,entity,event['fields']['140'],event['fields']['84'],69,event['fields']['424']&256,signed(r['arguments']['16'])])
19
+ return {'kind':kind,'state':[s['state_after'][k] for k in ['blocked','pending','waiting','wait_start']],'clients':[[c[str(o)] for o in [140,4324,304,308,3692,3700,300]] for c in s['clients_after']],'ready_exit':ready,'begin':begin,'log':log,'resets':resets}
20
+
21
+ def main():
22
+ b=binary();p=Path('docs/research/qvm-intermission-flow.json');data=p.read_bytes();rows=[]
23
+ for s in json.loads(data)['probes']:rows.append({'source':project(s),'rust':result(b,'flow',encode(s['input'],s['clients'])+[s['input'].get(k,0) for k in ['round_limit','round_active','round_count','countdown','time_limit','paused','start','score_limit','team_limit']])})
24
+ write_json(Path('docs/research/rust-intermission-flow.json'),{'source_report':str(p),'source_report_sha256':hashlib.sha256(data).hexdigest(),'probes':rows,'limits':'Rust composes recovered tie, readiness, begin and exit-log policies with prior exit timers/limits across150 continuous source fixtures. Atomic staged updates preserve shared pending/client state; reset and lower-level world/I/O effects remain explicit intentions. Source branch labels are independently classified by the tested composition oracle; captured requests and actual final source state supply effect parity.'})
25
+ if __name__=='__main__':main()
scripts/rust_intermission_support.py CHANGED
@@ -2,7 +2,7 @@
2
  import os,subprocess
3
  from pathlib import Path
4
  from rust_lifecycle_support import result
5
- FIELDS=[468,140,6156,248,452,3096,696,308,4036,'entity_flags',4324]
6
  def binary():
7
  subprocess.run(['cargo','build','--offline','--package','urt-gameplay','--bin','urt-intermission'],capture_output=True,text=True,check=True)
8
  return Path(os.environ.get('CARGO_TARGET_DIR','target'))/'debug'/'urt-intermission'
 
2
  import os,subprocess
3
  from pathlib import Path
4
  from rust_lifecycle_support import result
5
+ FIELDS=[468,140,6156,248,452,3096,696,308,4036,'entity_flags',4324,304,3692,3700,300]
6
  def binary():
7
  subprocess.run(['cargo','build','--offline','--package','urt-gameplay','--bin','urt-intermission'],capture_output=True,text=True,check=True)
8
  return Path(os.environ.get('CARGO_TARGET_DIR','target'))/'debug'/'urt-intermission'
scripts/test_rust_intermission_flow.py ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ import hashlib,json,unittest
2
+ from pathlib import Path
3
+ class ParityTests(unittest.TestCase):
4
+ def test_source_cases_and_length(self):
5
+ r=json.loads(Path('docs/research/rust-intermission-flow.json').read_text());source=json.loads(Path(r['source_report']).read_text());self.assertEqual(len(r['probes']),len(source['probes']));self.assertGreater(len(r['probes']),20)
6
+ for p in r['probes']:self.assertEqual(p['source'],p['rust'])
7
+ def test_source_identity(self):
8
+ r=json.loads(Path('docs/research/rust-intermission-flow.json').read_text());self.assertEqual(hashlib.sha256(Path(r['source_report']).read_bytes()).hexdigest(),r['source_report_sha256'])
9
+ if __name__=='__main__':unittest.main()
xtask/checks.tsv CHANGED
@@ -2229,3 +2229,18 @@ report scripts/analyze_qvm_intermission_flow.py docs/research/qvm-intermission-f
2229
  edge docs/research/qvm-intermission-flow.json scripts/test_qvm_intermission_flow.py
2230
  group intermission-flow scripts/analyze_qvm_intermission_flow.py
2231
  edge scripts/analyze_qvm_intermission_flow.py scripts/analyze_qvm_resumer_audit.py
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2229
  edge docs/research/qvm-intermission-flow.json scripts/test_qvm_intermission_flow.py
2230
  group intermission-flow scripts/analyze_qvm_intermission_flow.py
2231
  edge scripts/analyze_qvm_intermission_flow.py scripts/analyze_qvm_resumer_audit.py
2232
+ edge Cargo.toml scripts/analyze_rust_intermission_flow.py
2233
+ edge Cargo.lock scripts/analyze_rust_intermission_flow.py
2234
+ edge crates/urt-gameplay/Cargo.toml scripts/analyze_rust_intermission_flow.py
2235
+ edge crates/urt-gameplay/src/lib.rs scripts/analyze_rust_intermission_flow.py
2236
+ edge crates/urt-gameplay/src/intermission.rs scripts/analyze_rust_intermission_flow.py
2237
+ edge crates/urt-gameplay/src/bin/urt-intermission.rs scripts/analyze_rust_intermission_flow.py
2238
+ edge docs/research/qvm-intermission-flow.json scripts/analyze_rust_intermission_flow.py
2239
+ group rust-intermission-flow scripts/analyze_rust_health_impulse.py
2240
+ group gameplay scripts/analyze_rust_intermission_flow.py
2241
+ edge crates/urt-gameplay/src/exit_prefix.rs scripts/analyze_rust_intermission_flow.py
2242
+ edge crates/urt-gameplay/src/exit_rules.rs scripts/analyze_rust_intermission_flow.py
2243
+ report scripts/analyze_rust_intermission_flow.py docs/research/rust-intermission-flow.json
2244
+ edge docs/research/rust-intermission-flow.json scripts/test_rust_intermission_flow.py
2245
+ group rust-intermission-flow scripts/analyze_rust_intermission_flow.py
2246
+ edge scripts/analyze_rust_intermission_flow.py scripts/analyze_qvm_resumer_audit.py
xtask/src/lib.rs CHANGED
@@ -1048,6 +1048,7 @@ mod integration_tests {
1048
  ("qvm_format", "qvm_format"),
1049
  ("exit_prefix", "exit_prefix"),
1050
  ("exit_rules", "exit_class"),
 
1051
  ("leaderboard", "leaderboard"),
1052
  ("recipient_wire", "recipient_wire"),
1053
  ] {
@@ -1103,6 +1104,11 @@ mod integration_tests {
1103
  ("qvm-exit-limits", "exit_limits"),
1104
  ("qvm-exit-timers", "exit_timers"),
1105
  ("qvm-exit-complete", "exit_complete"),
 
 
 
 
 
1106
  ] {
1107
  let selected = plan(
1108
  &g,
 
1048
  ("qvm_format", "qvm_format"),
1049
  ("exit_prefix", "exit_prefix"),
1050
  ("exit_rules", "exit_class"),
1051
+ ("intermission", "intermission_flow"),
1052
  ("leaderboard", "leaderboard"),
1053
  ("recipient_wire", "recipient_wire"),
1054
  ] {
 
1104
  ("qvm-exit-limits", "exit_limits"),
1105
  ("qvm-exit-timers", "exit_timers"),
1106
  ("qvm-exit-complete", "exit_complete"),
1107
+ ("qvm-exit-predicate", "exit_predicate"),
1108
+ ("qvm-intermission-ready", "intermission_ready"),
1109
+ ("qvm-intermission-begin", "intermission_begin"),
1110
+ ("qvm-exit-log", "exit_log"),
1111
+ ("qvm-intermission-flow", "intermission_flow"),
1112
  ] {
1113
  let selected = plan(
1114
  &g,