nmarshall commited on
Commit
529c559
·
1 Parent(s): 85794cd

gameplay: port timer countdown and recovery ordering

Browse files
crates/urt-gameplay/src/client_timer.rs CHANGED
@@ -118,7 +118,7 @@ pub struct TimerDamage {
118
  pub means: u32,
119
  pub weapon: u32,
120
  }
121
- /// Original150810 loop subset with flag512 disabled. Damage effects are requests;
122
  /// supplied health is deliberately unchanged. Rate0 aliases the preceding word.
123
  pub fn timer_tick_loop(
124
  state: &mut TimerLoopState,
@@ -127,9 +127,15 @@ pub fn timer_tick_loop(
127
  if state.rate > 5 {
128
  return Err(Error::InvalidIndex);
129
  }
130
- if state.flags & 512 != 0 || state.residual as i32 > 10000 {
131
  return Err(Error::InvalidInput);
132
  }
 
 
 
 
 
 
133
  let mut next = *state;
134
  let mut damage = vec![];
135
  while next.residual as i32 >= 100 {
@@ -149,6 +155,26 @@ pub fn timer_tick_loop(
149
  weapon: input.weapon,
150
  });
151
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
152
  }
153
  *state = next;
154
  Ok(damage)
@@ -216,3 +242,54 @@ mod tick_tests {
216
  assert_eq!(s, old);
217
  }
218
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
118
  pub means: u32,
119
  pub weapon: u32,
120
  }
121
+ /// Original150810 complete finite-domain loop. Damage effects are requests;
122
  /// supplied health is deliberately unchanged. Rate0 aliases the preceding word.
123
  pub fn timer_tick_loop(
124
  state: &mut TimerLoopState,
 
127
  if state.rate > 5 {
128
  return Err(Error::InvalidIndex);
129
  }
130
+ if state.residual as i32 > 10000 {
131
  return Err(Error::InvalidInput);
132
  }
133
+ if state.residual as i32 >= 100
134
+ && state.flags & 512 != 0
135
+ && !f32::from_bits(state.timer).is_finite()
136
+ {
137
+ return Err(Error::NonFinite);
138
+ }
139
  let mut next = *state;
140
  let mut damage = vec![];
141
  while next.residual as i32 >= 100 {
 
155
  weapon: input.weapon,
156
  });
157
  }
158
+ if next.flags & 512 != 0 {
159
+ let mut timer = f32::from_bits(next.timer);
160
+ if next.flags & 256 != 0 && timer <= 0.0 {
161
+ next.flags &= !256;
162
+ next.rate = 0;
163
+ timer = 0.0;
164
+ }
165
+ if timer > 0.0 {
166
+ timer -= 0.1f32;
167
+ if timer <= 0.0 {
168
+ next.flags &= !256;
169
+ next.rate = 0;
170
+ timer = 0.0;
171
+ }
172
+ } else if next.flags & 128 != 0 {
173
+ next.flags &= !128;
174
+ next.status &= !1;
175
+ }
176
+ next.timer = timer.to_bits();
177
+ }
178
  }
179
  *state = next;
180
  Ok(damage)
 
242
  assert_eq!(s, old);
243
  }
244
  }
