A FrameXML template registered under a name already taken replaced the first, nothing was logged, and names differing only in case were stored twice. In 1.12.1 the registrar `0x6ee500` folds case (`0x64b3f0`, `0x64a4c0`) and keeps the first entry, logging "Virtual object named %s already exists" (`0x871188`), unless the old entry carries a non-zero script context and the new one zero (`0x6ee594`). The stock files load at context 0; `AddOn_Load` (`0x51f240`) loads an addon under its name unless its signature checked SECURE (`0x51dbfb`). Templates are now keyed case-folded with their context, an addon's load sets the context, an addon counts as secure only from the player's own data with `## Secure: 1`, and the refusal and the unnamed-node line (`0x871020`) reach FrameXML.log. Tests: duplicates and case variants refused, the context rule, the log lines. Fixes #556 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
183 lines
7.2 KiB
Rust
183 lines
7.2 KiB
Rust
//! The reference's load status (`CStatus`, `Status.cpp`, entries `{text, severity}`): each level of
|
|
//! a UI load (the add-on, its `.toc`, each XML document) reports into a record of its own, which is
|
|
//! merged into its caller's when it closes and drained, line by line, to `Logs\FrameXML.log`
|
|
//! (`0x842ecc`) at the load's end.
|
|
|
|
/// A trace, printed only under `FrameXML_Debug` (`"-- Creating %s named %s"`, `0x6ee2bc`).
|
|
pub const TRACE: u8 = 0;
|
|
/// An error the load goes on past: an unnamed template (`0x6ee1cc`), a template name already
|
|
/// registered (`0x6ee5af`).
|
|
pub const WARNING: u8 = 1;
|
|
/// A file-level failure: a file that did not open or did not parse (`0x6edad4`, `0x704c15`).
|
|
pub const FAILURE: u8 = 2;
|
|
|
|
/// One level's record: its lines in order and the running maximum severity (`+0x10`, which the
|
|
/// append `0x41a000` and the prepend `0x419ed0` raise, never a count).
|
|
#[derive(Clone, Debug, Default, PartialEq, Eq)]
|
|
pub struct Status {
|
|
lines: Vec<String>,
|
|
max: u8,
|
|
}
|
|
|
|
impl Status {
|
|
/// Append a line (`0x41a000`).
|
|
pub fn report(&mut self, severity: u8, text: impl Into<String>) {
|
|
self.lines.push(text.into());
|
|
self.max = self.max.max(severity);
|
|
}
|
|
|
|
/// Merge `other` in after this record's own lines, as `0x41a100` re-reports each entry.
|
|
pub fn merge(&mut self, other: Status) {
|
|
self.lines.extend(other.lines);
|
|
self.max = self.max.max(other.max);
|
|
}
|
|
|
|
/// Close one level into its caller: its banner goes first (a prepend at severity 0) when
|
|
/// `debug` is on or the level reported anything above a trace (`jg` on `FrameXML_Debug`, else
|
|
/// `jle` on the maximum, `0x6eddb6`/`0x6eddbb`, `0x6ee207`, `0x51f44f`), then the merge.
|
|
pub fn close_into(mut self, parent: &mut Status, debug: bool, banner: String) {
|
|
if debug || self.max > TRACE {
|
|
self.lines.insert(0, banner);
|
|
}
|
|
parent.merge(self);
|
|
}
|
|
|
|
/// The lines, in the order the drain writes them.
|
|
pub fn lines(&self) -> &[String] {
|
|
&self.lines
|
|
}
|
|
|
|
pub fn is_empty(&self) -> bool {
|
|
self.lines.is_empty()
|
|
}
|
|
|
|
/// The lines, leaving the record empty.
|
|
pub fn take_lines(&mut self) -> Vec<String> {
|
|
self.max = TRACE;
|
|
std::mem::take(&mut self.lines)
|
|
}
|
|
}
|
|
|
|
/// A path as the reference reports it: install-relative and `\`-separated.
|
|
pub fn install_path(path: &str) -> String {
|
|
path.replace('/', "\\")
|
|
}
|
|
|
|
/// Whether the load-one-file routine (`0x6ede10`) runs `path` as a chunk: the text from its last
|
|
/// `.` equals `.lua` (`0x8710c8`) under `_strnicmp` (`0x6edeea`-`0x6edf05`).
|
|
pub fn runs_as_lua(path: &str) -> bool {
|
|
path.rfind('.')
|
|
.is_some_and(|i| path[i..].eq_ignore_ascii_case(".lua"))
|
|
}
|
|
|
|
/// A file that did not open: `"Error loading %s"` (`0x872e50`) from the chunk arm (`0x704bc0`),
|
|
/// else `"Couldn't open %s"` (`0x846ff4`) from the XML arm (`0x6edaa0`).
|
|
pub fn missing(path: &str, lua: bool) -> String {
|
|
if lua {
|
|
format!("Error loading {}", install_path(path))
|
|
} else {
|
|
format!("Couldn't open {}", install_path(path))
|
|
}
|
|
}
|
|
|
|
/// A document that opened and did not parse (`0x846fd8`, `0x6edb11`).
|
|
pub fn unparsed(path: &str) -> String {
|
|
format!("Couldn't parse XML in {}", install_path(path))
|
|
}
|
|
|
|
/// An XML document's banner (`0x87100c`, `0x6ee21c`).
|
|
pub fn file_banner(path: &str) -> String {
|
|
format!("++ Loading file {}", install_path(path))
|
|
}
|
|
|
|
/// A `.toc`'s banner (`0x870fec`, `0x6eddc8`).
|
|
pub fn toc_banner(path: &str) -> String {
|
|
format!("** Loading table of contents {}", install_path(path))
|
|
}
|
|
|
|
/// An add-on's banner (`0x8539fc`, `0x51f464`).
|
|
pub fn addon_banner(name: &str) -> String {
|
|
format!("Loading add-on {name}")
|
|
}
|
|
|
|
/// Where an add-on's `.toc` is, as its banner names it: `Interface\AddOns\<name>\<name>.toc`.
|
|
pub fn addon_toc_path(name: &str) -> String {
|
|
format!("Interface\\AddOns\\{name}\\{name}.toc")
|
|
}
|
|
|
|
/// One drain of a load's record into `Logs\FrameXML.log`, in the order the loads ended.
|
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
|
pub enum LogWrite {
|
|
/// A UI load's drain (`UI_Init`, `0x490187`-`0x4901b6`): the log opens in mode 0 (`0x48ff06`),
|
|
/// created anew (`CREATE_ALWAYS`, `0x65a1c0`) at its first line (`0x65a930`), so a load that
|
|
/// reported nothing leaves the file as it was.
|
|
Rewrite(Vec<String>),
|
|
/// A runtime `LoadAddOn`'s drain (`0x46aac0`): mode 4 (`0x48ea4e`), opened (`OPEN_ALWAYS`)
|
|
/// and appended to at its first line.
|
|
Append(Vec<String>),
|
|
}
|
|
|
|
/// A VM's two load records and the drains they have made. `LoadAddOn` keeps its own record
|
|
/// (`ds:0xb4e3dc`), refcounted (`ds:0xb4e3e0`) so a nested call writes one block, and drained when
|
|
/// the count returns to zero (`0x48eab4`), even inside a UI load, whose own drain comes last.
|
|
#[derive(Debug, Default)]
|
|
pub(crate) struct LoadLog {
|
|
pub(crate) ui: Status,
|
|
pub(crate) demand: Status,
|
|
pub(crate) demand_depth: u32,
|
|
pub(crate) writes: Vec<LogWrite>,
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
/// The install's own log shape (the reference's `Logs\FrameXML.log`, `FrameXML_Debug` 0): the
|
|
/// add-on banner first, then the toc's, then the miss, and a later document with a message
|
|
/// under its own banner; a clean document says nothing.
|
|
#[test]
|
|
fn banners_frame_only_what_reported_something() {
|
|
let mut root = Status::default();
|
|
let mut addon = Status::default();
|
|
let mut toc = Status::default();
|
|
toc.report(FAILURE, missing("Interface/AddOns/A/Missing.xml", false));
|
|
let mut clean = Status::default();
|
|
clean.report(TRACE, "-- Creating Frame named X");
|
|
clean.close_into(&mut toc, false, file_banner("Interface/AddOns/A/Clean.xml"));
|
|
let mut broken = Status::default();
|
|
broken.report(FAILURE, missing("Interface/AddOns/A/b.lua", true));
|
|
broken.close_into(&mut toc, false, file_banner("Interface/AddOns/A/Core.xml"));
|
|
toc.close_into(&mut addon, false, toc_banner(&addon_toc_path("A")));
|
|
addon.close_into(&mut root, false, addon_banner("A"));
|
|
let mut quiet = Status::default();
|
|
Status::default().close_into(&mut quiet, false, toc_banner(&addon_toc_path("B")));
|
|
quiet.close_into(&mut root, false, addon_banner("B"));
|
|
assert_eq!(
|
|
root.lines(),
|
|
[
|
|
"Loading add-on A",
|
|
"** Loading table of contents Interface\\AddOns\\A\\A.toc",
|
|
"Couldn't open Interface\\AddOns\\A\\Missing.xml",
|
|
"-- Creating Frame named X",
|
|
"++ Loading file Interface\\AddOns\\A\\Core.xml",
|
|
"Error loading Interface\\AddOns\\A\\b.lua",
|
|
]
|
|
);
|
|
}
|
|
|
|
/// Under `FrameXML_Debug` every level prints its banner, reported or not.
|
|
#[test]
|
|
fn debug_prints_every_banner() {
|
|
let mut root = Status::default();
|
|
Status::default().close_into(&mut root, true, file_banner("x.xml"));
|
|
assert_eq!(root.lines(), ["++ Loading file x.xml"]);
|
|
}
|
|
|
|
#[test]
|
|
fn the_chunk_arm_is_the_lua_extension_alone() {
|
|
assert!(runs_as_lua("Interface/AddOns/A/core.LUA"));
|
|
assert!(!runs_as_lua("Interface/AddOns/A/core.lua.xml"));
|
|
assert!(!runs_as_lua("Interface/AddOns/A/core"));
|
|
}
|
|
}
|