1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
//! Acceptance test for `mercs2_engine::model::Model` — the cross-block assembly.
//!
//! For each model: the LOD chain it loaded, and then the FULL three-clause draw gate evaluated at
//! every LOD rung x {intact, wrecked}. A correct assembly means rung 0 (the camera up close) draws
//! the detailed body, not a 371-triangle `_lod_dm` proxy, and that wrecking it swaps the geometry
//! rather than piling wreck on top of hull.
//!
//! cargo run -p mercs2_probe --bin assembly_check
use mercs2_engine::model::Model;
use mercs2_engine::render_state::RenderState;
use mercs2_engine::wad;
use mercs2_formats::orchestrator as orch;
const MODELS: &[&str] = &[
"ch_veh_tank_ztz98",
"oc_veh_helicopter_md500",
"global_veh_klr650",
"civ_veh_car_van_crappy",
"al_veh_boat_destroyer",
"pmc_hum_mattias_v3",
];
fn main() {
let args: Vec<String> = std::env::args().skip(1).collect();
let names: Vec<String> =
if args.is_empty() { MODELS.iter().map(|s| s.to_string()).collect() } else { args };
let mut w = wad::resolve_vz_wad(None).and_then(|p| wad::open(&p).ok()).expect("vz.wad");
for name in &names {
let hash = mercs2_formats::hash::pandemic_hash_m2(name.trim_start_matches('_'));
let m = match Model::load(&mut w, hash) {
Ok(m) => m,
Err(e) => {
println!("{name}: {e}\n");
continue;
}
};
println!(
"{name} — {} rung(s), {} tri total, {} HIER, {} SEGM, lod_count {}",
m.rungs.len(),
m.triangles(),
m.hier.len(),
m.segm.len(),
m.lod_count()
);
for r in &m.rungs {
// Do all rungs live in the SAME space? If a fine rung's bbox doesn't sit on top of the
// resident one, merging them into one buffer stacks misaligned copies of the object.
let (mut lo, mut hi) = ([f32::MAX; 3], [f32::MIN; 3]);
for v in &r.vertices {
for i in 0..3 {
lo[i] = lo[i].min(v.pos[i]);
hi[i] = hi[i].max(v.pos[i]);
}
}
println!(
" bbox min [{:7.2} {:7.2} {:7.2}] max [{:7.2} {:7.2} {:7.2}]",
lo[0], lo[1], lo[2], hi[0], hi[1], hi[2]
);
println!(
" P{:03} block {:5} {:6} tri serves rungs {:?}",
r.level,
r.block,
r.triangles(),
(0..8u8).filter(|b| r.lod_bits() & (1 << b) != 0).collect::<Vec<_>>()
);
// The mask histogram AS THE MODEL BOUND IT — if a rung's segments resolved against the
// wrong SEGM row, the garbage shows up here as masks that don't belong to its band.
let mut hist: std::collections::BTreeMap<u8, (usize, u32, usize)> = Default::default();
for d in &r.draws {
let e = hist.entry(d.lod_mask).or_insert((0, 0, usize::MAX));
e.0 += 1;
e.1 += d.index_count / 3;
e.2 = e.2.min(d.group_index);
}
for (mask, (n, tris, _)) in &hist {
println!(" mask {mask:#04x} {n:3} draws {tris:6} tri");
}
}
// Full gate, per LOD rung, at full and zero health.
print!(" {:>10}", "LOD rung");
for n in 0..m.lod_count().min(8) {
print!(" {:>8}", n);
}
println!();
for (label, health) in [("intact", 1.0f32), ("wrecked", 0.0)] {
let node_enable = match &m.machine {
Some(sm) => {
let chosen = orch::node_states_for_health(sm, health, 0.99);
orch::machine_node_enable(sm, &m.hier, &chosen)
}
None => Vec::new(),
};
print!(" {label:>10}");
for n in 0..m.lod_count().min(8) {
let rs = RenderState {
lod: n as u8,
view_state: 1u8 << (n.min(7)),
node_enable: node_enable.clone(),
};
let tris: u32 =
m.visible_draws(&rs).iter().map(|(_, d)| d.index_count / 3).sum();
print!(" {tris:>8}");
}
println!();
}
println!();
}
}