245
+
246
+ #[cfg(test)]
247
+ mod recovery_tests {
248
+ use super::*;
249
+ fn input() -> TimerLoopInput {
250
+ TimerLoopInput {
251
+ health: 1,
252
+ weapon: 7,
253
+ target: 2,
254
+ owner: 3,
255
+ }
256
+ }
257
+ #[test]
258
+ fn zero_crossing_delays_secondary_cleanup_and_switches_divisor() {
259
+ let mut s = TimerLoopState {
260
+ flags: 896,
261
+ status: 3,
262
+ rate: 2,
263
+ phase: 3,
264
+ timer: 0.05f32.to_bits(),
265
+ residual: 100,
266
+ };
267
+ let p = timer_tick_loop(&mut s, input()).unwrap();
268
+ assert_eq!(p.len(), 1);
269
+ assert_eq!(
270
+ [s.flags, s.status, s.rate, s.phase, s.timer],
271
+ [640, 3, 0, 0, 0]
272
+ );
273
+ s.residual = 100;
274
+ timer_tick_loop(&mut s, input()).unwrap();
275
+ assert_eq!([s.flags, s.status, s.rate, s.phase], [512, 2, 0, 1]);
276
+ }
277
+ #[test]
278
+ fn initial_nonpositive_timer_cleans_immediately_and_nonfinite_rejects_atomically() {
279
+ let mut s = TimerLoopState {
280
+ flags: 896,
281
+ status: 3,
282
+ rate: 2,
283
+ phase: 3,
284
+ timer: (-0.1f32).to_bits(),
285
+ residual: 100,
286
+ };
287
+ timer_tick_loop(&mut s, input()).unwrap();
288
+ assert_eq!([s.flags, s.status, s.rate, s.timer], [512, 2, 0, 0]);
289
+ s.timer = f32::NAN.to_bits();
290
+ s.residual = 100;
291
+ let old = s;
292
+ assert_eq!(timer_tick_loop(&mut s, input()), Err(Error::NonFinite));
293
+ assert_eq!(s, old);
294
+ }
295
+ }
docs/FINDINGS.md CHANGED
@@ -1869,3 +1869,5 @@ RE-966: A288-case source loop contract covers signed residual thresholds, wrappi
1869
  RE-967: Rust matches288 original tick-loop cases with signed residual/remainder and wrapping phase behavior. Rate0 retains the preceding-word divisor1120403456. Typed damage requests retain all source arguments without mutating supplied health. Unit tests cover alias behavior, signed wrap, exact request ABI and atomic invalid-rate rejection. Flag512 is deliberately outside this first loop subset; the residual budget bounds execution. [Evidence](research/RUST_CLIENT_TIMER_TICKS.md).
1870
 
1871
  RE-968: A216-case source contract extends the tick loop through flags512/256/128 and f32 subtraction of0.1. Crossing zero resets flag256/rate/timer but delays secondary flag128/status cleanup until a later tick; initially nonpositive timers can clear immediately. Reset rate0 changes later modulo through the preceding-word alias. Damage stays captured and health supplied; nonfinite/unsafe table domains remain open. [Evidence](research/QVM_CLIENT_TIMER_RECOVERY.md).
 
 
 
1869
  RE-967: Rust matches288 original tick-loop cases with signed residual/remainder and wrapping phase behavior. Rate0 retains the preceding-word divisor1120403456. Typed damage requests retain all source arguments without mutating supplied health. Unit tests cover alias behavior, signed wrap, exact request ABI and atomic invalid-rate rejection. Flag512 is deliberately outside this first loop subset; the residual budget bounds execution. [Evidence](research/RUST_CLIENT_TIMER_TICKS.md).
1870
 
1871
  RE-968: A216-case source contract extends the tick loop through flags512/256/128 and f32 subtraction of0.1. Crossing zero resets flag256/rate/timer but delays secondary flag128/status cleanup until a later tick; initially nonpositive timers can clear immediately. Reset rate0 changes later modulo through the preceding-word alias. Damage stays captured and health supplied; nonfinite/unsafe table domains remain open. [Evidence](research/QVM_CLIENT_TIMER_RECOVERY.md).
1872
+
1873
+ RE-969: Rust matches216 original full-loop cases with repeated f32 subtraction, delayed secondary cleanup, raw flag/status preservation and rate0 divisor switching. The existing tick API now supports flag512 through these recovered branches. Unit tests cover countdown crossing versus initial nonpositive cleanup and atomic nonfinite rejection; damage effects remain requests against supplied health. [Evidence](research/RUST_CLIENT_TIMER_RECOVERY.md).
docs/PROVISIONAL_TODO.md CHANGED
@@ -1921,3 +1921,5 @@ RE-954–963 completed add/remove wrappers, resolved duration export, full-capac
1921
  - [x] RE-967 Rust100ms phase loop and captured damage requests ([evidence](research/RUST_CLIENT_TIMER_TICKS.md)).
1922
 
1923
  - [x] RE-968 Original float countdown and flag-recovery loop ([evidence](research/QVM_CLIENT_TIMER_RECOVERY.md)).
 
 
 
