nmarshall commited on
Commit
b0607b0
·
1 Parent(s): 672243a

gameplay: port method counter updates

Browse files
crates/urt-gameplay/src/bin/urt-lifecycle.rs CHANGED
@@ -105,6 +105,68 @@ fn run() -> Result<(), String> {
105
  refresh
106
  );
107
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
108
  _ => return Err("unsupported contract or arguments".into()),
109
  }
110
  Ok(())
 
105
  refresh
106
  );
107
  }
108
+ "classify" if w.len() == 1 => {
109
+ println!(
110
+ "{{\"slot\":{}}}",
111
+ urt_gameplay::method_stats::slot(w[0] as i32)
112
+ .map(i32::from)
113
+ .unwrap_or(-1)
114
+ );
115
+ }
116
+ "stats" if w.len() == 4 && w[1] <= 1 && w[3] <= 1 => {
117
+ use std::collections::BTreeMap;
118
+ use urt_gameplay::method_stats::{Side, apply, plan};
119
+ let p = plan(w[0] as i32, w[1] != 0, w[3] != 0);
120
+ let mut actor: BTreeMap<u32, u32> = p
121
+ .increments
122
+ .iter()
123
+ .filter(|(s, _)| *s == Side::Actor)
124
+ .map(|(_, o)| (*o, w[2]))
125
+ .collect();
126
+ let mut victim: BTreeMap<u32, u32> = p
127
+ .increments
128
+ .iter()
129
+ .filter(|(s, _)| *s == Side::Victim)
130
+ .map(|(_, o)| (*o, w[2]))
131
+ .collect();
132
+ let p = apply(w[0] as i32, w[1] != 0, w[3] != 0, &mut actor, &mut victim)
133
+ .map_err(|e| format!("unsupported input: {e:?}"))?;
134
+ let mut increments = p.increments;
135
+ increments.sort();
136
+ let changes: Vec<_> = increments
137
+ .iter()
138
+ .map(|(side, o)| {
139
+ format!(
140
+ "{{\"client\":\"{}\",\"offset\":{},\"before\":{},\"after\":{}}}",
141
+ if *side == Side::Actor {
142
+ "actor"
143
+ } else {
144
+ "victim"
145
+ },
146
+ o,
147
+ w[2],
148
+ if *side == Side::Actor {
149
+ actor[o]
150
+ } else {
151
+ victim[o]
152
+ }
153
+ )
154
+ })
155
+ .collect();
156
+ let mut calls = vec![];
157
+ if p.aggregate_pair {
158
+ calls.push(279720);
159
+ }
160
+ if p.report {
161
+ calls.extend([279586, 279586, 285227, 283622]);
162
+ }
163
+ let calls: Vec<_> = calls.iter().map(|c| c.to_string()).collect();
164
+ println!(
165
+ "{{\"changes\":[{}],\"calls\":[{}]}}",
166
+ changes.join(","),
167
+ calls.join(",")
168
+ );
169
+ }
170
  _ => return Err("unsupported contract or arguments".into()),
171
  }
172
  Ok(())
crates/urt-gameplay/src/lib.rs CHANGED
@@ -146,6 +146,7 @@ mod tests {
146
  }
147
 
148
  pub mod intercept_animation;
 
149
  pub mod mode10;
150
  pub mod protected;
151
  pub mod score;
 
146
  }
147
 
148
  pub mod intercept_animation;
149
+ pub mod method_stats;
150
  pub mod mode10;
151
  pub mod protected;
152
  pub mod score;
