nmarshall commited on
Commit
c987618
·
1 Parent(s): 182167b

gameplay: port ranking population scan

Browse files
crates/urt-gameplay/src/bin/urt-lifecycle.rs CHANGED
@@ -13,6 +13,19 @@ fn ranking_client(v: &[u32]) -> urt_gameplay::ranking::Client {
13
  awards: v[9] as i32,
14
  }
15
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
16
  fn main() {
17
  if let Err(e) = run() {
18
  eprintln!("{e}");
@@ -328,6 +341,31 @@ fn run() -> Result<(), String> {
328
  second.cached_ping as u32
329
  );
330
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
331
  "rank-compose" if w.len() >= 4 && w[3] <= 64 => {
332
  let n = w[3] as usize;
333
  if w.len() != 4 + 10 * n {
 
13
  awards: v[9] as i32,
14
  }
15
  }
16
+ fn population_json(p: &urt_gameplay::ranking_population::Population) -> String {
17
+ format!(
18
+ "{{\"34992848\":{},\"34992852\":{},\"34992580\":{},\"34992584\":{},\"34992588\":{},\"34994928\":{},\"34997012\":{},\"34997016\":{}}}",
19
+ p.first[0],
20
+ p.first[1],
21
+ p.order.len(),
22
+ p.nonspectators,
23
+ p.playing,
24
+ p.humans,
25
+ p.team_humans[0],
26
+ p.team_humans[1]
27
+ )
28
+ }
29
  fn main() {
30
  if let Err(e) = run() {
31
  eprintln!("{e}");
 
341
  second.cached_ping as u32
342
  );
343
  }
344
+ "rank-population" if !w.is_empty() && w[0] <= 64 => {
345
+ let n = w[0] as usize;
346
+ if w.len() != 1 + 11 * n {
347
+ return Err("invalid population words".into());
348
+ }
349
+ let slots: Vec<_> = w[1..]
350
+ .chunks_exact(11)
351
+ .map(|v| urt_gameplay::ranking_population::Slot {
352
+ client: ranking_client(v),
353
+ entity_flags: v[10],
354
+ rank: 77,
355
+ })
356
+ .collect();
357
+ let p = urt_gameplay::ranking_population::collect(&slots)
358
+ .map_err(|e| format!("unsupported input: {e:?}"))?;
359
+ println!(
360
+ "{{\"order\":[{}],\"globals\":{}}}",
361
+ p.order
362
+ .iter()
363
+ .map(|v| v.to_string())
364
+ .collect::<Vec<_>>()
365
+ .join(","),
366
+ population_json(&p)
367
+ );
368
+ }
369
  "rank-compose" if w.len() >= 4 && w[3] <= 64 => {
370
  let n = w[3] as usize;
371
  if w.len() != 4 + 10 * n {
crates/urt-gameplay/src/lib.rs CHANGED
@@ -159,3 +159,5 @@ pub mod score;
159
  pub mod stat_totals;
160
 
161
  pub mod rank_sort;
 
 
 
159
  pub mod stat_totals;
160
 
161
  pub mod rank_sort;
162
+
163
+ pub mod ranking_population;
crates/urt-gameplay/src/ranking_population.rs ADDED
@@ -0,0 +1,115 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ //! QVM5864 collection before sorting. Entity bot flag affects human counts only.
2
+ use crate::ranking::Client;
3
+ #[derive(Clone, Copy, Debug, PartialEq, Eq)]
4
+ pub struct Slot {
5
+ pub client: Client,
6
+ pub entity_flags: u32,
7
+ pub rank: u32,
8
+ }
9
+ #[derive(Clone, Debug, PartialEq, Eq)]
10
+ pub struct Population {
11
+ pub order: Vec<usize>,
12
+ pub first: [i32; 2],
13
+ pub nonspectators: usize,
14
+ pub playing: usize,
15
+ pub humans: usize,
16
+ pub team_humans: [usize; 2],
17
+ }
18
+ #[derive(Clone, Copy, Debug, PartialEq, Eq)]
19
+ pub enum PopulationError {
20
+ TooManyClients,
21
+ }
22
+ /// Source scan order. Connecting clients are collected; spectators contribute
23
+ /// only to connected count. First two playing slots can include bots.
24
+ pub fn collect(slots: &[Slot]) -> Result<Population, PopulationError> {
25
+ if slots.len() > 64 {
26
+ return Err(PopulationError::TooManyClients);
27
+ }
28
+ let mut p = Population {
29
+ order: vec![],
30
+ first: [-1, -1],
31
+ nonspectators: 0,
32
+ playing: 0,
33
+ humans: 0,
34
+ team_humans: [0, 0],
35
+ };
36
+ for (id, s) in slots.iter().enumerate() {
37
+ let c = s.client;
38
+ if c.connection == 0 {
39
+ continue;
40
+ }
41
+ p.order.push(id);
42
+ if c.team == 3 {
43
+ continue;
44
+ }
45
+ p.nonspectators += 1;
46
+ if c.connection != 2 {
47
+ continue;
48
+ }
49
+ p.playing += 1;
50
+ if s.entity_flags & 8 == 0 {
51
+ p.humans += 1;
52
+ if c.team == 1 {
53
+ p.team_humans[0] += 1;
54
+ } else if c.team == 2 {
55
+ p.team_humans[1] += 1;
56
+ }
57
+ }
58
+ if p.first[0] == -1 {
59
+ p.first[0] = id as i32;
60
+ } else if p.first[1] == -1 {
61
+ p.first[1] = id as i32;
62
+ }
63
+ }
64
+ Ok(p)
65
+ }
66
+ #[cfg(test)]
67
+ mod tests {
68
+ use super::*;
69
+ fn slot(state: i32, team: i32, flags: u32) -> Slot {
70
+ Slot {
71
+ client: Client {
72
+ spectator_state: 0,
73
+ follow_index: 0,
74
+ connection: state,
75
+ team,
76
+ spectator_time: 0,
77
+ time: 0,
78
+ class: 0,
79
+ score: 0,
80
+ deaths: 0,
81
+ awards: 0,
82
+ },
83
+ entity_flags: flags,
84
+ rank: 77,
85
+ }
86
+ }
87
+ #[test]
88
+ fn bots_and_connecting_spectators_have_distinct_counts() {
89
+ let s = [
90
+ slot(2, 1, 8),
91
+ slot(1, 1, 0),
92
+ slot(2, 3, 0),
93
+ slot(0, 2, 0),
94
+ slot(2, 2, 0),
95
+ ];
96
+ let p = collect(&s).unwrap();
97
+ assert_eq!(p.order, [0, 1, 2, 4]);
98
+ assert_eq!(p.first, [0, 4]);
99
+ assert_eq!(
100
+ (p.nonspectators, p.playing, p.humans, p.team_humans),
101
+ (3, 2, 1, [0, 1])
102
+ );
103
+ }
104
+ #[test]
105
+ fn boundary_and_scan_local_reset() {
106
+ assert_eq!(collect(&[]).unwrap().first, [-1, -1]);
107
+ let s = vec![slot(2, 1, 0); 64];
108
+ assert_eq!(collect(&s).unwrap().playing, 64);
109
+ assert_eq!(
110
+ collect(&vec![slot(2, 1, 0); 65]),
111
+ Err(PopulationError::TooManyClients)
112
+ );
113
+ assert_eq!(collect(&[]).unwrap().order, []);
114
+ }
115
+ }
docs/FINDINGS.md CHANGED
@@ -1385,3 +1385,6 @@ RE-780–789 exposes and bounds legacy resumer timing/byte-state flaws, migrates
1385
 
1386
 
1387
  **RE-795:** Rust composes recovered source sorting and rank writes, matching exact order and tied/team rank words for128 full caller fixtures. [Evidence](research/RUST_RANKING_COMPLETE.md).
 
 
 
 
1385
 
1386
 
1387
  **RE-795:** Rust composes recovered source sorting and rank writes, matching exact order and tied/team rank words for128 full caller fixtures. [Evidence](research/RUST_RANKING_COMPLETE.md).
1388
+
1389
+
1390
+ **RE-796:** Rust reproduces source collection, first-playing identifiers and bot/team counters across185 setup and complete caller fixtures. [Evidence](research/RUST_RANKING_POPULATION.md).
docs/PROVISIONAL_TODO.md CHANGED
@@ -1389,3 +1389,6 @@ RE-794: Rust now executes the recovered insertion/partition/median/ninther sort
1389
 
1390
 
1391
  RE-795: Rust composes recovered source sorting and rank writes, matching exact order and tied/team rank words for128 full caller fixtures. Full runtime completion remains open. [Evidence](research/RUST_RANKING_COMPLETE.md).
 
 
 
 
1389
 
1390
 
1391
  RE-795: Rust composes recovered source sorting and rank writes, matching exact order and tied/team rank words for128 full caller fixtures. Full runtime completion remains open. [Evidence](research/RUST_RANKING_COMPLETE.md).
1392
+
1393
+
1394
+ RE-796: Rust reproduces source collection, first-playing identifiers and bot/team counters across185 setup and complete caller fixtures. Full runtime completion remains open. [Evidence](research/RUST_RANKING_POPULATION.md).
docs/RESEARCH_LEDGER.md CHANGED
@@ -1639,3 +1639,8 @@ Rust now executes the recovered insertion/partition/median/ninther sort without
1639
  ## 2026-10-26 — RE-795 Rust eligible ranking composition through64 clients
1640
 
1641
  Rust composes recovered source sorting and rank writes, matching exact order and tied/team rank words for128 full caller fixtures. Focused `cargo xtask test-group rust-ranking-complete` 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.
 
 
 
 
 
 
1639
  ## 2026-10-26 — RE-795 Rust eligible ranking composition through64 clients
1640
 
1641
  Rust composes recovered source sorting and rank writes, matching exact order and tied/team rank words for128 full caller fixtures. Focused `cargo xtask test-group rust-ranking-complete` 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.
1642
+
1643
+
1644
+ ## 2026-10-26 — RE-796 Rust ranking population collection and all eight count globals
1645
+
1646
+ Rust reproduces source collection, first-playing identifiers and bot/team counters across185 setup and complete caller fixtures. Focused `cargo xtask test-group rust-ranking-population` 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.
docs/REVERSE_ENGINEERING_TODO.md CHANGED
@@ -1435,3 +1435,6 @@ Purpose: turn the supplied installation into an evidence-backed specification of
1435
 
1436
 
1437
  - [x] RE-795 Rust eligible ranking composition through64 clients ([evidence](research/RUST_RANKING_COMPLETE.md)).
 
 
 
 
1435
 
1436
 
1437
  - [x] RE-795 Rust eligible ranking composition through64 clients ([evidence](research/RUST_RANKING_COMPLETE.md)).
1438
+
1439
+
1440
+ - [x] RE-796 Rust ranking population collection and all eight count globals ([evidence](research/RUST_RANKING_POPULATION.md)).
docs/research/README.md CHANGED
@@ -975,3 +975,6 @@ The extension collector adds five focused tests for native tail calls, command-r
975
 
976
 
977
  [RE-795: Rust eligible ranking composition through64 clients](RUST_RANKING_COMPLETE.md).
 
 
 
 
975
 
976
 
977
  [RE-795: Rust eligible ranking composition through64 clients](RUST_RANKING_COMPLETE.md).
978
+
979
+
980
+ [RE-796: Rust ranking population collection and all eight count globals](RUST_RANKING_POPULATION.md).
docs/research/RUST_RANKING_POPULATION.md ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ # Rust ranking population collection and all eight count globals
2
+
3
+ RE-796. Reproduce with `python3 scripts/analyze_rust_ranking_population.py`; focused validation: `cargo xtask test-group rust-ranking-population`. [Machine report](rust-ranking-population.json).
4
+
5
+ A semantic Slot/Population API models the source scan without engine addresses. The research CLI maps its fields back to all eight observed globals. Independent tests cover connection/team gates, scan membership, bots in the first-playing slots, reset behavior, empty populations and64/65 boundaries. The original negative count case is represented by an empty Rust slice, with no unsafe ABI/count claim. Sorting and publication remain separate.
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": 58,
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": 60,
2989
  "source_report": "docs/research/qvm-health-impulse.json",
2990
  "source_report_sha256": "625fd8064004ea6dbdf398d51d0af844d9c635ff04c0a0790886dddb6ab81e71"
2991
  }
docs/research/rust-ranking-population.json ADDED
The diff for this file is too large to render. See raw diff
 
scripts/analyze_rust_ranking_population.py ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ import hashlib,json
3
+ from pathlib import Path
4
+ from analyze_rust_ranking import words
5
+ from rust_lifecycle_support import binary,result,invoke
6
+ from analyze_qvm import write_json
7
+
8
+ def main():
9
+ b=binary();rows=[];sources={}
10
+ for name in ['qvm-ranking-setup','qvm-ranking-large-caller','qvm-ranking-population']:
11
+ p=Path('docs/research/'+name+'.json');data=p.read_bytes();sources[str(p)]=hashlib.sha256(data).hexdigest()
12
+ for row in json.loads(data)['probes']:
13
+ c=row['input']['clients']
14
+ if name=='qvm-ranking-setup':c=[{'468':v['state'],'3096':v['team'],'flags':v['flags']} for v in c]
15
+ r=result(b,'rank-population',[len(c),*[v for client in c for v in [*words(client),client.get('flags',0)]]]);rows.append({'clients':c,'source_globals':row['globals'],'rust':r})
16
+ bad=invoke(b,'rank-population',[65],check=False);assert bad.returncode
17
+ write_json(Path('docs/research/rust-ranking-population.json'),{'source_reports':sources,'probes':rows,'rejections':[bad.returncode],'limits':'Rust5864 collection matches185 setup/full source fixtures. Source negative count fixture represented as an empty safe Rust slice; no raw pointer/count ABI claim. All count globals and scan order checked independently; no sorting/publication here.'})
18
+ if __name__=='__main__':main()
scripts/test_rust_ranking_population.py ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import hashlib,json,unittest
2
+ from pathlib import Path
3
+ from test_qvm_ranking_population import globals_for
4
+ class RustPopulationTests(unittest.TestCase):
5
+ def test_source_globals_and_independent_membership(self):
6
+ r=json.loads(Path('docs/research/rust-ranking-population.json').read_text());self.assertEqual(len(r['probes']),185)
7
+ for p in r['probes']:c=p['clients'];self.assertEqual(p['rust']['globals'],p['source_globals']);self.assertEqual(p['rust']['globals'],globals_for(c));self.assertEqual(p['rust']['order'],[j for j,v in enumerate(c) if v.get('468',2)!=0])
8
+ def test_identity_and_limit(self):
9
+ r=json.loads(Path('docs/research/rust-ranking-population.json').read_text())
10
+ for p,h in r['source_reports'].items():self.assertEqual(hashlib.sha256(Path(p).read_bytes()).hexdigest(),h)
11
+ self.assertTrue(all(r['rejections']))
12
+ if __name__=='__main__':unittest.main()
xtask/checks.tsv CHANGED
@@ -1820,3 +1820,16 @@ report scripts/analyze_rust_ranking_complete.py docs/research/rust-ranking-compl
1820
  edge docs/research/rust-ranking-complete.json scripts/test_rust_ranking_complete.py
1821
  group rust-ranking-complete scripts/analyze_rust_ranking_complete.py
1822
  edge scripts/analyze_rust_ranking_complete.py scripts/analyze_qvm_resumer_audit.py
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1820
  edge docs/research/rust-ranking-complete.json scripts/test_rust_ranking_complete.py
1821
  group rust-ranking-complete scripts/analyze_rust_ranking_complete.py
1822
  edge scripts/analyze_rust_ranking_complete.py scripts/analyze_qvm_resumer_audit.py
1823
+ edge Cargo.toml scripts/analyze_rust_ranking_population.py
1824
+ edge Cargo.lock scripts/analyze_rust_ranking_population.py
1825
+ edge crates/urt-gameplay/Cargo.toml scripts/analyze_rust_ranking_population.py
1826
+ edge crates/urt-gameplay/src/lib.rs scripts/analyze_rust_ranking_population.py
1827
+ edge crates/urt-gameplay/src/bin/urt-lifecycle.rs scripts/analyze_rust_ranking_population.py
1828
+ edge crates/urt-gameplay/src/ranking_population.rs scripts/analyze_rust_ranking_population.py
1829
+ edge crates/urt-gameplay/src/ranking_population.rs scripts/analyze_rust_health_impulse.py
1830
+ group gameplay scripts/analyze_rust_ranking_population.py
1831
+ group rust-ranking-population scripts/analyze_rust_health_impulse.py
1832
+ report scripts/analyze_rust_ranking_population.py docs/research/rust-ranking-population.json
1833
+ edge docs/research/rust-ranking-population.json scripts/test_rust_ranking_population.py
1834
+ group rust-ranking-population scripts/analyze_rust_ranking_population.py
1835
+ edge scripts/analyze_rust_ranking_population.py scripts/analyze_qvm_resumer_audit.py
xtask/src/lib.rs CHANGED
@@ -1042,6 +1042,7 @@ mod integration_tests {
1042
  ("recipient_payload", "recipient_payload"),
1043
  ("ranking", "ranking"),
1044
  ("rank_sort", "rank_sort"),
 
1045
  ("leaderboard", "leaderboard"),
1046
  ("recipient_wire", "recipient_wire"),
1047
  ] {
 
1042
  ("recipient_payload", "recipient_payload"),
1043
  ("ranking", "ranking"),
1044
  ("rank_sort", "rank_sort"),
1045
+ ("ranking_population", "ranking_population"),
1046
  ("leaderboard", "leaderboard"),
1047
  ("recipient_wire", "recipient_wire"),
1048
  ] {