1921
  - [x] RE-967 Rust100ms phase loop and captured damage requests ([evidence](research/RUST_CLIENT_TIMER_TICKS.md)).
1922
 
1923
  - [x] RE-968 Original float countdown and flag-recovery loop ([evidence](research/QVM_CLIENT_TIMER_RECOVERY.md)).
1924
+
1925
+ - [x] RE-969 Rust float countdown and ordered flag recovery ([evidence](research/RUST_CLIENT_TIMER_RECOVERY.md)).
docs/RESEARCH_LEDGER.md CHANGED
@@ -2543,3 +2543,7 @@ Rust matches288 original tick-loop cases with signed residual/remainder and wrap
2543
  ## 2026-10-28 — RE-968 Original float countdown and flag-recovery loop
2544
 
2545
  A216-case source contract extends the tick loop through flags512/256/128 and f32 subtraction of0.1. Crossing zero resets flag256/rate/timer but delays secondary flag128/status cleanup until a later tick; initially nonpositive timers can clear immediately. Reset rate0 changes later modulo through the preceding-word alias. Damage stays captured and health supplied; nonfinite/unsafe table domains remain open. Focused `cargo xtask test-group qvm-client-timer-recovery` passes with deterministic report regeneration, relevant regressions and storage/documentation/whitespace checks. Full suite deferred to the tenth-commit boundary. Original assets unchanged; native IO effects remain captured.
 
 
 
 
 
2543
  ## 2026-10-28 — RE-968 Original float countdown and flag-recovery loop
2544
 
2545
  A216-case source contract extends the tick loop through flags512/256/128 and f32 subtraction of0.1. Crossing zero resets flag256/rate/timer but delays secondary flag128/status cleanup until a later tick; initially nonpositive timers can clear immediately. Reset rate0 changes later modulo through the preceding-word alias. Damage stays captured and health supplied; nonfinite/unsafe table domains remain open. Focused `cargo xtask test-group qvm-client-timer-recovery` passes with deterministic report regeneration, relevant regressions and storage/documentation/whitespace checks. Full suite deferred to the tenth-commit boundary. Original assets unchanged; native IO effects remain captured.
2546
+
2547
+ ## 2026-10-28 — RE-969 Rust float countdown and ordered flag recovery
2548
+
2549
+ Rust matches216 original full-loop cases with repeated f32 subtraction, delayed secondary cleanup, raw flag/status preservation and rate0 divisor switching. The existing tick API now supports flag512 through these recovered branches. Unit tests cover countdown crossing versus initial nonpositive cleanup and atomic nonfinite rejection; damage effects remain requests against supplied health. Focused `cargo xtask test-group rust-client-timer-recovery` passes with deterministic report regeneration, relevant regressions and storage/documentation/whitespace checks. Full suite deferred to the tenth-commit boundary. Original assets unchanged; native IO effects remain captured.
docs/REVERSE_ENGINEERING_TODO.md CHANGED
@@ -1958,3 +1958,5 @@ RE-954–963 recover bounded command wrappers, resolved duration export, full-ca
1958
  - [x] RE-967 Rust100ms phase loop and captured damage requests ([evidence](research/RUST_CLIENT_TIMER_TICKS.md)).
1959
 
1960
  - [x] RE-968 Original float countdown and flag-recovery loop ([evidence](research/QVM_CLIENT_TIMER_RECOVERY.md)).
 
 
 
1958
  - [x] RE-967 Rust100ms phase loop and captured damage requests ([evidence](research/RUST_CLIENT_TIMER_TICKS.md)).
1959
 
1960
  - [x] RE-968 Original float countdown and flag-recovery loop ([evidence](research/QVM_CLIENT_TIMER_RECOVERY.md)).
1961
+
1962
+ - [x] RE-969 Rust float countdown and ordered flag recovery ([evidence](research/RUST_CLIENT_TIMER_RECOVERY.md)).
docs/research/README.md CHANGED
@@ -1493,3 +1493,5 @@ The extension collector adds five focused tests for native tail calls, command-r
1493
  - [RE-967: Rust100ms phase loop and captured damage requests](RUST_CLIENT_TIMER_TICKS.md).
1494
 
