benilla's own interface loads as one layer after the stock FrameXML and before the addons; the core loads StatsFrame.xml and runs LocaleProperties.lua once, as FrameXML.toc does (#562)
benilla's own interface now loads as one layer on top of the stock UI, as `docs/METHOD.md` now states: the core loads the stock FrameXML rows, the layer (`assets/ui/layer.toc`) loads after the last of them and before the third-party addons, and like FrameXML it has no AddOn API row and fires no `ADDON_LOADED` (the reference loads `FrameXML.toc` outside `AddOn_Load`, `UI_Init` `0x48fbf0`). - Moved into the layer unchanged: `ScrollTemplates.xml`, `KeyBindingsPage.xml`, `OptionsFrame.xml`, `ContainerFrameAdapters.xml`, `SpellBookAdapters.xml`, `ScriptLogFrame.xml`. `GameMenuFrame.xml` stays the core's last row until #553. - The host's Lua fixes to stock code (`FCF_ChatTabFadeFinished`, the `DurabilityFrame` re-seat) move into the layer's `FrameXMLFixes.xml`; the resize re-seat stays host-side as an engine adaptation. - The core now loads `StatsFrame.xml` and runs `LocaleProperties.lua` once, from `UIParent.xml`, so its chain rows are exactly the stock `FrameXML.toc` rows but the menu; the order is #555. - A dev build boots without the layer under `WOW_STOCK_UI=1`; the host's calls into the layer are guarded. Verified: new tests for the layer's file list, the core list against the stock toc, the load position (a `_G` definition log), absence from the AddOn API and the stock-only boot, each failing with the change undone; a before/after dump of every global, frame, draw order and saved variable differs only by `StatsFrame` and the moved fixes' source; `scripts/gates.sh` and `scripts/smoke.sh` green, and a live logout/re-login under `WOW_STOCK_UI=1` clean. Built on macOS; no platform seam touched. Part of #552 Fixes #542 Fixes #543 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
44fa1813ac
commit
22634279bd
@@ -0,0 +1,33 @@
|
||||
<Ui>
|
||||
<!--
|
||||
benilla's fixes to the stock FrameXML Lua, loaded after every stock file.
|
||||
-->
|
||||
<Script><![CDATA[
|
||||
-- Deviation: the stock FCF_ChatTabFadeFinished (FloatingChatFrame.lua:989-992) nils
|
||||
-- oldAlpha even under a live hover, so leaving and re-entering within
|
||||
-- CHAT_FRAME_FADE_TIME (a flick to the scroll buttons, outside the hover box, does it)
|
||||
-- stops the plate fading in (l.873) for the rest of the session; this keeps oldAlpha while
|
||||
-- hover is set.
|
||||
if FCF_ChatTabFadeFinished then
|
||||
function FCF_ChatTabFadeFinished(chatTab, chatFrame)
|
||||
chatTab:Hide()
|
||||
if not chatFrame.hover then
|
||||
chatFrame.oldAlpha = nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Deviation: re-runs UIParent_ManageFramePositions after DurabilityFrame's own OnEvent. The
|
||||
-- pass seats the frame 20 further in while a side glyph shows (UIParent.lua:1759-1768),
|
||||
-- but the stock frame runs it only from OnShow/OnHide, so a glyph appearing while the frame
|
||||
-- is shown leaves the shield's overhang off the screen edge. The stock handler runs first,
|
||||
-- unchanged, on the this/event/arg1 already set for the dispatch.
|
||||
if DurabilityFrame then
|
||||
local refOnEvent = DurabilityFrame:GetScript("OnEvent")
|
||||
DurabilityFrame:SetScript("OnEvent", function()
|
||||
if refOnEvent then refOnEvent() end
|
||||
UIParent_ManageFramePositions()
|
||||
end)
|
||||
end
|
||||
]]></Script>
|
||||
</Ui>
|
||||
@@ -1,6 +1,7 @@
|
||||
# The built-in interface, in load order: a bare filename is ours, from `assets/ui`; a path is
|
||||
# the reference's own file, off the player's patch chain. Entry 0, Fonts.xml, loads alone into
|
||||
# every boot VM, from Startup on; the rest loads on entering the world, then third-party addons.
|
||||
# The core interface, in load order: the reference's own `FrameXML.toc` files, off the player's
|
||||
# patch chain, and one bare filename of ours from `assets/ui`. Entry 0, Fonts.xml, loads alone
|
||||
# into every boot VM, from Startup on; the rest loads on entering the world, then benilla's layer
|
||||
# (`layer.toc`), then the third-party addons.
|
||||
# The font objects thus exist before the login screen; the reference loads Fonts.xml with the
|
||||
# rest of FrameXML.
|
||||
# `inherits=`, `parent=` and a named `relativeTo=` resolve only against names declared above; a
|
||||
@@ -19,8 +20,6 @@ Interface\FrameXML\Fonts.xml
|
||||
Interface\FrameXML\GlobalStrings.lua
|
||||
# `Localize()` runs at load and overwrites GlobalStrings keys, so this follows them.
|
||||
Interface\FrameXML\Localization.xml
|
||||
# `GetText` is a Lua global, not an engine binding: the client's `0x703bf0` calls it.
|
||||
Interface\FrameXML\LocaleProperties.lua
|
||||
Interface\FrameXML\BasicControls.xml
|
||||
# Before UIParent.xml, whose `LowHealthFrame` has `parent="WorldFrame"`.
|
||||
Interface\FrameXML\WorldFrame.xml
|
||||
@@ -52,15 +51,12 @@ Interface\FrameXML\TargetFrame.xml
|
||||
Interface\FrameXML\PetFrame.xml
|
||||
# Anchors `relativeTo="TargetFrame"`.
|
||||
Interface\FrameXML\ComboFrame.xml
|
||||
# Ours: the scroll parts of our options window and script log.
|
||||
ScrollTemplates.xml
|
||||
|
||||
Interface\FrameXML\UIPanelTemplates.lua
|
||||
|
||||
Interface\FrameXML\UIPanelTemplates.xml
|
||||
# The reference's three options windows, loaded hidden so addons find their frames by name.
|
||||
# Deviation: the player opens our own options window, `OptionsFrame.xml` below, because 1.12's
|
||||
# settings screens are much worse to use; the stock one holds `OptionsFrame`.
|
||||
# Deviation: the player opens the layer's own options window, because 1.12's settings screens are
|
||||
# much worse to use; the stock one holds `OptionsFrame`.
|
||||
Interface\FrameXML\OptionsFrame.xml
|
||||
Interface\FrameXML\SoundOptionsFrame.xml
|
||||
# Inherits `OptionsCheckButtonTemplate`, and declares `UIOptionsFrameCheckButtons`, which
|
||||
@@ -70,12 +66,6 @@ Interface\FrameXML\UIOptionsFrame.xml
|
||||
Interface\FrameXML\StaticPopup.xml
|
||||
Interface\FrameXML\CoinPickupFrame.xml
|
||||
|
||||
# Ours: the Keybindings page's templates and script, which OptionsFrame.xml uses at load.
|
||||
KeyBindingsPage.xml
|
||||
|
||||
# Ours: `BenillaOptionsFrame`; its rows read the option globals the stock windows above set.
|
||||
OptionsFrame.xml
|
||||
|
||||
# `MainMenuBar`, `MainMenuBarArtFrame` and `MainMenuExpBar`, which later files read at load.
|
||||
Interface\FrameXML\MainMenuBar.xml
|
||||
|
||||
@@ -115,16 +105,14 @@ Interface\FrameXML\PaperDollFrame.xml
|
||||
Interface\FrameXML\PetPaperDollFrame.xml
|
||||
Interface\FrameXML\MainMenuBarBagButtons.xml
|
||||
Interface\FrameXML\StackSplitFrame.xml
|
||||
# Ours: wraps the stock bag verbs; the keyring is a container (id -2).
|
||||
ContainerFrameAdapters.xml
|
||||
Interface\FrameXML\ReputationFrame.xml
|
||||
Interface\FrameXML\SkillFrame.xml
|
||||
# Brings HonorFrameTemplates.xml, whose row templates `Blizzard_InspectUI` inherits.
|
||||
Interface\FrameXML\HonorFrame.xml
|
||||
Interface\FrameXML\DressUpFrame.xml
|
||||
# `FrameXML.toc` has it between ItemButtonTemplate.xml and SpellBookFrame.xml.
|
||||
Interface\FrameXML\StatsFrame.xml
|
||||
Interface\FrameXML\SpellBookFrame.xml
|
||||
# Ours: the pet page's autocast widgets in the pet action button's proportions.
|
||||
SpellBookAdapters.xml
|
||||
# RaidFrame.xml names `parent="FriendsFrame"` and MailFrame.xml inherits `FriendsFrameTabTemplate`.
|
||||
Interface\FrameXML\FriendsFrame.xml
|
||||
Interface\FrameXML\RaidFrame.xml
|
||||
@@ -156,12 +144,11 @@ Interface\FrameXML\BonusActionBarFrame.xml
|
||||
Interface\FrameXML\WorldStateFrame.xml
|
||||
# `ClassTrainerListScrollFrameTemplate`, which on-demand addons inherit.
|
||||
Interface\FrameXML\ClassTrainerFrameTemplates.xml
|
||||
# Ours: the `/errors` script error log and our slash commands.
|
||||
ScriptLogFrame.xml
|
||||
Interface\FrameXML\HelpFrame.xml
|
||||
Interface\FrameXML\CinematicFrame.xml
|
||||
Interface\FrameXML\TabardFrame.xml
|
||||
Interface\FrameXML\TutorialFrame.xml
|
||||
# Ours: the ESC game menu. Last: it wraps `ToggleGameMenu`, `IsOptionFrameOpen` and
|
||||
# `UpdateMicroButtons` to count `BenillaOptionsFrame`.
|
||||
# Ours, in place of the stock GameMenuFrame.xml until the layer reshapes the stock menu (#553).
|
||||
# Last: it wraps `ToggleGameMenu`, `IsOptionFrameOpen` and `UpdateMicroButtons` to count
|
||||
# `BenillaOptionsFrame`.
|
||||
GameMenuFrame.xml
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
# benilla's own interface, one layer on the stock one (docs/METHOD.md): every file is ours, from
|
||||
# `assets/ui`, and loads in this order after every `benilla.toc` row and before the third-party
|
||||
# addons. It is listed in no addon list, fires no `ADDON_LOADED` and cannot be switched off; a dev
|
||||
# build boots without it under `WOW_STOCK_UI=1`. A test names its files, so it does not grow.
|
||||
|
||||
## Interface: 11200
|
||||
|
||||
# The scroll parts of our options window and script log.
|
||||
ScrollTemplates.xml
|
||||
# The Keybindings page's templates and script, which OptionsFrame.xml uses at load.
|
||||
KeyBindingsPage.xml
|
||||
# `BenillaOptionsFrame`; its rows read the option globals the stock options windows set.
|
||||
OptionsFrame.xml
|
||||
# Wraps the stock bag verbs; the keyring is a container (id -2).
|
||||
ContainerFrameAdapters.xml
|
||||
# The pet page's autocast widgets in the pet action button's proportions.
|
||||
SpellBookAdapters.xml
|
||||
# The `/errors` script error log and our slash commands.
|
||||
ScriptLogFrame.xml
|
||||
# Our fixes to the stock Lua.
|
||||
FrameXMLFixes.xml
|
||||
@@ -484,7 +484,7 @@ fn latch_and_dispatch(
|
||||
if let Some(chord_str) = captured {
|
||||
// The Keybindings page's own handler, not a binding body.
|
||||
if let Some(s) = script.as_mut() {
|
||||
if let Err(e) = s.run(&format!("KeyBindings_OnHostKey(\"{chord_str}\")")) {
|
||||
if let Err(e) = s.run(&crate::ui_script::host_key_capture(&chord_str)) {
|
||||
warn!("bindings(capture): {e}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1731,9 +1731,10 @@ pub(crate) static ABSENT: &[Absent] = &[
|
||||
// ── Misc ────────────────────────────────────────────────────────────────────────────
|
||||
// The nine `hidden="true" debug="true"` rows: benilla's instruments for these answer to the
|
||||
// dev plane's chords, not to a binding.
|
||||
// Stock `StatsFrame.lua` defines `ToggleStats`; the readout it shows is `GetDebugStats`.
|
||||
absent!(
|
||||
"TOGGLESTATS",
|
||||
["ToggleStats"],
|
||||
["GetDebugStats"],
|
||||
"a dev-plane instrument on a dev chord"
|
||||
),
|
||||
absent!(
|
||||
|
||||
@@ -291,13 +291,13 @@ fn slash_command(index: SlashIndex, args: &str) -> ParsedChat {
|
||||
// `/reload` is the rebuild `/console reloadUI` runs (`0x4035f0`, which reads no arguments).
|
||||
S::ReloadUi => ParsedChat::ReloadUi,
|
||||
S::ConvertRaid => ParsedChat::ConvertRaid,
|
||||
// `/errors`, benilla's own: toggles the script error log, a FrameXML window;
|
||||
// `/errors clear` empties it.
|
||||
// `/errors`, benilla's own: toggles the layer's script error log; `/errors clear` empties
|
||||
// it. A no-op when the run boots without the layer.
|
||||
S::ScriptErrors => ParsedChat::Lua {
|
||||
body: if args.trim().eq_ignore_ascii_case("clear") {
|
||||
"BenillaScriptLog_Clear()".into()
|
||||
crate::ui_script::ERRORS_CLEAR.into()
|
||||
} else {
|
||||
"BenillaScriptLog_Toggle()".into()
|
||||
crate::ui_script::ERRORS_TOGGLE.into()
|
||||
},
|
||||
},
|
||||
// `SlashCmdList["CONSOLE"]` (`ChatFrame.lua:671`) is `ConsoleExec(msg)`. A typed line runs
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
//! Where interfaces come from: addon discovery under [`root`] and the load walk ([`Walk::load`]).
|
||||
//! An [`Addon`] is a name, a parsed `.toc` and the [`Source`] its files come from. benilla's own
|
||||
//! interface is one too, with the compiled-in tree as its source, but not for the lifecycle: like
|
||||
//! FrameXML it loads outside `AddOn_Load` ([`super::manifest::load_ingame_ui`]) and gets no
|
||||
//! `ADDON_LOADED`.
|
||||
//! An [`Addon`] is a name, a parsed `.toc` and the [`Source`] its files come from. The core
|
||||
//! interface and benilla's layer are two more, with the compiled-in tree as their source, but not
|
||||
//! for the lifecycle: like FrameXML they load outside `AddOn_Load`
|
||||
//! ([`super::manifest::load_ingame_ui`]), get no `ADDON_LOADED` and have no registry row.
|
||||
|
||||
use std::collections::HashSet;
|
||||
use std::path::{Path, PathBuf};
|
||||
@@ -23,7 +23,8 @@ pub(crate) const LOAD_INSTRUCTION_BUDGET: u64 = 200_000_000;
|
||||
|
||||
/// Where one interface's files come from.
|
||||
pub(super) enum Source {
|
||||
/// benilla's own interface: the compiled-in tree, shadowed by `assets/ui` in a dev build.
|
||||
/// The core's and the layer's own files: the compiled-in tree, shadowed by `assets/ui` in a
|
||||
/// dev build.
|
||||
Builtin,
|
||||
/// The AddOns root on disk, not this addon's own folder: a request under `Interface/AddOns/`
|
||||
/// maps onto it, so a dependent can reach a shared library addon beside it.
|
||||
@@ -34,8 +35,8 @@ pub(super) enum Source {
|
||||
|
||||
/// One loadable interface: a name, its parsed manifest, and where its files come from.
|
||||
pub(super) struct Addon {
|
||||
/// The folder name (`"benilla"` for the builtin): what the AddOn API keys on and what
|
||||
/// `ADDON_LOADED` carries.
|
||||
/// The folder name (`"benilla"` for the core and the layer, which have no registry row): what
|
||||
/// the AddOn API keys on and what `ADDON_LOADED` carries.
|
||||
pub(super) name: String,
|
||||
/// The parsed `.toc`: the ordered file list plus every directive.
|
||||
pub(super) toc: Toc,
|
||||
@@ -48,15 +49,23 @@ impl Addon {
|
||||
Addon { name, toc, source }
|
||||
}
|
||||
|
||||
/// benilla's own interface, which is in the binary and so cannot be missing.
|
||||
pub(super) fn builtin() -> Self {
|
||||
let toc = content::read(super::manifest::MANIFEST)
|
||||
/// The core interface, [`super::manifest::MANIFEST`], which is in the binary and so cannot
|
||||
/// be missing.
|
||||
pub(super) fn core() -> Self {
|
||||
Self::shipped(super::manifest::MANIFEST)
|
||||
}
|
||||
|
||||
/// benilla's layer, [`super::manifest::LAYER_MANIFEST`], in the binary as the core is.
|
||||
pub(super) fn layer() -> Self {
|
||||
Self::shipped(super::manifest::LAYER_MANIFEST)
|
||||
}
|
||||
|
||||
/// The interface one shipped `.toc` names.
|
||||
fn shipped(manifest: &str) -> Self {
|
||||
let toc = content::read(manifest)
|
||||
.map(|t| Toc::parse(&t))
|
||||
.unwrap_or_else(|| {
|
||||
error!(
|
||||
"ui_script: {} is not in the shipped UI — no interface will load",
|
||||
super::manifest::MANIFEST
|
||||
);
|
||||
error!("ui_script: {manifest} is not in the shipped UI — it will not load");
|
||||
Toc::default()
|
||||
});
|
||||
Addon {
|
||||
@@ -67,7 +76,7 @@ impl Addon {
|
||||
}
|
||||
|
||||
/// One file's bytes, by a path already resolved into the source's path space. A `Dir` reads
|
||||
/// under the AddOns root, then the chain ([`read_addon_file`]); only the flat builtin falls
|
||||
/// under the AddOns root, then the chain ([`read_addon_file`]); only the flat shipped tree falls
|
||||
/// back to the basename, which for a `Dir` would rescue an escaping path. Bytes, not text: a
|
||||
/// `.lua` reaches Lua as it is on disk.
|
||||
fn read(&self, req: &str) -> Option<Vec<u8>> {
|
||||
@@ -85,7 +94,7 @@ impl Addon {
|
||||
}
|
||||
|
||||
/// The base this addon's manifest entries resolve against: `Interface/AddOns/<Folder>` for a
|
||||
/// `Dir`, empty for the builtin's flat tree and the chain's full paths.
|
||||
/// `Dir`, empty for the shipped flat tree and the chain's full paths.
|
||||
fn prefix(&self) -> String {
|
||||
match &self.source {
|
||||
Source::Builtin | Source::Chain => String::new(),
|
||||
@@ -95,7 +104,7 @@ impl Addon {
|
||||
|
||||
/// The chunk name, which addons parse: `"@%s"` (`0x8716e0`) over the resolved install path,
|
||||
/// built by `0x704bc0` for every `.lua`, as for `<Script file=>`. Ace2 libraries find their
|
||||
/// addon by splitting a `debugstack` frame on `\AddOns\` (`AceDB-2.0.lua:742`). The builtin's
|
||||
/// addon by splitting a `debugstack` frame on `\AddOns\` (`AceDB-2.0.lua:742`). The shipped
|
||||
/// flat tree keeps [`benilla_ui::script::addon_chunk_name`].
|
||||
fn chunk_name(&self, file: &str, path: &str) -> String {
|
||||
match &self.source {
|
||||
@@ -110,7 +119,7 @@ impl Addon {
|
||||
self.load_files(script, &self.toc.files)
|
||||
}
|
||||
|
||||
/// [`Addon::load`] over an explicit slice, for the builtin's two-phase boot. A manifest lists
|
||||
/// [`Addon::load`] over an explicit slice, for the core's two-phase boot. A manifest lists
|
||||
/// both kinds of file (the reference's `FrameXML.toc` opens with `GlobalStrings.lua`): a `.lua`
|
||||
/// runs as a chunk in the shared state, anything else is parsed as FrameXML and materialized.
|
||||
pub(super) fn load_files(&self, script: &UiScript, files: &[String]) -> Vec<String> {
|
||||
@@ -122,7 +131,7 @@ impl Addon {
|
||||
let path = benilla_ui::loader::join_ref(&self.prefix(), file);
|
||||
let Some(bytes) = self.read(&path) else {
|
||||
let e = format!("{}/{file}: not found", self.name);
|
||||
// Severity follows whose manifest is wrong. Ours (the builtin, or a `benilla.toc`
|
||||
// Severity follows whose manifest is wrong. Ours (a shipped file, or a `benilla.toc`
|
||||
// line the player's chain lacks) is an ERROR. A player's addon is the package's
|
||||
// fault, which the reference skips silently, and an ERROR line fails `smoke.sh`.
|
||||
match self.source {
|
||||
@@ -1127,7 +1136,7 @@ mod tests {
|
||||
);
|
||||
}
|
||||
assert!(
|
||||
!Addon::builtin()
|
||||
!Addon::core()
|
||||
.toc
|
||||
.files
|
||||
.iter()
|
||||
@@ -1641,9 +1650,9 @@ mod tests {
|
||||
}];
|
||||
let mut w = Walk::default();
|
||||
let _ = w.load(&mut script, &all, "EventProbe");
|
||||
// The builtin, loaded as production loads it: its own manifest, not the walk.
|
||||
let builtin = Addon::builtin();
|
||||
let _ = builtin.load_files(&script, builtin.toc.files.get(..1).unwrap_or_default());
|
||||
// The core, loaded as production loads it: its own manifest, not the walk.
|
||||
let core = Addon::core();
|
||||
let _ = core.load_files(&script, core.toc.files.get(..1).unwrap_or_default());
|
||||
|
||||
let log = event_log(&script);
|
||||
assert!(
|
||||
|
||||
@@ -1111,7 +1111,7 @@ fn a_glass_windows_plate_fades_in_on_every_hover_under_the_full_manifest() {
|
||||
|
||||
/// Leaving and re-entering within `CHAT_FRAME_FADE_TIME` traps the stock Lua (the tab fade's
|
||||
/// finished callback nils `oldAlpha` under a live hover): the bare chat stack must trap, and the
|
||||
/// shipped manifest, with `install_chat_plate_guard`, must not.
|
||||
/// shipped interface, with the layer's `FrameXMLFixes.xml`, must not.
|
||||
#[test]
|
||||
fn a_quick_exit_and_reentry_keeps_the_plates_hover_fade() {
|
||||
benilla_formats::wow_data_or_skip!();
|
||||
@@ -1178,7 +1178,7 @@ fn a_quick_exit_and_reentry_keeps_the_plates_hover_fade() {
|
||||
"the trapped state: tab up, oldAlpha nil, hover stuck"
|
||||
);
|
||||
|
||||
// The shipped interface, with the guard the manifest load installs.
|
||||
// The shipped interface, with the guard the layer installs.
|
||||
let mut shipped = UiScript::new().unwrap();
|
||||
shipped.set_screen_size(1600.0, 900.0);
|
||||
super::test_ui::load_ui(&shipped, "Interface\\FrameXML\\GlobalStrings.lua");
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
//! missing; `load_ui_files` and its `<Include>` provider read them here, never through `std::fs`.
|
||||
//! A dev build reads the source tree first, so an edit needs no recompile.
|
||||
|
||||
/// The shipped UI tree: `benilla.toc` and every file it names.
|
||||
/// The shipped UI tree: `benilla.toc`, `layer.toc` and every file of ours they name.
|
||||
static UI: include_dir::Dir<'_> = include_dir::include_dir!("$CARGO_MANIFEST_DIR/assets/ui");
|
||||
|
||||
/// The text of one shipped UI file, by path relative to `assets/ui`; the basename fallback for a
|
||||
@@ -20,8 +20,9 @@ fn read_source_tree(req: &str) -> Option<String> {
|
||||
std::fs::read_to_string(dir.join(req)).ok()
|
||||
}
|
||||
|
||||
/// FNV-1a (not cryptographic) over the manifest and each file it names, in load order, as eight
|
||||
/// hex digits: a stamp that tells two runs whether they loaded the same interface.
|
||||
/// FNV-1a (not cryptographic) over the core manifest and each file it names, then the layer's when
|
||||
/// this run loads it, in load order, as eight hex digits: a stamp that tells two runs whether they
|
||||
/// loaded the same interface.
|
||||
pub(crate) fn digest() -> String {
|
||||
let mut h: u64 = 0xcbf2_9ce4_8422_2325;
|
||||
let mut eat = |bytes: &[u8]| {
|
||||
@@ -30,15 +31,24 @@ pub(crate) fn digest() -> String {
|
||||
h = h.wrapping_mul(0x1000_0000_01b3);
|
||||
}
|
||||
};
|
||||
eat(super::manifest::MANIFEST.as_bytes());
|
||||
if let Some(toc) = read(super::manifest::MANIFEST) {
|
||||
eat(toc.as_bytes());
|
||||
let mut loaded = vec![(super::manifest::MANIFEST, super::addons::Addon::core())];
|
||||
if super::manifest::layer_enabled() {
|
||||
loaded.push((
|
||||
super::manifest::LAYER_MANIFEST,
|
||||
super::addons::Addon::layer(),
|
||||
));
|
||||
}
|
||||
for name in super::addons::Addon::builtin().toc.files {
|
||||
// Only a chain entry's name counts: its bytes are the player's install.
|
||||
eat(name.as_bytes());
|
||||
if let Some(text) = read(&name) {
|
||||
eat(text.as_bytes());
|
||||
for (manifest, addon) in loaded {
|
||||
eat(manifest.as_bytes());
|
||||
if let Some(toc) = read(manifest) {
|
||||
eat(toc.as_bytes());
|
||||
}
|
||||
for name in addon.toc.files {
|
||||
// Only a chain entry's name counts: its bytes are the player's install.
|
||||
eat(name.as_bytes());
|
||||
if let Some(text) = read(&name) {
|
||||
eat(text.as_bytes());
|
||||
}
|
||||
}
|
||||
}
|
||||
format!("{:08x}", h as u32)
|
||||
@@ -57,7 +67,7 @@ mod tests {
|
||||
for name in crate::ui_script::manifest::shipped_manifest_files() {
|
||||
assert!(
|
||||
super::read(&name).is_some(),
|
||||
"benilla.toc names {name}, which is not in assets/ui"
|
||||
"a manifest names {name}, which is not in assets/ui"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,9 +28,9 @@ fn harness() -> UiScript {
|
||||
load_xml(&s, f);
|
||||
}
|
||||
// Deviation: the seat refreshes on every alert recompute, because the stock one goes stale
|
||||
// when a side glyph appears while shown and hangs the shield off the screen edge. The app
|
||||
// installs this after every load.
|
||||
super::manifest::install_durability_reseat(&s).unwrap();
|
||||
// when a side glyph appears while shown and hangs the shield off the screen edge. The layer
|
||||
// installs this after every stock file.
|
||||
load_xml(&s, "FrameXMLFixes.xml");
|
||||
s
|
||||
}
|
||||
|
||||
@@ -374,7 +374,7 @@ fn the_quest_tracker_stacks_below_the_durability_guy() {
|
||||
] {
|
||||
load_xml(&s, f);
|
||||
}
|
||||
super::manifest::install_durability_reseat(&s).unwrap();
|
||||
load_xml(&s, "FrameXMLFixes.xml");
|
||||
|
||||
let mut inv: InventorySlots = Default::default();
|
||||
inv[18] = slot(2504, Some((0, 20)));
|
||||
|
||||
@@ -0,0 +1,258 @@
|
||||
//! benilla's layer ([`super::manifest::LAYER_MANIFEST`]) loads as more of the stock load: after
|
||||
//! every core file, before the first third-party addon, with no `ADDON_LOADED` and no AddOn API
|
||||
//! row, as 1.12.1 loads FrameXML (`UI_Init 0x48fbf0`: `FrameXML.toc` at `0x48ffed`, the addons at
|
||||
//! `0x4900a3`, only the latter through `AddOn_Load`); and a dev build boots without it.
|
||||
|
||||
use benilla_ui::script::UiScript;
|
||||
|
||||
use crate::local_state::test_env::{EnvGuard, ENV_LOCK};
|
||||
|
||||
/// Records each global's first definition in order: `__newindex` fires only for a new key, so a
|
||||
/// file that redefines a stock function is not logged, and one that loads stock code is.
|
||||
const DEFINE_LOG: &str = r#"
|
||||
DEFINE_LOG = {}
|
||||
setmetatable(getfenv(0), { __newindex = function(t, k, v)
|
||||
table.insert(DEFINE_LOG, k)
|
||||
rawset(t, k, v)
|
||||
end })
|
||||
"#;
|
||||
|
||||
/// Records every `ADDON_LOADED` from before the load.
|
||||
const ADDON_LOADED_LOG: &str = r#"
|
||||
ADDON_LOADED_LOG = {}
|
||||
local f = CreateFrame("Frame")
|
||||
f:RegisterEvent("ADDON_LOADED")
|
||||
f:SetScript("OnEvent", function() table.insert(ADDON_LOADED_LOG, arg1) end)
|
||||
"#;
|
||||
|
||||
/// A VM after the production in-game load, with one third-party addon, `ZZOrder`, in a hermetic
|
||||
/// AddOns root; the layer on unless `stock_ui`. Returns the VM and the load's failures.
|
||||
fn production_load(tag: &str, stock_ui: bool) -> (UiScript, Vec<String>) {
|
||||
let tmp = std::env::temp_dir().join(format!("benilla-layer-{tag}-{}", std::process::id()));
|
||||
let _ = std::fs::remove_dir_all(&tmp);
|
||||
let home = tmp.join("benilla-config");
|
||||
let dir = home.join("AddOns").join("ZZOrder");
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
std::fs::write(dir.join("ZZOrder.toc"), "## Interface: 11200\nprobe.lua\n").unwrap();
|
||||
std::fs::write(
|
||||
dir.join("probe.lua"),
|
||||
"ZZORDER_SAW_LAYER = BenillaOptionsFrame ~= nil\n",
|
||||
)
|
||||
.unwrap();
|
||||
let _c = EnvGuard::unset("WOW_CAPTURE");
|
||||
let _h = EnvGuard::set("BENILLA_HOME", home.to_str().unwrap());
|
||||
|
||||
let mut s = UiScript::new().unwrap();
|
||||
s.set_screen_size(1024.0, 768.0);
|
||||
s.set_unit(
|
||||
"player",
|
||||
Some(benilla_ui::script::UnitState {
|
||||
exists: true,
|
||||
name: Some("Probesix".into()),
|
||||
level: 60,
|
||||
..Default::default()
|
||||
}),
|
||||
);
|
||||
// A reply that hid nothing, so `GetNumAddOns` counts the registry.
|
||||
s.note_addon_info_reply(&[]);
|
||||
s.register_cvars(crate::cvars::registered_pairs());
|
||||
let mut failures = super::load_font_registry(&s);
|
||||
s.run(DEFINE_LOG).unwrap();
|
||||
s.run(ADDON_LOADED_LOG).unwrap();
|
||||
// The layer passed in, not set through `WOW_STOCK_UI`: every test in this process reads it.
|
||||
failures.extend(super::manifest::load_ingame_ui_with(
|
||||
&mut s,
|
||||
None,
|
||||
&[],
|
||||
true,
|
||||
!stock_ui,
|
||||
));
|
||||
failures.extend(s.errors());
|
||||
let _ = std::fs::remove_dir_all(&tmp);
|
||||
(s, failures)
|
||||
}
|
||||
|
||||
/// Where `name` was first defined in [`DEFINE_LOG`].
|
||||
fn defined_at(log: &[String], name: &str) -> usize {
|
||||
log.iter()
|
||||
.position(|n| n == name)
|
||||
.unwrap_or_else(|| panic!("{name} was never defined in the load"))
|
||||
}
|
||||
|
||||
/// The stock FrameXML's own globals, from the captured 1.12.1 `_G` (`reference/1.12-globals.tsv`):
|
||||
/// the `framexml` rows a FrameXML file defines at load, LoadOnDemand ones aside.
|
||||
fn stock_framexml_globals() -> std::collections::HashSet<String> {
|
||||
let tsv = include_str!(concat!(
|
||||
env!("CARGO_MANIFEST_DIR"),
|
||||
"/../../reference/1.12-globals.tsv"
|
||||
));
|
||||
tsv.lines()
|
||||
.filter(|l| !l.starts_with('#'))
|
||||
.filter_map(|l| {
|
||||
let mut cols = l.split('\t');
|
||||
let (name, kind, origin) = (cols.next()?, cols.next()?, cols.next()?);
|
||||
(origin == "framexml" && kind != "lod").then(|| name.to_string())
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// The layer loads after every stock file and before the first third-party addon: the core's last
|
||||
/// rows define before the layer's first, the layer's last before the addon, and nothing first
|
||||
/// defined from the layer's start on is a stock FrameXML global.
|
||||
#[test]
|
||||
fn the_layer_loads_after_every_stock_file_and_before_the_addons() {
|
||||
benilla_formats::wow_data_or_skip!();
|
||||
let _l = ENV_LOCK
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||
let (s, failures) = production_load("order", false);
|
||||
assert!(failures.is_empty(), "load failures: {failures:#?}");
|
||||
let log: Vec<String> = s.eval("return DEFINE_LOG").unwrap();
|
||||
|
||||
// The core's last stock row (TutorialFrame.lua), the core's own last row (GameMenuFrame.xml),
|
||||
// the layer's first file (ScrollTemplates.xml) and last (FrameXMLFixes.xml is all
|
||||
// redefinitions, so ScriptLogFrame.xml), then the addon.
|
||||
let order = [
|
||||
"TutorialFrame_OnHide",
|
||||
"GameMenuButton_Pending",
|
||||
"BenillaScrollBar_Step",
|
||||
"BenillaScriptLog_Toggle",
|
||||
"ZZORDER_SAW_LAYER",
|
||||
];
|
||||
let at: Vec<usize> = order.iter().map(|n| defined_at(&log, n)).collect();
|
||||
for w in 0..order.len() - 1 {
|
||||
assert!(
|
||||
at[w] < at[w + 1],
|
||||
"{} (#{}) must be defined before {} (#{})",
|
||||
order[w],
|
||||
at[w],
|
||||
order[w + 1],
|
||||
at[w + 1]
|
||||
);
|
||||
}
|
||||
let stock = stock_framexml_globals();
|
||||
assert!(stock.len() > 5000, "the reference's _G parse is broken");
|
||||
// The engine sets `this`, `event` and `arg1`-`arg9` around each handler it runs.
|
||||
let dispatch = |n: &str| {
|
||||
n == "this"
|
||||
|| n == "event"
|
||||
|| n.strip_prefix("arg")
|
||||
.is_some_and(|d| d.len() == 1 && d.as_bytes()[0].is_ascii_digit())
|
||||
};
|
||||
let late: Vec<&String> = log[at[2]..]
|
||||
.iter()
|
||||
.filter(|n| stock.contains(n.as_str()) && !dispatch(n))
|
||||
.collect();
|
||||
assert!(
|
||||
late.is_empty(),
|
||||
"stock FrameXML globals first defined after the layer began — a stock file loaded inside \
|
||||
or after the layer: {late:?}"
|
||||
);
|
||||
assert!(
|
||||
s.eval::<bool>("return ZZORDER_SAW_LAYER").unwrap(),
|
||||
"the addon's file scope sees the layer"
|
||||
);
|
||||
}
|
||||
|
||||
/// The layer is no addon: no `ADDON_LOADED` of its own, no row in the AddOn API, and no row on the
|
||||
/// character-select AddOns screen.
|
||||
#[test]
|
||||
fn the_layer_is_absent_from_the_addon_api() {
|
||||
benilla_formats::wow_data_or_skip!();
|
||||
let _l = ENV_LOCK
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||
let (s, failures) = production_load("api", false);
|
||||
assert!(failures.is_empty(), "load failures: {failures:#?}");
|
||||
|
||||
let loaded: Vec<String> = s.eval("return ADDON_LOADED_LOG").unwrap();
|
||||
assert_eq!(
|
||||
loaded,
|
||||
["ZZOrder"],
|
||||
"only the third-party addon announces itself"
|
||||
);
|
||||
|
||||
let names: Vec<String> = s
|
||||
.eval(
|
||||
"local out = {} \
|
||||
for i = 1, GetNumAddOns() do table.insert(out, (GetAddOnInfo(i))) end \
|
||||
return out",
|
||||
)
|
||||
.unwrap();
|
||||
assert!(
|
||||
names.iter().any(|n| n == "ZZOrder"),
|
||||
"the probe sees the registry: {names:?}"
|
||||
);
|
||||
for name in ["benilla", "layer"] {
|
||||
assert!(
|
||||
!names.iter().any(|n| n.eq_ignore_ascii_case(name)),
|
||||
"{name} is a row in the AddOn API: {names:?}"
|
||||
);
|
||||
assert!(
|
||||
!s.eval::<bool>(&format!(
|
||||
"return IsAddOnLoaded(\"{name}\") and true or false"
|
||||
))
|
||||
.unwrap(),
|
||||
"IsAddOnLoaded(\"{name}\") answers"
|
||||
);
|
||||
}
|
||||
assert!(
|
||||
!super::addons::installed_rows()
|
||||
.iter()
|
||||
.any(|a| a.name.eq_ignore_ascii_case("benilla")),
|
||||
"the AddOns screen lists benilla"
|
||||
);
|
||||
}
|
||||
|
||||
/// `WOW_STOCK_UI=1` drops the layer in a dev build alone; a player build always loads it.
|
||||
#[test]
|
||||
fn the_stock_ui_switch_is_a_dev_builds_alone() {
|
||||
use super::manifest::layer_enabled_by;
|
||||
assert!(
|
||||
!layer_enabled_by(true, Some("1")),
|
||||
"a dev build boots without it"
|
||||
);
|
||||
assert!(
|
||||
layer_enabled_by(false, Some("1")),
|
||||
"a player build ignores the switch"
|
||||
);
|
||||
assert!(layer_enabled_by(true, None) && layer_enabled_by(true, Some("0")));
|
||||
}
|
||||
|
||||
/// Booted without the layer, as `WOW_STOCK_UI=1` does: no layer file loads, the stock files raise
|
||||
/// nothing, and the host's calls into the layer are no-ops.
|
||||
#[test]
|
||||
fn the_stock_ui_switch_boots_the_core_without_the_layer() {
|
||||
benilla_formats::wow_data_or_skip!();
|
||||
let _l = ENV_LOCK
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||
let (s, failures) = production_load("stock", true);
|
||||
assert!(failures.is_empty(), "load failures: {failures:#?}");
|
||||
let log: Vec<String> = s.eval("return DEFINE_LOG").unwrap();
|
||||
// One global from each layer file that defines a new one.
|
||||
for name in [
|
||||
"BenillaScrollBar_Step",
|
||||
"KeyBindings_OnHostKey",
|
||||
"BenillaOptionsFrame_SelectCategory",
|
||||
"BENILLA_BAG_WAS_OPEN",
|
||||
"BenillaScriptLog_Toggle",
|
||||
] {
|
||||
assert!(
|
||||
!log.iter().any(|n| n == name),
|
||||
"{name} is defined: a layer file loaded under WOW_STOCK_UI=1"
|
||||
);
|
||||
}
|
||||
assert!(!s.eval::<bool>("return BenillaOptionsFrame ~= nil").unwrap());
|
||||
assert!(!s.eval::<bool>("return ZZORDER_SAW_LAYER").unwrap());
|
||||
// The host's calls into the layer, and the game menu's Options rung.
|
||||
for body in [
|
||||
super::ERRORS_TOGGLE.to_string(),
|
||||
super::ERRORS_CLEAR.to_string(),
|
||||
super::host_key_capture("CTRL-J"),
|
||||
"GameMenuButtonOptions:Click()".to_string(),
|
||||
] {
|
||||
s.run(&body).unwrap_or_else(|e| panic!("{body}: {e}"));
|
||||
}
|
||||
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
//! The in-game interface: its manifest, [`MANIFEST`], and the boot split that loads it in two
|
||||
//! phases. An entry with a path is the reference's own file off the player's patch chain, a bare
|
||||
//! filename is one we ship, and the manifest's order is the load order across both.
|
||||
//! The in-game interface: the core manifest, [`MANIFEST`], loaded in two phases, then benilla's
|
||||
//! layer, [`LAYER_MANIFEST`]. A core entry with a path is the reference's own file off the
|
||||
//! player's patch chain, a bare filename one we ship, and the manifest's order is the load order
|
||||
//! across both; every layer entry is ours and loads after the whole core.
|
||||
|
||||
use bevy::prelude::*;
|
||||
|
||||
@@ -9,13 +10,45 @@ use benilla_ui::script::UiScript;
|
||||
use super::addons::Addon;
|
||||
use super::reference_ui;
|
||||
|
||||
/// The built-in interface's manifest, relative to `assets/ui`.
|
||||
/// The core interface's manifest, relative to `assets/ui`.
|
||||
pub(super) const MANIFEST: &str = "benilla.toc";
|
||||
|
||||
/// The manifest's entries in load order, from both stores.
|
||||
/// benilla's own interface, one layer on the stock one, relative to `assets/ui`.
|
||||
pub(super) const LAYER_MANIFEST: &str = "layer.toc";
|
||||
|
||||
/// Whether this run loads the layer: always in a player build; a dev build boots the stock UI
|
||||
/// alone under `WOW_STOCK_UI=1`.
|
||||
pub(super) fn layer_enabled() -> bool {
|
||||
layer_enabled_by(
|
||||
crate::run_mode::dev_affordances(),
|
||||
std::env::var("WOW_STOCK_UI").ok().as_deref(),
|
||||
)
|
||||
}
|
||||
|
||||
/// [`layer_enabled`] over its two facts: a dev build, and the switch's value.
|
||||
pub(super) fn layer_enabled_by(dev_build: bool, stock_ui: Option<&str>) -> bool {
|
||||
!(dev_build && stock_ui == Some("1"))
|
||||
}
|
||||
|
||||
/// `/errors`: toggles the layer's script error log; a no-op when the run boots without the layer.
|
||||
pub(crate) const ERRORS_TOGGLE: &str =
|
||||
"if BenillaScriptLog_Toggle then BenillaScriptLog_Toggle() end";
|
||||
|
||||
/// `/errors clear`: empties the layer's script error log, or does nothing without the layer.
|
||||
pub(crate) const ERRORS_CLEAR: &str = "if BenillaScriptLog_Clear then BenillaScriptLog_Clear() end";
|
||||
|
||||
/// Hands a chord the layer's Keybindings page is capturing (`BenillaBindCapture`), a bind string
|
||||
/// such as `CTRL-J`, to its handler; a no-op without the layer.
|
||||
pub(crate) fn host_key_capture(chord: &str) -> String {
|
||||
format!("if KeyBindings_OnHostKey then KeyBindings_OnHostKey(\"{chord}\") end")
|
||||
}
|
||||
|
||||
/// Every entry in load order: the core's, then the layer's.
|
||||
#[cfg(test)]
|
||||
pub(super) fn manifest_files() -> Vec<String> {
|
||||
Addon::builtin().toc.files
|
||||
let mut files = Addon::core().toc.files;
|
||||
files.extend(Addon::layer().toc.files);
|
||||
files
|
||||
}
|
||||
|
||||
/// The manifest's entries that name a file we ship rather than one off the player's install.
|
||||
@@ -27,48 +60,20 @@ pub(super) fn shipped_manifest_files() -> Vec<String> {
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Runs `UIParent_ManageFramePositions` after the manifest has loaded, then installs the load-time
|
||||
/// repairs below. Never after `Fonts.xml` alone: `UIParent.lua` defines the pass.
|
||||
/// Runs `UIParent_ManageFramePositions` after the manifest has loaded, then the buff pass below.
|
||||
/// Never after `Fonts.xml` alone: `UIParent.lua` defines the pass.
|
||||
fn bootstrap_positions(script: &UiScript) -> Vec<String> {
|
||||
if let Err(e) = script.run("UIParent_ManageFramePositions()") {
|
||||
error!("ui_script: managed-positions bootstrap: {e}");
|
||||
return vec![format!("managed-positions bootstrap: {e}")];
|
||||
}
|
||||
if let Err(e) = install_durability_reseat(script) {
|
||||
error!("ui_script: durability re-seat hook: {e}");
|
||||
return vec![format!("durability re-seat hook: {e}")];
|
||||
}
|
||||
if let Err(e) = apply_buff_durations(script) {
|
||||
error!("ui_script: buff-duration layout: {e}");
|
||||
return vec![format!("buff-duration layout: {e}")];
|
||||
}
|
||||
if let Err(e) = install_chat_plate_guard(script) {
|
||||
error!("ui_script: chat plate guard: {e}");
|
||||
return vec![format!("chat plate guard: {e}")];
|
||||
}
|
||||
Vec::new()
|
||||
}
|
||||
|
||||
/// Deviation: the stock `FCF_ChatTabFadeFinished` (`FloatingChatFrame.lua:989-992`) nils `oldAlpha`
|
||||
/// even under a live hover, so leaving and re-entering within `CHAT_FRAME_FADE_TIME` (a flick to
|
||||
/// the scroll buttons, outside the hover box, does it) stops the plate fading in (l.873) for the
|
||||
/// rest of the session; this keeps `oldAlpha` while `hover` is set. Redefined, not wrapped, so a
|
||||
/// `ReloadUI` re-run cannot stack.
|
||||
pub(super) fn install_chat_plate_guard(script: &UiScript) -> Result<(), String> {
|
||||
script.run(CHAT_PLATE_GUARD).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
const CHAT_PLATE_GUARD: &str = r#"
|
||||
if FCF_ChatTabFadeFinished then
|
||||
function FCF_ChatTabFadeFinished(chatTab, chatFrame)
|
||||
chatTab:Hide()
|
||||
if not chatFrame.hover then
|
||||
chatFrame.oldAlpha = nil
|
||||
end
|
||||
end
|
||||
end
|
||||
"#;
|
||||
|
||||
/// Runs `BuffButtons_UpdatePositions` at load, which `BuffFrame_OnLoad` never calls: until the
|
||||
/// stock `UIOptionsFrame.lua`'s `VARIABLES_LOADED` arm runs it (l.206), the second buff row and
|
||||
/// the debuff row keep `BuffFrame.xml`'s anchors whatever `SHOW_BUFF_DURATIONS` says.
|
||||
@@ -83,7 +88,8 @@ pub(super) fn apply_buff_durations(script: &UiScript) -> Result<(), String> {
|
||||
/// bags, whose columns `updateContainerFrameAnchors` lays out by `GetScreenHeight()`.
|
||||
/// Deviation: [`FULLSCREEN_QUADS_RESEAT`] re-sizes the full-screen quads, which the stock files
|
||||
/// size only in their OnLoads, because a resize or `uiScale` change here keeps the loaded UI and
|
||||
/// the blackout would stop short of the new screen.
|
||||
/// the blackout would stop short of the new screen. Host-side, not in the layer: it adapts the
|
||||
/// engine's resizable window, and changes nothing the stock UI does at a fixed size.
|
||||
pub(super) fn on_screen_resized(script: &UiScript) {
|
||||
let _ = script.run("if UIParent_ManageFramePositions then UIParent_ManageFramePositions() end");
|
||||
if let Err(e) = script.run(FULLSCREEN_QUADS_RESEAT) {
|
||||
@@ -125,26 +131,6 @@ if UpperBlackBar and LowerBlackBar then
|
||||
end
|
||||
"#;
|
||||
|
||||
/// Deviation: re-runs `UIParent_ManageFramePositions` after `DurabilityFrame`'s own OnEvent. The
|
||||
/// pass seats the frame 20 further in while a side glyph shows (`UIParent.lua:1759-1768`), but the
|
||||
/// stock frame runs it only from OnShow/OnHide, so a glyph appearing while the frame is shown
|
||||
/// leaves the shield's overhang off the screen edge. Latched, so a `ReloadUI` cannot stack it; the
|
||||
/// stock handler runs first, unchanged, on the `this`/`event`/`arg1` already set for the dispatch.
|
||||
pub(super) fn install_durability_reseat(script: &UiScript) -> Result<(), String> {
|
||||
script.run(DURABILITY_RESEAT).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
const DURABILITY_RESEAT: &str = r#"
|
||||
if DurabilityFrame and not DurabilityFrame.benillaReseat then
|
||||
DurabilityFrame.benillaReseat = 1
|
||||
local refOnEvent = DurabilityFrame:GetScript("OnEvent")
|
||||
DurabilityFrame:SetScript("OnEvent", function()
|
||||
if refOnEvent then refOnEvent() end
|
||||
UIParent_ManageFramePositions()
|
||||
end)
|
||||
end
|
||||
"#;
|
||||
|
||||
/// Runs a UI load in the counted sound-suppression scope, as `CGGameUI::Initialize` (`0x48fbf0`,
|
||||
/// called at login `0x48f681` and `/reloadui` `0x495669`) runs its own: enter (`0x458f50`) at
|
||||
/// `0x48fbfa`, leave (`0x458f60`) at `0x49016d`, and `PlaySoundByName` (`0x458030`) drops every
|
||||
@@ -161,17 +147,20 @@ pub(super) fn silenced_ui_load<S: std::borrow::Borrow<UiScript>, R>(
|
||||
out
|
||||
}
|
||||
|
||||
/// Loads every [`MANIFEST`] entry at once, then the load-time repairs. Production splits this
|
||||
/// load at boot ([`load_font_registry`], [`load_ingame_ui`]), so the callers are the tests and
|
||||
/// [`crate::addon_harness`]. Returns every failure; a loader error is tagged
|
||||
/// `"<Addon>/<file>: <error>"`.
|
||||
/// Loads every [`MANIFEST`] entry at once, then the layer unless [`layer_enabled`] says no, then
|
||||
/// the load-time repairs. Production splits this load at boot ([`load_font_registry`],
|
||||
/// [`load_ingame_ui`]), so the callers are the tests and [`crate::addon_harness`]. Returns every
|
||||
/// failure; a loader error is tagged `"<Addon>/<file>: <error>"`.
|
||||
pub(crate) fn load_default_ui(script: &UiScript) -> Vec<String> {
|
||||
// The client's CVar table first: the stock `UIOptionsFrame.xml` reads CVars in its OnLoads,
|
||||
// and a bare `UiScript::new()` has only `benilla-ui`'s own. A re-register only refreshes
|
||||
// defaults.
|
||||
script.register_cvars(crate::cvars::registered_pairs());
|
||||
let mut failures = silenced_ui_load(&mut &*script, |script| {
|
||||
let mut failures = load_manifest(script, &Addon::builtin().toc.files);
|
||||
let mut failures = load_manifest(script, &Addon::core().toc.files);
|
||||
if layer_enabled() {
|
||||
failures.extend(load_layer(script));
|
||||
}
|
||||
failures.extend(bootstrap_positions(script));
|
||||
failures
|
||||
});
|
||||
@@ -189,7 +178,7 @@ pub(crate) fn load_default_ui(script: &UiScript) -> Vec<String> {
|
||||
/// Loads a slice of [`MANIFEST`] entries, each from its own store and one at a time, so the
|
||||
/// manifest's order is the load order across both.
|
||||
fn load_manifest(script: &UiScript, files: &[String]) -> Vec<String> {
|
||||
let builtin = Addon::builtin();
|
||||
let core = Addon::core();
|
||||
let reference = reference_ui::addon(
|
||||
files
|
||||
.iter()
|
||||
@@ -202,26 +191,30 @@ fn load_manifest(script: &UiScript, files: &[String]) -> Vec<String> {
|
||||
let from = if reference_ui::is_chain_entry(file) {
|
||||
&reference
|
||||
} else {
|
||||
&builtin
|
||||
&core
|
||||
};
|
||||
failures.extend(from.load_files(script, std::slice::from_ref(file)));
|
||||
}
|
||||
failures
|
||||
}
|
||||
|
||||
/// Every [`LAYER_MANIFEST`] entry, in order, after the whole core: the layer loads as more of the
|
||||
/// stock load, with no `ADDON_LOADED` and no registry row, as FrameXML does (`0x48ffed`).
|
||||
fn load_layer(script: &UiScript) -> Vec<String> {
|
||||
let layer = Addon::layer();
|
||||
layer.load_files(script, &layer.toc.files)
|
||||
}
|
||||
|
||||
/// The font-object registry alone, `Fonts.xml`, loaded at `Startup`.
|
||||
/// Deviation: it loads before the login screen because benilla's glue screens share its glyph
|
||||
/// atlas; the reference's glue has its own `GlueFonts.xml`. The font objects declared outside it
|
||||
/// (FRIZQT 13, 14 and 20, un-outlined) repeat combinations it has, so the atlas plan loses nothing.
|
||||
pub(crate) fn load_font_registry(script: &UiScript) -> Vec<String> {
|
||||
load_manifest(
|
||||
script,
|
||||
Addon::builtin().toc.files.get(..1).unwrap_or_default(),
|
||||
)
|
||||
load_manifest(script, Addon::core().toc.files.get(..1).unwrap_or_default())
|
||||
}
|
||||
|
||||
/// The in-game UI, every entry after `Fonts.xml`, then every third-party addon, on entering the
|
||||
/// world. The reference loads both in `CGGameUI::Initialize` (`0x48fbf0`), reached only from world
|
||||
/// The in-game UI, every core entry after `Fonts.xml`, then the layer unless [`layer_enabled`]
|
||||
/// says no, then every third-party addon, on entering the world. The reference loads both in `CGGameUI::Initialize` (`0x48fbf0`), reached only from world
|
||||
/// entry (`0x401570` from `0x46c236`), its addons at `0x4900a3` (`0x51f600`).
|
||||
///
|
||||
/// `identity`, `(realm, character)`, names the character's AddOn enable-state file, as the
|
||||
@@ -233,14 +226,25 @@ pub(crate) fn load_ingame_ui(
|
||||
identity: Option<&(String, String)>,
|
||||
roster: &[String],
|
||||
version_check: bool,
|
||||
) -> Vec<String> {
|
||||
load_ingame_ui_with(script, identity, roster, version_check, layer_enabled())
|
||||
}
|
||||
|
||||
/// [`load_ingame_ui`] with the layer decided by the caller.
|
||||
pub(super) fn load_ingame_ui_with(
|
||||
script: &mut UiScript,
|
||||
identity: Option<&(String, String)>,
|
||||
roster: &[String],
|
||||
version_check: bool,
|
||||
layer: bool,
|
||||
) -> Vec<String> {
|
||||
// Bounded, addons included: a chunk that never returns fails as a load error instead of
|
||||
// freezing the loading screen. The caller disarms the budget once the edge is done.
|
||||
script.set_instruction_budget(super::addons::LOAD_INSTRUCTION_BUDGET);
|
||||
let mut failures = load_manifest(
|
||||
script,
|
||||
Addon::builtin().toc.files.get(1..).unwrap_or_default(),
|
||||
);
|
||||
let mut failures = load_manifest(script, Addon::core().toc.files.get(1..).unwrap_or_default());
|
||||
if layer {
|
||||
failures.extend(load_layer(script));
|
||||
}
|
||||
failures.extend(bootstrap_positions(script));
|
||||
// Each addon's `ADDON_LOADED` fires as that addon finishes, as the reference does (`0x51f5ad`).
|
||||
failures.extend(super::addons::load_third_party(
|
||||
@@ -260,7 +264,7 @@ mod tests {
|
||||
/// [`load_ingame_ui`] the rest.
|
||||
#[test]
|
||||
fn the_manifest_is_a_toc_that_starts_with_the_font_registry() {
|
||||
let toc = Addon::builtin().toc;
|
||||
let toc = Addon::core().toc;
|
||||
assert_eq!(toc.interface_versions(), vec![11200]);
|
||||
assert_eq!(toc.directive("Title"), Some("benilla"));
|
||||
assert_eq!(
|
||||
|
||||
@@ -16,6 +16,8 @@ pub(crate) mod addons;
|
||||
mod content;
|
||||
pub(crate) mod extract;
|
||||
mod input;
|
||||
#[cfg(test)]
|
||||
mod layer_tests;
|
||||
mod manifest;
|
||||
|
||||
/// The stock FrameXML this client runs off the player's own patch chain; its header is the rule.
|
||||
@@ -31,6 +33,7 @@ pub(crate) use session::VmMemo;
|
||||
|
||||
// Not test-only: the addon harness loads the whole shipped interface under each addon.
|
||||
pub(crate) use manifest::load_default_ui;
|
||||
pub(crate) use manifest::{host_key_capture, ERRORS_CLEAR, ERRORS_TOGGLE};
|
||||
pub(crate) use manifest::{load_font_registry, load_ingame_ui};
|
||||
|
||||
/// Whether the pointer is over any UI (the egui dev overlay or a player-UI frame), combined by
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
//! [`super::addons::Source`]; the parse, `<Include>` and `<Script file=>` resolution and chunk
|
||||
//! naming are [`super::addons::Addon`]'s.
|
||||
//!
|
||||
//! `assets/ui/benilla.toc` is the one load order. An entry with a path separator comes off the
|
||||
//! chain, a bare filename is a file we ship ([`is_chain_entry`]), and a name defined by both goes
|
||||
//! to the later line. Without client data the chain files are absent, and the log says so once.
|
||||
//! `assets/ui/benilla.toc` is the core's load order, and `layer.toc` follows it. An entry with a
|
||||
//! path separator comes off the chain, a bare filename is a file we ship ([`is_chain_entry`]), and
|
||||
//! a name defined by both goes to the later line. Without client data the chain files are absent, and the log says so once.
|
||||
|
||||
use std::sync::OnceLock;
|
||||
|
||||
@@ -349,7 +349,7 @@ mod tests {
|
||||
.map(str::to_string)
|
||||
.collect();
|
||||
|
||||
let migrated: Vec<String> = super::super::addons::Addon::builtin()
|
||||
let migrated: Vec<String> = super::super::addons::Addon::core()
|
||||
.toc
|
||||
.files
|
||||
.iter()
|
||||
@@ -554,7 +554,7 @@ mod tests {
|
||||
"the method probe is not working — it answered {got:?} for {control:?}"
|
||||
);
|
||||
|
||||
let migrated: std::collections::HashSet<String> = super::super::addons::Addon::builtin()
|
||||
let migrated: std::collections::HashSet<String> = super::super::addons::Addon::core()
|
||||
.toc
|
||||
.files
|
||||
.iter()
|
||||
@@ -870,7 +870,7 @@ mod tests {
|
||||
};
|
||||
|
||||
// Every function each chain entry and its same-name `.lua` define.
|
||||
let toc = &super::super::addons::Addon::builtin().toc.files;
|
||||
let toc = &super::super::manifest::manifest_files();
|
||||
// Load order: each manifest entry at its line, ours included (`ours_wins` reads both
|
||||
// sides), then each reached addon's files after everything.
|
||||
let chain = gated_chain_entries();
|
||||
@@ -1185,6 +1185,13 @@ mod tests {
|
||||
(the producer gate carries the event). Gated as the addon is a reached \
|
||||
LoadOnDemand row.",
|
||||
),
|
||||
(
|
||||
"StatsFrame.xml",
|
||||
"GetDebugStats",
|
||||
"the debug readout's one engine verb (`0x488af0`), not built. `StatsFrame` loads \
|
||||
hidden and only its OnUpdate calls it; nothing stock shows it, and its binding, \
|
||||
`TOGGLESTATS`, is a `debug=\"true\"` row the release loader skips (`0x4b70a4`).",
|
||||
),
|
||||
(
|
||||
"StaticPopup.xml",
|
||||
"ReplaceTradeEnchant",
|
||||
@@ -1315,13 +1322,50 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
/// The core's chain rows are the reference's own `FrameXML.toc` rows, every one, but the stock
|
||||
/// `GameMenuFrame.xml`, which ours stands in for until the layer reshapes the stock menu. A
|
||||
/// row the stock toc lacks runs a file the reference does not, or twice: `UIParent.xml:4`
|
||||
/// sources `LocaleProperties.lua`, which has no row.
|
||||
#[test]
|
||||
fn the_cores_chain_rows_are_the_stock_tocs_rows() {
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
let toc = String::from_utf8_lossy(
|
||||
&super::read("Interface\\FrameXML\\FrameXML.toc").expect("the reference's own toc"),
|
||||
)
|
||||
.into_owned();
|
||||
let mut stock: Vec<String> = toc
|
||||
.lines()
|
||||
.map(str::trim)
|
||||
.filter(|l| !l.is_empty() && !l.starts_with('#'))
|
||||
.filter(|l| !l.eq_ignore_ascii_case("GameMenuFrame.xml"))
|
||||
.map(|l| format!("interface\\framexml\\{}", l.to_ascii_lowercase()))
|
||||
.collect();
|
||||
let mut core: Vec<String> = super::super::addons::Addon::core()
|
||||
.toc
|
||||
.files
|
||||
.iter()
|
||||
.filter(|f| super::is_chain_entry(f))
|
||||
.map(|f| f.to_ascii_lowercase())
|
||||
.collect();
|
||||
stock.sort();
|
||||
core.sort();
|
||||
let missing: Vec<&String> = stock.iter().filter(|r| !core.contains(r)).collect();
|
||||
let extra: Vec<&String> = core.iter().filter(|r| !stock.contains(r)).collect();
|
||||
assert!(
|
||||
missing.is_empty() && extra.is_empty(),
|
||||
"benilla.toc against the stock FrameXML.toc — stock rows it does not load: \
|
||||
{missing:?}; rows the stock toc does not have: {extra:?}"
|
||||
);
|
||||
assert_eq!(core, stock, "a stock row listed twice");
|
||||
}
|
||||
|
||||
/// A chain `.xml` that does not source its own `.lua` needs the `.lua` as a manifest line too,
|
||||
/// as the stock toc lists `MoneyInputFrame.lua` and `TextStatusBar.lua` (lines 11 and 32).
|
||||
/// Without it every global the file should define reads nil, and nothing errors.
|
||||
#[test]
|
||||
fn every_chain_xml_brings_its_own_lua() {
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
let toc = &super::super::addons::Addon::builtin().toc.files;
|
||||
let toc = &super::super::manifest::manifest_files();
|
||||
let listed: std::collections::HashSet<&str> = toc
|
||||
.iter()
|
||||
.map(|f| f.rsplit(['\\', '/']).next().unwrap_or(f))
|
||||
@@ -1463,7 +1507,7 @@ mod tests {
|
||||
#[test]
|
||||
fn nothing_we_ship_is_shadowed_by_a_later_chain_entry() {
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
let toc = &super::super::addons::Addon::builtin().toc.files;
|
||||
let toc = &super::super::manifest::manifest_files();
|
||||
let mut ours: std::collections::HashMap<String, String> = std::collections::HashMap::new();
|
||||
let mut shadowed: Vec<String> = Vec::new();
|
||||
for entry in toc.iter() {
|
||||
@@ -1493,7 +1537,7 @@ mod tests {
|
||||
#[test]
|
||||
fn every_template_the_manifest_inherits_is_declared_by_the_manifest() {
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
let toc = &super::super::addons::Addon::builtin().toc.files;
|
||||
let toc = &super::super::manifest::manifest_files();
|
||||
|
||||
let mut declared: std::collections::HashSet<String> = std::collections::HashSet::new();
|
||||
let mut wanted: Vec<(String, String)> = Vec::new();
|
||||
@@ -1566,7 +1610,7 @@ mod tests {
|
||||
fn reached_addons() -> Vec<String> {
|
||||
let dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("assets/ui");
|
||||
let mut out: Vec<String> = Vec::new();
|
||||
for entry in &super::super::addons::Addon::builtin().toc.files {
|
||||
for entry in &super::super::manifest::manifest_files() {
|
||||
let texts = if super::is_chain_entry(entry) {
|
||||
entry_sources(entry)
|
||||
} else {
|
||||
@@ -1594,7 +1638,7 @@ mod tests {
|
||||
/// Every chain file the shipped interface loads, in load order: the manifest's chain entries,
|
||||
/// then each reached addon's files in its own toc order.
|
||||
fn gated_chain_entries() -> Vec<String> {
|
||||
let mut out: Vec<String> = super::super::addons::Addon::builtin()
|
||||
let mut out: Vec<String> = super::super::addons::Addon::core()
|
||||
.toc
|
||||
.files
|
||||
.iter()
|
||||
|
||||
@@ -823,7 +823,7 @@ fn every_shipped_font_object_is_published_as_a_lua_global() {
|
||||
|
||||
// The names come from the manifest's own entries, chain files included.
|
||||
let mut names: Vec<String> = Vec::new();
|
||||
for entry in &super::addons::Addon::builtin().toc.files {
|
||||
for entry in &super::manifest::manifest_files() {
|
||||
if !entry.to_ascii_lowercase().ends_with(".xml") {
|
||||
continue;
|
||||
}
|
||||
@@ -1988,21 +1988,28 @@ fn the_stock_options_windows_load_and_save_are_reachable_for_addons() {
|
||||
);
|
||||
}
|
||||
|
||||
/// `assets/ui` does not grow: the interface is the stock 1.12 FrameXML off the player's own patch
|
||||
/// chain, and a new file means a window was authored rather than migrated. Point `benilla.toc` at
|
||||
/// `Interface\FrameXML\<Window>.xml`, delete ours, and build the engine verbs the stock file calls.
|
||||
/// benilla's own interface is one layer, and it does not grow: `layer.toc` names exactly these
|
||||
/// files, all ours (none off the chain), and `assets/ui` holds nothing else but the two manifests
|
||||
/// and the core's one file. A window is migrated, not authored: point `benilla.toc` at
|
||||
/// `Interface\FrameXML\<Window>.xml`, delete ours, and build the engine verbs the stock file
|
||||
/// calls; what changes the stock UI goes in one of these files.
|
||||
#[test]
|
||||
fn assets_ui_does_not_grow() {
|
||||
const SHIPPED: &[&str] = &[
|
||||
"ContainerFrameAdapters.xml",
|
||||
"GameMenuFrame.xml",
|
||||
fn the_layer_does_not_grow() {
|
||||
const LAYER: &[&str] = &[
|
||||
"ScrollTemplates.xml",
|
||||
"KeyBindingsPage.xml",
|
||||
"OptionsFrame.xml",
|
||||
"ScriptLogFrame.xml",
|
||||
"ScrollTemplates.xml",
|
||||
"ContainerFrameAdapters.xml",
|
||||
"SpellBookAdapters.xml",
|
||||
"benilla.toc",
|
||||
"ScriptLogFrame.xml",
|
||||
"FrameXMLFixes.xml",
|
||||
];
|
||||
assert_eq!(
|
||||
super::addons::Addon::layer().toc.files,
|
||||
LAYER,
|
||||
"layer.toc changed. The layer does not grow (docs/METHOD.md): a file that retired comes \
|
||||
off this list; a new one needs a reason this list can name."
|
||||
);
|
||||
let dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("assets/ui");
|
||||
let mut found: Vec<String> = std::fs::read_dir(&dir)
|
||||
.expect("assets/ui")
|
||||
@@ -2011,11 +2018,46 @@ fn assets_ui_does_not_grow() {
|
||||
.filter(|n| !n.starts_with('.'))
|
||||
.collect();
|
||||
found.sort();
|
||||
let mut shipped: Vec<String> = SHIPPED.iter().map(|s| s.to_string()).collect();
|
||||
let mut shipped: Vec<String> = LAYER
|
||||
.iter()
|
||||
.chain(&["benilla.toc", "layer.toc", "GameMenuFrame.xml"])
|
||||
.map(|s| s.to_string())
|
||||
.collect();
|
||||
shipped.sort();
|
||||
assert_eq!(
|
||||
found, shipped,
|
||||
"assets/ui changed. It does not grow: a window is migrated, not authored (docs/METHOD.md). \
|
||||
A file that retired comes off this list; a new one needs a reason this list can name."
|
||||
"assets/ui holds the two manifests, the layer's files and the core's one file, nothing else"
|
||||
);
|
||||
}
|
||||
|
||||
/// The core is the stock UI: `benilla.toc` names the reference's own files off the chain and one
|
||||
/// file of ours, `GameMenuFrame.xml`, which stands in for the stock one until the layer reshapes
|
||||
/// the stock menu, and whose row says so.
|
||||
#[test]
|
||||
fn the_core_toc_names_no_file_of_ours_but_the_game_menu() {
|
||||
let core = super::addons::Addon::core().toc.files;
|
||||
let ours: Vec<&String> = core
|
||||
.iter()
|
||||
.filter(|f| !super::reference_ui::is_chain_entry(f))
|
||||
.collect();
|
||||
assert_eq!(
|
||||
ours,
|
||||
["GameMenuFrame.xml"],
|
||||
"benilla.toc names a file of ours: benilla's own interface is the layer (layer.toc), \
|
||||
which loads after every stock file"
|
||||
);
|
||||
let toc = super::content::read(super::manifest::MANIFEST).expect("benilla.toc");
|
||||
let lines: Vec<&str> = toc.lines().collect();
|
||||
let at = lines
|
||||
.iter()
|
||||
.position(|l| l.trim() == "GameMenuFrame.xml")
|
||||
.expect("the row");
|
||||
assert!(
|
||||
lines[..at]
|
||||
.iter()
|
||||
.rev()
|
||||
.take_while(|l| l.starts_with('#'))
|
||||
.any(|l| l.contains("#553")),
|
||||
"GameMenuFrame.xml's row names the issue that retires it"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -107,7 +107,7 @@ fn load_entry(s: &UiScript, entry: &str, strict_templates: bool, no_warnings: bo
|
||||
.expect("the multibar stand-ins");
|
||||
}
|
||||
// Our options window's Graphics rows read stock `OptionsFrameSliders` (`OptionsFrame.lua`) at
|
||||
// OnLoad; the manifest loads it one seat above ours, so a kit seats it before our file.
|
||||
// OnLoad; the core loads it before the layer, so a kit seats it before our file.
|
||||
if path
|
||||
.rsplit('/')
|
||||
.next()
|
||||
|
||||
@@ -27,8 +27,8 @@ taken.
|
||||
lives in its own crate on top of benilla, started through `benilla_app::run_with` as in
|
||||
`crates/benilla-app/examples/extended_launcher.rs`. A deviation from the reference is the
|
||||
maintainer's call and is recorded where it lives; a pull request is not the place to propose one.
|
||||
- Anything from a WoW install: art, models, sounds, maps, data. The one exception is interface
|
||||
code (FrameXML and GlueXML), and only through the migration recipe in `docs/METHOD.md`.
|
||||
- Anything from a WoW install: art, models, sounds, maps, data, and interface code (FrameXML and
|
||||
GlueXML), which runs off the player's own install (`docs/METHOD.md`).
|
||||
- Big or mixed changes. One change per pull request, small enough to read in one sitting.
|
||||
|
||||
## How a change is judged
|
||||
@@ -96,7 +96,8 @@ out of scope is closed with the reason.
|
||||
`series` and `hotspot`.
|
||||
- **The loop.** `cargo play` builds and runs the play profile. `scripts/check.sh` verifies a
|
||||
round of work; `scripts/gates.sh` is the full chain, and it opens a window for the engine boot
|
||||
checks, so it needs a display. Work on a branch.
|
||||
checks, so it needs a display. `WOW_STOCK_UI=1` boots a dev build on the stock interface
|
||||
alone, without benilla's layer. Work on a branch.
|
||||
|
||||
## Reporting a bug
|
||||
|
||||
|
||||
+2
-1
@@ -174,7 +174,7 @@
|
||||
- `WOW_BUBBLE_TRACE` — benilla-app/src/chat_bubble.rs
|
||||
- `WOW_CAM_CHANGED` — benilla-app/src/perf/mod.rs
|
||||
- `WOW_CAM_DUMP` — benilla-app/src/player/camera.rs
|
||||
- `WOW_CAPTURE` — benilla-app/src/capture/mod.rs, benilla-app/src/capture/scenarios.rs, benilla-app/src/char_select/addons.rs, benilla-app/src/cvars.rs, benilla-app/src/lib.rs, benilla-app/src/local_state.rs, benilla-app/src/login/mod.rs, benilla-app/src/name_persist.rs, benilla-app/src/net.rs, benilla-app/src/player/camera_saved.rs, benilla-app/src/run_mode.rs, benilla-app/src/sound/mod.rs, benilla-app/src/ui_chat/settings.rs, benilla-app/src/ui_chat/tests.rs, benilla-app/src/ui_macro/tests.rs, benilla-app/src/ui_saved.rs, benilla-app/src/ui_script/addons.rs, benilla-app/src/ui_script/mod.rs, benilla-app/src/ui_script/world_entry_tests.rs, benilla-app/src/video.rs, benilla-world/src/bgwin.rs, benilla-world/src/clouds/mod.rs, benilla-world/src/dev_state.rs
|
||||
- `WOW_CAPTURE` — benilla-app/src/capture/mod.rs, benilla-app/src/capture/scenarios.rs, benilla-app/src/char_select/addons.rs, benilla-app/src/cvars.rs, benilla-app/src/lib.rs, benilla-app/src/local_state.rs, benilla-app/src/login/mod.rs, benilla-app/src/name_persist.rs, benilla-app/src/net.rs, benilla-app/src/player/camera_saved.rs, benilla-app/src/run_mode.rs, benilla-app/src/sound/mod.rs, benilla-app/src/ui_chat/settings.rs, benilla-app/src/ui_chat/tests.rs, benilla-app/src/ui_macro/tests.rs, benilla-app/src/ui_saved.rs, benilla-app/src/ui_script/addons.rs, benilla-app/src/ui_script/layer_tests.rs, benilla-app/src/ui_script/mod.rs, benilla-app/src/ui_script/world_entry_tests.rs, benilla-app/src/video.rs, benilla-world/src/bgwin.rs, benilla-world/src/clouds/mod.rs, benilla-world/src/dev_state.rs
|
||||
- `WOW_CAPTURE_AGE` — benilla-app/src/capture/mod.rs
|
||||
- `WOW_CAPTURE_DEADLINE` — benilla-app/src/capture/mod.rs
|
||||
- `WOW_CAPTURE_OUT` — benilla-app/src/capture/mod.rs
|
||||
@@ -467,6 +467,7 @@
|
||||
- `WOW_STATIC_GX_PROP` — benilla-world/src/static_gx/mod.rs
|
||||
- `WOW_STATIC_GX_WMO` — benilla-world/src/static_gx/mod.rs
|
||||
- `WOW_STATIC_MERGE` — benilla-world/src/terrain_stream/merge.rs
|
||||
- `WOW_STOCK_UI` — benilla-app/src/ui_script/manifest.rs
|
||||
- `WOW_STREAM_TRACE` — benilla-app/src/perf/mod.rs
|
||||
- `WOW_TAB_TRACE` — benilla-app/src/target/scan.rs
|
||||
- `WOW_TABHOVER` — benilla-app/src/capture/fixtures.rs
|
||||
|
||||
+11
-8
@@ -36,19 +36,22 @@ comment naming the reference fact and why we differ. Anything else is a fork.
|
||||
## Hard rules
|
||||
|
||||
- **Never commit anything from the install.** Art, models, sounds, maps, data: the install is
|
||||
read at runtime from a gitignored path, and everyone provides their own. The one exception is
|
||||
interface code: FrameXML and GlueXML run off the player's own patch chain, and our own
|
||||
counterparts under `assets/ui` stay until they retire.
|
||||
read at runtime from a gitignored path, and everyone provides their own. Interface code is no
|
||||
exception: FrameXML and GlueXML run off the player's own patch chain, and `assets/ui` holds
|
||||
only benilla's own files.
|
||||
- **The install is read-only.** benilla never writes into the WoW folder: no screenshot, log,
|
||||
cache or scratch file. `scripts/smoke.sh` fails a run that leaves the install changed.
|
||||
- **Local state lives in one folder**, `benilla-config/`, at the repo root in a dev build and
|
||||
beside the binary in the player build; every path to it resolves through `crate::local_state`.
|
||||
Player settings are CVars persisted as a diff in `benilla-config/config.toml`.
|
||||
- **UI is stock-first: a window is migrated, not authored.** The end state is the stock 1.12
|
||||
FrameXML executed off the player's own chain. `assets/ui` does not grow: a test names its
|
||||
files and fails on a new one. A window is built by pointing `benilla.toc` at the stock file,
|
||||
deleting ours and building the engine verbs it calls, never stubbing one to make the file
|
||||
load.
|
||||
- **The core UI is the stock UI; benilla's own is one layer on top.** The core loads the stock
|
||||
1.12 FrameXML off the player's own chain, byte for byte, and builds the engine verbs those
|
||||
files call, never stubbing one to make a file load. benilla's own interface (its options
|
||||
window, its game-menu look, its fixes to the stock Lua) is one layer that loads after the
|
||||
stock files and before third-party addons, the way an addon does: written against the 1.12 API
|
||||
alone, listed in no addon list and always on, and offering nothing another addon can build on.
|
||||
Anything that changes what the stock UI does or shows lives in the layer, and the layer does
|
||||
not grow: a test names its files and fails on a new one.
|
||||
- **A setting's default is the reference's default.** Every option boots at the stock 1.12
|
||||
value. Shipping another value costs an explicit `Deviates` row with the reason, and a test
|
||||
fails a row that drifts either way.
|
||||
|
||||
@@ -66,7 +66,7 @@ awk -F'\t' -v ref="$REF" -v show_missing="$show_missing" -v show_beyond="$show_b
|
||||
n_ref["engine"] + n_ref["framexml"] + n_ref["lua"]
|
||||
printf " engine %5d functions, %5d other <- benilla implements these in Rust\n", \
|
||||
n_ref_fn["engine"], n_ref["engine"] - n_ref_fn["engine"]
|
||||
printf " framexml %5d functions, %5d other <- benilla transcribes these into assets/ui\n", \
|
||||
printf " framexml %5d functions, %5d other <- the stock FrameXML defines these\n", \
|
||||
n_ref_fn["framexml"], n_ref["framexml"] - n_ref_fn["framexml"]
|
||||
printf " lua %5d functions, %5d other <- mlua provides these\n", \
|
||||
n_ref_fn["lua"], n_ref["lua"] - n_ref_fn["lua"]
|
||||
|
||||
Reference in New Issue
Block a user