crates/urt-gameplay/src/method_stats.rs ADDED
@@ -0,0 +1,167 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ //! Numeric278733/278834 counters; aggregation/reporting remains caller work.
2
+ use std::collections::BTreeMap;
3
+ #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
4
+ pub enum Side {
5
+ Victim,
6
+ Actor,
7
+ }
8
+ #[derive(Clone, Debug, PartialEq, Eq)]
9
+ pub struct Plan {
10
+ pub increments: Vec<(Side, u32)>,
11
+ pub aggregate_pair: bool,
12
+ pub report: bool,
13
+ }
14
+ #[derive(Clone, Copy, Debug, PartialEq, Eq)]
15
+ pub enum StatsError {
16
+ MissingCounter,
17
+ }
18
+ pub fn slot(method: i32) -> Option<u8> {
19
+ Some(match method {
20
+ 14 => 9,
21
+ 15 => 8,
22
+ 16 => 11,
23
+ 17 => 5,
24
+ 18 => 4,
25
+ 19 => 2,
26
+ 20 => 0,
27
+ 21 => 7,
28
+ 28 => 6,
29
+ 30 => 1,
30
+ 36 => 10,
31
+ 38 => 3,
32
+ 39 => 12,
33
+ 40 => 13,
34
+ 41 => 14,
35
+ 42 => 15,
36
+ 43 => 16,
37
+ 44 => 17,
38
+ 45 => 18,
39
+ _ => return None,
40
+ })
41
+ }
42
+ /// All participants are existing clients with valid source IDs. Self aliases
43
+ /// resolve to the actor's storage; an absent actor exits before any requests.
44
+ pub fn plan(method: i32, self_hit: bool, actor_absent: bool) -> Plan {
45
+ use Side::{Actor, Victim};
46
+ let mut result = Plan {
47
+ increments: vec![],
48
+ aggregate_pair: false,
49
+ report: !actor_absent,
50
+ };
51
+ if actor_absent {
52
+ return result;
53
+ }
54
+ let victim = if self_hit { Actor } else { Victim };
55
+ if let Some(slot) = slot(method) {
56
+ result.increments = vec![
57
+ (Actor, 916 + 76 * u32::from(slot)),
58
+ (victim, 920 + 76 * u32::from(slot)),
59
+ ];
60
+ result.aggregate_pair = true;
61
+ return result;
62
+ }
63
+ let single = match method {
64
+ 0..=5 | 8 | 9 => Some(2512),
65
+ 6 => Some(2488),
66
+ 7 => Some(2492),
67
+ 31 => Some(2504),
68
+ 32 | 33 | 35 => Some(2508),
69
+ 47 => Some(2496),
70
+ 22 if self_hit => Some(2460),
71
+ 25 if self_hit => Some(2452),
72
+ _ => None,
73
+ };
74
+ if let Some(offset) = single {
75
+ result.increments.push((victim, offset));
76
+ return result;
77
+ }
78
+ let pair = match method {
79
+ 12 => Some((2396, 2468)),
80
+ 13 => Some((2400, 2472)),
81
+ 22 => Some((2384, 2456)),
82
+ 23 => Some((2408, 2480)),
83
+ 24 => Some((2404, 2476)),
84
+ 25 => Some((2376, 2448)),
85
+ 34 => Some((2428, 2500)),
86
+ 37 => Some((2392, 2464)),
87
+ 48 => Some((2412, 2484)),
88
+ _ => None,
89
+ };
90
+ if let Some((a, v)) = pair {
91
+ result.increments = vec![(Actor, a), (victim, v)];
92
+ result.aggregate_pair = method != 34;
93
+ }
94
+ result
95
+ }
96
+ /// Validate storage before mutation, then apply source wrapping increments.
97
+ /// The returned plan describes reporting intent without executing callbacks.
98
+ pub fn apply(
99
+ method: i32,
100
+ self_hit: bool,
101
+ actor_absent: bool,
102
+ actor: &mut BTreeMap<u32, u32>,
103
+ victim: &mut BTreeMap<u32, u32>,
104
+ ) -> Result<Plan, StatsError> {
105
+ let p = plan(method, self_hit, actor_absent);
106
+ for (side, offset) in &p.increments {
107
+ if !(if *side == Side::Actor {
108
+ &*actor
109
+ } else {
110
+ &*victim
111
+ })
112
+ .contains_key(offset)
113
+ {
114
+ return Err(StatsError::MissingCounter);
115
+ }
116
+ }
117
+ for (side, offset) in &p.increments {
118
+ let counters = if *side == Side::Actor {
119
+ &mut *actor
120
+ } else {
121
+ &mut *victim
122
+ };
123
+ let v = counters.get_mut(offset).unwrap();
124
+ *v = v.wrapping_add(1);
125
+ }
126
+ Ok(p)
127
+ }
128
+ #[cfg(test)]
129
+ mod tests {
130
+ use super::*;
131
+ #[test]
132
+ fn all_slots_and_invalid_signed_extremes() {
133
+ let slots: std::collections::BTreeSet<_> = (0..50).filter_map(slot).collect();
134
+ assert_eq!(slots, (0..19).collect());
135
+ assert_eq!(slot(i32::MIN), None);
136
+ assert_eq!(slot(i32::MAX), None);
137
+ }
138
+ #[test]
139
+ fn self_alias_and_fallback_exceptions() {
140
+ assert_eq!(plan(22, true, false).increments, vec![(Side::Actor, 2460)]);
141
+ assert_eq!(plan(25, true, false).increments, vec![(Side::Actor, 2452)]);
142
+ assert!(!plan(34, false, false).aggregate_pair);
143
+ assert!(plan(20, false, false).aggregate_pair);
144
+ }
145
+ #[test]
146
+ fn apply_wraps_and_keeps_other_counters() {
147
+ let mut actor = BTreeMap::from([(916, u32::MAX), (999, 17)]);
148
+ let mut victim = BTreeMap::from([(920, 3)]);
149
+ apply(20, false, false, &mut actor, &mut victim).unwrap();
150
+ assert_eq!(actor, BTreeMap::from([(916, 0), (999, 17)]));
151
+ assert_eq!(victim[&920], 4);
152
+ }
153
+ #[test]
154
+ fn absent_actor_and_missing_storage_do_not_partially_mutate() {
155
+ let mut a = BTreeMap::from([(916, 4)]);
156
+ let mut v = BTreeMap::new();
157
+ assert_eq!(
158
+ apply(20, false, false, &mut a, &mut v),
159
+ Err(StatsError::MissingCounter)
160
+ );
161
+ assert_eq!(a[&916], 4);
162
+ let p = apply(20, false, true, &mut a, &mut v).unwrap();
163
+ assert!(!p.report);
164
+ assert!(p.increments.is_empty());
165
+ assert_eq!(a[&916], 4);
166
+ }
167
+ }
docs/ASSET_PIPELINE_TODO.md CHANGED
@@ -925,3 +925,6 @@ Research progress: RE-753 [report](research/QVM_MODE10_RESOLVED.md); original as
925
 