1495
  - [RE-968: Original float countdown and flag-recovery loop](QVM_CLIENT_TIMER_RECOVERY.md).
 
 
 
1493
  - [RE-967: Rust100ms phase loop and captured damage requests](RUST_CLIENT_TIMER_TICKS.md).
1494
 
1495
  - [RE-968: Original float countdown and flag-recovery loop](QVM_CLIENT_TIMER_RECOVERY.md).
1496
+
1497
+ - [RE-969: Rust float countdown and ordered flag recovery](RUST_CLIENT_TIMER_RECOVERY.md).
docs/research/RUST_CLIENT_TIMER_RECOVERY.md ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ # Rust float countdown and ordered flag recovery
2
+
3
+ RE-969. Focused validation: `cargo xtask test-group rust-client-timer-recovery`. [Machine report](rust-client-timer-recovery.json).
4
+
5
+ Rust matches216 original full-loop cases with repeated f32 subtraction, delayed secondary cleanup, raw flag/status preservation and rate0 divisor switching. The existing tick API now supports flag512 through these recovered branches. Unit tests cover countdown crossing versus initial nonpositive cleanup and atomic nonfinite rejection; damage effects remain requests against supplied health.
docs/research/RUST_CLIENT_TIMER_TICKS.md CHANGED
@@ -3,3 +3,5 @@
3
  RE-967. Focused validation: `cargo xtask test-group rust-client-timer-ticks`. [Machine report](rust-client-timer-ticks.json).
4
 
5
  Rust matches288 original tick-loop cases with signed residual/remainder and wrapping phase behavior. Rate0 retains the preceding-word divisor1120403456. Typed damage requests retain all source arguments without mutating supplied health. Unit tests cover alias behavior, signed wrap, exact request ABI and atomic invalid-rate rejection. Flag512 is deliberately outside this first loop subset; the residual budget bounds execution.
 
 
 
3
  RE-967. Focused validation: `cargo xtask test-group rust-client-timer-ticks`. [Machine report](rust-client-timer-ticks.json).
4
 
5
  Rust matches288 original tick-loop cases with signed residual/remainder and wrapping phase behavior. Rate0 retains the preceding-word divisor1120403456. Typed damage requests retain all source arguments without mutating supplied health. Unit tests cover alias behavior, signed wrap, exact request ABI and atomic invalid-rate rejection. Flag512 is deliberately outside this first loop subset; the residual budget bounds execution.
6
+
7
+ RE-969 extends the same Rust loop API through flag512 and float-countdown branches; see [recovery parity](RUST_CLIENT_TIMER_RECOVERY.md). The earlier288-case subset remains a regression.
docs/research/rust-client-timer-recovery.json ADDED
The diff for this file is too large to render. See raw diff
 
docs/research/rust-health-impulse.json CHANGED
@@ -2985,7 +2985,7 @@
2985
  "exit_code": 2
2986
  }
2987
  ],
2988
- "rust_unit_tests": 224,
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": 226,
2989
  "source_report": "docs/research/qvm-health-impulse.json",
2990
  "source_report_sha256": "625fd8064004ea6dbdf398d51d0af844d9c635ff04c0a0790886dddb6ab81e71"
2991
  }
