Files
benilla-octo/crates/benilla-formats/examples/chain_shadow.rs
T
gromandClaude Opus 5 f4960efc53 The realm update lands clean, and three of the "269 known red" were real
His 2026-10-28 20:18 update rewrote `Data/patch-4.mpq` (1.37 GB) and
`patch-5.mpq`
(230 MB). Verified rather than assumed, and nothing of ours broke: the realm's
`FrameXML.toc` went 146 → 151 rows and patch-5 holds the richest copy, so
`chain::demote_stale_snapshots` fires and it wins the manifest; the five new
rows
are realm modules (`OctoChallengesSession`, `OctoHardcoreBGMenu`, `OctoReplay`,
`OctoReplayBar`, `OctoReport`) that `unnamed_chain_rows` picks up by design and
that load with **0 load errors and 0 session errors**, every global they name
resolving; DBC field counts are unchanged everywhere, so no parser is at risk,
while record counts moved (Spell 27992 → 28026, CreatureDisplayInfo 13423 →
13451), which is the proof the new data is live; a `WOW_CAPTURE=vista` at
Ironforge prints **0 ERROR lines**; and both golden baselines held exactly, 269
with an identical failing name set and 80, `goldshire_cell` still at 1874.

Two examples answer "did it land", because "the file is in the archive" never
does. The reference mounts `patch-?.MPQ` by case-folded name, so every
`patch-A..Z` outranks `patch-4`/`patch-5`, and the toc-subset demotion cannot
see
an archive that carries no toc at all. `chain_report` prints the `FrameXML.toc`
copies in priority order and, with `--toc`, the winner of every row of the
realm's manifest with a tally — 68 to patch-4, 43 to patch-5, 32 to `patch.MPQ`,
7 to `interface.MPQ`, 1 to patch-3, and **none to a stale letter patch**.
`chain_shadow` answers from the archive's side: patch-4 carries 20326 paths of
which 10220 are shadowed, patch-5's 1494 are not shadowed at all.

Then, grouping the red bucket by panic *site* rather than trusting its label:
252 of the 269 are shipped-UI tests and **136 share one site**. Three of the
rest
were real, and are fixed here.

Six `OptionsFrame_*` enable/disable helpers were missing outright. `benilla.toc`
is the only thing that loads two of the reference's settings windows — the
realm's own toc lists neither `SoundOptionsFrame.xml` nor `UIOptionsFrame.xml` —
stock 1.12 keeps those six beside them in its own `OptionsFrame.lua`, and the
realm replaced that file with Turtle's rewrite, which drops them. Checked
against
all 151 interface rows, Lua and XML: nothing defines one. So those windows held
controls that could not grey out, with the first raise waiting on the click that
reached one. `assets/ui/OptionsFrameAdapters.lua` carries the reference's own
bodies, read out of `patch.MPQ` rather than from memory.

`CHARACTERFRAME_SUBFRAMES` was asserted to hold exactly five. The realm's
`CharacterFrame.lua` carries a sixth, `ArenaFrame` — which this client already
stubs (`CharacterFrameAdapters.xml`), so the count was the only thing wrong. It
now checks the reference's five by membership and order, which is what the test
is for; a realm may append.

And the kit's stand-ins were anonymous. `CreateFrame("Frame")` with no name
answers `GetName()` nil and `GetID()` 0, the publish is `setglobal` either way,
so naming them costs nothing and the absence cost two of the 136's three errors:
`FCF_SetWindowName` concatenating a nil name and `SetChatWindowDocked` refusing
window 0. `ChatFrame1`/`ChatFrame2` are no longer seated at all — every row of
`UIPARENT_MANAGED_FRAME_POSITIONS` is read through `frame = _G[index]; if
( frame )`, so a missing one is skipped, and seating them only shadowed the real
windows for good. The pass runs at load, from `ShapeshiftBar_OnLoad`, twelve toc
rows before the chat files, which is why the stand-ins were always first.

That leaves 136 on one cause, and the two tests added to `loader/tests.rs` are
what ruled out the first hypothesis: a template's `parent="UIParent"` does reach
the frame that inherits it, one level and two, which is how `ChatFrame1` is
built. The next suspects are written down in the gates memory.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
2026-10-28 21:15:41 -06:00

70 lines
2.9 KiB
Rust

//! `cargo run -p benilla-formats --example chain_shadow` — every file a named archive carries that
//! some *higher-priority* archive also carries, i.e. the part of that archive the client will never
//! read.
//!
//! The question a realm update raises: the reference mounts `patch-?.MPQ` by case-folded name, so a
//! letter patch outranks a digit patch (`chain::mount_order`). When an update ships as
//! `patch-4`/`patch-5` and the install also holds `patch-H`, `patch-M`, `patch-O`… , every file the
//! two sets share resolves to the *letter* one. `chain::demote_stale_snapshots` only catches an
//! archive whose `FrameXML.toc` is a strict subset of another's, and a letter patch carrying no toc
//! at all is not a candidate — so this is the measurement that says whether the update landed.
//!
//! Argument: one or more archive file names as they sit in `Data` (default `patch-4.mpq`,
//! `patch-5.mpq`).
fn main() {
let Some(data) = benilla_formats::wow_data() else {
eprintln!("no install");
std::process::exit(1);
};
let args: Vec<String> = std::env::args().skip(1).collect();
let wanted: Vec<String> = if args.is_empty() {
vec!["patch-4.mpq".into(), "patch-5.mpq".into()]
} else {
args
};
let chain = benilla_formats::Chain::open(&data).expect("chain opens");
let all = chain.list().expect("chain lists");
println!("chain holds {} distinct paths", all.len());
for want in &wanted {
// Every path this archive carries, from the chain's own per-file copy list.
let mut carried = 0usize;
let mut shadowed: Vec<(String, String)> = Vec::new();
for e in &all {
let copies = chain.copies(&e.name);
let mine = copies
.iter()
.any(|(p, _)| p.file_name().and_then(|n| n.to_str()) == Some(want.as_str()));
if !mine {
continue;
}
carried += 1;
let winner = chain
.find_file_archive(&e.name)
.and_then(|p| p.file_name())
.and_then(|n| n.to_str())
.unwrap_or("?");
if winner != want {
shadowed.push((e.name.clone(), winner.to_string()));
}
}
println!(
"\n== {want}: {carried} paths, {} shadowed ==",
shadowed.len()
);
let mut by_winner: std::collections::BTreeMap<String, Vec<&String>> = Default::default();
for (path, winner) in &shadowed {
by_winner.entry(winner.clone()).or_default().push(path);
}
for (winner, paths) in &by_winner {
println!(" lost to {winner}: {} path(s)", paths.len());
for p in paths.iter().take(12) {
println!(" {p}");
}
if paths.len() > 12 {
println!(" … {} more", paths.len() - 12);
}
}
}
}