926
 
927
  Research progress: RE-754 [report](research/RUST_SCORE_UPDATE.md); original assets unchanged.
 
 
 
 
925
 
926
 
927
  Research progress: RE-754 [report](research/RUST_SCORE_UPDATE.md); original assets unchanged.
928
+
929
+
930
+ Research progress: RE-755 [report](research/RUST_METHOD_STATS.md); original assets unchanged.
docs/FINDINGS.md CHANGED
@@ -1271,3 +1271,6 @@ RE-725–734 batch: [health, contact and Rust impulse findings](research/QVM_HEA
1271
 
1272
 
1273
  **RE-754 — Rust wrapping score primitive and ranking request contract:** Rust player/team score updates and ranking-request decisions match67 source fixtures, with explicit active team-index rejection. [Evidence](research/RUST_SCORE_UPDATE.md).
 
 
 
 
1271
 
1272
 
1273
  **RE-754 — Rust wrapping score primitive and ranking request contract:** Rust player/team score updates and ranking-request decisions match67 source fixtures, with explicit active team-index rejection. [Evidence](research/RUST_SCORE_UPDATE.md).
1274
+
1275
+
1276
+ **RE-755 — Rust method classification wrapping counters and reporting intent:** Rust classification and method-counter mutation now match53 mapping and205 counter fixtures, including self aliases and wrapping increments. [Evidence](research/RUST_METHOD_STATS.md).
docs/PROVISIONAL_TODO.md CHANGED
@@ -1263,3 +1263,6 @@ Research progress: RE-753 [report](research/QVM_MODE10_RESOLVED.md); full native
1263
 
1264
 
1265
  Research progress: RE-754 [report](research/RUST_SCORE_UPDATE.md); full native/platform/game scope remains open.
 
 
 
 
1263
 
1264
 
1265
  Research progress: RE-754 [report](research/RUST_SCORE_UPDATE.md); full native/platform/game scope remains open.
1266
+
1267
+
1268
+ Research progress: RE-755 [report](research/RUST_METHOD_STATS.md); full native/platform/game scope remains open.
docs/RESEARCH_LEDGER.md CHANGED
@@ -1422,3 +1422,8 @@ Additional RE-753 shared-health regression: `cargo xtask test-group health` pass
1422
  ## 2026-10-21 — RE-754 Rust wrapping score primitive and ranking request contract
1423
 
1424
  Rust player/team score updates and ranking-request decisions match67 source fixtures, with explicit active team-index rejection. Focused `cargo xtask test-group gameplay` passes (16 Python tests); selected reports regenerate twice byte-identically, with storage, links and whitespace guards. New report: 33247 bytes. Full-suite validation is deferred to the tenth batch commit. Rust gameplay crate changed; focused gameplay regenerates all five parity contracts and scoped package tests, without requesting workspace/full-suite tests.
 
 
 
 
 
 
1422
  ## 2026-10-21 — RE-754 Rust wrapping score primitive and ranking request contract
1423
 
1424
  Rust player/team score updates and ranking-request decisions match67 source fixtures, with explicit active team-index rejection. Focused `cargo xtask test-group gameplay` passes (16 Python tests); selected reports regenerate twice byte-identically, with storage, links and whitespace guards. New report: 33247 bytes. Full-suite validation is deferred to the tenth batch commit. Rust gameplay crate changed; focused gameplay regenerates all five parity contracts and scoped package tests, without requesting workspace/full-suite tests.
1425
+
1426
+
1427
+ ## 2026-10-21 — RE-755 Rust method classification wrapping counters and reporting intent
1428
+
1429
+ Rust classification and method-counter mutation now match53 mapping and205 counter fixtures, including self aliases and wrapping increments. Focused `cargo xtask test-group gameplay` passes (19 Python tests); selected reports regenerate twice byte-identically, with storage, links and whitespace guards. New report: 172131 bytes. Full-suite validation is deferred to the tenth batch commit. Rust gameplay crate changed; focused gameplay regenerates six parity contracts and scoped gameplay tests. No workspace/full-suite run before commit ten.
docs/REVERSE_ENGINEERING_TODO.md CHANGED
@@ -1306,3 +1306,6 @@ Purpose: turn the supplied installation into an evidence-backed specification of
1306
 
1307
 
1308
  - [x] RE-754 Rust wrapping score primitive and ranking request contract ([report](research/RUST_SCORE_UPDATE.md)).
 
 
 
 
1306
 
1307
 
1308
  - [x] RE-754 Rust wrapping score primitive and ranking request contract ([report](research/RUST_SCORE_UPDATE.md)).
1309
+
1310
+
1311
+ - [x] RE-755 Rust method classification wrapping counters and reporting intent ([report](research/RUST_METHOD_STATS.md)).
docs/research/README.md CHANGED
@@ -861,3 +861,6 @@ The extension collector adds five focused tests for native tail calls, command-r
861
 
862
 
863
  [RE-754: Rust wrapping score primitive and ranking request contract](RUST_SCORE_UPDATE.md).
 
 
 
 
861
 
862
 
863
  [RE-754: Rust wrapping score primitive and ranking request contract](RUST_SCORE_UPDATE.md).
864
+
865
+
866
+ [RE-755: Rust method classification wrapping counters and reporting intent](RUST_METHOD_STATS.md).
docs/research/RUST_METHOD_STATS.md ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ # Rust method classification wrapping counters and reporting intent
2
+
3
+ RE-755. Reproduce with `python3 scripts/analyze_rust_method_stats.py`; validate with `cargo xtask test-group gameplay`. See the [machine report](rust-method-stats.json).
4
+
5
+ Added typed counter ownership, source method mapping, numeric offset plans and transactional storage validation before wrapping mutation. Four Rust unit tests cover complete slot range, fallback self exceptions, untouched counters, overflow, absent actor and missing storage. The adapter checks exact changes and reporting intent against source; aggregate/report callbacks and numeric field-to-runtime data migration remain open.
6
+
7
+ No original executable/full VM/native world collision or rendering runs. Supplied callback/native responses and fixture domains remain explicit; complete macOS/Linux Rust/Bevy/Solari gameplay remains open.
docs/research/rust-health-impulse.json CHANGED
@@ -2985,7 +2985,7 @@
2985
  "exit_code": 2
2986
  }
