diff --git a/crates/benilla-app/examples/addon_harness.rs b/crates/benilla-app/examples/addon_harness.rs index 0b037a4a..658a79f7 100644 --- a/crates/benilla-app/examples/addon_harness.rs +++ b/crates/benilla-app/examples/addon_harness.rs @@ -383,6 +383,18 @@ fn main() { " loaded without a single load error : {loaded}/{}", reports.len() ); + // **The reconciliation line, printed always** (decision 2155). `loaded` counts what RAISED; + // a manifest entry naming a file the package does not contain is not that — the reference logs + // `Couldn't open %s` and carries on — and it used to be counted here. Every past record's + // figure was the stricter one, so the stricter one is printed beside the honest one rather + // than left for a reader to reconstruct: two numbers cannot be silently confused, one can. + let strict = reports.iter().filter(|r| r.errors.is_empty()).count(); + println!( + " (…{} of those name a file their own package does not contain, which the reference \ + logs and carries on from; the pre-2155 column counted those as failures: {strict}/{})", + loaded - strict, + reports.len() + ); println!( " ...and calling nothing we lack : {clean}/{}", reports.len() diff --git a/crates/benilla-app/src/addon_harness/mod.rs b/crates/benilla-app/src/addon_harness/mod.rs index 69f3bf88..221c2724 100644 --- a/crates/benilla-app/src/addon_harness/mod.rs +++ b/crates/benilla-app/src/addon_harness/mod.rs @@ -107,7 +107,19 @@ pub struct AddonReport { /// `## Interface`, as written. 1.12 is `11200`; the corpus is full of older values, and we /// deliberately do not refuse them (decision 1191 §6). pub interface: Vec, - /// Did every file in its manifest load without an error? + /// **Did anything in its manifest RAISE, fail to parse, or drop a frame?** + /// + /// Not "did every entry resolve" — that is [`Self::absent_own_files`] and + /// [`Self::absent_foreign_files`], and it is a different question because the reference + /// answers it differently: a manifest line naming no file is `"Couldn't open %s"` in + /// FrameXML.log and the walk carries straight on (wow-re + /// `ui/scratch/xml-toc-path-resolution.md` §4, VERIFIED). An addon shipping an incomplete zip + /// *works* on a real client, so counting it here made the survey say the opposite of the truth + /// about 24 addons — FuBar itself among them, on two locale files it does not ship. + /// + /// **[`Self::errors`] is unchanged and still carries those rows verbatim** (1213), so every + /// number in every past record is still readable off it; what changed, deliberately and with + /// decision 2155 behind it, is which of them this flag counts. The report prints both. pub loaded: bool, /// Load errors, verbatim, tagged by file. pub errors: Vec, @@ -577,7 +589,7 @@ fn survey_one( // makes a high-water mark a valid cut of a log that also evicts and dedupes. let warn_mark = script.diagnostics().last().map_or(0, |d| d.seq); - let (errors, absent) = load_addon_files(&script, root, name, &toc); + let files = load_addon_files(&script, root, name, &toc); // The registry has to agree with the VM about what has loaded: the live walk stamps each // addon at `0x51f5ad`'s position (`ui_script::addons`'s `mark_addon_loaded`, just before // `ADDON_LOADED`), and without it here `IsAddOnLoaded` answered nil for an addon whose files @@ -647,10 +659,10 @@ fn survey_one( AddonReport { name: name.to_string(), interface: toc.interface_versions(), - loaded: errors.is_empty(), - errors, - absent_own_files: absent.own, - absent_foreign_files: absent.foreign, + loaded: !files.raised, + errors: files.errors, + absent_own_files: files.absent.own, + absent_foreign_files: files.absent.foreign, missing_globals: wants.missing_globals, missing_deps, missing_templates, @@ -1427,10 +1439,11 @@ fn missing_inherits(script: &UiScript, root: &Path, name: &str, toc: &Toc) -> Ve /// tree hanging off them. An addon's real Lua often hangs off its XML rather than its `.toc`, the /// same trap the 1.12 corpus set in decision 1190. fn source_files(root: &Path, name: &str, toc: &Toc) -> Vec { + let base = addon_base(name); let mut pending: Vec = toc .files .iter() - .map(|f| benilla_ui::loader::join_ref(name, f)) + .map(|f| benilla_ui::loader::join_ref(&base, f)) .collect(); let mut seen: BTreeSet = BTreeSet::new(); let mut out = Vec::new(); @@ -1864,23 +1877,40 @@ pub struct AbsentFiles { pub foreign: Vec, } +/// What one addon's manifest did: every load failure verbatim, which of them were files that +/// simply are not there, and whether anything actually **raised**. +struct FileLoad { + /// Every load failure, in order, unchanged in content and meaning (1213: `errors` is never + /// quietly shrunk — every past record's number is still readable off this list). + errors: Vec, + /// The subset of [`Self::errors`] that is a named file the provider does not have, split by + /// whose package is incomplete. + absent: AbsentFiles, + /// **Did anything raise, fail to parse, or drop a frame** — the question + /// [`AddonReport::loaded`] asks (decision 2155). + /// + /// It is `false` for an addon whose only failures are absent files, because on the reference + /// that addon *loads*: `0x6edaa0` logs `"Couldn't open %s"` and returns null, and the walk + /// carries on to the next manifest entry with nothing raised (wow-re + /// `ui/scratch/xml-toc-path-resolution.md` §4, VERIFIED — the same rule 2107 unified for the + /// two loaders that reach it from Lua). Until this existed the survey scored 24 corpus addons + /// as not surviving a session start on nothing but a locale file their own zip omits, FuBar + /// and 22 of its plugins among them. + raised: bool, +} + /// Run the addon's manifest through the same two arms the real loader uses — `.lua` as a chunk, -/// anything else as FrameXML — with the AddOns root as the provider's path space (decision 1186). -/// -/// Returns the errors **and**, beside them, the split of which entries did not resolve. The -/// absent ones are still in `errors` — 1213's rule, for the fifth time: this asks a new question -/// and gets a new column, it does not quietly shrink an old one. -fn load_addon_files( - script: &UiScript, - root: &Path, - name: &str, - toc: &Toc, -) -> (Vec, AbsentFiles) { +/// anything else as FrameXML — in the client's **install-relative** path space (decision 2155: +/// `Interface/AddOns/`, exactly what `ui_script::addons::Addon::prefix` hands its own +/// loader, so a chunk name here and a chunk name in a live session are the same string). +fn load_addon_files(script: &UiScript, root: &Path, name: &str, toc: &Toc) -> FileLoad { let provider = |req: &str| -> Option> { read_under(root, req) }; + let base = addon_base(name); let mut errors = Vec::new(); let mut absent = AbsentFiles::default(); + let mut raised = false; for file in &toc.files { - let path = benilla_ui::loader::join_ref(name, file); + let path = benilla_ui::loader::join_ref(&base, file); let Some(bytes) = read_under(root, &path) else { errors.push(format!("{file}: not found")); // WHOSE package is incomplete. `join_ref` has already collapsed the `..`s the way the @@ -1889,7 +1919,7 @@ fn load_addon_files( // inside the addon's own folder is the addon shipping a manifest it does not satisfy, // while one pointing out of it wants a neighbour that is not installed. let own = path - .strip_prefix(name) + .strip_prefix(base.as_str()) .is_some_and(|rest| rest.starts_with('/')); if own { absent.own.push(file.clone()); @@ -1901,11 +1931,13 @@ fn load_addon_files( if is_lua(file) { // Named as the client names it, so the survey sees what a player would: an addon // that PARSES a traceback for its own folder (the whole FuBar family) needs the real - // `Interface\AddOns\\` shape, not mlua's Rust-caller default. - if let Err(e) = - script.run_chunk_named(&bytes, &benilla_ui::script::addon_chunk_name(name, file)) + // `Interface\AddOns\\` shape, not mlua's Rust-caller default. Built + // from the RESOLVED path, like the loader's own `