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
//! `block_content_grep` — grep the DECOMPRESSED CONTENTS of every WAD block.
//!
//! NOTE: `mercs2_probe block-grep` only greps block PATH NAMES (`diag::block_grep`
//! walks `wad::block_paths`), which is easy to mistake for a content search — it
//! silently reports "0 blocks match" for any string that only exists *inside* a
//! block. This is the content-side counterpart: it decompresses each block and
//! searches the bytes, printing the block index/path plus the matching strings.
//!
//! ```text
//! cargo run -p mercs2_probe --bin block_content_grep -- bone_ [--wad path]
//! ```
use mercs2_engine::wad;
fn main() {
let args: Vec<String> = std::env::args().skip(1).collect();
let needle = args
.iter()
.find(|a| !a.starts_with("--"))
.cloned()
.unwrap_or_else(|| {
eprintln!("usage: block_content_grep <needle> [--wad <path>]");
std::process::exit(2);
});
let wadpath = args
.iter()
.position(|a| a == "--wad")
.and_then(|i| args.get(i + 1).cloned())
.or_else(|| wad::resolve_vz_wad(None))
.unwrap_or_else(|| "game-files/vz.wad".into());
// Console (Xbox/PS3) WAD payloads are 32-bit byte-swapped, so ASCII is scrambled WITHIN each
// 4-byte word ("UCFX" reads as "XFCU"). `--swap` un-swaps each block before searching. Without
// it, a console WAD yields a silent, bogus "0 matches".
let swap = args.iter().any(|a| a == "--swap");
let mut w = wad::open(&wadpath).expect("open wad");
let paths: Vec<String> = wad::block_paths(&w).to_vec();
let n = paths.len();
eprintln!("[content-grep] scanning {n} blocks of {wadpath} for {needle:?} (swap={swap})");
let pat = needle.as_bytes();
let mut blocks_hit = 0usize;
// Decompression stats — a block that fails to decompress must NOT look like "no match".
let (mut ok, mut fail) = (0usize, 0usize);
for bi in 0..n {
let data = match wad::decompress_block_index(&mut w, bi as u16) {
Ok(d) => {
ok += 1;
d
}
Err(_) => {
fail += 1;
continue;
}
};
let data = if swap {
let mut d = data;
for c in d.chunks_exact_mut(4) {
c.reverse();
}
d
} else {
data
};
if bi % 1000 == 0 {
eprintln!(" [{bi}/{n}]");
}
// collect the printable-ASCII runs that contain the needle
let mut found: Vec<String> = Vec::new();
let mut i = 0usize;
while i + pat.len() <= data.len() {
if data[i..].starts_with(pat) {
// widen to the surrounding identifier run
let mut s = i;
while s > 0 && is_id(data[s - 1]) {
s -= 1;
}
let mut e = i + pat.len();
while e < data.len() && is_id(data[e]) {
e += 1;
}
if let Ok(t) = std::str::from_utf8(&data[s..e]) {
if !found.iter().any(|f| f == t) {
found.push(t.to_string());
}
}
i = e.max(i + 1);
} else {
i += 1;
}
}
if !found.is_empty() {
blocks_hit += 1;
println!("\nblock={bi:<6} {} ({} matches)", paths[bi], found.len());
for f in &found {
println!(" {f}");
}
}
}
eprintln!(
"[content-grep] {blocks_hit} blocks contain {needle:?} (decompressed ok={ok} failed={fail})"
);
}
fn is_id(b: u8) -> bool {
b.is_ascii_alphanumeric() || b == b'_' || b == b'.' || b == b'-'
}