2987
  ],
2988
- "rust_unit_tests": 22,
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": 26,
2989
  "source_report": "docs/research/qvm-health-impulse.json",
2990
  "source_report_sha256": "625fd8064004ea6dbdf398d51d0af844d9c635ff04c0a0790886dddb6ab81e71"
2991
  }
docs/research/rust-method-stats.json ADDED
The diff for this file is too large to render. See raw diff
 
scripts/analyze_rust_method_stats.py ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """Rust method classification and wrapping counter plans against source fixtures."""
3
+ import hashlib,json
4
+ from pathlib import Path
5
+ from rust_lifecycle_support import binary,result
6
+ from analyze_qvm import write_json
7
+
8
+ def main():
9
+ b=binary();path=Path('docs/research/qvm-method-stat-updates.json');data=path.read_bytes();rows=[]
10
+ for p in json.loads(data)['probes']:
11
+ a=p['input'];args=[a['method'],int(a['self']),a['initial'],int(a.get('actor_absent',False))];e={'changes':p['changes'],'calls':[c['target'] for c in p['requests']]};rows.append({'input':a,'source':e,'rust':result(b,'stats',args)})
12
+ slots=Path('docs/research/qvm-method-stat-slots.json');raw=slots.read_bytes();classification=[{'method':p['method'],'source_slot':p['slot'],'rust':result(b,'classify',[p['method']])} for p in json.loads(raw)['probes']]
13
+ write_json(Path('docs/research/rust-method-stats.json'),{'source_reports':{str(path):hashlib.sha256(data).hexdigest(),str(slots):hashlib.sha256(raw).hexdigest()},'probes':rows,'classification':classification,'limits':'Rust numeric classification matches53 source fixtures; counter mutation/reporting intent matches205 source fixtures including self alias and wrapping. Existing actor/victim clients and valid IDs required. Actual callback/report/aggregation effects and platform/native parity remain open. Missing counter storage rejects before mutation.'})
14
+ if __name__=='__main__':main()
scripts/test_rust_method_stats.py ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import hashlib,json,unittest
2
+ from pathlib import Path
3
+ from test_qvm_method_stat_updates import expected
4
+ from test_qvm_method_stat_slots import SLOTS
5
+ class RustMethodStatsTests(unittest.TestCase):
6
+ @classmethod
7
+ def setUpClass(cls):cls.r=json.loads(Path('docs/research/rust-method-stats.json').read_text())
8
+ def test_all_source_counter_mutations_and_request_intents(self):
9
+ self.assertEqual(len(self.r['probes']),205)
10
+ for p in self.r['probes']:
11
+ self.assertEqual(p['rust'],p['source'],p['input']);changes,calls=expected(p['input']);self.assertEqual(p['rust'],{'changes':changes,'calls':calls})
12
+ def test_source_classification_and_independent_mapping(self):
13
+ self.assertEqual(len(self.r['classification']),53)
14
+ for p in self.r['classification']:self.assertEqual(p['rust']['slot'],p['source_slot']);self.assertEqual(p['source_slot'],SLOTS.get(p['method'],-1))
15
+ def test_source_identity(self):
16
+ for path,sha in self.r['source_reports'].items():self.assertEqual(hashlib.sha256(Path(path).read_bytes()).hexdigest(),sha)
17
+ if __name__=='__main__':unittest.main()
xtask/checks.tsv CHANGED
@@ -1252,3 +1252,13 @@ edge crates/urt-gameplay/src/score.rs scripts/analyze_rust_health_impulse.py
1252
  group gameplay scripts/analyze_rust_score_update.py