scripts/analyze_rust_client_timer_recovery.py ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Compare recovered Rust policy with original QVM fixtures."""
2
+ import hashlib,json,os,subprocess
3
+ from pathlib import Path
4
+ from analyze_qvm import write_json
5
+ from rust_lifecycle_support import result
6
+
7
+ def main():
8
+ subprocess.run(['cargo','build','--offline','--package','urt-gameplay','--bin','urt-client-timer'],check=True,capture_output=True)
9
+ binary=Path(os.environ.get('CARGO_TARGET_DIR','target'))/'debug'/'urt-client-timer'
10
+ path=Path('docs/research/qvm-client-timer-recovery.json');data=path.read_bytes();probes=[]
11
+ for p in json.loads(data)['probes']:
12
+ probes.append(dict(source={k:p[k] for k in ['damage', 'state', 'health']},rust=result(binary,'tick',encode(p['input']))))
13
+ write_json(Path('docs/research/rust-client-timer-recovery.json'),dict(source_report=str(path),source_report_sha256=hashlib.sha256(data).hexdigest(),probes=probes,limits='Finite source/Rust contract parity; native time/file/network replies remain supplied or captured.'))
14
+
15
+ def encode(a):
16
+ from analyze_qvm_acceleration import word
17
+ return [a['flags'],a['status'],a['rate'],a['phase'],word(a['timer']),a['residual'],a['health'],a['weapon']]
18
+ if __name__=='__main__':main()
scripts/test_rust_client_timer_recovery.py ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ import hashlib,json,unittest
2
+ from pathlib import Path
3
+ class PortTests(unittest.TestCase):
4
+ def test_original_parity(self):
5
+ r=json.loads(Path('docs/research/rust-client-timer-recovery.json').read_text());self.assertGreaterEqual(len(r['probes']),12)
6
+ for p in r['probes']:self.assertEqual(p['source'],p['rust'])
7
+ def test_identity(self):
8
+ r=json.loads(Path('docs/research/rust-client-timer-recovery.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
@@ -3470,3 +3470,23 @@ report scripts/analyze_qvm_client_timer_recovery.py docs/research/qvm-client-tim
3470
  edge docs/research/qvm-client-timer-recovery.json scripts/test_qvm_client_timer_recovery.py
3471
  group qvm-client-timer-recovery scripts/analyze_qvm_client_timer_recovery.py
3472
  edge scripts/analyze_qvm_client_timer_recovery.py scripts/analyze_qvm_resumer_audit.py
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3470
  edge docs/research/qvm-client-timer-recovery.json scripts/test_qvm_client_timer_recovery.py
3471
  group qvm-client-timer-recovery scripts/analyze_qvm_client_timer_recovery.py
3472
  edge scripts/analyze_qvm_client_timer_recovery.py scripts/analyze_qvm_resumer_audit.py
3473
+ report scripts/analyze_rust_client_timer_recovery.py docs/research/rust-client-timer-recovery.json
3474
+ edge docs/research/rust-client-timer-recovery.json scripts/test_rust_client_timer_recovery.py
3475
+ group rust-client-timer-recovery scripts/analyze_rust_client_timer_recovery.py
3476
+ edge scripts/analyze_rust_client_timer_recovery.py scripts/analyze_qvm_resumer_audit.py
3477
+ edge Cargo.toml scripts/analyze_rust_client_timer_recovery.py
3478
+ edge Cargo.lock scripts/analyze_rust_client_timer_recovery.py
3479
+ edge crates/urt-gameplay/Cargo.toml scripts/analyze_rust_client_timer_recovery.py
3480
+ edge crates/urt-gameplay/src/lib.rs scripts/analyze_rust_client_timer_recovery.py
3481
+ edge crates/urt-gameplay/src/filter_loading.rs scripts/analyze_rust_client_timer_recovery.py
3482
+ edge crates/urt-gameplay/src/filter_lifecycle.rs scripts/analyze_rust_client_timer_recovery.py
3483
+ edge crates/urt-gameplay/src/native_policy.rs scripts/analyze_rust_client_timer_recovery.py
3484
+ edge crates/urt-gameplay/src/bin/urt-filter-loading.rs scripts/analyze_rust_client_timer_recovery.py
3485
+ edge crates/urt-gameplay/src/qvm_format.rs scripts/analyze_rust_client_timer_recovery.py
3486
+ edge docs/research/qvm-client-timer-recovery.json scripts/analyze_rust_client_timer_recovery.py
3487
+ group rust-client-timer-recovery scripts/analyze_rust_health_impulse.py
3488
+ group gameplay scripts/analyze_rust_client_timer_recovery.py
3489
+ edge crates/urt-gameplay/src/filter_commands.rs scripts/analyze_rust_client_timer_recovery.py
3490
+ edge crates/urt-gameplay/src/bin/urt-filter-commands.rs scripts/analyze_rust_client_timer_recovery.py
3491
+ edge crates/urt-gameplay/src/client_timer.rs scripts/analyze_rust_client_timer_recovery.py
3492
+ edge crates/urt-gameplay/src/bin/urt-client-timer.rs scripts/analyze_rust_client_timer_recovery.py