comments cite the reference by address, not by private note (#30)

Comments that pointed into the maintainer's private reverse-engineering notes (a note by name, a
path, a section or finding id, the analysis's own name for a client function, a process word) now
carry the fact itself, cited by the reference client's address, by vmangos file and line, or as
"the reference". Nobody outside could follow those pointers.

Comment text only, plus the test-assert and log strings that carried the same pointers. No claim
changes: a comment the notes have since corrected keeps its wording here, and correcting it is
separate work.

The two reference generators, `scripts/gen-reference-globals.py` and
`scripts/gen-reference-verb-events.py`, named the private notes' layout to find their inputs. They
now take the input files as arguments (`--capture`, `--framexml`, `--names`, `--extents`,
`--disasm`). Run on the same inputs, both write the committed tables row for row; only the two
header lines that named the old environment variable change.

Checked per file:
- the code is identical: each file with its comments stripped compares equal before and after; a
  changed string literal is compared under a mask that proves only literal text moved and every
  literal kept its line count;
- no client address the old text cited was dropped;
- a second reviewer checked every added address against its derivation;
- a detector for the notes' names, paths, ids, function names and process words finds none left;
- `scripts/gates.sh` is green.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Sam Sung
2026-09-24 12:14:08 +07:00
committed by GitHub
co-authored by Claude Opus 5.5
parent beb6cc854f
commit 7b66dac9db
887 changed files with 10649 additions and 11632 deletions
+49 -55
View File
@@ -27,14 +27,13 @@
//!
//! ## The load order is the reference's, not a topological sort
//!
//! `AddOn_Load 0x51f240` is **recursive**, and wow-5875-re has it byte-verified
//! (`system/ui/ui.md`, the SavedVariables §5 quad): for each addon, in order —
//! `AddOn_Load 0x51f240` is **recursive**: for each addon, in order —
//! **OptionalDeps first (failures ignored)** → **RequiredDeps (a failure ABORTS this addon's
//! load)** → **its own `.toc`-listed files, in listed order**. We match that shape, because a
//! pre-sorted flat list cannot express "a missing hard dependency drops exactly this addon and
//! nothing else".
//!
//! **Our own interface is not an addon for lifecycle purposes.** The same source records that
//! **Our own interface is not an addon for lifecycle purposes.** In the reference,
//! *FrameXML does not go through `AddOn_Load`, so it gets no `ADDON_LOADED`* — and `benilla.toc`
//! is excluded exactly as FrameXML is. Structurally, not by a filter: it loads through
//! [`super::manifest::load_ingame_ui`], while [`Walk`] only ever walks what [`discover`] found.
@@ -187,11 +186,10 @@ impl Addon {
///
/// `path` is the entry already resolved into the install's path space, which is the only thing
/// the client names a file chunk after: `"@%s"` (`0x8716e0`) over the resolved path, built by
/// `0x704bc0` for every `.lua` the loader touches (wow-re
/// `ui/scratch/include-lua-dispatch.md` §7). So an addon's file is `Interface\AddOns\<Folder>
/// \<File>` and a chain file is `Interface\FrameXML\…` — the second being both what the real
/// client names it and what keeps FrameXML *out* of the `\AddOns\` pattern the Ace2 family
/// matches against, because FrameXML is not an addon.
/// `0x704bc0` for every `.lua` the loader touches. So an addon's file is
/// `Interface\AddOns\<Folder>\<File>` and a chain file is `Interface\FrameXML\…` — the second
/// being both what the real client names it and what keeps FrameXML *out* of the `\AddOns\`
/// pattern the Ace2 family matches against, because FrameXML is not an addon.
///
/// **One rule for both doors, which is the point** (decision 2155). `Loader::run` names a
/// `<Script file=>` chunk by exactly this rule over exactly this space, so a manifest entry and
@@ -297,10 +295,10 @@ impl Addon {
let report = benilla_ui::loader::load_in(script, &doc, &path, &provider);
// `FrameXML_Debug`'s trace lines, if the player turned it on (2160). Log-only, and
// never retained: the reference files them into the same per-document record as the
// errors, but at severity 0 and *where that record surfaces is an open question in
// wow-re* (`framexml-debug-trace-flag.md`, and `xml-template-name-lookup.md` §9 for
// severity 1). So they go where a trace can go without claiming a surface we have not
// derived — and the switch that produces them is off unless an addon asks.
// errors, but at severity 0 (`0x6ee2bc`), and *where that record surfaces is an open
// question*, for severity 1 as well. So they go where a trace can go without claiming a
// surface we have not derived — and the switch that produces them is off unless an
// addon asks.
for t in &report.traces {
info!("ui_script({}/{file}): {t}", self.name);
}
@@ -387,16 +385,15 @@ fn read_under(root: &Path, rel: &str) -> Option<Vec<u8>> {
/// an addon's file, `Interface/FrameXML/…` for one that walked out of the AddOns tree. The
/// reference has exactly one such space — `0x647e60` answers a name from a hash index of the
/// install tree keyed by **install-root-relative path**, and then, attempt #4, from the MPQ chain
/// (wow-re `ui/scratch/include-lua-dispatch.md` §4.1, VERIFIED; loose *before* archive, which is
/// the order below). Two spaces is what this client had, and it cost three separate things:
/// (loose *before* archive, which is the order below). Two spaces is what this client had, and it
/// cost three separate things:
///
/// - `..\Blizzard_AuctionUI\Blizzard_AuctionUITemplates.xml`, a `.toc` line in `Auctioneer` and
/// in `BeanCounter`, collapses to `Interface/AddOns/Blizzard_AuctionUI/…` — a folder that is a
/// `.pub` decoy on disk with the real files inside `patch.MPQ`. Only the chain leg has it.
/// - `..\..\FrameXML\Fonts.xml`, a `.toc` line in `JIM_toolbox` and in `SpecialTalentUI`,
/// collapses out of the AddOns tree entirely, to `Interface/FrameXML/Fonts.xml`. Same leg.
/// (wow-re records both by name as **RESOLVING** on the real client —
/// `ui/scratch/xml-toc-path-resolution.md` §5 cases 1 and 2.)
/// (Both **RESOLVE** on the real client: `0x6ede10` collapses the `..` before the open.)
/// - and the one that was invisible: with the addon space rooted at the AddOns folder, a
/// `<Script file=>` chunk was NAMED after that space — `@AtlasLoot\Core\AtlasLoot.lua` — while
/// a `.toc`-listed one was named `@Interface\AddOns\AtlasLoot\…`. See [`Addon::chunk_name`].
@@ -504,13 +501,13 @@ fn chain_addons() -> Vec<Addon> {
/// Every registered addon, in the reference's own **registration order**: the archive pass first,
/// then the loose folders (decision 2175).
///
/// **Two passes, archive before loose, and the first pass wins a duplicate.** Byte-read in wow-re
/// `system/ui/scratch/addon-registry-scan-and-order.md` §1–§5: `AddOn_ScanAddOnDir 0x51c760` runs
/// **Two passes, archive before loose, and the first pass wins a duplicate.**
/// `AddOn_ScanAddOnDir 0x51c760` runs
/// `0x401470` over each mounted archive's `(listfile)` (`0x648fb0`, textual line order) and only
/// then `0x42ad10`'s `FindFirstFileW` walk of the loose directories; both funnel into
/// `0x51c9b0`, which probes the name hash and **returns immediately on a hit** (`0x51ca10`), and
/// links each new record at the TAIL (`0x521ad0` mode 2 — a tail insert with no comparison of any
/// kind, so registry order *is* registration order). §10 measures the consequence on a stock
/// kind, so registry order *is* registration order). Measured on a stock
/// install: the twelve `Blizzard_*` `.toc`s are the only ones in any archive, so Blizzard occupies
/// registry positions 1..12 and third-party always follows.
///
@@ -521,11 +518,12 @@ fn chain_addons() -> Vec<Addon> {
///
/// The load walk barely notices (all twelve are `## LoadOnDemand: 1`, so `0x51f600` skips them),
/// but the registry order is what the Lua index space is built from, and it is what decides which
/// addon's copy of a shared global wins — the last writer, per §9.
/// addon's copy of a shared global wins — the last writer, in the one shared Lua state
/// (`0x7040d0`).
fn discover() -> Vec<Addon> {
// PASS 1 — the archive. `BLIZZARD_ADDONS` is in ascending name order, which is also the order
// the shipped `patch.MPQ` listfile happens to carry those twelve lines in (§10, measured). The
// note is explicit that a re-implementation may NOT rely on a listfile being sorted, so this is
// the shipped `patch.MPQ` listfile happens to carry those twelve lines in (measured). A
// re-implementation may NOT rely on a listfile being sorted, so this is
// our deterministic choice for a set we enumerate ourselves, not a claim about the format.
let mut found = chain_addons();
// PASS 2 — the loose folders, minus anything the archive already registered.
@@ -541,11 +539,10 @@ fn discover() -> Vec<Addon> {
/// **The reference's directory-listing order, as a sort key** — the one thing 2166 §7 left open.
///
/// The reference's loose-folder pass is `0x42ad10`'s kernel32 `FindFirstFileW`/`FindNextFileW`
/// walk (wow-re `system/ui/scratch/addon-registry-scan-and-order.md`), and that walk **imposes no
/// ordering of its own**: the client never sorts, so its walk order is whatever the host
/// filesystem hands back. wow-re states the consequence outright — *"a re-implementation cannot be
/// walk, and that walk **imposes no ordering of its own**: the client never sorts, so its walk
/// order is whatever the host filesystem hands back. So a re-implementation cannot be
/// order-identical to the reference, because the reference's order is not a property of the
/// reference."*
/// reference.
///
/// So there is no order to copy, and exactly one order worth choosing: **NTFS's**. It is the order
/// every 1.12 install actually walked in, because that is the filesystem the client shipped on, and
@@ -564,8 +561,8 @@ fn discover() -> Vec<Addon> {
/// positions move** — `Fubar_*` interleaves with `FuBar_*` instead of following it wholesale,
/// `oRA2` climbs 31 places, `zBar` overtakes `Zorlen`, and `_LazyPig`/`_Nameplates` fall from the
/// middle to dead last. Since the client runs every addon in one Lua state and adds no versioning
/// (wow-re §9: *which provider of a shared library wins is exactly "whichever addon's files run
/// last"*), that reshuffle decides which copy of `Tablet-2.0` a corpus of seventy providers ends up
/// (`0x7040d0`: which provider of a shared library wins is exactly whichever addon's files run
/// last), that reshuffle decides which copy of `Tablet-2.0` a corpus of seventy providers ends up
/// with.
///
/// The corpus corroborates the mechanism from the other side: the `!`-prefix convention
@@ -773,9 +770,8 @@ pub(crate) struct InstalledAddOn {
pub(crate) url: Option<String>,
pub(crate) dependencies: Vec<String>,
/// `## Interface` as the version gate reads it (decision 1292): the leading integer, `0`
/// when absent — and now ENFORCED by the load walk when `checkAddonVersion` is on,
/// superseding 1191 §6's report-only interim (the RE answer it was waiting for landed:
/// wow-re `addon-version-gate.md`).
/// when absent — and now ENFORCED by the load walk when `checkAddonVersion` is on
/// (`0x51e876`), superseding 1191 §6's report-only interim.
pub(crate) interface: u32,
/// `## LoadOnDemand: 1` — shown as a status hint rather than a checkbox state, because a
/// LoadOnDemand addon is not "off", it is waiting for a `LoadAddOn` call (1191 §6).
@@ -853,12 +849,11 @@ pub(crate) fn installed_rows() -> Vec<InstalledAddOn> {
/// **One node per character on the realm's character list, each holding the explicit rows that
/// character's `AddOns.txt` carried** — the reference's `ADDONSTATELIST` (anchor `0xbe1bd0`, head
/// `ds:0xbe1bd8`), which `AddOnList_LoadCharacter 0x51ebe0` fills one node per character at
/// char-list population (wow-5875-re `system/ui/scratch/addon-enable-store.md` §1/§5).
/// char-list population.
///
/// That the node set is the **char-list, in wire order, rebuilt whole** is verified rather than
/// assumed (wow-re `addon-defaultstate-and-node-set.md`, decision 2316): the per-record callback
/// `0x472300` is handed to an enumerator that walks every `SMSG_CHAR_ENUM` record with no filter
/// and no early-out, and its driver destroys every existing node first (`0x51f0b0(NULL)`).
/// The node set is the **char-list, in wire order, rebuilt whole** (decision 2316): the per-record
/// callback `0x472300` is handed to an enumerator that walks every `SMSG_CHAR_ENUM` record with no
/// filter and no early-out, and its driver destroys every existing node first (`0x51f0b0(NULL)`).
///
/// **A node with no file is EMPTY, not absent**, and the difference is the whole point: an empty
/// node contributes no opinion to the aggregate, but it is still a character whose enable bit has
@@ -888,7 +883,7 @@ impl EnableStore {
.unwrap_or_default();
let hash = rows
.into_iter()
// Last line wins, like the reference's hash insert (§5's duplicate law).
// Last line wins, like the reference's hash insert (`0x51eeef`).
.map(|(name, on)| (name.to_ascii_lowercase(), on))
.collect();
(character.clone(), hash)
@@ -982,10 +977,10 @@ fn store_nodes(identity: Option<&(String, String)>, roster: &[String]) -> Vec<St
/// **Merges rather than replaces, because that is what the reference's writer structurally IS.**
/// `0x51ef20` walks the in-memory enable **hash** and emits one `"%s: %s\r\n"` line per entry
/// (`0x853968`), and that hash is built by the reader `0x51ebe0` from `AddOns.txt` itself — which
/// "creates entries for every line, both states", strdup'ing each name as written. So a row for an
/// creates entries for every line, both states, strdup'ing each name as written. So a row for an
/// addon that is not installed right now was loaded, is never removed, and is written straight back
/// out; and the spelling that survives is the FILE's, not any folder's, because the folder list
/// never enters the hash at all (wow-5875-re `system/ui/scratch/addon-enable-store.md` §5/§6).
/// never enters the hash at all.
///
/// Both halves matter and both were divergent on the logout path, which used to rebuild the file
/// from `addon_enable_states()` — the installed-folder registry. One logout erased
@@ -1206,7 +1201,7 @@ impl Walk {
/// Load one addon by name, its dependencies first. `Err` means "this addon did not load", and
/// is what a hard dependency's failure propagates.
///
/// The order inside is `AddOn_Load 0x51f240`'s, byte-verified in wow-5875-re (`system/ui/ui.md`):
/// The order inside is `AddOn_Load 0x51f240`'s:
///
/// > OptionalDeps (failures ignored) → RequiredDeps (a failure aborts) → **this addon's own
/// > `.toc`-listed files, in listed order** (`0x51f3fa`) → `Bindings.xml` (`0x51f400`) → the
@@ -1340,8 +1335,8 @@ impl Walk {
mod tests {
/// **The walk order is NTFS's, not byte order** — the fact 2166 §7 left open, pinned here.
///
/// The reference's loose pass is `FindFirstFileW` and sorts nothing (wow-re
/// `addon-registry-scan-and-order.md`), so its order is the host filesystem's; NTFS's `$I30`
/// The reference's loose pass is `FindFirstFileW` and sorts nothing
/// (`0x42ad10`), so its order is the host filesystem's; NTFS's `$I30`
/// collation is the one we adopt, because it is the order the entire vanilla corpus was
/// written against. Two rules distinguish it from `str`'s own `Ord`, and this asserts both:
/// **case folds away** (so a lowercase-initial name sorts among its letter's block rather
@@ -1731,10 +1726,10 @@ mod tests {
/// `IsAddOnLoadOnDemand(nil)` and AtlasLoot's AceDB keyed itself on a raw traceback. 80 of the
/// 219-addon corpus ship at least one `<Script file=>`.
///
/// The reference names both the same way and the note says why: a `.toc` line resolves against
/// `Interface\AddOns\<Addon>\` and a bare `<Script file=X>` against `dirname(referrer)` —
/// the same directory — and the chunk name is `"@%s"` over the resolved path in both cases
/// (wow-re `ui/scratch/xml-toc-path-resolution.md` §1, `include-lua-dispatch.md` §7).
/// The reference names both the same way: a `.toc` line resolves against
/// `Interface\AddOns\<Addon>\` and a bare `<Script file=X>` against `dirname(referrer)`
/// (`0x6ee07b`) — the same directory — and the chunk name is `"@%s"` over the resolved path in
/// both cases (`0x704bc0`).
#[test]
fn an_xml_referenced_lua_is_named_like_a_toc_listed_one() {
let tmp =
@@ -2144,8 +2139,8 @@ mod tests {
/// **`benilla` never appears in an `ADDON_LOADED`.**
///
/// FrameXML does not go through `AddOn_Load` and gets no such event (wow-5875-re
/// `system/ui/ui.md`), and our own interface is FrameXML's counterpart. An addon that watches
/// In the reference, FrameXML does not go through `AddOn_Load` and gets no such event, and
/// our own interface is FrameXML's counterpart. An addon that watches
/// `ADDON_LOADED` to detect *another* addon would otherwise see a name no reference client
/// ever sends.
///
@@ -2499,8 +2494,7 @@ mod tests {
/// **The client registers `GetAddOnInfo` twice, as two different functions**, and this test
/// used to assert the wrong one. It was written off `Interface\GlueXML\AddonList.lua`, which
/// describes glue's `0x46d460`: eight values, `url` at slot 4, `newVersion` at 8. The in-game
/// binding `0x48e390` answers seven, with **`enabled`** at slot 4 and no `url` at all
/// (wow-re `system/ui/scratch/addon-version-gate.md`).
/// binding `0x48e390` answers seven, with **`enabled`** at slot 4 and no `url` at all.
///
/// Its own doc comment named the failure it then failed to catch — *"a row that shifts by one
/// puts an addon's notes in its URL field, which nothing would notice"*. The row was shifted at
@@ -2815,8 +2809,8 @@ mod tests {
/// **A `.toc` line naming a file the package does not ship does not stop a demand load**
/// (decision 2107) — 1450's rule, applied by `LoadAddOn` and not only by the startup walk.
///
/// The reference logs `Couldn't open %s` and carries on (wow-re
/// `xml-toc-path-resolution.md` §4). Ours sent the miss to the **script-error** channel
/// The reference logs `Couldn't open %s` and carries on
/// (`0x6edaa0`). Ours sent the miss to the **script-error** channel
/// instead, and the corpus paid for it the moment `LoadAddOn` became reachable at scale:
/// FuBar's `LoadLoadOnDemandPlugins` demand-loads 55 plugins whose `.toc`s each list an
/// `AmmoFuLocale-koKR.lua` none of them ships, and every one of them scored a session
@@ -2929,7 +2923,7 @@ mod tests {
/// then loaded them anyway, so the load walk resolves through the same [`EnableStore`]: the
/// reference's `AddOn_CanLoad` check 3 is `0x51e470(name, character, useDefault = 1)`, which
/// for a character with no explicit entry takes the explicit-only aggregate over the realm's
/// other characters (wow-5875-re `addon-enable-store.md` §4).
/// other characters.
///
/// The control is the second addon: one character left it on, so the roster does **not** agree
/// about it, and the newcomer gets its manifest's `## DefaultState` instead — enabled. Without
@@ -3184,8 +3178,8 @@ mod tests {
/// `0x7043f0`/`0x704480`): `NAME = value`, bracketed keys, TAB indent, a trailing comma on
/// every entry, and the file split by scope. `["t"]`'s `[1] = 1,` is the reference's shape for
/// a list too — its writer emits a bracketed key for every table shape and never a bare
/// positional entry (wow-re `system/ui/scratch/lua-table-storage-and-next-order.md` §Q5) —
/// and it reloads in index order because of what 1.12's *parser* does with it (decision 2111).
/// positional entry — and it reloads in index order because of what 1.12's *parser* does
/// with it (decision 2111).
///
/// Asserted as bytes rather than by re-reading, because "it round-trips through our own
/// loader" is exactly the check that passes for a private format. The reference's own client
@@ -590,9 +590,9 @@ fn hovered_bag_tooltip_fills_itself_when_the_stats_land() {
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
}
/// At a vendor, a bag hover shows the engine-truth sell-price money row (SellPrice × stack, wow-re
/// tooltip-money.md 0x52b650@0x52e376) — or the ITEM_UNSELLABLE "No sell price" line — and arms
/// the pouch cursor (ShowContainerSellCursor → Buy over a Point base, cursor-system.md §7);
/// At a vendor, a bag hover shows the engine-truth sell-price money row (SellPrice × stack,
/// 0x52b650@0x52e376) — or the ITEM_UNSELLABLE "No sell price" line — and arms
/// the pouch cursor (ShowContainerSellCursor 0x4fa460 → Buy over a Point base);
/// leaving resets it.
#[test]
fn vendor_bag_hover_shows_sell_price_and_arms_the_pouch_cursor() {
@@ -1839,10 +1839,9 @@ fn a_key_dropped_on_the_button_files_itself() {
);
// And it does not RAISE either, which is the half this test could not assert until the bytes
// came back. `AddMessage`'s text is fetched through `lua_isstring 0x6f3510` whose failure edge
// is a silent jump to the function's own epilogue — no line, no error (wow-5875-re
// `addmessage-text-gate-silent-skip.md`, 4-worker cross-check). Ours took an mlua `String` and
// raised, which put a script-error dialog on an ordinary player gesture; the fix is in
// `messageframe/mod.rs::message_text` and its own test carries the full law.
// is a silent jump to the function's own epilogue (`0x79562c`) — no line, no error. Ours took
// an mlua `String` and raised, which put a script-error dialog on an ordinary player gesture;
// the fix is in `messageframe/mod.rs::message_text` and its own test carries the full law.
//
// So a full keyring is a DEAD CLICK on a real 1.12 client, and now here.
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
@@ -767,8 +767,8 @@ fn the_backpack_button_still_hovers_with_bagnon_holding_its_script() {
}
/// **Bug 3.** `Bagnon_Forever/database/ui.lua:61` sizes its character dropdown from
/// `button:GetTextWidth()` on a **CheckButton** — a real 1.12 Button method (wow-re
/// `widget-api-batch-benilla.md` Q8 lists it present, and `GetStringWidth` absent, on Button).
/// `button:GetTextWidth()` on a **CheckButton** — a real 1.12 Button method (`0x782290`, and
/// `GetStringWidth` is absent on Button).
#[test]
fn a_button_reports_its_own_label_width() {
benilla_formats::wow_data_or_skip!();
@@ -966,7 +966,7 @@ fn the_whole_bagnon_window_survives_being_used() {
// **Empty, and that is the list's best state, not a reason to delete it.** Four rows sat here
// when it was written, and each retired the way the rule above says: the bag-BUTTON cursor
// trio (`PutItemInBackpack`, `PutItemInBag`, `PickupBagFromSlot` — `Bag.lua:199-215`) once
// wow-5875-re's verdict landed and `script/cursor/bag_verbs.rs` bound all three; and money on
// `script/cursor/bag_verbs.rs` bound all three; and money on
// the cursor (`OpenCoinPickupFrame`, `Frame.lua:537-543`) when the money kit went stock and
// the reference's own `CoinPickupFrame.xml` joined the manifest with the money cursor under
// it (decision 1962). With nothing on the list, "unexplained" is every error, which is the
@@ -1105,8 +1105,8 @@ fn a_stack_count_wears_the_font_object_its_font_attr_names() {
/// `<Gradient>`, and dropped it in silence, so the white art rendered untinted.
///
/// `<Gradient>` survives beside a `file=` where `<Color>` does not, and that asymmetry is the
/// point: they land in different fields (vertex colours `+0xb8` vs the texture `+0xcc` —
/// wow-re `texture-color-composition.md` §1-2).
/// point: they land in different fields (vertex colours `+0xb8`, written by `0x77f910`, vs the
/// texture `+0xcc`, written by `0x770360`).
#[test]
fn a_texture_gradient_tints_the_art_it_sits_on() {
benilla_formats::wow_data_or_skip!();
@@ -1366,7 +1366,7 @@ fn bagnon_forevers_records_survive_the_logout_boundary() {
// `GetInventoryItemCount("player", ContainerIDToInventoryID(0))`, and `ContainerIDToInventoryID`
// has no backpack arm (`0x4f94e0`: `id - 1 < 4` → `+20`), so bag 0 asks inventory slot 19 —
// the tabard slot. `GetInventoryItemCount 0x4c8680` pushes **1** for an empty slot
// (`0x4c8797`; wow-re `inventory-item-count-law.md` §3, carved at 5a377f4bb), and 1 for a
// (`0x4c8797`), and 1 for a
// worn tabard's stack count too, so the real client writes this row as `16,1,` either way.
let record = "local r = BagnonForeverData[GetRealmName()][UnitName('player')] \
return tostring(r[0] and r[0].s), tostring(r[0] and r[0][1])";
@@ -1542,8 +1542,8 @@ fn a_bag_slot_click_toggles_bagnon_not_the_native_window() {
/// **Bug 7 — the director's unlit backpack button — and where it stands since the bag bar went
/// stock.** Bagnon lights `MainMenuBarBackpackButton` from its window's OnShow/OnHide
/// (`Bagnon.lua:33/39` — `SetChecked(1/0)`), and OnShow dispatch is synchronous (wow-re
/// `onshow-onhide-dispatch-order.md`), so whoever writes checked AFTER the toggle wins. On the
/// (`Bagnon.lua:33/39` — `SetChecked(1/0)`), and OnShow dispatch is synchronous
/// (`0x775750` → `0x76ae10`), so whoever writes checked AFTER the toggle wins. On the
/// reference's own click path the stock `BackpackButton_OnClick` tail
/// (`MainMenuBarBagButtons.lua:55-69`) runs last: it re-derives checked by scanning the NATIVE
/// container frames, finds none for bag 0 while an addon holds the bags, and writes 0 — so the
@@ -723,8 +723,8 @@ fn the_temporary_enchant_row_shows_a_weapon_enchant_and_moves_the_top_row_aside(
// The direct probe, and its exact shape is the point. `BuffButtonTempEnchant` inherits
// `BuffButtonTemplate` and blanks its three aura handlers with `<OnLoad>`↵`</OnLoad>` — a
// WHITESPACE body, which 1.12 compiles into a valid empty function rather than storing nil
// (`SetScript 0x7025c0` tests `text[0]`, and nothing trims — wow-5875-re
// `xml-script-empty-element.md`). So each handler is still a FUNCTION here, and it is the
// (`SetScript 0x7025c0` tests `text[0]`, and nothing trims). So each handler is still a
// FUNCTION here, and it is the
// reference's own no-op rather than `BuffButton_OnLoad`.
//
// Asserting `== nil` instead would be asserting a bug: an addon reading
@@ -1009,11 +1009,11 @@ fn a_settled_buff_button_rewrites_alpha_and_duration_as_the_reference_does() {
/// The duration line against the **real shipped strings**, end to end — the leg the engine's own
/// synthetic-template tests cannot reach.
///
/// The engine formats through `tooltip::duration_text` (`0x52fa50`'s ladder, wow-re
/// §3-BUFF-TIME-FORMAT) over whatever `GlobalStrings.lua` the player's install carries; this runs
/// The engine formats through `tooltip::duration_text` (`0x52fa50`'s ladder) over whatever
/// `GlobalStrings.lua` the player's install carries; this runs
/// that file into a real VM exactly as the boot's `load_global_strings` does, then asserts the
/// wording that comes back. Every expectation below is a reading the PREVIOUS two-arm formatter got
/// wrong, so this is the carve's whole delta in one place. Skips without client data.
/// wrong, so this is the whole delta in one place. Skips without client data.
#[test]
fn the_duration_line_reads_the_real_global_strings() {
let data = benilla_formats::wow_data_or_skip!();
@@ -420,7 +420,7 @@ fn the_fill_draws_beneath_the_border_art() {
);
}
/// A StatusBar fill CROPs its texture — it never squeezes it (wow-re `nameplate-vkey.md`). At a
/// A StatusBar fill CROPs its texture — it never squeezes it (`0x770410`). At a
/// fraction f the quad is f·width wide AND samples u ∈ [0, f], so `UI-StatusBar`'s left-to-right
/// ramp keeps its true gradient at every fill level.
#[test]
@@ -126,7 +126,7 @@ fn newest_line_sits_at_the_bottom() {
/// The whole fade round trip as it reaches a real chat window: a line ramps down, a scroll brings
/// it back to full, the scrolled-up view then holds it there, and returning to the bottom lets it
/// ramp again. The re-arm half is `msgframe-fade-rearm-law.md` — every scroll entry reaches
/// ramp again. The re-arm half: every scroll entry reaches
/// `0x788b80` or the relayout's `0x788af0`; before it, a faded-out chat could not be recovered by
/// any input the client offers (director-reported, 2026-08-29).
#[test]
@@ -387,8 +387,8 @@ fn chat_click_dismisses_a_stuck_spell_but_not_an_item() {
/// `ChatTypeInfo` carries the shipped default chat colors twice: once in
/// `Interface\FrameXML\ChatFrame.xml` for addons to read, and once in
/// [`crate::ui_chat::default_color`] for our own feed to render. Both are the same wow-re byte
/// table (`chat-color-table.md`, the static registry at `.rdata 0x804710`) — so this is the gate
/// [`crate::ui_chat::default_color`] for our own feed to render. Both are the same reference byte
/// table (the static registry at `.rdata 0x804710`) — so this is the gate
/// that makes the duplication safe: every kind we model must agree to the byte, and the table's shape (`sticky`/`id`) must be the reference's.
#[test]
fn chat_type_info_matches_the_host_color_table() {
@@ -2,9 +2,9 @@
//!
//! `CinematicFrame` is fullscreen, keyboard-enabled, and carries an `OnKeyDown` that answers
//! ESCAPE. In the reference that is enough to swallow **every** key while it is up, because the
//! key-down walk's gate is EXISTENCE, not handling (wow-re `ui/scratch/frame-key-script-delivery.md`
//! §3, VERIFIED): a shown keyboard frame with the slot set consumes the key whatever its script
//! does with it, and a 1.12 handler has no way to signal "not handled" (§3.1).
//! key-down walk's gate is EXISTENCE, not handling (`0x76b7d0`): a shown keyboard frame with the
//! slot set consumes the key whatever its script does with it, and a 1.12 handler has no way to
//! signal "not handled" (`0x76ba25`).
//!
//! The reference's own Lua is the proof, and it is why these tests exist: that same `OnKeyDown` has
//! to call `RunBinding("SCREENSHOT")` **by hand** to get one key back. It would not need to if
@@ -44,9 +44,10 @@ fn picker() -> UiScript {
///
/// It is emphatically **not** the identity, and not a simple quantize either: `SetColorRGB` rounds
/// half-up into bytes and stores HSV floats, while every read path floors, so 9.75 % of colours come
/// back a step low on one channel (wow-re `system/ui/scratch/colorselect-color-law.md`, exhaustive
/// over all 256³). `benilla_ui::script::colorselect`'s own tests pin that law against wow-re's
/// measured witnesses; these tests use it as a given so they stay about the Dewdrop plumbing.
/// back a step low on one channel (`SetColorRGB 0x78eae0`, the read-back floor `0x7bbec0`;
/// measured exhaustively over all 256³). `benilla_ui::script::colorselect`'s own tests pin that law
/// against the reference's measured witnesses; these tests use it as a given so they stay about the
/// Dewdrop plumbing.
fn after_round_trip(r: f64, g: f64, b: f64) -> (f64, f64, f64) {
let mut cs = benilla_ui::widget::ColorSelectState::default();
cs.set_rgb(r, g, b);
@@ -336,8 +336,7 @@ fn combat_text_master_toggle_unregisters() {
/// fast arm into a plain `lua_gettable(LUA_GLOBALSINDEX)` — so whatever is in the global when the
/// world-entry load edge closes is what the numbers draw in. Stock `Fonts.xml` puts Friz there;
/// MikScrollingBattleText and pfUI put their own face there at `ADDON_LOADED`, which the reference
/// reads because `0x6c8470` runs *after* the addon load, not at CRT init as three wow-re notes
/// had it.
/// reads because `0x6c8470` runs *after* the addon load, not at CRT init.
///
/// Skips without client data.
#[test]
@@ -115,8 +115,8 @@ fn armor_guy_shows_red_broken_yellow_damaged_and_hides_clean() {
);
assert_eq!(cell_color(&mut s, WEAPON_CELL), Some(RED), "broken → red");
// Damaged legs (3 points left — the byte law's ABSOLUTE 1..=5, wow-re inventory-alert-law
// `cmp [D+0xa0],5`): every body piece shows, legs yellow, the un-alerted head the faded
// Damaged legs (3 points left — the byte law's ABSOLUTE 1..=5, `cmp [D+0xa0],5` at
// `0x4c8012`): every body piece shows, legs yellow, the un-alerted head the faded
// white(0.5); the weapon glyph (repaired now) hides.
let mut inv: InventorySlots = Default::default();
inv[7] = slot(39, Some((3, 25)));
@@ -162,7 +162,7 @@ fn armor_guy_shows_red_broken_yellow_damaged_and_hides_clean() {
assert!(s.errors().is_empty(), "errors: {:?}", s.errors());
}
/// The FLAGS bits (wow-re inventory-alert-law): `0x10` forces red regardless of durability;
/// The FLAGS bits (`0x4c7faa`, `0x4c7fc8`): `0x10` forces red regardless of durability;
/// `0x08` (wrapped gift) silences the region entirely. And the client's 12th region — low ammo
/// (carried count <= 20 → 3) — answers through `GetInventoryAlertStatus(12)` even though the
/// 1.12 FrameXML never reads it.
@@ -139,9 +139,8 @@ fn escape_closes_bag_and_panel_releases_loot_and_clears_cursor() {
);
// **ESC does NOT drop the cursor's item.** Our retired ladder opened with
// `if CursorHasItem() then ClearCursor() end`; the reference's `ToggleGameMenu`
// (`UIParent.lua:1465-1497`) has no such arm, and wow-re's cursor carve places the
// ESC→ClearCursor wiring in FrameXML rather than the engine
// (`ui/scratch/cursor-dragdrop-payload.md`) — so in 1.12 the held item survives the key
// (`UIParent.lua:1465-1497`) has no such arm, and the ESC→ClearCursor wiring is inferred to
// live in FrameXML rather than the engine — so in 1.12 the held item survives the key
// (1988).
assert!(
s.cursor_item().is_some(),
@@ -19,7 +19,8 @@
//!
//! ## What is compared, and what the table can and cannot say
//!
//! The oracle is wow-re's two byte-level censuses, vendored by `scripts/gen-reference-events.py`.
//! The oracle is two byte-level censuses of the reference, vendored by
//! `scripts/gen-reference-events.py`.
//! Its `conf` column carries the gate rule per row, exactly as `1.12-shapes.tsv` does:
//!
//! * `exact` — every producer that reaches the event has a known shape. **This is the column this
@@ -115,8 +115,8 @@ fn the_exhaustion_tick_marks_where_rested_runs_out() {
s.fire_event("PLAYER_ENTERING_WORLD", vec![]);
s.resolve();
// The binding trio underneath (the app-feed shape the XML consumes) — the byte-verified
// contract (wow-re rested-xp-bindings.md, decision 1087).
// The binding trio underneath (the app-feed shape the XML consumes) — the reference's
// contract (`0x48d350`, `0x48d3f0`, `0x516ea0`; decision 1087).
let (id, name, mult) = s
.eval::<(i64, String, f64)>("return GetRestState()")
.unwrap();
@@ -11,10 +11,7 @@
//! (`hsv_to_rgb` scales all three outputs by `v` and by nothing else, so `rgb(h, s, v)` really is
//! `v · rgb(h, s, 1)`).
//!
//! Everything below is byte-verified in wow-re's `system/ui/scratch/colorselect-drawn-appearance.md`
//! (a §5 cross-check dispatched from this repo for exactly this window: six workers in three pairs,
//! one of each briefed cold, plus the orchestrator's own contiguous derivation). What it settled,
//! and what each fact cost to get wrong:
//! The reference's drawn appearance, and what each fact cost to get wrong:
//!
//! * **The disc is a 128×128 generated ARGB texture** (`0x78b580`, 16384 dwords into a
//! function-local static, uploaded once per process), drawn unmodulated — not a vertex-coloured
@@ -25,7 +22,7 @@
//! * **The texel lattice is integers**, `X, Y ∈ [−64, 63]`, drawn iff `X² + Y² ≤ 0x1000` —
//! boundary *inclusive* — and outside is `0x00000000`, a hard alpha cut with no mask and no
//! feathering. `S = √(X²+Y²)/64`; `H = (atan2(Y, X) + π)·180/π`, the exact inverse of the pick
//! law (`colorselect-color-law.md` §5), which is what makes clicking a texel select the colour
//! law (`0x78bd80`), which is what makes clicking a texel select the colour
//! that texel shows. Row 0 is `Y = +63`: the top.
//! * **The strip is an 8×8 solid texture of `HSV(H, S, 1)` times a vertical vertex gradient**,
//! black at the bottom and white at the top (`0x78b8a0` + `0x78b92a call 0x77f910`, and
@@ -140,8 +137,8 @@ mod tests {
}
/// The one property the disc must have: the colour a texel *shows* is the colour a click on
/// that texel *selects*. The right-hand side here is the pick law from wow-re
/// `colorselect-color-law.md` §5, which `benilla-ui`'s own `wheel_hs` transcribes — if the two
/// that texel *selects*. The right-hand side here is the reference's pick law
/// (`0x78bd80`), which `benilla-ui`'s own `wheel_hs` transcribes — if the two
/// ever part company, one of these sides moves and this fails.
#[test]
fn every_texel_shows_the_colour_a_click_on_it_would_pick() {
@@ -170,8 +167,9 @@ mod tests {
}
/// The disc's cardinal points, spelled out — this is what pins the *orientation* of the whole
/// image, and it is the half a sign error would silently mirror. wow-re read it off the fill
/// loop: **red LEFT, chartreuse BOTTOM, cyan RIGHT, violet TOP**, hue rising counter-clockwise.
/// image, and it is the half a sign error would silently mirror. Off the reference's fill loop
/// (`0x78b580`): **red LEFT, chartreuse BOTTOM, cyan RIGHT, violet TOP**, hue rising
/// counter-clockwise.
#[test]
fn the_discs_cardinal_hues_are_the_pick_laws() {
let (w, h, rgba) = wheel_pixels();
+13 -12
View File
@@ -574,9 +574,9 @@ pub(super) fn tick_script(
};
let (w, h) = (window.width(), window.height());
// The 768-virtual UI space (decision 0582 — byte law: the client's FrameXML space is ALWAYS
// 768 units tall, every aspect, mapped to the window; wow-re ui.md's converter identity
// `f(screenH) = 768` + the caret `H_px/192` law), times the uiScale dial (decision 0584 —
// the VM's screen is `768/uiScale` units tall, so a dial below 1 shrinks everything). The VM
// 768 units tall, every aspect, mapped to the window; the converter identity `f(screenH) = 768`
// (`0x41ad10`) + the caret `H_px/192` law (`0x77b8c0`)), times the uiScale dial (decision 0584
// — the VM's screen is `768/uiScale` units tall, so a dial below 1 shrinks everything). The VM
// lives entirely in that space: this seam scales quads ×s on the way out, mouse ÷s on the
// way in (input.rs), and measures ÷s on the way back. At a 768-tall window with the dial at
// 1 the whole pipeline is bit-identical to the pre-virtual behavior.
@@ -922,7 +922,7 @@ pub(super) fn paint_script(
script.set_editbox_advances(req.id, req.key, cum, rows, cell_h / s);
}
}
// The focused edit box's text-UI geometry (RF-0082 leaves caret/highlight geometry to the
// The focused edit box's text-UI geometry (the engine leaves caret/highlight geometry to the
// host): which Text quad is the box's, the scroll window to draw, and the caret/selection
// x-spans within it — advance-derived engine-side (the blink phase too, `0x77a790`'s 0.5 s
// law in the engine tick), matched in the Text arm below.
@@ -1454,7 +1454,7 @@ fn convert_entry(
} => {
use crate::portrait::PortraitSource;
// A FILE pane (decision 2008): publish what the tile renderer needs — the pane's
// device-pixel size, the render law's unit ladder off it, and where the composite
// device-pixel size, the reference's unit ladder off it, and where the composite
// goes (its rect, paint key, alpha and clip). The request is idempotent, so the
// memoized conversion may re-publish it freely; which panes draw is the engine's
// paint list, and the quad itself is the renderer's per-frame output
@@ -1475,18 +1475,19 @@ fn convert_entry(
crate::ui_models::TileRequest {
path: path.to_string(),
size_px,
// `1 model unit = 1280 · modelScale · layoutScale` FrameXML units (§3).
// `1 model unit = 1280 · modelScale · layoutScale` FrameXML units
// (`0x76d1a0`).
px_per_unit: 1280.0 * model_scale * layout * s * dpi,
// `SetPosition` is in layout units: `768 · √(a²+1)` FrameXML per unit.
pos_px_per_unit: 768.0 * diag * layout * s * dpi,
// A particle's half-extent: eye space, neither scale (§6).
// A particle's half-extent: eye space, neither scale (`0x7b2a50`).
star_px_per_unit: 768.0 * diag * s * dpi,
facing,
position: Vec3::new(position.0, position.1, position.2),
// The PERSPECTIVE leg's root (decision 2027), which is in model units,
// not pixels: `T(pos · layoutScale) · R(facing) · S(s)` with
// `s = G48·(5/3)·modelScale·layoutScale` — and `G48·(5/3)` is exactly
// `√((4/3)²+1)/√(a²+1)`, the 4:3 renormalizer (camera-law §11.2). The
// `√((4/3)²+1)/√(a²+1)`, the 4:3 renormalizer (`0x80655c`). The
// camera is carried through the same matrix, so both terms cancel for
// framing; they are here because the record's near/far are NOT scaled
// with them, and because the geometry has to be drawn somewhere.
@@ -1498,7 +1499,7 @@ fn convert_entry(
icon: icon.clone(),
rect,
z_key: eq.z,
// The instance draws at the widget's OWN alpha (render law §4.4).
// The instance draws at the widget's OWN alpha (`0x76d120`).
alpha: own_alpha,
clip,
},
@@ -1646,7 +1647,7 @@ fn convert_entry(
// used to fall through to `color.unwrap_or(WHITE)` with a `None` texture, which
// `ui_pass` renders as the shared 1×1 white image tinted white: an opaque white
// rectangle at the region's rect, which is how B221's macro icons reached the
// director's screen. wow-re settles what the reference does: `TextureCreate` does
// director's screen. What the reference does: `TextureCreate` does
// build an 8×8 placeholder, but `CSimpleTexture::SetTexture` (`0x770200`) checks the
// status severity and at ≥2 releases it and returns **without touching the widget's
// texture** — the widget keeps what it had, and Lua gets `nil`. Nothing goes white.
@@ -2690,7 +2691,7 @@ mod extract_gate_tests {
before - 1,
"the cleared marker's quad is gone from the stitched list"
);
// The follow-up (0 → 1 quads) splices against the RE-DERIVED spans.
// The follow-up (0 → 1 quads) splices against the re-derived spans.
app.world_mut().resource_mut::<UiQuads>().dirty = false;
app.world_mut()
.non_send_resource_mut::<UiScript>()
@@ -2776,7 +2777,7 @@ mod extract_gate_tests {
);
// The reference must share the app's HISTORY, not just its end state: `Show()` on a
// hidden frame re-stacks it to the tail of its draw bucket — the client's own
// `effective_visible_show 0x76ae10` re-adding it to the level's intrusive list — so B
// effective-visibility show `0x76ae10` re-adding it to the level's intrusive list — so B
// draws above C afterwards. (The splice got that right on its own; this reference did
// not, which is how the difference surfaced.)
let mut reference = app_from_script(&format!("{BUILD}\nhidden:Hide()\nhidden:Show()"));
@@ -332,7 +332,7 @@ pub(super) fn emit(
}
// The probe's seat line: the drawn INK rows (glyph-quad union, logical px, relative to the
// rect top) — the measurable half of the vertical-seat law
// (`fontstring-vertical-placement.md`): compare `ink` against the law's `d + ascender` seat
// (`0x5d1360`): compare `ink` against the law's `d + ascender` seat
// when hunting a vertical offset. Fill quads only (the shadow pass above would smear the
// bounds one px down-right).
let vpl = style.world_seat && benilla_assets::trace::enabled_for("vpl");
@@ -382,7 +382,7 @@ pub(super) fn emit(
#[allow(clippy::cast_precision_loss)]
let top = top + ui.caret_row as f32 * cell_h;
// caret_x is engine UI units (÷scale advances); the 1-px bar width stays device-thin
// (the client's 4-unit caret is a named residual, ui.md's caret law).
// (the client's 4-unit caret, `0x77b8c0`, is a named residual).
let cx = x0 + ui.caret_x * host.scale;
out.push(UiQuad {
rect: Rect::new(cx, top, cx + 1.0, top + cell_h),
@@ -14,8 +14,8 @@
//! **Our side is read from the ENGINE, never from our XML.** An attribute-vs-attribute diff is
//! precisely what let the class hide, twice over: benilla renames templates (`ChatFrameTemplate` →
//! `BenillaChatFrameTemplate`), so a name-keyed text diff reports a gap of zero on a window that is
//! entirely missing the flag; and the `<Scripts>` **auto-enable** law (wow-re
//! `ui/scratch/scripts-auto-enable.md` §1, VERIFIED — an `<OnEnter>`/`<OnLeave>`/`<OnMouseDown>`/
//! entirely missing the flag; and the `<Scripts>` **auto-enable** law (an
//! `<OnEnter>`/`<OnLeave>`/`<OnMouseDown>`/
//! `<OnMouseUp>`/`<OnDragStart>` reaches the same enable primitive `0x76af00(2,-1)` the attribute
//! does) makes `enableMouse=` a poor proxy for whether the frame actually takes the mouse. Asking
//! the loaded engine — `IsToplevel()`, `IsMouseEnabled()`, `GetID()`, `GetParent()` — is immune to
@@ -103,7 +103,7 @@ const FRAME_TAGS: &[&str] = &[
];
/// The five `<Scripts>` handler names that auto-enable the MOUSE kind, and only those five
/// (wow-re `ui/scratch/scripts-auto-enable.md` §1's kind-2 OR-chain, `0x769fb7`..`0x76a022`).
/// (the kind-2 OR-chain, `0x769fb7`..`0x76a022`).
/// `OnDragStop`/`OnReceiveDrag` bind a slot and trip no enable — they are deliberately absent.
const MOUSE_HANDLERS: &[&str] = &[
"OnEnter",
@@ -399,7 +399,7 @@ fn declares_handler(
}
/// Whether the reference's frame of this name **takes the mouse** once loaded — the three ways
/// `0x76af00(2, -1)` is reached, per `scripts-auto-enable.md` §1.3: the widget's own ctor, the
/// `0x76af00(2, -1)` is reached: the widget's own ctor, the
/// `enableMouse` attribute, or an auto-enabling `<Scripts>` handler.
fn reference_takes_mouse(name: &str, frames: &HashMap<String, Element>) -> bool {
let Some(el) = frames.get(name) else {
@@ -124,7 +124,7 @@ fn the_window_opens_on_friends_with_the_guild_tab_disabled() {
/// `FriendsFrameTab1..4` inherit `FriendsFrameTabTemplate`, which inherits
/// `CharacterFrameTabButtonTemplate` (the reference's own file, on the chain since 1993). The
/// middle template declares an `<OnClick>` and nothing else, and handler replacement is **per
/// handler name** (wow-re `template-onload-replacement-law.md`) — so the base template's
/// handler name** (`0x76a0d0`) — so the base template's
/// `<OnShow>` fit still runs, two hops down. That is the arrangement this pins: a row of tabs
/// still wearing the base template's authored 115 would mean the OnShow was lost on the way.
#[test]
@@ -884,9 +884,9 @@ fn the_column_headers_sort_by_their_own_keys() {
/// **The Show Offline Members checkbox EXISTS.** The reference declares it `virtual="true"` inside
/// a `<Frames>` block, which reads like "this is a template, do not build it" — and is not: the
/// `virtual` attribute is only consulted by the top-level file loader (wow-5875-re rf24,
/// `0x6ede10`), while a `<Frames>` child goes straight to the instantiator via `LoadChildFrames`
/// (rf26). So the box is a real frame in the reference and must be one here.
/// `virtual` attribute is only consulted by the top-level file loader (`0x6ede10`), while a
/// `<Frames>` child goes straight to the instantiator via `LoadChildFrames 0x76a060`. So the box
/// is a real frame in the reference and must be one here.
///
/// Its click also DROPS THE SELECTION before re-filtering: the roster is about to be re-ordered,
/// so index 7 will not be the member index 7 was.
@@ -281,7 +281,7 @@ fn the_bar_takes_the_fraction_and_the_faction_colour() {
let _data = benilla_formats::wow_data_or_skip!();
let s = shown_honor_page();
// Against the binding's own arithmetic, not against `191/255`. The client MULTIPLIES by the
// f32 nearest 1/255 (`0x3B808081`, wow-re `honor-panel-law.md` `0x51aace`) rather than
// f32 nearest 1/255 (`0x3B808081`, `0x51aace`) rather than
// dividing, and the two answers differ in the eighth decimal — a tolerance loose enough to
// accept both would stop pinning the correction the moment it was made.
let want = 191.0 * f64::from(f32::from_bits(0x3B80_8081));
+10 -10
View File
@@ -262,7 +262,7 @@ pub(super) fn feed_ui_input(
capture.typing = script.has_keyboard_focus();
// The per-key half is this frame's alone (a frame's existence gate ate THIS key).
capture.consumed.clear();
// ── The alt-arrow exemption (wow-re `ignorearrows-alt-arrow-gate.md`, §5 VERIFIED) ─────────
// ── The alt-arrow exemption ────────────────────────────────────────────────────────────────
// A focused EditBox in alt-arrow mode (`ignoreArrows` in XML, `SetAltArrowKeyMode` in Lua)
// does NOT consume LEFT/UP/RIGHT/DOWN unless ALT is held: the reference's handler returns 0
// at `0x77b1c4` and the strata walk carries the key down to `CGWorldFrame`, which runs its
@@ -310,7 +310,7 @@ pub(super) fn feed_ui_input(
// `frame_key_input` walks in the engine's own order and declines at a focused box, so the
// box's editing keys can never be stolen by a frame below it; a `true` here means a frame
// consumed the key, which suppresses BOTH the chord below and the key's binding (the
// reference's existence gate, wow-re `frame-key-script-delivery.md` §3).
// reference's existence gate, `0x76b7d0`).
let frame_named = match ev.key_code {
KeyCode::Backspace => Some("BACKSPACE"),
KeyCode::Delete => Some("DELETE"),
@@ -333,12 +333,12 @@ pub(super) fn feed_ui_input(
}
// EVERY OTHER KEY reaches a keyboard frame by name too. The reference's key-down walk
// carries the whole key table — its `arg1` comes from the *same* table the keybinding
// chord uses (wow-re `frame-key-script-delivery.md` §4.2), which is [`chord::key_token`]
// here — and the frame's gate is EXISTENCE, not handling: a shown keyboard frame with an
// `OnKeyDown` swallows the key whatever its script does with it (§3, §3.1). This host used
// to feed only the ten names above, so `CinematicFrame` — fullscreen, keyboard-enabled, an
// `OnKeyDown` that answers ESCAPE — consumed ESC and let W straight through, and the player
// walked around underneath their own intro cinematic.
// chord uses (`0x4b66b0`), which is [`chord::key_token`] here — and the frame's gate is
// EXISTENCE, not handling: a shown keyboard frame with an `OnKeyDown` swallows the key
// whatever its script does with it (`0x76b7d0`, `0x76ba25`). This host used to feed only
// the ten names above, so `CinematicFrame` — fullscreen, keyboard-enabled, an `OnKeyDown`
// that answers ESCAPE — consumed ESC and let W straight through, and the player walked
// around underneath their own intro cinematic.
//
// The reference's own Lua is the proof this is the law and not an over-reading: that
// handler has to call `RunBinding("SCREENSHOT")` **by hand** to get one key back. It would
@@ -391,8 +391,8 @@ pub(super) fn feed_ui_input(
// because `arrows_fall_through` exempts exactly these four from the typing gate.
keymap::Chord::Edit(_) => {}
// The clipboard trio needs the OS pasteboard, so it resolves here against the held
// [`HostClipboard`]: copy/cut pull the selection out of the box (RF-0082 §4
// `0x77e1d0` — selection required; a password box yields its mask run, never the
// [`HostClipboard`]: copy/cut pull the selection out of the box
// (`0x77e1d0` — selection required; a password box yields its mask run, never the
// real text), paste sanitizes+inserts.
keymap::Chord::Copy => {
if let Some(text) = script.editbox_copy() {
@@ -244,8 +244,8 @@ fn inspect_unit_refuses_out_of_range() {
/// The two verified thresholds and their **different operators** — `CanInspect` refuses only on a
/// strict `100.0 < d²` (so exactly 100.0 is IN range), while `CheckInteractDistance` admits only on
/// a strict `d² < table[type-1]` (so exactly 100.0 is OUT). That asymmetry is the binary's own
/// (wow-re §5: `test ah,0x41; jne` vs `test ah,0x5; jp`), and it is the kind of detail a rewrite
/// silently normalizes — hence a test on the boundary itself.
/// (`test ah,0x41; jne` at `0x48a28b` vs `test ah,0x5; jp` at `0x48bb0a`), and it is the kind of
/// detail a rewrite silently normalizes — hence a test on the boundary itself.
#[test]
fn range_predicates_transcribe_the_verified_thresholds() {
let _data = benilla_formats::wow_data_or_skip!();
@@ -280,7 +280,7 @@ fn range_predicates_transcribe_the_verified_thresholds() {
"the same distance is out of range for the 10-yard type"
);
// **A token the object manager holds no unit for answers nil — the null-object arm** (report
// B316, wow-re `dist2-null-unit-arm.md` VERIFIED). This is the party member outside the local
// B316, `0x48babe`). This is the party member outside the local
// area: the roster wire gives them a GUID, so the token resolves, and the object lookup then
// misses silently. It used to answer 1, which lit every distance row for exactly the member
// who was furthest away.
@@ -533,7 +533,7 @@ fn the_active_tab_is_inert_and_toggle_inspect_closes() {
/// reference's own behaviour** (decision 1834 settled what 1832 had to leave open).
///
/// `InspectFrame_OnHide` sets `this.unit = nil`, so a re-open runs `InspectFrame_OnShow` with no
/// token. wow-re censused the whole binding table: `SetPortraitTexture 0x519ef0` gates its unit
/// token. In the reference, `SetPortraitTexture 0x519ef0` gates its unit
/// argument at `0x519fb4` with `lua_isstring` and raises `Usage: SetPortraitTexture(texture,
/// "unit")`, and `luaL_error` does not return — so the reference raises here too, on its own
/// shipped code. The widget clear sits six instructions PAST the gate, so the portrait is not even
@@ -544,7 +544,7 @@ fn the_active_tab_is_inert_and_toggle_inspect_closes() {
/// abandons the handler the same way. So: the window shows, the portrait and name do not update,
/// and there is exactly one error — which is what this now asserts.
///
/// One correction from that census worth keeping: stock 1.12 ships **no** inspect keybind (0 of 234
/// One correction worth keeping: stock 1.12 ships **no** inspect keybind (0 of 234
/// `Binding name=` entries) and no caller of `ToggleInspect` outside the addon's own tab buttons,
/// which are children of the hidden `InspectFrame`. So on the stock UI's own paths this is
/// unreachable — only an addon or a macro gets here. It is still ours to reproduce.
@@ -550,8 +550,8 @@ fn the_wheel_bubbles_from_the_rows_and_the_bar_rides_the_gutter() {
// `FauxScrollFrameTemplate` carries a real `<ScrollChild>` which `FauxScrollFrame_Update` sizes
// to `numItems * valueStep` (~5275px with every section open), our old template had none, and
// the range is measured by unioning the scroll child's whole SUBTREE — the reference's own
// `0x786f80` recursion, which "re-enters itself for each shown child frame" with no clip or
// ScrollFrame exception (wow-re `system/ui/ui.md`, decision 1338). The reference would measure
// `0x786f80` recursion, which re-enters itself for each shown child frame with no clip or
// ScrollFrame exception (decision 1338). The reference would measure
// the same; it simply never nests a faux list inside a real-scroll page, which our options
// window is alone in doing.
//
+12 -21
View File
@@ -46,8 +46,7 @@ pub(crate) fn setup_script(world: &mut World) {
/// with `_G`, and the native `CSimpleFont` registry with the frame-script owner (`0x7839c0`, torn
/// down at `0x490c97`). Because [`end_ui_session`] routes every login through here, every session's
/// VM gets the registry and every `inherits="GameFontNormal"` in FrameXML resolves. A design that
/// dropped the VM and skipped index 0 on the in-game load would fail exactly there — verified in
/// wow-re's §5 for this change (2277).
/// dropped the VM and skipped index 0 on the in-game load would fail exactly there (2277).
fn install_boot_vm(world: &mut World) {
let mut script = match UiScript::new() {
Ok(s) => s,
@@ -73,7 +72,7 @@ fn install_boot_vm(world: &mut World) {
/// it where the process already is, and [`UiClock`] is re-anchored to match in the same breath.
///
/// The reference's `GetTime` (`0x515ea0`) reads `KERNEL32!GetTickCount` and scales by 0.001 (the
/// thunk `0x42c010` → `0x42b790`, wow-re's `core` boundary row): an OS clock, with no relationship
/// thunk `0x42c010` → `0x42b790`): an OS clock, with no relationship
/// to the Lua VM, that cannot restart. Ours is a Lua global the VM owns, and since 1290/1291 the VM
/// is destroyed and rebuilt at every logout/login and every `ReloadUI` — so without this seed the
/// clock went back to zero on each of those, and every host value already converted onto it
@@ -277,8 +276,7 @@ fn unpark_boot_vm(world: &mut World) {
/// 2226). It said `UI_Init 0x48fbf0` fires the world-enter cascade from inside itself, so a UI-less
/// client never sees a unit event; that is 1348's reading and the bytes refute it, because the
/// cascade call at `0x490168` is gated on an active-player GUID that is still 0/0 on a fresh login
/// (see [`finish_ui_init`]). The conclusion survives, on four independent structural grounds, all
/// re-verified in wow-5875-re:
/// (see [`finish_ui_init`]). The conclusion survives, on four independent structural grounds:
///
/// - World entry is a **category-5** callee (`0x420d63`), dispatched after the same iteration's
/// **category-6** inbound drain (`0x420d55`) has already returned.
@@ -775,8 +773,7 @@ pub(crate) fn seat_from_roster(
// lua_pushnil`. `UnitLevel 0x517fc0` likewise carries no `"player"` compare at all and
// reaches its total-miss arm `0x51813e push 0; push 0` — the **number 0**, not nil. The
// two misses are deliberately asymmetric and a client that modelled "no data" uniformly
// would be wrong on one of them (wow-re `ui/scratch/unit-verbs-before-player-object.md`
// §2-§3; it also corrected that repo's own `main`, which had published the opposite).
// would be wrong on one of them.
//
// This seat is no longer what makes the player *answerable* — the record above is — so it
// no longer has to claim a unit exists in order to deliver a name. What it still carries
@@ -866,20 +863,17 @@ pub(crate) fn arm_leaving_world_on_self_create(
}
}
/// **The UI shutdown, in the reference's own order** — `0x490bd0`, whose ordered tail wow-5875-re
/// carves as (`system/ui/ui.md`):
/// **The UI shutdown, in the reference's own order** — `0x490bd0`, whose ordered tail is:
///
/// > `PLAYER_LEAVING_WORLD` (273) → **`PLAYER_LOGOUT`** (271, `0x490c2a`) → `layout-cache.txt` →
/// > **the flat saved file** (`0x490c7e`) → **the per-addon files** (`0x490c83`) → `AddOns.txt`
/// > (`0x490c88`) → destroy the frame-script owner (`0x490c97`) → nil all 216 C bindings out of
/// > `_G` (`0x490cba` → `0x490ce0`)
///
/// **The last step is not "destroy the Lua state"** — which is what three of wow-re's own notes
/// said, until this client's teardown made the question load-bearing and a §5 cross-check settled
/// it (`system/ui/scratch/lua-state-lifecycle.md`; 2277). `0x490c97` is the frame-script owner's
/// **The last step is not "destroy the Lua state"** (2277). `0x490c97` is the frame-script owner's
/// scalar-deleting destructor — the widget tree and the native virtual-font registry — and the Lua
/// state outlives it. The state is closed and re-opened at `0x703b80`, which reaches `InitLua` by a
/// **tail-`jmp`** (`0x703b8e`) rather than a call, which is exactly why a call-census missed it.
/// **tail-`jmp`** (`0x703b8e`) rather than a call, which is exactly why a call-census misses it.
/// Its three callers are `UI_Init 0x48fbf0` (at its *head*), `ShutdownGame 0x491180`, and the glue
/// builder `0x46a7b0` — so a logout/login cycle runs through four distinct states, and the glue
/// screen gets its own.
@@ -1090,9 +1084,7 @@ pub(crate) fn shutdown_on_exit(
/// The UI-init sequence's ordered tail, once every file — ours and every addon's — has loaded:
/// the saved-variables chunk and `VARIABLES_LOADED`, then `PLAYER_LOGIN`.
///
/// **The order is the reference's**, byte-verified in wow-5875-re (`system/ui/ui.md`, and the
/// cascade in `system/ui/scratch/mail-pending-countdown.md`). Inside `UI_Init 0x48fbf0`, in
/// straight-line address order:
/// **The order is the reference's**. Inside `UI_Init 0x48fbf0`, in straight-line address order:
///
/// | | |
/// |---|---|
@@ -1106,7 +1098,7 @@ pub(crate) fn shutdown_on_exit(
/// and a window that waits on `PLAYER_LOGIN` expects both — so it is worth being able to assert.
///
/// **The third row does not run on a fresh login, and this doc used to say it did** (corrected
/// 2226; the claim came from 1348 and was re-checked in wow-5875-re against the bytes). The call
/// 2226; the claim came from 1348 and was re-checked against the bytes). The call
/// at `0x490168` is gated three instructions earlier:
///
/// ```text
@@ -1126,8 +1118,8 @@ pub(crate) fn shutdown_on_exit(
/// resolved.** Our `finish_ui_init` fires the cascade inside the load, which matches the reload
/// case and not the fresh-login one; whether to split those is its own change with its own
/// measurements, and 2226 (which was about the VM's identity, not the cascade's timing) is not it.
/// wow-re's own `DEFERRED:` entry — which of `0x4908c0`'s two data-dependent entries fires first on
/// a real login — is still open, so the target shape is not yet known.
/// Which of `0x4908c0`'s two data-dependent entries fires first on a real login is still open, so
/// the target shape is not yet known.
///
/// **`PLAYER_LOGIN` is the conditional one; `PLAYER_ENTERING_WORLD` is not.** The cascade fires
/// the former only when `[0xb4e260]` is set, and only the FrameXML-loader path sets it, clearing
@@ -1150,8 +1142,7 @@ pub(crate) fn finish_ui_load(script: &mut UiScript) {
/// The reference's login is `FrameXML → addons + ADDON_LOADED (0x4900a3) → VARIABLES_LOADED
/// (0x4900b2) → the chat-cache reader's burst (0x4900d6, firing synchronously through the
/// register-or-fire-now trampoline 0x498a20) → PLAYER_LOGIN (0x490959) → PLAYER_ENTERING_WORLD
/// (0x49096a)` — byte-derived in wow-re `system/ui/scratch/login-chat-colour-pipeline.md`, §5
/// cross-checked. 2119 put the restore ahead of `VARIABLES_LOADED`, which is one step too early:
/// (0x49096a)`. 2119 put the restore ahead of `VARIABLES_LOADED`, which is one step too early:
/// an addon reading its chat colours out of a `VARIABLES_LOADED` handler would see the file's
/// values where the reference shows it the boot ones.
///
@@ -889,7 +889,7 @@ fn shipped_loot_frame_hands_a_master_row_to_a_candidate() {
);
}
/// **The raid arm of the master-loot menu** (decision 1675) — the half the wow-re §5 corrected.
/// **The raid arm of the master-loot menu** (decision 1675).
///
/// In a raid the candidate array is not the wire order: the client files each candidate into its
/// own subgroup's five-slot block, leaving holes. `GroupLootDropDown_Initialize` reads those holes
@@ -1185,7 +1185,7 @@ fn loot_row_awaiting_its_template_opens_clean() {
// It is a REOPEN and not a repaint on purpose. The app fires `LOOT_UPDATE` when an open
// window's content changes, and `LOOT_UPDATE` is not a 1.12 event — it appears nowhere in the
// reference's FrameXML and `LootFrame_OnLoad` does not register it — so nothing in the stock
// file listens. What the real client does when a template arrives mid-window is out at wow-re
// file listens. What the real client does when a template arrives mid-window is still open
// (the arrival callback `0x4c2ac0` is recorded as clearing the pending flag and nothing else);
// this test deliberately asserts only what is settled.
s.fire_event("LOOT_CLOSED", vec![]);
@@ -1221,8 +1221,7 @@ fn loot_row_awaiting_its_template_opens_clean() {
/// `<PushedTexture>` and no `<HighlightTexture>` at all: no Quickslot border on the icons, and
/// nothing to light under the mouse. The gate is wrong about the reference: `CLootButton`'s
/// geometry vtable differs from `CSimpleButton`'s in exactly one slot — the destructor thunk — and
/// `LoadXML` is not it, so `0x7788c0` parses a `<LootButton>` element verbatim (wow-re
/// `ui/scratch/lootbutton-widget-type.md` §4).
/// `LoadXML` is not it, so `0x7788c0` parses a `<LootButton>` element verbatim.
///
/// Asserted through the ENGINE's hover path rather than off the state: what a player sees is the
/// emitted quad, and the quad is what was missing.
@@ -641,8 +641,8 @@ fn the_tab_highlight_is_exactly_its_tab() {
// **The two tabs carry DIFFERENT highlight widths, and that is the reference's,
// not a loader bug.** Exactly one `<OnLoad>` runs — the most-derived one; the
// template's is installed and then *destroyed*, because `SetScript` releases the
// slot's single ref before it even looks at the new body (wow-re
// `template-onload-replacement-law.md`). So a tab gets whichever formula ITS OWN
// slot's single ref before it even looks at the new body (`0x7025ec`). So a tab
// gets whichever formula ITS OWN
// body ends with:
//
// * tab 2's body is just `PanelTemplates_TabResize(-15, nil, nil, 150)`, whose
+2 -3
View File
@@ -256,8 +256,7 @@ end
/// Bindings.xml, the AddOns, the saved variables and the world-enter cascade it calls at
/// `0x490168` (`PLAYER_LOGIN`), so both of its callers (login `0x48f681` and `/reloadui`
/// `0x495669`) load without a sound. The only reader of that depth is `PlaySoundByName
/// 0x458030`, which drops the call outright (wow-re
/// `system/ui/scratch/framexml-load-sound-suppression.md`).
/// 0x458030`, which drops the call outright.
///
/// This is the mechanism, not a workaround for one noisy handler: stock `TargetFrame_OnHide`
/// really does fire at load (the frame ships shown and its OnLoad hides it) and really does call
@@ -380,7 +379,7 @@ pub(crate) fn load_font_registry(script: &UiScript) -> Vec<String> {
/// The reference does the same at `CGGameUI::Initialize 0x48fbf0`, reached only from world entry
/// (`0x401570` ← `0x46c236`), and loads its addons from that same function (`0x4900a3` →
/// `0x51f600`); its glue screens run GlueXML with their own `GlueFonts.xml` registry, which is why
/// the reference has no equivalent of our shared-atlas coupling (wow-5875-re, 1051).
/// the reference has no equivalent of our shared-atlas coupling (1051).
///
/// Addons load **after** the built-in interface, not interleaved with it: an addon may reference
/// our templates and globals (that is the point of 1178's seam), and nothing of ours may depend on
@@ -305,7 +305,7 @@ fn a_click_on_the_minimap_parks_a_centre_relative_ping_request() {
}
/// `Minimap:GetPingPosition()` answers **two numbers always** (the reference recomputes them
/// from statics nothing clears — wow-re `minimap-ping-law.md`), and the stock
/// from statics nothing clears — `0x4eefd0`), and the stock
/// `Minimap_OnUpdate` leans on exactly that: while its 5 s timer runs it multiplies the answer
/// with no nil test. The pair is the app's last publish, `(0, 0)` before any.
#[test]
@@ -448,7 +448,7 @@ fn the_meeting_stone_icon_follows_the_queue_across_meetingstone_changed() {
/// **The ping's pixels are the stock `<Model>`'s own file** (decision 2008): shown by
/// `MINIMAP_PING` with the file's facts landed, the extract publishes ONE tile request for the
/// pane — at its device size, the render law's unit ladder, and the composite's rect/key/alpha
/// pane — at its device size, the reference's unit ladder, and the composite's rect/key/alpha
/// — and pushes no quad of its own. Once the renderer has handed back a cell, the cell draws as
/// one premultiplied quad over the pane's rect **on the very next frame, with nothing else in
/// the interface moving** (decision 2023): the composite is the renderer's per-frame output in
+7 -7
View File
@@ -180,7 +180,7 @@ pub(crate) struct PlayerUiHover(pub(crate) Option<u32>);
/// **Who owns this frame's keys** — written by [`feed_ui_input`], read by the binding dispatch and the
/// dev keyboard readers, all of which are ordered after `UiInput` so they see this frame's value and
/// not last frame's. The app-side twin of the client's `DAT_00cf4dc8 != 0` gate (RF-0082 §1).
/// not last frame's. The app-side twin of the client's `DAT_00cf4dc8 != 0` gate.
///
/// **Two fields because the reference has two mechanisms, and collapsing them into one boolean was
/// bug 2196.** A focused EditBox swallows *every* key for as long as it holds focus
@@ -189,7 +189,7 @@ pub(crate) struct PlayerUiHover(pub(crate) Option<u32>);
/// and nothing more. Neither releases anything already held: the only things that clear the
/// reference's direction bits are the OS **window deactivate** (`0x514490`, whose sole caller
/// `0x493058` hangs off the WM_ACTIVATE callback slot) and the world-enter cascade (`0x5144c0`).
/// A UI focus change clears nothing — wow-re `rf79-autorun-cancel-set.md`, and the reason holding
/// A UI focus change clears nothing — the reason holding
/// W keeps you running while you type or read the map. See `decisions/2196`.
#[derive(Resource, Default)]
pub(crate) struct UiKeyboardCapture {
@@ -197,7 +197,7 @@ pub(crate) struct UiKeyboardCapture {
/// the alt-arrow one below). Whole-frame, because there is at most one focused box.
pub(crate) typing: bool,
/// The keys a shown keyboard-enabled **frame** consumed this frame (decision 1319's existence
/// gate, wow-re `frame-key-script-delivery.md` §3) — `WorldMapFrame`'s fullscreen `OnKeyDown`,
/// gate, `0x76b7d0`) — `WorldMapFrame`'s fullscreen `OnKeyDown`,
/// `CinematicFrame`, the stack-split spinner. **Per key, not per frame**: the map eating its
/// own `M` must not also suppress an unrelated binding, and — the bug this list exists for —
/// must not be mistaken for a text box taking focus.
@@ -209,7 +209,7 @@ pub(crate) struct UiKeyboardCapture {
/// (`ignoreArrows` / `SetAltArrowKeyMode`) and ALT is not held, so the reference's own key
/// handler declines LEFT/UP/RIGHT/DOWN at `0x77b1c4` and the strata walk carries them down to
/// `CGWorldFrame`, which runs their bindings. That is what lets you turn while the chat box
/// has focus (wow-re `ignorearrows-alt-arrow-gate.md`, §5 VERIFIED).
/// has focus.
///
/// It is a whole-frame flag rather than a per-key one because both of its terms are:
/// there is at most one focused box, and ALT is read off the same modifier mirror. Only the
@@ -384,8 +384,8 @@ impl Default for UiScaleCvar {
/// director's eye — the taste call this dial exists for).
///
/// **It also happens to be what the reference itself lands on above ~853 px tall**, which we did
/// not know when it was chosen (wow-re `system/ui/scratch/modelframe-uiscale-law.md`, out of the
/// autocast-shine thread): `0x492f70` computes `max((H > 768) ? 768/H : 1.0, 0.9)` and is called
/// not know when it was chosen: `0x492f70` computes
/// `max((H > 768) ? 768/H : 1.0, 0.9)` and is called
/// **both** on a mode set and from the `useUiScale`-OFF leg (`0x4908ad`) — and OFF is the shipped
/// default (`0x8430c0` = `"0"`). So the reference is 1.0 at 768 and below, 0.96 at 1280×800, and
/// **0.9 at 1080p and 1440p**. Our flat 0.9 therefore agrees with it on every modern window and
@@ -933,7 +933,7 @@ impl benilla_ui::script::TextMeasure for FixedWidthFont {
#[cfg(test)]
pub(crate) mod test_ui;
/// The chat loader's two login events (wow-re chat-cache-grammar.md §8; `ui_chat::settings`):
/// The chat loader's two login events (`0x498a60`; `ui_chat::settings`):
/// `UPDATE_CHAT_WINDOWS` once, then `UPDATE_CHAT_COLOR` for every registry entry. The reference's
/// `FloatingChatFrame_Update` docks, hides and colours the windows on the first and
/// `ChatFrame_OnEvent` fills `ChatTypeInfo` from the second, so a test VM that loads the
@@ -265,7 +265,8 @@ fn shipped_multibars_drive_end_to_end() {
/// The stance bar (StanceBar.xml) through the REAL shipped XML: hidden at zero forms, sized to
/// the pushed list (buttons past numForms hide), the checked ring on the active form, the 0.4
/// grey on a not-castable one, a click queuing the form's spell id, and an emptied push hiding
/// the whole frame again — the wow-re shapeshift-bar-api mechanism driven end to end.
/// the whole frame again — the reference's form list (`0xb71100`) and its four bindings driven
/// end to end.
#[test]
fn shipped_stance_bar_drives_end_to_end() {
benilla_formats::wow_data_or_skip!();
@@ -636,7 +637,7 @@ fn the_vertical_multibars_exist_hidden_on_the_reference_pages() {
/// **Every extra bar is down until its own toggle says otherwise, and `MultiBarLeft` needs two.**
///
/// The four bits of `PLAYER_FIELD_BYTES` byte 2 map to bars 1-4 at the FrameXML layer (the binary
/// is bar-agnostic — wow-5875-re `system/ui/scratch/action-bar-toggles.md`), and a fresh
/// is bar-agnostic — `SetActionBarToggles 0x4e76e0` packs four unnamed bits), and a fresh
/// character's byte is 0, which is the whole of "off by default": nothing here fakes a default,
/// the bars simply have nothing telling them to show.
///
@@ -966,8 +967,8 @@ fn the_grid_option_holds_the_extra_bars_empty_wells_open() {
/// DIVERGENCES, chosen in 1782. In stock 1.12.1 the dim is fanned to every button and then never
/// cleared by anything: `SetNormalTexture(path)` reuses the region and writes only its texture
/// handle, and a 3-argument `SetVertexColor` is `SetVertexColor(r, g, b, currentAlpha)` — so the
/// first spell anyone picks up leaves every border at alpha 0x80 for the session (wow-re
/// `button-state-texture-path-setter.md` §2/§7, byte-verified). That is a stuck state keyed to
/// first spell anyone picks up leaves every border at alpha 0x80 for the session (the reuse leg
/// `0x778f10`, the alpha-keeping `SetVertexColor` `0x79abd0`). That is a stuck state keyed to
/// nothing the player can see. We take the look the dim was written for and let go of it with the
/// payload, which is what the last two assertions here pin.
#[test]
@@ -1097,10 +1098,10 @@ fn a_held_payload_ghosts_the_empty_wells_it_opens() {
);
// What happens to that ring NEXT is decision 1782's open question, deliberately not asserted:
// the reference's `SetVertexColor(1, 1, 1)` in `ActionButton_UpdateUsable` keeps the alpha
// already on the region (byte-pinned, wow-re button-state-texture-path-setter.md §7 — the
// "stuck dim" a 1.12 player sees after a drag), while this engine's three-argument call still
// resets it to 1.0 until the director calls 1782. A test that pinned either outcome would be
// enshrining a divergence as fidelity.
// already on the region (`SetVertexColor` `0x79abd0` — the "stuck dim" a 1.12 player sees
// after a drag), while this engine's three-argument call still resets it to 1.0 until the
// director calls 1782. A test that pinned either outcome would be enshrining a divergence as
// fidelity.
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
}
@@ -1108,9 +1109,9 @@ fn a_held_payload_ghosts_the_empty_wells_it_opens() {
///
/// The whole round trip, over the REAL bindings (`benilla_ui::script::action_bar_toggles`): the
/// row's setter writes a global, re-derives the bars and posts `CMSG_SET_ACTIONBAR_TOGGLES` with
/// the WHOLE byte — bits `0x01/0x02/0x04/0x08`, verified against the 1.12.1 binary (wow-5875-re
/// `417c2d31`). Coming back, the server's descriptor push is the only thing that moves the getter,
/// and `UIParent.xml`'s `PLAYER_ENTERING_WORLD` arm reads it exactly once as the seed.
/// the WHOLE byte — bits `0x01/0x02/0x04/0x08` (`SetActionBarToggles 0x4e76e0`). Coming back,
/// the server's descriptor push is the only thing that moves the getter, and `UIParent.xml`'s
/// `PLAYER_ENTERING_WORLD` arm reads it exactly once as the seed.
///
/// Every `Set` is one packet, deliberately (the binding gates nothing), which is why the drain is a
/// list and each step below checks the packet it just caused.
@@ -1745,7 +1746,7 @@ fn the_middle_strip_tiles_along_its_length_only() {
/// alone, both end caps down.
///
/// The reference fires `UPDATE_SHAPESHIFT_FORMS` for exactly one thing: the form LIST changed
/// (learn / unlearn / rank — wow-re `shapeshift-bar-api.md`, the fires at `0x4b28ff`/`0x4b2e43`).
/// (learn / unlearn / rank — the fires at `0x4b28ff`/`0x4b2e43`).
/// Our feed fired it for any change in the pushed view — a stance switch, a castable flip, the
/// shared 1 s category cooldown arming and then EXPIRING. Every fire runs the stock
/// `ShapeshiftBar_Update` (BonusActionBarFrame.lua l.170): `ShapeshiftBarMiddle:Show()`
@@ -4249,8 +4249,8 @@ fn the_max_camera_distance_slider_stores_a_factor_and_reads_out_yards() {
/// Controls-page dropdown, and the setting that decides whether the camera returns to behind the
/// character at all. What is pinned here is the trap: the reference's own dropdown writes `1/2/3`,
/// but the ENGINE's tables are indexed `0 = Never · 1 = Smart · 2 = Always`, and `3` is not a style
/// — the validator accepts it while the terrain-tilt consumer indexes off the end of its table
/// (wow-re `camera-smooth-style.md` §2/§4). So our entries carry the engine's numbers in the
/// — the validator (`0x50c060`) accepts it while the terrain-tilt consumer (`0x50dbc0`) indexes
/// off the end of its table. So our entries carry the engine's numbers in the
/// reference's display order, a stray `3` still reads as Never rather than as the numerically
/// nearest "Always", and the plate follows the SELECTION the way that dropdown's own does.
#[test]
@@ -1271,8 +1271,8 @@ fn a_dead_player_opens_only_whiledead_windows() {
s.eval::<bool>("return GetLeftFrame() == nil").unwrap(),
"the refused window took no slot"
);
// ...and the refusal is heard: NotWhileDeadError (the binary's 0x48d340 — push 0x7e, wow-re
// cross-checked) queued the catalog row's key for the app to resolve and toast.
// ...and the refusal is heard: NotWhileDeadError (the binary's 0x48d340 — push 0x7e) queued
// the catalog row's key for the app to resolve and toast.
assert_eq!(
s.take_ui_errors(),
vec!["ERR_PLAYER_DEAD"],
@@ -2,7 +2,7 @@
//! `PetFrame` over synthetic `"pet"` snapshots and the events the app's feed fires.
//!
//! The frame's whole job is to be right about **which unit an event names**, and that is what most
//! of these test: `UNIT_PET` names the OWNER (`arg1 == "player"`, wow-re §9), every other `UNIT_*`
//! of these test: `UNIT_PET` names the OWNER (`arg1 == "player"`, `0x4bc84f`), every other `UNIT_*`
//! names the pet itself, and a frame that mixes the two repaints off the player's health.
use benilla_ui::script::{
@@ -541,7 +541,7 @@ fn the_happiness_icon_shows_per_bucket_and_hides_for_a_non_hunter_pet() {
/// **Bucket 0 keeps the icon up.** The reference hides on `not happiness`, and `0` is truthy in
/// Lua — so a client that folded bucket 0 into nil would hide a frame the reference shows. This is
/// the trap wow-re calls out by name, tested where it would actually bite: in the frame.
/// the `GetPetHappiness 0x4be900` trap, tested where it would actually bite: in the frame.
#[test]
fn happiness_bucket_zero_keeps_the_icon_showing() {
benilla_formats::wow_data_or_skip!();
@@ -484,11 +484,10 @@ fn hovering_the_diet_icon_lists_what_the_pet_eats() {
// **The pane's scene draws AFTER the icon, and that is the reference's own order.**
// `PetPaperDollPetInfo` is a sibling frame of `PetModelFrame` (same parent, so the same
// `(strata, level)` bucket) and its happiness art is a BACKGROUND texture on it; a model's
// scene drains out of that bucket's ARTWORK batch, last (wow-re
// `ui/scratch/model-frame-draw-order.md`: `0x76d160` registers the render callback only for
// layer 2, and `0x76fb00` drains quads, then text, then callbacks). The icon stays visible
// because a model pane is not an opaque quad — the bake is transparent everywhere the pet
// is not, here as in the client. This assertion is the ordering, not the pixels.
// scene drains out of that bucket's ARTWORK batch, last (`0x76d160` registers the render
// callback only for layer 2, and `0x76fb00` drains quads, then text, then callbacks). The icon
// stays visible because a model pane is not an opaque quad — the bake is transparent everywhere
// the pet is not, here as in the client. This assertion is the ordering, not the pixels.
assert!(
pane.z > icon.z,
"the pane's scene draws out of the ARTWORK batch, after a sibling's BACKGROUND art \
@@ -168,7 +168,8 @@ fn shipped_questlog_frame_drives_end_to_end() {
);
// Row 1 carries entry 1's title (indented), the count line reads "Quests: |cffffffff8/20|r", and nothing having been
// selected, the first non-header entry auto-selects (pin §2's SetFirstValidSelection).
// selected, the first non-header entry auto-selects (`QuestLog_SetFirstValidSelection`,
// `QuestLogFrame.lua:295`).
assert!(s
.eval::<String>("return QuestLogTitle1NormalText:GetText()")
.unwrap()
@@ -474,7 +475,7 @@ fn shift_click_toggles_the_watch_checkbox_and_the_tracker_hud() {
/// Shift-clicking a quest with zero objectives (ref `QUEST_WATCH_NO_OBJECTIVES`) and shift-clicking
/// past `GetNumQuestWatches() >= 5` (ref `QUEST_WATCH_TOO_MANY`) both refuse the watch and put the
/// ref's red line on the errors frame through its own `UIErrorsFrame:AddMessage`; the click still
/// selects the row exactly like any other click (pin §5).
/// selects the row exactly like any other click.
#[test]
fn watch_guards_no_op_without_erroring() {
let _data = benilla_formats::wow_data_or_skip!();
@@ -587,7 +588,7 @@ fn watch_guards_no_op_without_erroring() {
/// The auto quest watch (ref `AUTO_QUEST_WATCH` default-on, QuestLogFrame.lua:702-786 —
/// "Quests are automatically watched for 5 minutes when you achieve a quest objective."): the
/// engine's per-quest `BENILLA_QUEST_PROGRESS(logIndex)` arms a 300 s timed watch (the native
/// QUEST_WATCH_UPDATE carries the WATCH-LIST index per the §5-verified byte law — repaint only,
/// QUEST_WATCH_UPDATE carries the WATCH-LIST index from `0x4df880` — repaint only,
/// the shipped 1.12 auto-watch chain being broken at that seam), fresh progress
/// re-arms it, and expiry (the watch frame's OnUpdate) unwatches and hides the empty HUD.
#[test]
@@ -1234,7 +1235,7 @@ fn wheel_over_the_detail_pane_changes_vertical_scroll() {
);
}
/// Pin §3/§5's doNotScroll asymmetry (QUESTLOG-PIN.md, `QuestLogFrame.lua:348/455-457`): a manual
/// The reference's doNotScroll asymmetry (`QuestLogFrame.lua:348/455-457`): a manual
/// reselect always snaps the detail pane's scroll back to the top, but the `QUEST_LOG_UPDATE`
/// data-refresh path must NOT — a quest updating while already being read shouldn't yank the
/// reader's scroll position out from under them.
@@ -1295,7 +1296,7 @@ fn selection_change_resets_detail_scroll_but_a_quest_log_update_refresh_does_not
s.eval::<f32>("return QuestLogDetailScrollFrame:GetVerticalScroll()")
.unwrap(),
10.0,
"QUEST_LOG_UPDATE's data-refresh path must not yank the scroll position (pin §5)"
"QUEST_LOG_UPDATE's data-refresh path must not yank the scroll position"
);
// A manual reselect (the row-click path — no doNotScroll) DOES reset it.
@@ -1858,7 +1859,7 @@ fn share_quest_click_queues_the_selected_quests_id() {
/// It is correct, and the reading was wrong about our own engine: the title FontString carries a
/// DECLARED height (`<ButtonText …><AbsDimension x="0" y="10"/>`), which arms both overflow
/// regimes — the line stack and the height-gated ellipsis — so the paint is one truncated line,
/// which is the reference's own result (wow-re `fontstring-overflow.md`: the ellipsis gate is
/// which is the reference's own result (`0x771ec0`'s ellipsis gate is
/// `boxW > 0 && boxH > 0`, and only an AUTO-height FontString escapes it). The render half is
/// pinned on the real font in `ui_text::layout`'s
/// `a_capped_quest_log_title_ellipsizes_on_one_line`; this is the geometry half — that the row
@@ -259,7 +259,7 @@ mod tests {
/// Strip what is not code, before any call census over a FrameXML file.
///
/// Both `Name(` and `:Name(` counted calls inside comments until decision 1800: the round that
/// built `PickupMerchantItem` also asked wow-re for `ShowInventorySellCursor`, which
/// built `PickupMerchantItem` also asked about `ShowInventorySellCursor`, which
/// [`chain_gap_report`] had named as `PaperDollFrame.xml`'s last engine gap — and the answer
/// was that stock `PaperDollFrame.lua:754-756` has the call **commented out**, all three
/// lines. A real binding, never called, blocking a window that was not blocked.
@@ -433,7 +433,7 @@ mod tests {
// Order still decides, and it is still the whole rule — so it is asserted directly rather
// than through whichever window happens to collide this month. Two chunks, the same name,
// and the later one stands; `publish_global`'s non-overwriting rule (RF-0023) applies to
// and the later one stands; `publish_global`'s non-overwriting rule (`0x701bd0`) applies to
// FRAMES, never to a plain Lua global, and confusing the two has produced confident wrong
// diagnoses before.
s.run("function _order_probe() return 1 end").unwrap();
@@ -482,7 +482,7 @@ mod tests {
///
/// A candidate whose frame NAMES our own shipped file already owns produces failures that are
/// artefacts of the probe, not of the window. `publish_global` is deliberately non-overwriting
/// (RF-0023), so the second frame to claim a name gets a wrapper that `_G` never points at —
/// (`0x701bd0`), so the second frame to claim a name gets a wrapper that `_G` never points at —
/// and any reference body using the `getglobal(this:GetName())` idiom then reads a DIFFERENT
/// table than the `this` it just wrote to.
///
@@ -998,11 +998,11 @@ mod tests {
// wrong (1801). So this one runs the load itself — the same fresh-VM-plus-manifest
// pass the readiness probe does — and reports it in the same row.
// **A `LOAD:` line for a window we ALSO ship is suspect**, and the reason is the same
// one `chain_readiness_report` carries: `publish_global` is non-overwriting (RF-0023),
// so loading the stock file on top of our identically-named one leaves every colliding
// frame's global pointing at OUR frame while the stock file's handlers run against
// THEIRS. A field set on `this` in an OnLoad is then invisible to `getglobal(name)`,
// and the failure looks like a bug in whatever read it back.
// one `chain_readiness_report` carries: `publish_global` is non-overwriting
// (`0x701bd0`), so loading the stock file on top of our identically-named one leaves
// every colliding frame's global pointing at OUR frame while the stock file's handlers
// run against THEIRS. A field set on `this` in an OnLoad is then invisible to
// `getglobal(name)`, and the failure looks like a bug in whatever read it back.
//
// Worked example, because it cost an hour: stock `TradeFrame.xml` reported
// `MoneyFrame.xml:525: attempt to index local 'info'`. Nothing was wrong with our
@@ -1523,7 +1523,7 @@ mod tests {
(
"SkillFrame.xml",
"BuySkillTier",
"a 5875 binding (wow-re `bindings.md`: marshals and delegates to a C++ \
"a 5875 binding (`0x4d3e50`: marshals and delegates to a C++ \
method/net-send) of the pre-1.12 skill-point purchase UI. the detail bar's LearnSkillButton calls it, and \
that button shows only while `UnitCharacterPoints`'s second value or a row's \
step/rank cost is non-zero — which no 1.12 server sends. Unreachable until the \
@@ -1532,7 +1532,7 @@ mod tests {
(
"SkillFrame.xml",
"AddSkillUp",
"a 5875 binding (wow-re `bindings.md`: marshals and delegates to a C++ \
"a 5875 binding (`0x4d3c30`: marshals and delegates to a C++ \
method/net-send) of the pre-1.12 skill-point purchase UI. the detail bar's RightArrow calls it, and \
that button shows only while `UnitCharacterPoints`'s second value or a row's \
step/rank cost is non-zero — which no 1.12 server sends. Unreachable until the \
@@ -1541,7 +1541,7 @@ mod tests {
(
"SkillFrame.xml",
"RemoveSkillUp",
"a 5875 binding (wow-re `bindings.md`: marshals and delegates to a C++ \
"a 5875 binding (`0x4d3c70`: marshals and delegates to a C++ \
method/net-send) of the pre-1.12 skill-point purchase UI. the detail bar's LeftArrow calls it, and \
that button shows only while `UnitCharacterPoints`'s second value or a row's \
step/rank cost is non-zero — which no 1.12 server sends. Unreachable until the \
@@ -1593,8 +1593,8 @@ mod tests {
(
"StaticPopup.xml",
"ReplaceTradeEnchant",
"a registered 1.12 binding whose body is uncarved (wow-re `bindings.md`, structural row only); a \
wow-re orchestrator is out on it and it is built when the carve lands (1960). Reached by TRADE_REPLACE_ENCHANT's Accept, an event this engine does not fire yet.",
"a registered 1.12 binding (`0x48d330`) whose body is not yet known; it is built \
once it is (1960). Reached by TRADE_REPLACE_ENCHANT's Accept, an event this engine does not fire yet.",
),
];
@@ -2331,8 +2331,8 @@ mod tests {
"TRADE_REQUEST",
"UIParent.lua — the trade ASK dialog, and the one entry on this list that is \
UNPRODUCEABLE rather than unbuilt: the 5875 client registers the event and \
signals it from NOWHERE (a whole-image census, wow-re \
ui/scratch/incoming-trade-request-law.md §3), so StaticPopupDialogs[\"TRADE\"] is \
signals it from NOWHERE (a whole-image census: no signal site passes its id, \
`0x11d`), so StaticPopupDialogs[\"TRADE\"] is \
dead code THERE too. benilla wired the dialog up once and took it back out — \
decision 1764. Producing this would be a divergence, not a fix",
),
@@ -2362,8 +2362,8 @@ mod tests {
(
"SHOW_COMPARE_TOOLTIP",
"PaperDollFrame.lua — the second `TRADE_REQUEST` (decision 1764): event 377 is \
registered in 5875 and signalled from NOWHERE (zero fire sites in wow-re's own \
census, `merchant-compare-item-law.md` §8), so this listener is dead code THERE \
registered in 5875 and signalled from NOWHERE (zero fire sites in the whole \
image), so this listener is dead code THERE \
too. benilla fired it from 0283 until 2202, then drove the plates itself on a \
shift-held hover until 2210; both were supersets. Nothing in this engine seats a \
shopping plate now — the reference's own callers do (`MerchantFrame.xml:63-80`, \
+12 -13
View File
@@ -15,9 +15,9 @@
//! `1` — reads a `true` as "not held". So the kinds column is gated as well, wherever the table
//! says it is trustworthy (decision 2118).
//!
//! The table is wow-re's `re/audit/binding-shapes.tsv`, vendored: 1722 rows over all 82 registrar
//! tables, one per registered binding, generated and differentially tested against that repo's
//! other harvester. It carries the gate rule **per row** rather than in prose, which is the whole
//! The table is a harvest of the reference binary, vendored: 1722 rows over all 82 registrar
//! tables, one per registered binding, generated and differentially tested against a second
//! harvester. It carries the gate rule **per row** rather than in prose, which is the whole
//! reason it can be enforced instead of remembered:
//!
//! * `arity_conf = exact` — sound. This is the column to gate on.
@@ -71,9 +71,9 @@ const NOT_YET_ASSERTED: &[(&str, usize, &str)] = &[];
/// two it held did exactly that, within the day.
///
/// **It is empty.** It held `GetSendMailItem`, whose no-attachment leg the shapes table types as
/// two numbers and no recorded note gave the values of. wow-re answered it at the bytes —
/// `0x4ae590` pushes `(nil, nil, 0, 0)`, and the stray `1` we had was borrowed from
/// `GetAuctionSellItemInfo`'s empty leg — and 2129 folded the verdict back.
/// two numbers without their values. The bytes answer it — `0x4ae590` pushes `(nil, nil, 0, 0)`,
/// and the stray `1` we had was borrowed from `GetAuctionSellItemInfo`'s empty leg — and 2129
/// folded the verdict back.
const KINDS_NOT_YET_ASSERTED: &[(&str, &str, &str)] = &[];
/// The widget kind gate's shrinking list — see [`KINDS_NOT_YET_ASSERTED`].
@@ -124,12 +124,11 @@ fn rows_of_kind(kind: &str) -> Vec<Row> {
return None;
}
// **`agree` is necessary and not sufficient, and the second condition is the table's
// own too** (2150 + wow-re `scratch/binding-kinds-stack-overwrite.md`). The kinds
// come from tracing each binding's `lua_push*` calls; a row whose notes carry
// `delegated-push` has a CALLEE that pushes on its own behalf, which the trace sees
// one level too high up, so its tuple covers the binding's own ops only. `eax` is
// unaffected, so arity stays gated on every row — it is the kinds claim alone that
// narrows.
// own too** (2150). The kinds come from tracing each binding's `lua_push*` calls; a row
// whose notes carry `delegated-push` has a CALLEE that pushes on its own behalf, which
// the trace sees one level too high up, so its tuple covers the binding's own ops only.
// `eax` is unaffected, so arity stays gated on every row — it is the kinds claim alone
// that narrows.
let delegated = f.len() > 11 && f[11].contains("delegated-push");
Some(Row {
name: f[0].to_string(),
@@ -322,7 +321,7 @@ fn every_query_binding_answers_the_reference_s_return_arity() {
const BASELIB_PROBES: &[(&str, &str)] = &[
("collectgarbage", ""),
("date", ""),
// The five shipped stubs — `xor eax,eax; ret` in the image (`lua-dialect.md` §3a), so calling
// The five shipped stubs — `xor eax,eax; ret` in the image (`0x7027e0`–`0x702820`), so calling
// one is as safe here as it is there, and their `0 exact ()` rows are still worth holding.
("debugbreak", ""),
("debugdump", ""),
@@ -581,10 +581,9 @@ fn every_archive_path_a_shipped_lua_chunk_names_survives_its_own_escaping() {
/// **Every `text=` in the shipped UI is answerable against the REAL `GlobalStrings.lua`** — the
/// tripwire for the defect that put "CREATE_MACROS" across the macro window's title bar (0991).
///
/// `text=` is a global-string LOOKUP, not a literal (wow-re rf28 l.36/l.115 → `FrameScript_GetText
/// 0x703bf0`). The loader didn't do the lookup at all, so **23 key-shaped values across six
/// windows** were rendering as their own key names — and nothing anywhere said so. Two halves, both
/// needed:
/// `text=` is a global-string LOOKUP, not a literal (`FrameScript_GetText 0x703bf0`). The loader
/// didn't do the lookup at all, so **23 key-shaped values across six windows** were rendering as
/// their own key names — and nothing anywhere said so. Two halves, both needed:
///
/// - a **key-shaped** value (`SCREAMING_SNAKE`) must resolve to a non-empty string, or it reaches a
/// real screen as its own key;
@@ -1978,7 +1977,7 @@ fn the_stock_video_options_window_loads_hidden_and_owns_its_own_name() {
.unwrap(),
"…so the dropdown is left empty but ENABLED — the greying branch is the driver-quirk one, \
and inventing a 0 to reach it would be inventing a value the binary never produces \
(`IsEnabled` answers a NUMBER — wow-re `binding-shapes.tsv` 0x7800b0)"
(`IsEnabled` answers a NUMBER — `0x7800b0`)"
);
// The optional index argument is tolerated, which is the shape `SetScreenResolution` shares.
assert!(
@@ -2013,7 +2012,8 @@ fn the_stock_video_options_window_loads_hidden_and_owns_its_own_name() {
"`not hasTripleBuffering` must stay FALSE (the dead clause) while `== 1` is also false"
);
// `SetScreenResolution`'s three carved argument behaviours, and the one deliberate divergence.
// `SetScreenResolution`'s three argument behaviours in the reference, and the one deliberate
// divergence.
assert!(
s.eval::<bool>("SetScreenResolution() return GetCVar(\"gxResolution\") == \"1280x720\"")
.unwrap(),
@@ -2086,7 +2086,7 @@ fn the_stock_video_options_window_loads_hidden_and_owns_its_own_name() {
}
/// **The display-brightness pair** (decision 2182) — `GetGamma`/`SetGamma` and the Graphics page
/// row that drives them, held to the reference's own carve.
/// row that drives them, held to the reference's own behaviour.
///
/// The claim that needs an assertion rather than a comment is the **unit**: `0x4891d0` is FSUBR,
/// so `GetGamma()` is `1.0 − gamma` and `SetGamma(v)` writes `gamma := 1.0 − v`. Read as
@@ -2099,9 +2099,6 @@ fn the_stock_video_options_window_loads_hidden_and_owns_its_own_name() {
/// And the second claim: **there is no clamp**, anywhere, in the reference (the positive control
/// is `baseMip`'s validating callback `0x689090`). `SetGamma(5)` writes `"-4.000000"` and the
/// store keeps it — benilla's clamp is at the render consumer, where it cannot lie to `GetCVar`.
///
/// (wow-re `ui/scratch/video-options-verbs.md` §3 and
/// `ffxeffects/scratch/whole-frame-grade-verdict.md` §(a), both VERIFIED.)
#[test]
fn the_display_brightness_pair_speaks_the_reference_slider_unit() {
let _data = benilla_formats::wow_data_or_skip!();
@@ -2211,8 +2208,6 @@ fn the_display_brightness_pair_speaks_the_reference_slider_unit() {
/// exist in the reference, with a capital F, while `value.func` is the lowercase `"farclip"`. The
/// stock client takes the CVar path for far clip **only because `getglobal` is case-sensitive** — so
/// this asserts that too, against a name we DO define.
///
/// (wow-re `ui/scratch/video-options-verbs.md` §5 and §7.7, both VERIFIED.)
#[test]
fn the_video_windows_ten_composed_names_stay_nil() {
let _data = benilla_formats::wow_data_or_skip!();
@@ -2265,7 +2260,7 @@ fn the_video_windows_ten_composed_names_stay_nil() {
/// gap rather than hiding it, and saying in as many words that "the day something registers it,
/// THIS assertion goes red and gets deleted". That day is this change: `UIOptionsFrameSliders`
/// row 3 is `cameraYawMoveSpeed`, `_Load` does `slider:SetValue(GetCVar(value.cvar))`, and
/// `Slider:SetValue` is a shape-A binding (`0x790980`, wow-re `numeric-arg-coercion-law.md`) that
/// `Slider:SetValue` is a shape-A binding (`0x790980`) that
/// raises on a nil in the reference too. All four slider CVars are registered, so the walk reaches
/// its end — and `_SetDefaults`, which does the same through `GetCVarDefault`, with it.
#[test]
@@ -8,7 +8,7 @@
//! proficiencies, which the server really does report as `1/1`, and the **single-rank** lines
//! (class skills, Dual Wield, racials, the per-mount riding lines), whose `skillMaxRank` the
//! engine overrides to `1` off `SkillRaceClassInfo.flags & 0x400` however high the server's own
//! descriptor is (`benilla-ui`'s `SkillEntry::mono`; wow-re `0x4d3610`'s `4d38b1` branch). A
//! descriptor is (`benilla-ui`'s `SkillEntry::mono`; `0x4d3610`'s `4d38b1` branch). A
//! hunter's `Beast Mastery` on vmangos arrives as `300/300` and must still read gray and
//! numberless, exactly as it does in the real client.
//!
@@ -374,11 +374,11 @@ fn the_pages_close_button_sits_where_the_reference_seats_it_and_closes_the_windo
// Its label is the CLOSE global string's seat, in the panel-button gold.
// **"Close", not "CLOSE".** An XML `text=` attribute is a GlobalStrings LOOKUP, not a literal
// (`loader::Loader::resolve_text`, wow-re `rf28-typed-widget-loadxml.md` l.36 —
// `FrameScript_GetText 0x703bf0`), and `CLOSE = "Close"` (`GlobalStrings.lua:760`). This test
// read "CLOSE" while the page's list carried no `GlobalStrings.lua`: it was asserting the
// miss-fallback (the raw attribute), which is the REFERENCE's own arm too (`0x778c31` /
// `0x771032`), not the label a client with the strings loaded draws.
// (`loader::Loader::resolve_text` — `FrameScript_GetText 0x703bf0`), and `CLOSE = "Close"`
// (`GlobalStrings.lua:760`). This test read "CLOSE" while the page's list carried no
// `GlobalStrings.lua`: it was asserting the miss-fallback (the raw attribute), which is the
// REFERENCE's own arm too (`0x778c31` / `0x771032`), not the label a client with the strings
// loaded draws.
// [`super::test_ui::CHARACTER_UI`] loads the player's own strings first, as the app does, so
// this is the real label now.
let label = page_quads(&s)
+2 -2
View File
@@ -216,7 +216,7 @@ fn load_entry(s: &UiScript, entry: &str, strict_templates: bool, no_warnings: bo
/// answers), then runs the reference's own body. First use rather than load time, and unnamed,
/// because both registries keep the first writer: the arena's name map
/// (`named_registry_is_non_overwriting`) and `_G` itself — a named frame is published
/// non-overwriting (RF-0023: a `lua_gettable` nil check on GLOBALSINDEX, which is why a
/// non-overwriting (`0x701bd0`: a `lua_gettable` nil check on GLOBALSINDEX, which is why a
/// metatable fallback would block the real publish just the same). A stand-in seated before the
/// real declaration shadows the real frame for good; the first cut did exactly that, 126
/// failures, every tabbed panel's `numTabs` read off the stand-in. `fire_chat_login`'s
@@ -268,7 +268,7 @@ pub(super) const UIPARENT_STAND_INS: &str = r#"
PETACTIONBAR_XPOS = PETACTIONBAR_XPOS or 36
-- The frames these three read UNGUARDED, seated on the call rather than at load: a frame's
-- publish to _G is non-overwriting (RF-0023), so a stand-in seated before the real file loads
-- publish to _G is non-overwriting (0x701bd0), so a stand-in seated before the real file loads
-- would shadow the real window for good.
local function benilla_seat(names)
for _, name in ipairs(names) do
@@ -506,7 +506,7 @@ fn buff_hover_hangs_below_left_of_the_button() {
s.resolve();
// Through the template's real `<OnEnter>` — the reference keeps the SetOwner/SetPlayerBuff pair
// inline there rather than in a named function, so the handler body itself is what this drives.
// `this` is set by hand because the engine sets it only when it *fires* a handler (RF-0025);
// `this` is set by hand because the engine sets it only when it *fires* a handler (`0x704d50`);
// calling the compiled function directly does not, and the body reads `this`, not its argument.
s.run("this = BuffButton0; BuffButton0:GetScript(\"OnEnter\")(BuffButton0)")
.unwrap();
@@ -7,7 +7,7 @@
//! **1.12.1 has no hover compare, and neither does benilla now.** `SHOW_COMPARE_TOOLTIP` (event
//! 377) has zero fire sites in 5875, so `PaperDollFrame.lua:621-640` is dead code there and the
//! vendor row (plus the auction row, 1971) is the only live consumer of the shopping plates
//! (wow-re `merchant-compare-item-law.md` §8). benilla fired that event until 2202 and then drove
//! (`SetMerchantCompareItem 0x536080`). benilla fired that event until 2202 and then drove
//! the plates itself on a shift-held hover until 2210; both were supersets of a client that
//! compares only where its own FrameXML asks. The plates' geometry and lifetime belong to that
//! FrameXML — this engine seats none of them.
@@ -412,7 +412,7 @@ fn shipped_merchant_row_raises_the_compare_plates() {
}
/// An armour template in one `InventoryType` — the CLASS is what the selection law compares by
/// (wow-re `merchant-compare-item-law.md` §3).
/// (the class test at `0x536262` in `SetMerchantCompareItem 0x536080`).
fn armor_template(name: &str, inventory_type: u32) -> ItemTemplateView {
ItemTemplateView {
name: name.into(),
@@ -243,7 +243,7 @@ fn an_addon_may_call_gametooltip_onload_the_reference_way_with_no_argument() {
// The tint the bare call was there to apply (ref-GameTooltip.lua l.79-82's two colours),
// QUANTIZED: the reference's backdrop colour field is a packed `0xAARRGGBB` byte quad and the
// setter converts `×255 + 0.5` through `__ftol` (wow-re `numeric-arg-coercion-law.md` Q4), so
// setter converts `×255 + 0.5` through `__ftol` (`SetBackdropColor 0x777d30`), so
// `0.09` stores as 23 and reads back as `23/255`. This used to compare against `0.09` exactly,
// which was our lossless `[f32; 4]` showing through a store the client cannot make.
let q = |x: f32| f32::from((x * 255.0 + 0.5) as u8) / 255.0;
@@ -7,8 +7,8 @@
//! What places the label is the template's `<NormalFont inherits="GameFontNormal"
//! justifyH="LEFT"/>`: `SetText`'s lazy label creation funnels through the adopter
//! `CSimpleButton::SetFontString 0x778d20`, which anchors an unanchored label to the button by the
//! NORMAL embedded font's justify word (`[button+0x390]`: LEFT→LEFT, RIGHT→RIGHT, else CENTER —
//! wow-re `system/ui/scratch/resize-bounds-and-button-fontstring.md` §5.2, VERIFIED). Decision 1996.
//! NORMAL embedded font's justify word (`[button+0x390]`: LEFT→LEFT, RIGHT→RIGHT, else CENTER).
//! Decision 1996.
use benilla_ui::script::UiScript;
@@ -747,8 +747,9 @@ fn shipped_target_frame_runs_the_level_law() {
.unwrap();
assert!(ok, "hostile +10: the skull shows ({:?})", s.errors());
// A DEAD mob is NOT a corpse (§5: UnitIsCorpse is a pure TYPEID_CORPSE object check, and
// UnitLevel has no health test) — the ref shows a dead mob's NUMBER, not the skull.
// A DEAD mob is NOT a corpse (UnitIsCorpse `0x5161c0` is a pure TYPEID_CORPSE object check,
// and UnitLevel `0x517fc0` has no health test) — the ref shows a dead mob's NUMBER, not the
// skull.
s.set_unit(
"target",
Some(UnitState {
@@ -156,7 +156,7 @@ const ELSEWHERE: &[(&str, &str, &str)] = &[
];
/// Pairs nothing fires on the verb's path, each with its reason. A row is a gap with a name,
/// never a reason to stop; the reference's own site is in the table for the grep into wow-re.
/// never a reason to stop; the reference's own site is in the table.
const GAP: &[(&str, &str, &str)] = &[
(
"CancelSkillUps",
@@ -170,15 +170,15 @@ const GAP: &[(&str, &str, &str)] = &[
"ClickTargetTradeButton",
"TRADE_REPLACE_ENCHANT",
"the reference runs the enchant clash check 0x496170 when a spell is on the cursor and \
fires this for the replace dialog (wow-re staticpopup-dialog-bindings.md §3.1); casting \
fires this for the replace dialog; casting \
an enchant onto the partner's slot is not built here — `ui_trade.rs` only mirrors the \
wire's enchant slot — so the verb is the money arm alone",
),
(
"CloseTrade",
"PLAYER_TRADE_MONEY",
"fired only when the trade did not complete (`[0xb71748] == 0`, wow-re \
incoming-trade-request-law.md), zeroing the cancelled offer under a frame TRADE_CLOSED \
"fired only when the trade did not complete (`[0xb71748] == 0`), \
zeroing the cancelled offer under a frame TRADE_CLOSED \
hides; here `TradeSession::begin` resets the whole session at the next trade, so the \
next window opens at zero either way",
),
@@ -199,8 +199,8 @@ const GAP: &[(&str, &str, &str)] = &[
(
"PetDismiss",
"PET_DISMISS_START",
"the worker 0x4bd6e0 sends the dismiss and fires this with a duration (`%d`, 10000 — \
wow-re pet-action-bar-api.md §10.8); no shipped file listens, the dismiss reaches the \
"the worker 0x4bd6e0 sends the dismiss and fires this with a duration (`%d`, 10000); \
no shipped file listens, the dismiss reaches the \
wire through the app's drain, and the pet frame follows the unit's departure",
),
(
@@ -1747,7 +1747,7 @@ fn taken_kit_names(world: &mut World) -> Vec<String> {
/// **A login and a `/reload` load without a sound** — `0x48fbf0` brackets itself in the counted
/// suppression scope (`0x48fbfa` → `0x49016d`) across the TOC walk, the addons, the saved
/// variables and the login cascade, and both of its callers (login `0x48f681`, `/reloadui`
/// `0x495669`) go through it (wow-re `system/ui/scratch/framexml-load-sound-suppression.md`).
/// `0x495669`) go through it.
///
/// Stock `TargetFrame_OnLoad` → `TargetFrame_Update` → `Hide()` → `TargetFrame_OnHide` really does
/// call `PlaySound("INTERFACESOUND_LOSTTARGETUNIT")` at load; the engine drops it. Before the
@@ -52,7 +52,7 @@ fn harness() -> UiScript {
/// button and the elapsed time its one argument. Everything on the sheet — the arrow, the party
/// and raid blips, the battleground teammates and flags, the corpse — is seated here.
/// The map arrow's file facts (`MinimapArrow.m2`: one looping 3.333 s Stand keying no bone; the
/// header box `x ∈ [−0.0127, 0.0135]`, `y ∈ [−0.0118, 0.0145]` — render law §2), handed to the
/// header box `x ∈ [−0.0127, 0.0135]`, `y ∈ [−0.0118, 0.0145]`), handed to the
/// engine the way the app does once the asset lands (2007/2015).
fn arrow_facts(s: &mut UiScript) {
use benilla_ui::widget::{ModelFileFacts, SequenceFacts};
@@ -148,7 +148,7 @@ fn leaving_the_zone_clears_the_readout() {
);
}
/// The dynamic icon is a SECOND slot, not a swap (wow-re `worldstate-ui-law.md` §12; decision
/// The dynamic icon is a SECOND slot, not a swap (`GetWorldStateUIInfo 0x4c5a70`; decision
/// 1604). The `Icon` column and the `DynamicIcon` column feed two different regions — a 42x42
/// static slot and a 32x32 button off the row's right edge — and only `uiState == 2`, the
/// flag-taken value, lights the second one. The first pass replaced the static art whenever the
@@ -369,11 +369,11 @@ fn ink_box(chain: &mut benilla_formats::Chain, path: &str) -> (f32, f32, f32, f3
/// have. Every icon the readout names is a sprite authored into the UPPER-LEFT corner of a
/// power-of-two canvas (`AllianceTower` fills 16x16 of 32x32; `UI-PVP-Alliance` ~40x40 of 64x64),
/// nothing is cropped, and the reference compensates *geometrically* — a 42x42 slot hung 6 units
/// off the row's left edge with the label seated 10 above its centreline (wow-re
/// `worldstate-ui-law.md` §12). Pin the OUTCOME rather than the constants: whatever the numbers,
/// the visible art must sit beside its label and share its line. A snug icon box — the obvious
/// thing to write, and what we shipped — puts the ink up and to the left of its own slot, which is
/// exactly what the director saw. Skips without client data.
/// off the row's left edge with the label seated 10 above its centreline (`WorldStateFrame.xml`).
/// Pin the OUTCOME rather than the constants: whatever the numbers, the visible art must sit beside
/// its label and share its line. A snug icon box — the obvious thing to write, and what we shipped
/// — puts the ink up and to the left of its own slot, which is exactly what the director saw.
/// Skips without client data.
#[test]
fn the_visible_ink_sits_beside_its_label_not_adrift_of_it() {
let data = benilla_formats::wow_data_or_skip!();
@@ -172,7 +172,7 @@ fn abbey_grounds_subzone_hop_shows_no_territory_line() {
);
}
/// The abbey repro, indoor half — the byte-corrected feed (wow-re `zonetext-indoor-bit.md` (d)):
/// The abbey repro, indoor half — the byte-corrected feed (`0x67e670`):
/// crossing the abbey threshold, the zone-name override SKIPS (the whole-WMO name
/// "Northshire Abbey" equals the yard subzone), so the ZONE slot stays "Elwynn Forest" and the
/// group row's name re-populates the SUBZONE ("Main Hall"). ZoneTextFrame's text never changes ⇒
@@ -208,7 +208,7 @@ fn abbey_interior_shows_the_room_in_the_small_line_alone() {
/// The INN case — the override FIRES (the whole-WMO name differs from the street subzone):
/// zone slot = the inn's name, subzone nulls (unnamed group rows). The big splash shows the inn
/// name, and — the FIFO dispatch law (wow-re `event-dispatch-order.md`: SubZoneTextFrame
/// name, and — the FIFO dispatch law (`RegisterEvent 0x702140` tail-appends: SubZoneTextFrame
/// registered second fires LAST, its SetZoneText(1) is the last writer) — the territory line
/// shows under it, exactly as on a NEW_AREA splash.
#[test]