1253
  report scripts/analyze_rust_score_update.py docs/research/rust-score-update.json
1254
  edge docs/research/rust-score-update.json scripts/test_rust_score_update.py
 
 
 
 
 
 
 
 
 
 
 
1252
  group gameplay scripts/analyze_rust_score_update.py
1253
  report scripts/analyze_rust_score_update.py docs/research/rust-score-update.json
1254
  edge docs/research/rust-score-update.json scripts/test_rust_score_update.py
1255
+ edge Cargo.toml scripts/analyze_rust_method_stats.py
1256
+ edge Cargo.lock scripts/analyze_rust_method_stats.py
1257
+ edge crates/urt-gameplay/Cargo.toml scripts/analyze_rust_method_stats.py
1258
+ edge crates/urt-gameplay/src/lib.rs scripts/analyze_rust_method_stats.py
1259
+ edge crates/urt-gameplay/src/method_stats.rs scripts/analyze_rust_method_stats.py
1260
+ edge crates/urt-gameplay/src/bin/urt-lifecycle.rs scripts/analyze_rust_method_stats.py
1261
+ edge crates/urt-gameplay/src/method_stats.rs scripts/analyze_rust_health_impulse.py
1262
+ group gameplay scripts/analyze_rust_method_stats.py
1263
+ report scripts/analyze_rust_method_stats.py docs/research/rust-method-stats.json
1264
+ edge docs/research/rust-method-stats.json scripts/test_rust_method_stats.py