His trade window has no money amounts: one empty box where the gold/silver/ copper row belongs. The cause is four regions that answer the wrong thing. pfUI skins that row by walking the gold EditBox's regions for the first Texture whose texture names "MoneyIcons", showing it, and re-anchoring it to the right of the box — then the silver box hangs off that icon and the copper box off the silver one (api/ui-widgets.lua:1225, api/api.lua:1693). Its scan reads a nil GetTexture() as "no name to match against, take this one". An EditBox's first four regions are the engine's own: the three selection highlights and the caret, which GetRegions lists ahead of the authored <Layers> ones. They paint host-side here, so they carried no texture and no fill and answered nil — and pfUI took the caret for the gold coin. Silver and copper then anchored off a zero-size region and the whole row collapsed. The reference builds those four as colour-set textures, not empty ones, so GetTexture answers them "Solid Texture" — the string is in WoW.exe, and pfUI's scan does filter that out. They answer it here now. Nothing renders differently: the host still draws the highlight and caret from sel_start / sel_end / caret_shown, and the fill stays unset. The new test runs pfUI's scan itself rather than asserting on our side of it, because what matters is which region that loop picks. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
125 lines
5.1 KiB
Rust
125 lines
5.1 KiB
Rust
//! `cargo run -p benilla-formats --example chain_report [--toc | <file-in-chain>…]` — which archive
|
|
//! the client will actually read a file from, and why.
|
|
//!
|
|
//! The question a realm update raises. The reference mounts `patch-?.MPQ` by case-folded name, so a
|
|
//! letter patch outranks a digit patch ([`benilla_formats::Chain`]'s `mount_order`), and an update
|
|
//! shipped as `patch-4`/`patch-5` lands *below* every `patch-A..Z` already on disk. One deviation
|
|
//! corrects part of that — an archive whose `FrameXML.toc` is a strict subset of another's is
|
|
//! demoted as a stale snapshot — but an archive carrying no toc at all is not a candidate for it,
|
|
//! so after an update this is the measurement that says whether the new files are the ones being
|
|
//! read. `chain_shadow` answers the same question from the archive's side.
|
|
//!
|
|
//! - no argument: the `FrameXML.toc` copies in priority order, which is the evidence the demotion
|
|
//! keys on, plus the winner of a handful of files an update moves.
|
|
//! - `--toc`: every row of the realm's own interface manifest with the archive that wins it, and a
|
|
//! tally per archive. This is the one to read after an update: a row won by a *months-old* letter
|
|
//! patch is a file the realm replaced and the client is not reading.
|
|
//! - file names: the winner for each, as the client's backslash paths.
|
|
//! - `--cat <file>`: the **winning** copy's bytes on stdout, which is the copy the client reads.
|
|
//! Reading a shipped file any other way — unzipping an archive by hand, or trusting a copy on
|
|
//! disk — answers a different question than "what does the client see", and after an update
|
|
//! those two answers diverge, which is the whole reason this example exists.
|
|
fn main() {
|
|
let Some(data) = benilla_formats::wow_data() else {
|
|
eprintln!("no install");
|
|
std::process::exit(1);
|
|
};
|
|
println!("data: {}", data.display());
|
|
let chain = benilla_formats::Chain::open(&data).expect("chain opens");
|
|
let toc = "Interface\\FrameXML\\FrameXML.toc";
|
|
let args: Vec<String> = std::env::args().skip(1).collect();
|
|
|
|
if let Some(i) = args.iter().position(|a| a == "--cat") {
|
|
let Some(name) = args.get(i + 1) else {
|
|
eprintln!("--cat needs a file name");
|
|
std::process::exit(2);
|
|
};
|
|
let name = name.replace('/', "\\");
|
|
match chain.read(&name) {
|
|
Ok(bytes) => {
|
|
eprintln!(
|
|
"-- {name}: {} bytes, from {} --",
|
|
bytes.len(),
|
|
winner(&chain, &name)
|
|
);
|
|
use std::io::Write;
|
|
std::io::stdout().write_all(&bytes).expect("stdout");
|
|
}
|
|
Err(e) => {
|
|
eprintln!("{name}: not in the chain ({e})");
|
|
std::process::exit(1);
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
|
|
if args.iter().any(|a| a == "--toc") {
|
|
let bytes = chain.read(toc).expect("the realm has a FrameXML.toc");
|
|
let rows: Vec<String> = String::from_utf8_lossy(&bytes)
|
|
.lines()
|
|
.map(|l| l.trim().trim_start_matches('\u{feff}').to_string())
|
|
.filter(|l| !l.is_empty() && !l.starts_with('#'))
|
|
.collect();
|
|
println!(
|
|
"\n-- the realm's {} interface rows, and who wins each --",
|
|
rows.len()
|
|
);
|
|
let mut tally: std::collections::BTreeMap<String, usize> = Default::default();
|
|
for row in &rows {
|
|
let name = format!("Interface\\FrameXML\\{}", row.replace('/', "\\"));
|
|
let who = winner(&chain, &name);
|
|
*tally.entry(who.clone()).or_default() += 1;
|
|
println!(" {:<22} {row}", who);
|
|
}
|
|
println!("\n-- tally --");
|
|
for (who, n) in &tally {
|
|
println!(" {who:<22} {n}");
|
|
}
|
|
return;
|
|
}
|
|
|
|
println!("\n-- FrameXML.toc copies, ascending priority (last wins) --");
|
|
for (path, size) in chain.copies(toc) {
|
|
println!(" {:<22} {size:>8} bytes", name_of(path));
|
|
}
|
|
|
|
let files: Vec<String> = if args.is_empty() {
|
|
[
|
|
toc,
|
|
"Interface\\FrameXML\\GlobalStrings.lua",
|
|
"Interface\\FrameXML\\Minimap.lua",
|
|
"Interface\\FrameXML\\TargetFrame.lua",
|
|
"Interface\\GlueXML\\GlueXML.toc",
|
|
"DBFilesClient\\Spell.dbc",
|
|
"DBFilesClient\\AreaTable.dbc",
|
|
"DBFilesClient\\Map.dbc",
|
|
"DBFilesClient\\CreatureDisplayInfo.dbc",
|
|
]
|
|
.iter()
|
|
.map(|s| (*s).to_string())
|
|
.collect()
|
|
} else {
|
|
args
|
|
};
|
|
println!("\n-- winners --");
|
|
for f in &files {
|
|
println!(
|
|
" {f:<46} {:<22} ({} copies)",
|
|
winner(&chain, f),
|
|
chain.copies(f).len()
|
|
);
|
|
}
|
|
}
|
|
|
|
/// The archive the chain resolves `name` from, or `ABSENT`.
|
|
fn winner(chain: &benilla_formats::Chain, name: &str) -> String {
|
|
chain
|
|
.find_file_archive(name)
|
|
.map(|p| name_of(p).to_string())
|
|
.unwrap_or_else(|| "ABSENT".to_string())
|
|
}
|
|
|
|
fn name_of(p: &std::path::Path) -> &str {
|
|
p.file_name().and_then(|n| n.to_str()).unwrap_or("?")
|
|
}
|