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:
committed by
GitHub
co-authored by
Claude Opus 5.5
parent
beb6cc854f
commit
7b66dac9db
@@ -222,8 +222,7 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
|
||||
// GetActionText(slot) → the MACRO's own name, or nil. 1-based (`0x4e7072 dec eax`).
|
||||
//
|
||||
// **The classification is binary, not four-way** (`0x4e7050`, wow-re
|
||||
// `bag-language-combat-action-bindings.md` §4, §5-cross-checked). The binding tests exactly
|
||||
// **The classification is binary, not four-way** (`0x4e7050`). The binding tests exactly
|
||||
// one thing — top nibble `raw & 0xf0000000 == 0x40000000` — and **spell, item and empty all
|
||||
// collapse to the identical nil arm**. It settles nothing about how spells or items are
|
||||
// tagged, because it never asks.
|
||||
@@ -330,7 +329,7 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
)?;
|
||||
|
||||
// `ChangeActionBarPage()` — four instructions in the reference (`0x4e7650`: `mov ecx,0xd4;
|
||||
// call FrameScript_SignalEvent; xor eax,eax; ret` — wow-re `action-bar-toggles.md` §6, `ui.md`):
|
||||
// call FrameScript_SignalEvent; xor eax,eax; ret`):
|
||||
// it fires `ACTIONBAR_PAGE_CHANGED` and does NOTHING else. The page is FrameXML state
|
||||
// (`CURRENT_ACTIONBAR_PAGE`, which `ActionBar_PageUp/Down` and the SHIFT-n bindings write before
|
||||
// calling this), the buttons repaint from `ActionButton_OnEvent`, and the up-arrow's OnEvent
|
||||
@@ -584,7 +583,7 @@ mod tests {
|
||||
assert_eq!(uses(&mut s), vec![(10, false)]);
|
||||
}
|
||||
|
||||
// ── `GetActionText` (wow-re `bag-language-combat-action-bindings.md` §4) ────────────────────
|
||||
// ── `GetActionText` (`0x4e7050`) ────────────────────────────────────────────────────────────
|
||||
|
||||
/// The classification is **binary**: macro or not. A SPELL slot and an ITEM slot answer the
|
||||
/// identical `nil` an empty slot does — the binding tests one nibble and asks nothing else.
|
||||
|
||||
@@ -1,18 +1,16 @@
|
||||
//! `GetActionBarToggles` / `SetActionBarToggles` — the four extra action bars' visibility, as one
|
||||
//! server-owned nibble. Byte-VERIFIED against the 1.12.1 binary: wow-re
|
||||
//! `system/ui/scratch/action-bar-toggles.md` (a §5 trio cross-check, `417c2d31`), whose section
|
||||
//! numbers the comments below cite.
|
||||
//! server-owned nibble, byte-verified against the 1.12.1 binary.
|
||||
//!
|
||||
//! ## The shape, and why it is not the shape it looks like
|
||||
//!
|
||||
//! Four booleans in, four booleans out — but **the getter and the setter do not talk to each
|
||||
//! other**. `SetActionBarToggles 0x4e76e0` packs its four arguments into a stack byte, posts
|
||||
//! `CMSG_SET_ACTIONBAR_TOGGLES` and returns zero values; every store in its body is into its own
|
||||
//! `ebp` frame (§4.1). `GetActionBarToggles 0x4e7660` reads the live descriptor at
|
||||
//! `[[player+0xe68]+0x102a]` — `PLAYER_FIELD_BYTES` byte 2 — on every call, with no cache (§5). The
|
||||
//! `ebp` frame. `GetActionBarToggles 0x4e7660` reads the live descriptor at
|
||||
//! `[[player+0xe68]+0x102a]` — `PLAYER_FIELD_BYTES` byte 2 — on every call, with no cache. The
|
||||
//! only writer of that cell in the whole image is the generic `SMSG_UPDATE_OBJECT` value-apply
|
||||
//! (`apply_update_fields 0x466590`), and **nothing is notified when it lands**: all 49 field-change
|
||||
//! registrations at `0x468070` were enumerated and none sits at an offset ≥ `0x1000` (§4.2).
|
||||
//! (`0x466590`), and **nothing is notified when it lands**: all 49 field-change
|
||||
//! registrations at `0x468070` were enumerated and none sits at an offset ≥ `0x1000`.
|
||||
//!
|
||||
//! So `SetActionBarToggles(1)` followed immediately by `GetActionBarToggles()` returns the **old**
|
||||
//! value, for a whole round trip. That is not a bug to paper over — it is the mechanism, and it is
|
||||
@@ -22,22 +20,23 @@
|
||||
//!
|
||||
//! The reference UI does not feel the lag because it never asks: it keeps `SHOW_MULTI_ACTIONBAR_1..4`
|
||||
//! as Lua globals, updates them on the checkbox click, and reads the binding exactly once — in
|
||||
//! `UIParent.lua`'s `PLAYER_ENTERING_WORLD` handler (§7). Our Lua layer copies that split.
|
||||
//! `UIParent.lua`'s `PLAYER_ENTERING_WORLD` handler. Our Lua layer copies that split.
|
||||
//!
|
||||
//! ## Four bits, and only four
|
||||
//!
|
||||
//! The setter's loop runs `i = 0..3` (`0x4e770e cmp esi,4`) and ORs `1 << i` for argument `i + 1`;
|
||||
//! the accumulator is written by exactly two instructions image-wide — the `mov BYTE [ebp-0x4],0`
|
||||
//! that zeroes it and the loop's `or` (§2). Two consequences we reproduce exactly:
|
||||
//! that zeroes it (`0x4e76eb`) and the loop's `or` (`0x4e7709`). Two consequences we reproduce
|
||||
//! exactly:
|
||||
//!
|
||||
//! - A **fifth argument is silently dropped**. The shipped `UIOptionsFrame_Save` passes
|
||||
//! `ALWAYS_SHOW_MULTIBARS` as a fifth; the binding never fetches it, so it never reaches the byte
|
||||
//! or the wire (and, consistently, it is the one option of the five that FrameXML saves locally).
|
||||
//! - A `Set` **destroys the high nibble**. It starts from zero, so whatever `0x10..0x80` the server
|
||||
//! happened to hold is overwritten with 0 by the next post. The getter never tests those bits
|
||||
//! either (§5), so the client's view of this field is genuinely 4-bit.
|
||||
//! either, so the client's view of this field is genuinely 4-bit.
|
||||
//!
|
||||
//! The bit→bar meaning is a FrameXML convention, not the binary's (§7) — the engine stores four
|
||||
//! The bit→bar meaning is a FrameXML convention, not the binary's — the engine stores four
|
||||
//! unnamed bits and the names live in the UI layer.
|
||||
|
||||
use mlua::{Lua, MultiValue, Value};
|
||||
@@ -61,7 +60,7 @@ impl super::UiScript {
|
||||
/// What the VM last had pushed into it, or `None` if nothing ever has — the app's own read,
|
||||
/// and the tests'. `None` and `Some(0)` are indistinguishable to Lua by design: with no local
|
||||
/// player the reference's chain fails soft and the getter returns four `nil`s, which is exactly
|
||||
/// what a zero byte returns (§5).
|
||||
/// what a zero byte returns (`0x4e7660`).
|
||||
pub fn action_bar_toggles(&self) -> Option<u8> {
|
||||
self.model_ref().action_bar_toggles
|
||||
}
|
||||
@@ -105,7 +104,7 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
)?;
|
||||
|
||||
// SetActionBarToggles(a, b, c, d) -> nothing. Four argument slots, no more: a fifth is never
|
||||
// fetched (§2), which matters because the shipped Options panel passes one.
|
||||
// fetched (`0x4e76e0`), which matters because the shipped Options panel passes one.
|
||||
//
|
||||
// The arguments are read through `0x6f1c10`, NOT Lua truthiness ([`bool_or_default`]) — the
|
||||
// panel that calls this hands option bindings the strings "0"/"1", and `"0"` is truthy in Lua.
|
||||
@@ -240,7 +239,7 @@ mod tests {
|
||||
let mut s = UiScript::new().unwrap();
|
||||
|
||||
// Nothing pushed: four nils, and no error. Indistinguishable from a zero byte, exactly as
|
||||
// "no local player" is in the reference (§5).
|
||||
// "no local player" is in the reference (`0x4e7660`).
|
||||
assert!(s
|
||||
.eval::<bool>(
|
||||
"local a,b,c,d = GetActionBarToggles() \
|
||||
@@ -274,7 +273,8 @@ mod tests {
|
||||
|
||||
/// Round trip: what the setter packs is what the getter reads back — but only once the value
|
||||
/// has travelled through the app's push, because the setter deliberately does not touch the
|
||||
/// local copy. The stale read in the middle is the mechanism, not an oversight (§4.1).
|
||||
/// local copy. The stale read in the middle is the mechanism, not an oversight (`0x466590`,
|
||||
/// the only writer).
|
||||
#[test]
|
||||
fn the_setter_does_not_touch_the_local_copy_so_the_read_lags_a_round_trip() {
|
||||
let mut s = UiScript::new().unwrap();
|
||||
|
||||
@@ -28,13 +28,13 @@
|
||||
//! ## The load law lives next door
|
||||
//!
|
||||
//! Every "can this load, and why not" answer comes from [`super::addon_gate`] — the reference's
|
||||
//! `AddOn_CanLoad 0x51e780` as one pure function (decision 1292). The RE answer 1191 §6 was
|
||||
//! missing has landed (wow-re `addon-version-gate.md`, §5-verified): the version gate is an
|
||||
//! `AddOn_CanLoad 0x51e780` as one pure function (decision 1292). What 1191 §6 was missing has
|
||||
//! now landed: the version gate is an
|
||||
//! exact `== 11200` whose refusal the `checkAddonVersion` CVar suppresses by **actively
|
||||
//! resetting** the reason — so `INTERFACE_VERSION` is now enforced here exactly as the client
|
||||
//! enforces it, with the *Load out of date AddOns* toggle as the player's escape, instead of
|
||||
//! 1191's report-but-never-act interim. The CVar is read live per query (§2.2), which is why a
|
||||
//! checkbox click needs nothing but a list repaint.
|
||||
//! 1191's report-but-never-act interim. The CVar is read live per query (`0x51e780`), which is
|
||||
//! why a checkbox click needs nothing but a list repaint.
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
@@ -99,8 +99,7 @@ pub struct AddOnInfo {
|
||||
///
|
||||
/// The reply covers exactly the `## Secure:` addons, in the order the client sent them, and
|
||||
/// the 2006 retail capture answers `status = 2` for all twelve — which is why the stock
|
||||
/// AddOns list shows the player's addons and none of Blizzard's (wow-re
|
||||
/// `system/net/scratch/cmsg-auth-session-addon-block.md` §6).
|
||||
/// AddOns list shows the player's addons and none of Blizzard's.
|
||||
pub hidden: bool,
|
||||
/// The addon's files sit in the player's patch chain, not the AddOns folder — Blizzard's own
|
||||
/// LoadOnDemand addons (`Blizzard_TrainerUI` and its eleven siblings), which the reference
|
||||
@@ -196,7 +195,7 @@ fn addon_key(lua: &Lua, model: &Model, key: &Value, usage: &'static str) -> mlua
|
||||
|
||||
/// **Rebuild the Lua index space** — the tail block `[0x51dc30, 0x51dcdf)` of
|
||||
/// `AddOn_ReadAddonInfoReply 0x51da70`, and the only place that array is ever built
|
||||
/// (decision 2175, wow-re `system/ui/scratch/addon-registry-scan-and-order.md` §7).
|
||||
/// (decision 2175).
|
||||
///
|
||||
/// Three properties, all byte-read, all easy to get wrong:
|
||||
///
|
||||
@@ -267,7 +266,7 @@ fn gate_rows(model: &Model) -> Vec<GateRow<'_>> {
|
||||
}
|
||||
|
||||
/// The live `checkAddonVersion` read — the gate's CVar half, re-read per query like the
|
||||
/// reference's (`IsAddonVersionCheckEnabled 0x51f180` inside `AddOn_CanLoad`, §2.2). An absent
|
||||
/// reference's (`IsAddonVersionCheckEnabled 0x51f180` inside `AddOn_CanLoad`). An absent
|
||||
/// table (a bare test VM, or a query before the host's seed) answers the registrar default:
|
||||
/// check ON, `"1"`.
|
||||
fn version_check(model: &Model) -> bool {
|
||||
@@ -319,9 +318,9 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
// **`enabled`**, not `url`.
|
||||
//
|
||||
// **The client registers `GetAddOnInfo` TWICE, as two different functions**, and this is the
|
||||
// in-game one. wow-re `system/ui/scratch/addon-version-gate.md`: glue `0x46d460` returns EIGHT
|
||||
// with `url` at slot 4 and `newVersion` at 8; in-game `0x48e390` returns SEVEN with `enabled`
|
||||
// at slot 4 and no `url` at all. They differ in behaviour too — the in-game one passes `dl=1`,
|
||||
// in-game one. Glue `0x46d460` returns EIGHT with `url` at slot 4 and `newVersion` at 8;
|
||||
// in-game `0x48e390` returns SEVEN with `enabled` at slot 4 and no `url` at all. They differ in
|
||||
// behaviour too — the in-game one passes `dl=1`,
|
||||
// so `NOT_DEMAND_LOADED` is reachable here and never from glue, and it short-circuits an
|
||||
// already-loaded addon to `1, nil` before the gate.
|
||||
//
|
||||
@@ -354,7 +353,7 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
// The STRING form is not existence-checked: it answers seven placeholders, two
|
||||
// of which are not nil — and **slot 1 is the caller's own string echoed back**
|
||||
// (`0x48e401`, non-NULL by `lua_isstring`). We used to answer the literal
|
||||
// `"NoSuchAddon"`, which is the wow-re note's *example call*, not a constant
|
||||
// `"NoSuchAddon"`, which was an illustrative example, not a constant
|
||||
// the image contains. Slot 4 is `enabled` in-game (the glue table's fourth is
|
||||
// `url`, and its eighth is an appended `newVersion` — nothing shifts).
|
||||
None => {
|
||||
@@ -458,10 +457,10 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
})?,
|
||||
)?;
|
||||
|
||||
// **One argument, index or name — the `(character, index)` form is GLUE-ONLY** (byte-carved,
|
||||
// wow-re `addon-enable-store.md`: in-game `0x48e690`/`0x48e760` read arg1 alone and key the
|
||||
// store on `0x5abdc0()`, the logged-in character; the two-argument shape belongs to the glue
|
||||
// registrars `0x46d7b0`/`0x46d8a0`, and our glue screen is native — no caller exists).
|
||||
// **One argument, index or name — the `(character, index)` form is GLUE-ONLY** (in-game
|
||||
// `0x48e690`/`0x48e760` read arg1 alone and key the store on `0x5abdc0()`, the logged-in
|
||||
// character; the two-argument shape belongs to the glue registrars `0x46d7b0`/`0x46d8a0`, and
|
||||
// our glue screen is native — no caller exists).
|
||||
// A numeric index out of range is a **Lua error** in the reference (`luaL_error` via
|
||||
// `0x51df00`); an unknown NAME is where we diverge, disclosed: the reference creates a
|
||||
// phantom enable-hash entry for the typo, we no-op — the safer direction, and `resolve`'s
|
||||
@@ -498,9 +497,9 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
)?;
|
||||
}
|
||||
|
||||
// `ResetDisabledAddOns 0x48e830` — **revert-to-last-saved**, byte-carved (wow-re
|
||||
// `addon-enable-store.md`): the reference destroys the current character's enable hash and
|
||||
// reloads it from the on-disk `AddOns.txt` — so unsaved toggles of BOTH polarities revert,
|
||||
// `ResetDisabledAddOns 0x48e830` — **revert-to-last-saved**: the reference destroys the
|
||||
// current character's enable hash and reloads it from the on-disk `AddOns.txt` — so unsaved
|
||||
// toggles of BOTH polarities revert,
|
||||
// and a disable already on disk stays disabled. Within one session the current character's
|
||||
// file is immutable until the shutdown tail writes it (the glue can only edit it with no
|
||||
// session up), so the registration-time `enabled` — read from that very file — IS the
|
||||
@@ -573,7 +572,7 @@ fn load_addon(lua: &Lua, i: usize) -> Result<(), String> {
|
||||
// The one arbiter (decision 1292): the reference's own shape — `LoadAddOn 0x48e980` refuses
|
||||
// through `AddOn_CanLoad` (via `AddOn_Load`'s step 3) and re-derives the reason from the
|
||||
// same gate on failure. The version gate is in here now: an out-of-date addon demand-loads
|
||||
// only under force-load, exactly as `0x48ea8c`'s carve records.
|
||||
// only under force-load, exactly as `0x48ea8c` records.
|
||||
{
|
||||
let model = lua.app_data_ref::<Model>().expect("model");
|
||||
if let refused @ Verdict::Refused { .. } = verdict(&model, i) {
|
||||
@@ -618,7 +617,7 @@ fn load_addon(lua: &Lua, i: usize) -> Result<(), String> {
|
||||
// **The consequence is real and is the reference's own**: inside a cycle an addon is flagged
|
||||
// loaded before its own files run, so `IsAddOnLoaded("A")` answers 1 for the whole of B's
|
||||
// execution, and `ADDON_LOADED` fires B before A. That is not a wart we are copying blindly —
|
||||
// it is what makes the recursion finite, and wow-re executed it against the real bytes.
|
||||
// it is what makes the recursion finite.
|
||||
{
|
||||
let mut model = lua.app_data_mut::<Model>().expect("model");
|
||||
model.addons[i].loaded = true;
|
||||
@@ -644,7 +643,9 @@ fn load_addon(lua: &Lua, i: usize) -> Result<(), String> {
|
||||
if !loaded {
|
||||
load_addon(lua, d).map_err(|r| {
|
||||
if r.starts_with("DEP_") {
|
||||
r // §2.3: the prefix applies exactly once at any nesting depth
|
||||
// The prefix applies exactly once at any nesting depth
|
||||
// (`0x51e8ce`/`0x51e8cf`).
|
||||
r
|
||||
} else {
|
||||
format!("DEP_{r}")
|
||||
}
|
||||
@@ -909,7 +910,7 @@ impl super::UiScript {
|
||||
///
|
||||
/// The reply carries no count and no names: it is one record per `## Secure:` addon, in the
|
||||
/// order the client itself sent them in `CMSG_AUTH_SESSION`, so the caller does the pairing
|
||||
/// and hands us the names (wow-re `system/net/scratch/cmsg-auth-session-addon-block.md` §6).
|
||||
/// and hands us the names (`0x51da70`).
|
||||
/// Calling this with an empty slice is meaningful and different from never calling it: it
|
||||
/// records that a reply arrived and hid nothing.
|
||||
pub fn note_addon_info_reply(&mut self, hidden: &[String]) {
|
||||
|
||||
@@ -7,12 +7,11 @@
|
||||
//! `not_loadable`, `LoadAddOn`'s inline checks, and the startup walk's disabled set — and the
|
||||
//! screens were about to become a fourth. This module is the one copy they all consult.
|
||||
//!
|
||||
//! The check order, byte-verified (wow-re `system/ui/scratch/addon-version-gate.md` §2, a §5
|
||||
//! pair + arbitration): **missing → visiting-guard (returns loadable — a cycle resolves
|
||||
//! The check order: **missing → visiting-guard (returns loadable — a cycle resolves
|
||||
//! optimistically) → enabled → banned → corrupt → the version gate → required-deps recursion →
|
||||
//! demand gate**. The first check that fires decides.
|
||||
//!
|
||||
//! ## The version gate (§2.1, byte for byte)
|
||||
//! ## The version gate, byte for byte
|
||||
//!
|
||||
//! `cmp [rec+0x1c], 11200` — an **exact `==` against a hard-coded immediate** (`0x51d7d0` is
|
||||
//! `mov eax,0x2bc0; ret`, one caller image-wide). `## Interface: 11201` is as out of date as
|
||||
@@ -23,7 +22,7 @@
|
||||
//! one; there is no "loadable but flagged" state. Force-load is a fall-through, not a success:
|
||||
//! the dependency and demand checks still run.
|
||||
//!
|
||||
//! ## The reason rendering (§2.3)
|
||||
//! ## The reason rendering (`0x51e930`)
|
||||
//!
|
||||
//! `reason != 0` → its token; `reason == 0 && depReason != 0` → `DEP_<token>`, applied
|
||||
//! **exactly once at any nesting depth** (the recursion passes the caller's `depReasonOut` as
|
||||
@@ -46,13 +45,12 @@ pub struct GateRow<'a> {
|
||||
/// the leading integer, `0` when absent.
|
||||
pub interface: u32,
|
||||
pub load_on_demand: bool,
|
||||
/// Loaded this session. A loaded dependency satisfies the recursion outright — VERIFIED at
|
||||
/// the bytes (wow-re `addon-enable-store.md`, the follow-up carve): the short-circuit lives
|
||||
/// inside `AddOn_CanLoad` itself (`0x51e8ba call AddOn_IsLoaded; jne` skips the recursion),
|
||||
/// and the recursion propagates the caller's own `demandOnly` (`0x51e790` spill →
|
||||
/// `0x51e8c9` reload). The concrete verdicts match this implementation exactly: a
|
||||
/// LoadOnDemand addon over a loaded ordinary dep is loadable/nil; over an
|
||||
/// enabled-but-UNLOADED ordinary dep it reports `DEP_NOT_DEMAND_LOADED`.
|
||||
/// Loaded this session. A loaded dependency satisfies the recursion outright: the short-circuit
|
||||
/// lives inside `AddOn_CanLoad` itself (`0x51e8ba call AddOn_IsLoaded; jne` skips the
|
||||
/// recursion), and the recursion propagates the caller's own `demandOnly` (`0x51e790` spill →
|
||||
/// `0x51e8c9` reload). The reference's concrete answers match this implementation exactly: a
|
||||
/// LoadOnDemand addon over a loaded ordinary dep is loadable/nil; over an enabled-but-UNLOADED
|
||||
/// ordinary dep it reports `DEP_NOT_DEMAND_LOADED`.
|
||||
pub loaded: bool,
|
||||
/// `## Dependencies` / `## RequiredDeps` / any `## Dep*` (one list in the reference).
|
||||
pub dependencies: Vec<&'a str>,
|
||||
@@ -93,7 +91,7 @@ impl Verdict {
|
||||
/// `demand_only` is the in-game flavour (`dl=1`): an enabled, up-to-date, **non**-LoadOnDemand
|
||||
/// addon that has not loaded reports `NOT_DEMAND_LOADED` — a state the glue (`dl=0`) never
|
||||
/// produces. `version_check` is the live `checkAddonVersion` read; both re-run per query, which
|
||||
/// is why toggling the checkbox needs no rescan (§2.2).
|
||||
/// is why toggling the checkbox needs no rescan (`0x51e780`).
|
||||
pub fn can_load(rows: &[GateRow], index: usize, demand_only: bool, version_check: bool) -> Verdict {
|
||||
let mut visiting = vec![false; rows.len()];
|
||||
walk(rows, index, demand_only, version_check, &mut visiting)
|
||||
@@ -120,7 +118,7 @@ fn walk(
|
||||
};
|
||||
}
|
||||
// Checks 4/5 (banned / corrupt): no server signature state to read — never produced.
|
||||
// Check 6: the version gate — exact ==, then refuse or actively fall through (§2.1).
|
||||
// Check 6: the version gate — exact ==, then refuse or actively fall through (`0x51e876`).
|
||||
if row.interface != CLIENT_INTERFACE && version_check {
|
||||
return Verdict::Refused {
|
||||
reason: Some("INTERFACE_VERSION"),
|
||||
@@ -128,7 +126,7 @@ fn walk(
|
||||
};
|
||||
}
|
||||
// Check 7: required dependencies, recursively; the first failure decides, and the deepest
|
||||
// failure's raw token is the one `DEP_` wraps (§2.3's shared out-param).
|
||||
// failure's raw token is the one `DEP_` wraps (`0x51e8ce`'s shared out-param).
|
||||
visiting[i] = true;
|
||||
for dep in &row.dependencies {
|
||||
let found = rows.iter().position(|r| r.name.eq_ignore_ascii_case(dep));
|
||||
@@ -139,7 +137,7 @@ fn walk(
|
||||
dep: None,
|
||||
},
|
||||
// A loaded dependency satisfies the walk outright — `0x51e8ba`, before the
|
||||
// recursion (the row doc has the carve).
|
||||
// recursion (see the row doc).
|
||||
Some(d) if rows[d].loaded => Verdict::Loadable,
|
||||
Some(d) => walk(rows, d, demand_only, version_check, visiting),
|
||||
};
|
||||
@@ -191,8 +189,9 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
/// §2.1 byte for byte: exact `==` (11201 is out of date), missing `## Interface` is 0 and
|
||||
/// out of date, and force-load RESETS the reason — indistinguishable from up to date.
|
||||
/// The version gate (`0x51e876`) byte for byte: exact `==` (11201 is out of date), missing
|
||||
/// `## Interface` is 0 and out of date, and force-load RESETS the reason — indistinguishable
|
||||
/// from up to date.
|
||||
#[test]
|
||||
fn the_version_gate_is_exact_and_force_load_erases_it() {
|
||||
let mut a = row("A", vec![]);
|
||||
@@ -213,8 +212,8 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
/// §2.3: `DEP_` is applied exactly once at any depth — the deepest failure's raw token is
|
||||
/// what the top level wraps.
|
||||
/// `DEP_` is applied exactly once at any depth (`0x51e8ce`) — the deepest failure's raw token
|
||||
/// is what the top level wraps.
|
||||
#[test]
|
||||
fn dep_prefix_applies_once_at_any_depth() {
|
||||
let a = row("A", vec!["B"]);
|
||||
@@ -256,13 +255,13 @@ mod tests {
|
||||
assert_eq!(
|
||||
can_load(&rows, 0, true, false).token().as_deref(),
|
||||
Some("NOT_DEMAND_LOADED"),
|
||||
"force-load falls THROUGH, it does not succeed (§2.1 fact 2)"
|
||||
"force-load falls THROUGH, it does not succeed (`0x51e89d`)"
|
||||
);
|
||||
}
|
||||
|
||||
/// A LoadOnDemand addon whose required dep already loaded at startup is loadable from the
|
||||
/// in-game surface — the loaded short-circuit, VERIFIED at `0x51e8ba` (inside `AddOn_CanLoad`,
|
||||
/// before the recursion; the carve's own concrete A/B verdict is this assertion).
|
||||
/// in-game surface — the loaded short-circuit at `0x51e8ba` (inside `AddOn_CanLoad`,
|
||||
/// before the recursion; the reference's own concrete A/B verdict is this assertion).
|
||||
#[test]
|
||||
fn a_loaded_dependency_satisfies_the_demand_query() {
|
||||
let mut a = row("A", vec!["B"]);
|
||||
|
||||
@@ -15,8 +15,7 @@
|
||||
//! `language` field to `LANG_ADDON` (`0xFFFFFFFF`). That sentinel is the *only* thing separating
|
||||
//! addon data from speech, on both sides of the wire.
|
||||
//!
|
||||
//! VERIFIED in `WoW.exe` (5875) — wow-re `system/ui/scratch/addon-chat-law.md` §5, a §5 trio
|
||||
//! (three independent derivations, orchestrator-arbitrated). The binding is `0x49f920`, body
|
||||
//! VERIFIED in `WoW.exe` (5875). The binding is `0x49f920`, body
|
||||
//! `[0x49f920, 0x49fb2a)`:
|
||||
//!
|
||||
//! ```text
|
||||
@@ -44,8 +43,8 @@
|
||||
//! Three consequences the shape forces, and each is a thing an implementation gets wrong:
|
||||
//!
|
||||
//! - **The prefix is not a wire field.** It is glued to the message with a literal TAB and the
|
||||
//! pair rides as one C-string. The receiver splits on the FIRST tab (`0x49a8d0`, the same note
|
||||
//! §3). Nothing on the wire knows a prefix exists.
|
||||
//! pair rides as one C-string. The receiver splits on the FIRST tab (`0x49a8d0`). Nothing on
|
||||
//! the wire knows a prefix exists.
|
||||
//! - **There is no `target` argument.** The body fetches Lua indices `1`, `2` and `3` and no
|
||||
//! other — there is no arg-4 fetch anywhere in its 522 bytes. 1.12 has no whispered addon
|
||||
//! message; the modern 4-argument `SendAddonMessage(prefix, msg, "WHISPER", target)` does not
|
||||
@@ -201,8 +200,8 @@ impl AddonDistribution {
|
||||
/// roster at `0xb712a8`, and the binding `GetNumRaidMembers` (`0x4bb530`, `Ui\RaidInfo.cpp`)
|
||||
/// returns that cell verbatim. So the reference's test `[0xb713e0] == 0` *is*
|
||||
/// `GetNumRaidMembers() == 0`, and reading our own `GetNumRaidMembers` backing is the
|
||||
/// mechanism rather than a stand-in for it. (1235 recorded this identity as corroborated-not-
|
||||
/// carved and hedged accordingly; the RE it dispatched carved it, and the hedge is retired.)
|
||||
/// mechanism rather than a stand-in for it. (1235 recorded this identity as corroborated but
|
||||
/// hedged; the hedge is now retired.)
|
||||
pub fn effective(self, in_raid: bool) -> Self {
|
||||
match self {
|
||||
Self::Raid if !in_raid => Self::Party,
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
//! anything. [`super::UiScript::take_auction_sorts`] hands the click to the app, which owns the
|
||||
//! stacks and pushes both the reordered rows and the stack back. The selection rides through all
|
||||
//! of it untouched, because it is stored as an auction **id** and only resolved to a row position
|
||||
//! when something asks (wow-re §5 TU-5).
|
||||
//! when something asks (`0x4cfda0`/`0x4cfec0`).
|
||||
//!
|
||||
//! ## The deposit is computed here, and it is the *client's* arithmetic
|
||||
//!
|
||||
@@ -53,7 +53,8 @@ pub const BIDDER: usize = 1;
|
||||
pub const OWNER: usize = 2;
|
||||
|
||||
/// The eight sort keys the reference's headers pass. Order here is the API's, not the comparator's
|
||||
/// (decision 1511's INTERIM: the comparator's own mode order is pinned to the in-flight wow-re §5).
|
||||
/// (decision 1511's INTERIM: the comparator's own mode order is pinned to an in-flight
|
||||
/// investigation).
|
||||
pub const SORT_KEYS: [&str; 8] = [
|
||||
"level", "quality", "bid", "duration", "buyout", "status", "name", "seller",
|
||||
];
|
||||
@@ -239,7 +240,7 @@ impl super::UiScript {
|
||||
let mut model = self.model_mut();
|
||||
// Only opening or closing the session drops the selection. A new PAGE does not, and
|
||||
// neither does a re-sort: the selection is an auction **id**, so it survives the row
|
||||
// moving and simply stops resolving if the auction leaves the page (wow-re §5 TU-5).
|
||||
// moving and simply stops resolving if the auction leaves the page (`0x4cfda0`/`0x4cfec0`).
|
||||
if model.auction.is_none() || state.is_none() {
|
||||
model.auction_selected = [0; 3];
|
||||
}
|
||||
@@ -390,7 +391,7 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
let Some(r) = row else {
|
||||
// The null tail is TWELVE values, not a lone nil — an unknown type string, an
|
||||
// out-of-range index and an item-cache miss all share it, with a hard `count = 1`
|
||||
// and `quality = -1` (wow-re §5 TU-3, one shared exit at `0x4cf1ec`). Callers
|
||||
// and `quality = -1` (one shared exit at `0x4cf1ec`). Callers
|
||||
// destructure all twelve unguarded, so a short return throws.
|
||||
return Ok(MultiValue::from_vec(vec![
|
||||
Value::Nil, // name
|
||||
@@ -461,7 +462,7 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
let model = lua.app_data_ref::<Model>().expect("model app_data");
|
||||
row_at(&model, &kind, index).and_then(|r| r.link)
|
||||
};
|
||||
// A miss returns NO values at all — not nil (wow-re §5 TU-3). `DressUpItemLink(nil)`
|
||||
// A miss returns NO values at all — not nil (`0x4cf45b`). `DressUpItemLink(nil)`
|
||||
// and `nil` reaching a chat insert behave differently from an empty argument list.
|
||||
Ok(match link {
|
||||
Some(l) => MultiValue::from_vec(vec![Value::String(lua.create_string(&l)?)]),
|
||||
@@ -472,7 +473,8 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
|
||||
// GetSelectedAuctionItem(type) → the 1-based selected row, or 0 for none.
|
||||
//
|
||||
// Stored as the **auction id**, resolved to a row position on the way out (wow-re §5 TU-5).
|
||||
// Stored as the **auction id**, resolved to a row position on the way out
|
||||
// (`0x4cfda0`/`0x4cfec0`).
|
||||
// That indirection is the whole reason a re-sort cannot silently move the selection onto a
|
||||
// different auction: the id follows the row wherever the comparator puts it, and an auction
|
||||
// that has left the page simply stops resolving instead of pointing at its neighbour.
|
||||
@@ -613,7 +615,7 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
// `0x4cfab0`: no class (index out of range, or its `0x807060` flag 0) → nothing; a FOUND
|
||||
// subclass offers the list only with `ItemSubClass.Flags & 0x200` (pushed as
|
||||
// `has_inv_types`); and a subclass index the scan runs off the end of skips that gate and
|
||||
// offers all fourteen (wow-re auction-house §9.3 — the exhaust edge lands past the test).
|
||||
// offers all fourteen (the exhaust edge at `0x4cfb61` lands past the gate at `0x4cfb63`).
|
||||
g.set(
|
||||
"GetAuctionInvTypes",
|
||||
lua.create_function(|lua, (class_index, sub_index): (usize, usize)| {
|
||||
@@ -1137,7 +1139,8 @@ mod tests {
|
||||
|
||||
/// `0x4cf9c0` / `0x4cfab0` return nothing for a class whose `0x807060` flag is 0, and
|
||||
/// `0x4cfab0` gates a FOUND subclass on `ItemSubClass.Flags & 0x200` but pushes all fourteen
|
||||
/// when the scan runs off the end (wow-re auction-house §9.1, §9.3).
|
||||
/// when the scan runs off the end (the exhaust edge at `0x4cfb61` lands past the gate at
|
||||
/// `0x4cfb63`).
|
||||
#[test]
|
||||
fn subclass_and_inv_type_menus_follow_the_reference_gates() {
|
||||
let mut s = UiScript::new().unwrap();
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
//! reference's `0xbc6040` insertion-ordered cache (`benilla::ui_aura`), a cache position is simply a
|
||||
//! 0-based index into `model.auras["player"]`. No second structure exists, and none is needed.
|
||||
//!
|
||||
//! # Byte-verified signatures (the 1.12.1 binary, wow-5875-re `system/ui/ledger.tsv`)
|
||||
//! # Byte-verified signatures (the 1.12.1 binary)
|
||||
//!
|
||||
//! Read off the disassembly, not off a wiki — a later client changed every one of these shapes, and a
|
||||
//! wrong shape fails *silently*.
|
||||
@@ -133,7 +133,7 @@ pub struct AuraState {
|
||||
/// The player's active tracking aura — the engine mirror of the reference's tracking global
|
||||
/// (`DAT_00bc6378`): during the aura-cache rebuild the client *excludes* a tracking-effect spell
|
||||
/// (Find Minerals, Track Beasts, …) from the display cache and records it here instead;
|
||||
/// `GetTrackingTexture` is the one reader (wow-re `aura-display-pipeline.md` §3 — the
|
||||
/// `GetTrackingTexture` is the one reader (`0x4e4a20`, the
|
||||
/// `{0x2c,0x2d,0x97}` effect test). Pushed by the app's aura feed via [`UiScript::set_tracking`];
|
||||
/// `None` = no tracking active (the minimap frame hides).
|
||||
#[derive(Clone, Debug, Default, PartialEq)]
|
||||
@@ -202,8 +202,8 @@ pub(super) fn cancel_authorized(a: &AuraState) -> bool {
|
||||
/// rather than a fabricated value.
|
||||
/// The **1.12** return tuple, which is a different shape and not a prefix of the Era one: the first
|
||||
/// value is the TEXTURE, not the name. `UnitBuff` returns `(texture, applications)` (`0x519500`) and
|
||||
/// `UnitDebuff` returns `(texture, applications, dispelType)` (`0x5198f0`) — both `verified` in
|
||||
/// wow-re's ledger, and both are what the reference's own FrameXML reads
|
||||
/// `UnitDebuff` returns `(texture, applications, dispelType)` (`0x5198f0`) — both are what the
|
||||
/// reference's own FrameXML reads
|
||||
/// (`TargetFrame.lua:287-290` binds `debuff, debuffStack, debuffType` and then
|
||||
/// `SetTexture(debuff)`). Decision 1818, which also records why the Era shape was serving nobody.
|
||||
fn returns_1121(lua: &Lua, a: &AuraState, with_dispel_type: bool) -> mlua::Result<MultiValue> {
|
||||
@@ -588,7 +588,7 @@ mod tests {
|
||||
|
||||
/// The **1.12** shape, which is not a prefix of the Era one: `UnitBuff` → `(texture,
|
||||
/// applications)`, `UnitDebuff` → `(texture, applications, dispelType)`, and the FIRST value is
|
||||
/// the texture. Decision 1818; `0x519500`/`0x5198f0`, both `verified` in wow-re's ledger. The
|
||||
/// the texture. Decision 1818; `0x519500`/`0x5198f0`. The
|
||||
/// reference's own FrameXML reads exactly this (`TargetFrame.lua:287-290`), and so does every
|
||||
/// one of the 184 call sites in the addon corpus.
|
||||
#[test]
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
//! The frame `Backdrop` mechanism — byte-faithful to `wow-5875-re`'s
|
||||
//! `system/ui/scratch/backdrop-mechanism.md` (the RE derivation; every constant below cites its
|
||||
//! section). A backdrop is the tiled bg + 8-piece border a `<Backdrop>` element or Lua `SetBackdrop`
|
||||
//! The frame `Backdrop` mechanism — byte-faithful to the 1.12.1 client. A backdrop is the tiled
|
||||
//! bg + 8-piece border a `<Backdrop>` element or Lua `SetBackdrop`
|
||||
//! installs on a frame (the tooltip/dialog/panel plate). This module owns the **data** ([`Backdrop`])
|
||||
//! and the **geometry** ([`pieces`]); the Lua verbs live in [`super::object`] and the XML parse in
|
||||
//! [`crate::loader`]. The renderer is the app's job — [`pieces`] hands it screen-space quads with
|
||||
@@ -9,12 +8,12 @@
|
||||
use crate::layout::Rect;
|
||||
|
||||
/// The default `edgeSize`, **logical** px: the ctor writes device `DAT_0081c9b0 = 0.025`, which is
|
||||
/// logical 32 through the ×1/1024·0.8 logical→device transform (spec §Backdrop-object, `+0x48`, and
|
||||
/// logical 32 through the ×1/1024·0.8 logical→device transform (`+0x48`, and
|
||||
/// the usage string `edgeSize = 32`). Our layout is already in logical px, so we carry 32 directly.
|
||||
pub const DEFAULT_EDGE_SIZE: f32 = 32.0;
|
||||
|
||||
/// The `BackgroundInsets` — the bg texture is the frame rect inset by these (the border is *not*
|
||||
/// inset; it sits flush inside the frame rect). Default all 0 (spec §1). Field order matches the
|
||||
/// inset; it sits flush inside the frame rect). Default all 0 (`0x77e5f0`). Field order matches the
|
||||
/// struct offsets `+0x50 top / +0x54 bottom / +0x58 left / +0x5c right`.
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq)]
|
||||
pub struct Insets {
|
||||
@@ -24,9 +23,10 @@ pub struct Insets {
|
||||
pub bottom: f32,
|
||||
}
|
||||
|
||||
/// A frame's installed backdrop (spec §Backdrop-object: the 0x68-byte struct at `frame+0x1ac`). The
|
||||
/// A frame's installed backdrop (the 0x68-byte struct at `frame+0x1ac`). The
|
||||
/// field set is *exactly* what the compiled reader accepts — `bgFile, edgeFile, tile, tileSize,
|
||||
/// edgeSize, insets` — plus the two colors (spec §1 "Field-set summary"). Colors default to opaque
|
||||
/// edgeSize, insets` — plus the two colors; no other keys exist in the compiled reader
|
||||
/// (`0x7776e0`). Colors default to opaque
|
||||
/// **white** (the ctor `+0x60/+0x64 = 0xffffffff`); the XML `<Color>`/`<BorderColor>` *parser*
|
||||
/// separately defaults a *present-but-partial* element to black, which the loader applies.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
@@ -37,7 +37,8 @@ pub struct Backdrop {
|
||||
pub edge_file: Option<String>,
|
||||
/// `tile` (`+0x40`): tile the bg (else stretch it). Default false.
|
||||
pub tile: bool,
|
||||
/// `tileSize` (`+0x4c`), logical px; `0.0` ⇒ use `edge_size` as the tile period (spec §3). Default 0.
|
||||
/// `tileSize` (`+0x4c`), logical px; `0.0` ⇒ use `edge_size` as the tile period (`0x77f0c0`).
|
||||
/// Default 0.
|
||||
pub tile_size: f32,
|
||||
/// `edgeSize` (`+0x48`), logical px — the border piece size and the edge tiling period. Default 32.
|
||||
pub edge_size: f32,
|
||||
@@ -51,7 +52,7 @@ pub struct Backdrop {
|
||||
|
||||
impl Default for Backdrop {
|
||||
fn default() -> Self {
|
||||
// The ctor (spec §Backdrop-object, 0x77e5f0): files empty, tile 0, tileSize 0,
|
||||
// The ctor (`0x77e5f0`): files empty, tile 0, tileSize 0,
|
||||
// edgeSize logical 32, insets 0, both colors 0xffffffff.
|
||||
Self {
|
||||
bg_file: None,
|
||||
@@ -77,7 +78,7 @@ impl Backdrop {
|
||||
|
||||
/// One drawable backdrop piece (the bg, or one of the 8 border pieces). Carries four explicit corner
|
||||
/// **positions** (not a rect) so the shipped BR-inset bug's slant is representable, plus four explicit
|
||||
/// per-corner **UVs** (the TOP/BOTTOM edges are rotated 90°, spec §3). Both arrays are in
|
||||
/// per-corner **UVs** (the TOP/BOTTOM edges are rotated 90°, `0x77f0c0`). Both arrays are in
|
||||
/// `[top-left, top-right, bottom-right, bottom-left]` screen order (y-up, the layout convention).
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub struct BackdropPiece {
|
||||
@@ -87,7 +88,7 @@ pub struct BackdropPiece {
|
||||
pub uvs: [[f32; 2]; 4],
|
||||
/// The texture needs REPEAT (wrap) addressing — the edge strips run UVs `[0..N]` and a tiled bg
|
||||
/// `[0..w/period]`. The corners' UVs stay in `[0,1]` but share the edge texture, so they carry
|
||||
/// `true` too (one texture identity ⇒ the 9 pieces batch, spec §2's shared edge texture).
|
||||
/// `true` too (one texture identity ⇒ the 9 pieces batch, `0x77e8d0`'s shared edge texture).
|
||||
pub tile: bool,
|
||||
/// The bg piece (`true`) draws with [`Backdrop::bg_file`]/`bg_color`; a border piece (`false`)
|
||||
/// with `edge_file`/`border_color`. The app resolves the path + tint from the [`Backdrop`].
|
||||
@@ -155,13 +156,13 @@ fn aa_corners(x0: f32, x1: f32, y_bottom: f32, y_top: f32) -> [[f32; 2]; 4] {
|
||||
|
||||
/// Compute the backdrop's drawable pieces for a frame whose resolved rect is `frame` (y-up), in the
|
||||
/// client's paint order: the bg (layer BACKGROUND) first, then the 8 border pieces (layer BORDER) —
|
||||
/// spec §2. Empty when neither file is set. All geometry is spec §2; all UVs spec §3.
|
||||
/// `0x77e8d0`. Empty when neither file is set. All geometry is `0x77e8d0`; all UVs `0x77f0c0`.
|
||||
pub fn pieces(frame: Rect, bd: &Backdrop) -> Vec<BackdropPiece> {
|
||||
let mut out = Vec::with_capacity(9);
|
||||
let e = bd.edge_size;
|
||||
let (l, r, b, t) = (frame.left, frame.right, frame.bottom, frame.top);
|
||||
|
||||
// ── The background piece (spec §2 bg row) ──────────────────────────────────────────────────
|
||||
// ── The background piece (`0x77e8d0`, the bg anchor row) ───────────────────────────────────
|
||||
if bd.has_bg() {
|
||||
let (il, ir, it, ib) = (
|
||||
bd.insets.left,
|
||||
@@ -169,8 +170,8 @@ pub fn pieces(frame: Rect, bd: &Backdrop) -> Vec<BackdropPiece> {
|
||||
bd.insets.top,
|
||||
bd.insets.bottom,
|
||||
);
|
||||
// The four SetPoint anchors, y-up (spec §2 bg row). **⚠ BR uses the TOP inset**, not bottom
|
||||
// — the shipped 1.12.1 bug (spec §2 "The BR-inset quirk", bytes `8b 46 50` @ 0x77e9ae:
|
||||
// The four SetPoint anchors, y-up. **⚠ BR uses the TOP inset**, not bottom
|
||||
// — the shipped 1.12.1 bug ("the BR-inset quirk", bytes `8b 46 50` @ 0x77e9ae:
|
||||
// `SetPoint(8, frame+0x24, 8, −right, +top, 0)` loads `+0x50` = top, not `+0x54` = bottom).
|
||||
// Invisible whenever top==bottom (every shipping backdrop); reproduced for fidelity.
|
||||
let corners = [
|
||||
@@ -179,7 +180,7 @@ pub fn pieces(frame: Rect, bd: &Backdrop) -> Vec<BackdropPiece> {
|
||||
[r - ir, b + it], // BR ← frame BR (−right, +top) ⚠ bug: top inset
|
||||
[l + il, b + ib], // BL ← frame BL (+left, +bottom)
|
||||
];
|
||||
// Tiled bg UVs (spec §3 bg row): period = tileSize, or edgeSize when tileSize==0. When not
|
||||
// Tiled bg UVs (`0x77f0c0`): period = tileSize, or edgeSize when tileSize==0. When not
|
||||
// tiled, no SetTexCoord runs and the bg keeps [0,1] (stretch).
|
||||
let uvs = if bd.tile {
|
||||
let period = if bd.tile_size != 0.0 { bd.tile_size } else { e };
|
||||
@@ -198,10 +199,10 @@ pub fn pieces(frame: Rect, bd: &Backdrop) -> Vec<BackdropPiece> {
|
||||
});
|
||||
}
|
||||
|
||||
// ── The 8 border pieces (spec §2 edge rows + §3 UV rows) ───────────────────────────────────
|
||||
// ── The 8 border pieces (`0x77e8d0` edge rows + `0x77f0c0` UV rows) ────────────────────────
|
||||
if bd.has_border() {
|
||||
// Run lengths (spec §3): the edge strips tile `run` periods between the two corner squares
|
||||
// (no upper clamp — they TILE, never stretch); clamped to 0 below 2·edgeSize.
|
||||
// Run lengths (`0x77f0c0`): the edge strips tile `run` periods between the two corner
|
||||
// squares (no upper clamp — they TILE, never stretch); clamped to 0 below 2·edgeSize.
|
||||
let w_run = ((r - l) / e - 2.0).max(0.0);
|
||||
let h_run = ((t - b) / e - 2.0).max(0.0);
|
||||
|
||||
@@ -408,7 +409,7 @@ mod tests {
|
||||
assert_eq!(br.uvs[2], [1.0, 1.0]);
|
||||
}
|
||||
|
||||
// The BR-inset quirk (spec §2 ⚠, bytes 8b 46 50 @ 0x77e9ae): with top != bottom the bg's
|
||||
// The BR-inset quirk (bytes 8b 46 50 @ 0x77e9ae): with top != bottom the bg's
|
||||
// BOTTOMRIGHT corner rides the TOP inset, not the bottom. Asymmetric insets make it assertable.
|
||||
#[test]
|
||||
fn bg_bottomright_rides_top_inset_bug() {
|
||||
@@ -433,7 +434,8 @@ mod tests {
|
||||
}
|
||||
|
||||
// Run clamp at tiny sizes: a frame narrower/shorter than 2·edgeSize clamps both runs to 0 (no
|
||||
// negative tiling), so the edge strips carry a zero-length run (spec §3, "clamped to 0 if < 0").
|
||||
// negative tiling), so the edge strips carry a zero-length run (`0x77f0c0`, clamped to 0 if
|
||||
// < 0).
|
||||
#[test]
|
||||
fn run_clamps_to_zero_below_two_edges() {
|
||||
// 20×20 frame, edgeSize 16 ⇒ width/16 - 2 = -0.75 → 0.
|
||||
|
||||
@@ -131,8 +131,7 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
|
||||
// BankButtonIDToInvSlotID(id, isBag) — the pure button→live-slot map (module doc).
|
||||
//
|
||||
// CARVED (`0x4f8530`, 118 bytes; wow-re `system/ui/scratch/bank-button-invslot-law.md`, §5
|
||||
// trio + orchestrator byte arbitration 2026-08-31). Three findings, and two of them corrected
|
||||
// `0x4f8530` (118 bytes). Three findings, and two of them corrected
|
||||
// this binding:
|
||||
//
|
||||
// 1 · **The arithmetic is `+59` / `+39`, and the bag arm takes the CONTAINER id 5..10** — not
|
||||
@@ -352,8 +351,8 @@ mod tests {
|
||||
/// 39..62, bank bags 63..68).
|
||||
///
|
||||
/// The bag arm's numbering is checked here because getting it wrong reads six slots off the
|
||||
/// end of the band and draws an empty bag row — which is what this binding did until the
|
||||
/// `0x4f8530` carve.
|
||||
/// end of the band and draws an empty bag row — which is what this binding did before
|
||||
/// `0x4f8530` was read.
|
||||
#[test]
|
||||
fn bank_button_to_inv_slot() {
|
||||
let s = UiScript::new().unwrap();
|
||||
@@ -390,8 +389,7 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
/// The three things the `0x4f8530` carve corrected, each of which this binding had wrong or
|
||||
/// invented (wow-re `scratch/bank-button-invslot-law.md`).
|
||||
/// The three things `0x4f8530` corrected, each of which this binding had wrong or invented.
|
||||
#[test]
|
||||
fn bank_button_to_inv_slot_follows_the_carved_abi() {
|
||||
let s = UiScript::new().unwrap();
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
//! The battleground **position** family (decision 1980; wow-re
|
||||
//! `system/ui/scratch/worldmap-arrow-and-positions.md` §3): the six verbs the stock
|
||||
//! The battleground **position** family (decision 1980; fed by the wire handler `0x4aad40` for
|
||||
//! opcode `0x2E9`): the six verbs the stock
|
||||
//! `WorldMapFrame.lua` and `Blizzard_BattlefieldMinimap.lua` poll to place teammates and the
|
||||
//! flag carrier on the map.
|
||||
//!
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
//! The battleground **list and queue** family (decision 1974; wow-re
|
||||
//! `system/ui/scratch/battlefield-verb-family.md` §2.1, §2.2, §3.1–3.6, §5.2): the fourteen
|
||||
//! The battleground **list and queue** family (decision 1974): the fourteen
|
||||
//! verbs the stock `BattlefieldFrame.lua` and `Minimap.xml` call, over the instance list and the
|
||||
//! three queue slots the app pushes. The scoreboard half of the same TU is
|
||||
//! [`super::battlefield_score`]; `AcceptBattlefieldPort` is [`super::dialog_verbs`]'s.
|
||||
@@ -7,15 +6,16 @@
|
||||
//! ## What is the app's and what is the VM's
|
||||
//!
|
||||
//! The reference keeps three caches in `BattlefieldInfo.cpp`: the instance-id vector the
|
||||
//! `SMSG_BATTLEFIELD_LIST` handler fills (§2.1), the fixed three-slot queue array
|
||||
//! `SMSG_BATTLEFIELD_STATUS` writes (§2.2), and four scalars beside them — the battlemaster
|
||||
//! guid, the listed map, the bracket-adjusted level pair, and the **selected instance id**. Every
|
||||
//! `SMSG_BATTLEFIELD_LIST` handler fills (`0xb6e860`, `0x4aa6c0`), the fixed three-slot queue array
|
||||
//! `SMSG_BATTLEFIELD_STATUS` writes (`0xb6e9d0`, `0x4aa850`), and four scalars beside them — the
|
||||
//! battlemaster guid, the listed map, the bracket-adjusted level pair, and the **selected instance
|
||||
//! id**. Every
|
||||
//! verb below reads those and nothing else. The app owns the wire, the clock and the Map.dbc
|
||||
//! resolves, so it pushes the list ([`BattlefieldListView`]) when a list lands and the slots
|
||||
//! ([`BattlefieldQueueSlot`], with every clock-shaped value already in milliseconds) every frame;
|
||||
//! the VM owns the selection (`[0xb6eba0]`), the index arithmetic and the getters' shapes.
|
||||
//!
|
||||
//! ## The selection is a value, not an index (§3.5)
|
||||
//! ## The selection is a value, not an index
|
||||
//!
|
||||
//! `SetSelectedBattlefield(n)` stores the **instance id** at list position `n`, and
|
||||
//! `GetSelectedBattlefield()` scans the current list for that id — so a fresh list between the
|
||||
@@ -23,7 +23,7 @@
|
||||
//! GetSelectedBattlefield()` highlight then follows. A client that remembered an index would
|
||||
//! diverge the moment a list reorders.
|
||||
//!
|
||||
//! ## Shapes (§3, §3.1)
|
||||
//! ## Shapes
|
||||
//!
|
||||
//! Every required index is shape A (`lua_isnumber`, so a numeric string passes; truncated toward
|
||||
//! zero; a non-number raises the binding's own `Usage:`), 1-based with the reference's unsigned
|
||||
@@ -38,7 +38,7 @@ use mlua::{Lua, MultiValue, Value};
|
||||
use super::binding_abi::{bool_or_default, flag, number_arg};
|
||||
use super::Model;
|
||||
|
||||
/// One queue slot as the app pushes it — the reference's `0x20`-byte slot (§2.2) plus the map
|
||||
/// One queue slot as the app pushes it — the reference's `0x20`-byte slot (`0xb6e9d0`) plus the map
|
||||
/// name its `GetBattlefieldStatus` resolves off Map.dbc, and the three clock-shaped fields
|
||||
/// already reduced against the app's clock.
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Default)]
|
||||
@@ -63,7 +63,7 @@ pub struct BattlefieldQueueSlot {
|
||||
pub time_waited_ms: u32,
|
||||
}
|
||||
|
||||
/// The Map.dbc half of `GetBattlefieldInfo` (§3.4), resolved by the app for the listed map.
|
||||
/// The Map.dbc half of `GetBattlefieldInfo` (`0x4ab0b0`), resolved by the app for the listed map.
|
||||
#[derive(Clone, Debug, PartialEq, Default)]
|
||||
pub struct BattlefieldMapInfo {
|
||||
/// The localized map name (value 1).
|
||||
@@ -93,7 +93,8 @@ pub struct BattlefieldListView {
|
||||
pub bracket_max: u32,
|
||||
/// The listed map's row, `None` when the id resolves to no row (the second zero-values gate).
|
||||
pub info: Option<BattlefieldMapInfo>,
|
||||
/// The listed map row's group-queue flag (`[row+0xa0]`, §3.6) — `CanJoinBattlefieldAsGroup`.
|
||||
/// The listed map row's group-queue flag (`[row+0xa0]`, `0x4ac380`) —
|
||||
/// `CanJoinBattlefieldAsGroup`.
|
||||
pub group_queue: bool,
|
||||
}
|
||||
|
||||
@@ -114,7 +115,7 @@ fn player_exists(model: &Model) -> bool {
|
||||
}
|
||||
|
||||
/// `[slot+0x04]` → the status string, through the reference's four-entry jump table with its
|
||||
/// `"error"` default (§3.2).
|
||||
/// `"error"` default (`0x4ab604`).
|
||||
fn status_text(status: u32) -> &'static str {
|
||||
match status {
|
||||
0 => "none",
|
||||
@@ -127,12 +128,13 @@ fn status_text(status: u32) -> &'static str {
|
||||
|
||||
impl super::UiScript {
|
||||
/// Push the instance list. The selection (`[0xb6eba0]`) is left alone — a new list does not
|
||||
/// clear it, it only decides whether `GetSelectedBattlefield` still finds it (§3.5).
|
||||
/// clear it, it only decides whether `GetSelectedBattlefield` still finds it (`0x4ab360`).
|
||||
pub fn set_battlefield_list(&mut self, list: BattlefieldListView) {
|
||||
self.model_mut().battlefield_list = list;
|
||||
}
|
||||
|
||||
/// The world-enter reset's half of this family (§8): the selection cleared with the list.
|
||||
/// The world-enter reset's half of this family (`0x4a9db0`): the selection cleared with the
|
||||
/// list.
|
||||
pub fn reset_battlefield_selection(&mut self) {
|
||||
self.model_mut().battlefield_selected = 0;
|
||||
}
|
||||
@@ -152,7 +154,7 @@ impl super::UiScript {
|
||||
|
||||
/// `JoinBattlefield` calls since the last drain: `(instance id, 0 = first available; as a
|
||||
/// group)`. The app adds the map, the cached battlemaster guid (which picks the opcode) and
|
||||
/// the group-size refusal (§5.2).
|
||||
/// the group-size refusal (`0x4a9f60`).
|
||||
pub fn take_battlefield_join_requests(&mut self) -> Vec<(u32, bool)> {
|
||||
std::mem::take(&mut self.model_mut().battlefield_join_requests)
|
||||
}
|
||||
@@ -177,7 +179,7 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
)?;
|
||||
|
||||
// `GetBattlefieldInfo()` — 0 args, never raises; ZERO values behind either gate (no player, no
|
||||
// map row), else nine (§3.4).
|
||||
// map row), else nine (`0x4ab0b0`).
|
||||
g.set(
|
||||
"GetBattlefieldInfo",
|
||||
lua.create_function(|lua, ()| {
|
||||
@@ -205,7 +207,7 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
)?;
|
||||
|
||||
// `GetBattlefieldInstanceInfo(index)` — raises with GetBattlefieldInfo's usage string (the
|
||||
// shipped mislabel, §3.1); no player or out of range → zero values; else the instance id,
|
||||
// shipped mislabel, `0x845c48`); no player or out of range → zero values; else the instance id,
|
||||
// pushed SIGNED (`fild dword`).
|
||||
g.set(
|
||||
"GetBattlefieldInstanceInfo",
|
||||
@@ -227,7 +229,8 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
|
||||
// `JoinBattlefield(index [, asGroup])` — arg 1 shape A, arg 2 the never-raising optional
|
||||
// boolean reader; the instance is `index−1 < count ? list[index−1] : 0` (0 = first
|
||||
// available, and what an out-of-range index — 0 included — degrades to), §5.2. The refusal
|
||||
// available, and what an out-of-range index — 0 included — degrades to), `0x4a9f60`. The
|
||||
// refusal
|
||||
// leg and the opcode choice are the app's and invisible to Lua.
|
||||
g.set(
|
||||
"JoinBattlefield",
|
||||
@@ -246,7 +249,7 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
g.set("CloseBattlefield", lua.create_function(|_, ()| Ok(()))?)?;
|
||||
|
||||
// `SetSelectedBattlefield(index)` — stores the instance id at that position, or 0 when out
|
||||
// of range (§3.5).
|
||||
// of range (`0x4ab300`).
|
||||
g.set(
|
||||
"SetSelectedBattlefield",
|
||||
lua.create_function(|lua, index: Value| {
|
||||
@@ -274,7 +277,7 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
})?,
|
||||
)?;
|
||||
|
||||
// `GetBattlefieldStatus(index)` — five values on EVERY leg (§3.2): off 1..3 it is
|
||||
// `GetBattlefieldStatus(index)` — five values on EVERY leg (`0x4ab5c7`): off 1..3 it is
|
||||
// `(nil, nil, 0, 0, 0)`, never a raise and never zero values.
|
||||
g.set(
|
||||
"GetBattlefieldStatus",
|
||||
@@ -305,8 +308,9 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
})?,
|
||||
)?;
|
||||
|
||||
// The three per-slot clock-shaped getters (§3.3): shape A index, 0 off 1..3, one number
|
||||
// always. The app reduces each against its clock; the values arrive in ms.
|
||||
// The three per-slot clock-shaped getters (`0x4ab620`/`0x4ab790`/`0x4ab820`): shape A index,
|
||||
// 0 off 1..3, one number always. The app reduces each against its clock; the values arrive
|
||||
// in ms.
|
||||
for (name, usage, read) in [
|
||||
(
|
||||
"GetBattlefieldPortExpiration",
|
||||
@@ -365,8 +369,8 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
})?,
|
||||
)?;
|
||||
|
||||
// `CanJoinBattlefieldAsGroup()` — `1` or nil off the listed map's `+0xa0` flag (§3.6). The
|
||||
// join-time size check reads a different column and lives with the sender.
|
||||
// `CanJoinBattlefieldAsGroup()` — `1` or nil off the listed map's `+0xa0` flag (`0x4ac380`).
|
||||
// The join-time size check reads a different column and lives with the sender.
|
||||
g.set(
|
||||
"CanJoinBattlefieldAsGroup",
|
||||
lua.create_function(|lua, ()| {
|
||||
|
||||
@@ -1,18 +1,17 @@
|
||||
//! The battleground **scoreboard** family (decision 1972; wow-re
|
||||
//! `system/ui/scratch/battlefield-verb-family.md` §2.3, §3.7–3.9, §5.1, §5.3, §6): the eleven
|
||||
//! The battleground **scoreboard** family (decision 1972): the eleven
|
||||
//! verbs the stock `WorldStateFrame.lua` score frame calls, over a board the app pushes.
|
||||
//!
|
||||
//! ## What is the app's and what is the VM's
|
||||
//!
|
||||
//! The wire (`MSG_PVP_LOG_DATA`) carries rows in wire order with GUIDs; the reference resolves
|
||||
//! every name through its name cache first (§6.1 — the board is not rebuilt, and the event not
|
||||
//! fired, until the last name arrives) and derives each row's **team** from the resolved race
|
||||
//! (§6.2: `0` Horde, `1` Alliance, `-1` neither). That resolution is the app's — it owns the name
|
||||
//! every name through its name cache first (`0x4aa580` — the board is not rebuilt, and the event
|
||||
//! not fired, until the last name arrives) and derives each row's **team** from the resolved race
|
||||
//! (`0` Horde, `1` Alliance, `-1` neither). That resolution is the app's — it owns the name
|
||||
//! cache and the race tables — so the app pushes [`BattlefieldScores`] with names, teams, race and
|
||||
//! class strings already in place. The VM owns what the reference's `0x4aa200` rebuild owns: the
|
||||
//! faction filter, the sort, the filtered count, and the getters' shapes.
|
||||
//!
|
||||
//! ## The sort, and the filter that is a sort (§6.2.1, §6.3)
|
||||
//! ## The sort, and the filter that is a sort (`0x4aa350`, `0x4aa200`)
|
||||
//!
|
||||
//! Rows are ordered by: the filter's team first (only when a filter is set), then killing blows
|
||||
//! descending, deaths ascending, honor gained descending, name ascending. `GetNumBattlefieldScores`
|
||||
@@ -20,7 +19,7 @@
|
||||
//! by the **wire** count — an index past the filtered count still answers a real row of the other
|
||||
//! team, exactly as the reference does, because the filter sorts rather than compacts.
|
||||
//!
|
||||
//! Every raise is the reference's own `Usage:` (§3.1); `SetBattlefieldScoreFaction` never raises.
|
||||
//! Every raise is the reference's own `Usage:`; `SetBattlefieldScoreFaction` never raises.
|
||||
|
||||
use mlua::{Lua, MultiValue, Value};
|
||||
|
||||
@@ -75,13 +74,13 @@ pub(crate) struct ScoreBoard {
|
||||
/// `SetBattlefieldScoreFaction`'s store: `-1` every team (the reset value), `0`, `1`.
|
||||
pub(crate) filter: i32,
|
||||
pub(crate) order: Vec<usize>,
|
||||
/// The filtered count (§6.3) — the rows whose team equals the filter, or all with `-1`.
|
||||
/// The filtered count (`0xb6ebc4`) — the rows whose team equals the filter, or all with `-1`.
|
||||
pub(crate) filtered: usize,
|
||||
}
|
||||
|
||||
impl Default for ScoreBoard {
|
||||
/// The filter starts at `-1`, every team — the value the status-3 arm resets it to
|
||||
/// (`or ecx,-1; call 0x4aa5a0`, §4.2).
|
||||
/// (`or ecx,-1; call 0x4aa5a0`).
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
scores: BattlefieldScores::default(),
|
||||
@@ -93,7 +92,7 @@ impl Default for ScoreBoard {
|
||||
}
|
||||
|
||||
impl ScoreBoard {
|
||||
/// `0x4aa200`'s recount and sort (§6.2/§6.2.1), over the pushed rows.
|
||||
/// `0x4aa200`'s recount and sort, over the pushed rows.
|
||||
fn rebuild(&mut self) {
|
||||
let rows = &self.scores.rows;
|
||||
let filter = self.filter;
|
||||
@@ -120,7 +119,7 @@ impl ScoreBoard {
|
||||
|
||||
fn row(&self, index_1based: i32) -> Option<&BattlefieldScoreRow> {
|
||||
let i = usize::try_from(index_1based).ok()?.checked_sub(1)?;
|
||||
// Bounded by the WIRE count, over the sorted order (§6.3).
|
||||
// Bounded by the WIRE count (`0xb6ebc0`), over the sorted order.
|
||||
self.order.get(i).map(|&r| &self.scores.rows[r])
|
||||
}
|
||||
}
|
||||
@@ -128,7 +127,7 @@ impl ScoreBoard {
|
||||
impl super::UiScript {
|
||||
/// Push the resolved board. The filter is the VM's and survives a push; the order is rebuilt.
|
||||
/// The app fires `UPDATE_BATTLEFIELD_SCORE` itself after this, so the reference's ordering
|
||||
/// against `UPDATE_BATTLEFIELD_STATUS` on the status-3 message holds (§4.2).
|
||||
/// against `UPDATE_BATTLEFIELD_STATUS` on the status-3 message holds (`0x4aaa5a`/`0x4aab05`).
|
||||
pub fn set_battlefield_scores(&mut self, scores: BattlefieldScores) {
|
||||
let mut model = self.model_mut();
|
||||
model.battlefield_board.scores = scores;
|
||||
@@ -180,7 +179,7 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
})?,
|
||||
)?;
|
||||
|
||||
// `GetBattlefieldScore(index)` — nine values on every leg (§3.7): name, killingBlows,
|
||||
// `GetBattlefieldScore(index)` — nine values on every leg (`0x4ab9d0`): name, killingBlows,
|
||||
// honorableKills, deaths, honorGained, faction, rank, race, class; the fail leg is
|
||||
// `nil, 0,0,0,0,0,0, nil, nil`. Bounded by the wire count over the sorted order.
|
||||
g.set(
|
||||
@@ -237,7 +236,7 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
|
||||
// `SetBattlefieldScoreFaction([faction])` — never raises: a non-number means -1; only
|
||||
// {-1, 0, 1} store, and a stored value recounts, re-sorts and fires UPDATE_BATTLEFIELD_SCORE
|
||||
// (§3.8) — here on the deferred lane, the next dispatch.
|
||||
// (`0x4abc90`) — here on the deferred lane, the next dispatch.
|
||||
g.set(
|
||||
"SetBattlefieldScoreFaction",
|
||||
lua.create_function(|lua, faction: Option<Value>| {
|
||||
@@ -293,7 +292,7 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
|
||||
// `GetBattlefieldStatData(playerIndex, statIndex)` — both required numbers; one number on
|
||||
// every leg: the row's stat, or 0 for a bad row or a stat index outside 1..=8 (the reference
|
||||
// admits 9 and reads one dword past its block, §3.9's anomaly — ours answers 0 there).
|
||||
// (`0x4abdc0`) admits 9 and reads one dword past its block — ours answers 0 there).
|
||||
g.set(
|
||||
"GetBattlefieldStatData",
|
||||
lua.create_function(|lua, (player, stat): (Value, Value)| {
|
||||
@@ -312,7 +311,7 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
)?;
|
||||
|
||||
// `RequestBattlefieldScoreData()` — 0 args, 0 returns; the app sends `MSG_PVP_LOG_DATA` empty
|
||||
// under the client's 5000 ms throttle (§5.1).
|
||||
// under the client's 5000 ms throttle (`0x4aa170`).
|
||||
g.set(
|
||||
"RequestBattlefieldScoreData",
|
||||
lua.create_function(|lua, ()| {
|
||||
@@ -322,7 +321,8 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
})?,
|
||||
)?;
|
||||
|
||||
// `LeaveBattlefield()` — a gate, then a payload (§5.3): nothing at all until the scoreboard's
|
||||
// `LeaveBattlefield()` — a gate, then a payload (`0x4abe66`): nothing at all until the
|
||||
// scoreboard's
|
||||
// "ended" byte has arrived; then `CMSG_LEAVE_BATTLEFIELD` with the active slot's map (the
|
||||
// app's, which holds the queue).
|
||||
g.set(
|
||||
@@ -389,8 +389,9 @@ mod tests {
|
||||
s
|
||||
}
|
||||
|
||||
/// The sort law (§6.2.1): killing blows descending, deaths ascending, honor descending, name;
|
||||
/// a filter puts its team first and shrinks the count without compacting the array (§6.3).
|
||||
/// The sort law (`0x4aa350`): killing blows descending, deaths ascending, honor descending,
|
||||
/// name; a filter puts its team first and shrinks the count without compacting the array
|
||||
/// (`0xb6ebc4`/`0xb6ebc0`).
|
||||
#[test]
|
||||
fn the_board_sorts_like_the_client_and_the_filter_is_a_sort() {
|
||||
let s = board();
|
||||
|
||||
@@ -24,8 +24,7 @@
|
||||
//! it — declining simply drops the question.
|
||||
//!
|
||||
//! All three decisions are **client-local**: the client reads the cached item template's `bonding`
|
||||
//! and defers its own send. No packet raises any of them (VERIFIED, wow-re
|
||||
//! `system/object-layer/scratch/bind-confirm-law.md`).
|
||||
//! and defers its own send. No packet raises any of them.
|
||||
//!
|
||||
//! The app owns the pending records, so this module holds only the intents.
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
//! The **registered-binding argument/error ABI** — the marshalling contract every one of build
|
||||
//! 5875's ~700 registered Lua C-bindings opens with, in one place because getting it wrong is
|
||||
//! wrong 700 times (wow-re `ui/scratch/binding-arg-error-contract.md`, §5-cross-checked with
|
||||
//! orchestrator byte-arbitration).
|
||||
//! wrong 700 times.
|
||||
//!
|
||||
//! ## The three helpers the reference uses, and what they accept
|
||||
//!
|
||||
@@ -20,8 +19,7 @@
|
||||
//! `0x6f4940` is `luaL_error`, and it **does not return**: `luaL_where` + `lua_pushvfstring` +
|
||||
//! `lua_concat` + `lua_error`, whose own chain (`0x6f4440` → `0x6fc780` → `0x6f5d80`) ends in a
|
||||
//! CRT `longjmp` or `exit(1)` on both legs. The `xor eax,eax; ret` that MSVC emits after every
|
||||
//! such call inside a binding is **unreachable boilerplate**, not a "returns zero values" arm — a
|
||||
//! reading five committed wow-re notes had inverted, corrected at the bytes on 2026-08-11.
|
||||
//! such call inside a binding is **unreachable boilerplate**, not a "returns zero values" arm.
|
||||
//!
|
||||
//! So a binding's bad-argument path **abandons the caller's statement** and unwinds to the
|
||||
//! enclosing protected call. It returns neither `nil` nor zero values, and a client that answers
|
||||
@@ -75,8 +73,7 @@ pub(crate) fn number_arg(lua: &Lua, v: Value, usage: &'static str) -> mlua::Resu
|
||||
/// a negative and NaN are false, and `1`, `0.5` and `"1"` are true.
|
||||
///
|
||||
/// The distinction from [`number_arg`] is the whole point: shape A *raises* `Usage:` on a
|
||||
/// non-number, shape D shrugs. Which shape an argument takes is per binding — wow-re
|
||||
/// `ui/scratch/tooltip-nameonly-p4-census.md` §3 for this one.
|
||||
/// non-number, shape D shrugs. Which shape an argument takes is per binding.
|
||||
pub(crate) fn positive_number_flag(lua: &Lua, v: Value) -> mlua::Result<bool> {
|
||||
Ok(lua.coerce_number(v)?.is_some_and(|n| n > 0.0))
|
||||
}
|
||||
@@ -86,9 +83,9 @@ pub(crate) fn positive_number_flag(lua: &Lua, v: Value) -> mlua::Result<bool> {
|
||||
/// unparseable string all land on **`0.0`** and the binding completes.
|
||||
///
|
||||
/// This is the counterpart to [`number_arg`] (shape A, which raises `Usage:`), and which shape a
|
||||
/// given argument takes is **per binding, not a global law** — wow-re
|
||||
/// `scratch/numeric-arg-coercion-law.md`, which censused all 408 widget-registrar entries and
|
||||
/// found 110 gated positions against 64 ungated ones. The clustering is the useful part: C is the
|
||||
/// given argument takes is **per binding, not a global law** — a census of all 408
|
||||
/// widget-registrar entries found 110 gated positions against 64 ungated ones. The clustering is
|
||||
/// the useful part: C is the
|
||||
/// colour/coordinate tuples (`Set*Color`'s r/g/b, `SetTexCoord`, `SetPosition`), A is the single
|
||||
/// scalar setters (`SetAlpha`, `SetWidth`, `SetValue`, `SetID`). Do not reach for this one because
|
||||
/// an argument "looks optional" — check the census.
|
||||
@@ -122,7 +119,7 @@ pub(crate) fn string_arg(lua: &Lua, v: Value, usage: &'static str) -> mlua::Resu
|
||||
///
|
||||
/// The pair is 1717's rule applied to string positions — *which* shape a position takes is settled
|
||||
/// per binding, never generalised — and the model-pane and region constructors are the two ends of
|
||||
/// it, verified together (wow-re `ui/scratch/xml-template-name-lookup.md` §5.2):
|
||||
/// it, verified together:
|
||||
///
|
||||
/// | position | fetch | a table there |
|
||||
/// |---|---|---|
|
||||
@@ -131,8 +128,7 @@ pub(crate) fn string_arg(lua: &Lua, v: Value, usage: &'static str) -> mlua::Resu
|
||||
/// | `CreateFrame` `name`, `inherits` | `0x6f3690` **unguarded** | absent — this fn |
|
||||
///
|
||||
/// **The discriminator is not the argument's type; it is whether the binding TESTS its parser's
|
||||
/// return.** That is the sentence `numeric-arg-coercion-law.md` §6 had wrong ("an unrecognised
|
||||
/// string argument raises"), refuted by this round.
|
||||
/// return.**
|
||||
///
|
||||
/// A **number** is accepted and stringified, exactly as `lua_tostring` coerces it — so
|
||||
/// `CreateFrame("Frame", 5)` names the frame `"5"`. Callers that must *not* coerce a number (the
|
||||
@@ -168,7 +164,7 @@ pub(crate) fn optional_string(lua: &Lua, v: &Value) -> Option<String> {
|
||||
/// This is `Option<String>`'s conversion with exactly one arm changed: a **string** becomes its
|
||||
/// lossy text. Every other type behaves as before — absent and `nil` are `None`, a number
|
||||
/// coerces, a table or a function raises mlua's own conversion error — so the raise/absent split
|
||||
/// each binding already had (§ [`optional_string`]'s table) is untouched.
|
||||
/// each binding already had ([`optional_string`]'s table) is untouched.
|
||||
pub(crate) fn text_arg(lua: &Lua, v: Option<Value>) -> mlua::Result<Option<String>> {
|
||||
match v {
|
||||
None | Some(Value::Nil) => Ok(None),
|
||||
@@ -178,10 +174,8 @@ pub(crate) fn text_arg(lua: &Lua, v: Option<Value>) -> mlua::Result<Option<Strin
|
||||
}
|
||||
|
||||
/// `GetBoolOrDefault` (`0x6f1c10`) — the reference's **boolean argument** coercion, which is *not*
|
||||
/// Lua truthiness and gets three arms backwards from the obvious reading. Byte-VERIFIED: wow-re
|
||||
/// `system/ui/scratch/action-bar-toggles.md` §2.1 re-derives the whole jump table at `0x6f1ce8`
|
||||
/// (`ui.md`, `tooltip-content-law.md` and `object-layer/scratch/helm-cloak-hide.md` cite the same
|
||||
/// helper).
|
||||
/// Lua truthiness and gets three arms backwards from the obvious reading. The whole jump table
|
||||
/// sits at `0x6f1ce8`.
|
||||
///
|
||||
/// `v` is `None` for an **absent** argument (`LUA_TNONE` = −1, unsigned-compares above the table's
|
||||
/// bound and takes the default arm). mlua collapses "missing" into `Value::Nil` for a plain
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
//! The `Button`/`CheckButton` method surface — per-kind behavior over the frame arena
|
||||
//! (`CSimpleButton` `0x6eeab0` / `CSimpleCheckbox` `0x6eeb30`).
|
||||
//!
|
||||
//! Grounded in wow-re's byte-verified LoadXML tables (RF-28): the four state textures
|
||||
//! Grounded in the client's LoadXML table (`0x7788c0`): the four state textures
|
||||
//! (`Normal/Pushed/Disabled/Highlight`), the `ButtonText` fontstring + `text` attribute, the
|
||||
//! `OnClick` script slot (`+0x4cc`); CheckButton runs Button's loader first and adds
|
||||
//! `CheckedTexture`/`DisabledCheckedTexture` + the `checked` bool (`+0x4dc`). Which texture *shows*
|
||||
@@ -111,8 +111,7 @@ fn normal_font_justify(model: &Model, bs: &ButtonState) -> Justify {
|
||||
/// (`SetFontString`): anchor it to the button **only if it has no anchor of its own** and **by the
|
||||
/// NORMAL font's justify word** (`[button+0x390]`: LEFT→LEFT, RIGHT→RIGHT, else CENTER — never
|
||||
/// the string's own `+0x120`, which on a fresh string is the ctor's CENTER), then apply the
|
||||
/// button's per-state font to it on the spot (`0x779810`). wow-re
|
||||
/// `system/ui/scratch/resize-bounds-and-button-fontstring.md` §5.2, every clause VERIFIED.
|
||||
/// button's per-state font to it on the spot (`0x779810`).
|
||||
///
|
||||
/// The word's source is the whole bug this fixes (decision 1996): a `UIMenuButtonTemplate` row
|
||||
/// has no `<ButtonText>` — its label is born from the reference's `UIMenu_AddButton` →
|
||||
@@ -159,8 +158,8 @@ fn adopt_label(model: &mut Model, owner: FrameHandle, rh: RegionHandle) {
|
||||
/// label reports that state's font and ours still reports the normal one. The paint is unaffected
|
||||
/// either way, and no corpus site reads a label's font mid-state.
|
||||
///
|
||||
/// `font::repaint` honours the severance mask, so a `<FontHeight>` or `SetTextColor` the label set
|
||||
/// for itself survives the link (the rule wow-re pinned in `font-object-lua-surface.md`), and the
|
||||
/// `font::repaint` honours the severance mask (`CSimpleFontString+0xd4`), so a `<FontHeight>` or
|
||||
/// `SetTextColor` the label set for itself survives the link, and the
|
||||
/// local justify goes behind the same mask.
|
||||
fn apply_normal_font(model: &mut Model, owner: FrameHandle) {
|
||||
let (rh, name, local_justify) = {
|
||||
@@ -276,8 +275,7 @@ fn with_button<T>(
|
||||
///
|
||||
/// **The restore is NOT symmetric, and this comment used to claim it was** — *"one array and one
|
||||
/// writer, so an addon's `DisableDrawLayer("HIGHLIGHT")` is undone by the next `Enable()`"*. Both
|
||||
/// halves are false (wow-re `scratch/button-disabled-state-texture-law.md` §7, sharpened
|
||||
/// 2026-09-09 off `0x779160` read contiguously for exactly this question). The indexed form
|
||||
/// halves are false (`0x779160`). The indexed form
|
||||
/// `[reg + 4*reg + 0x198]` has **three** sites image-wide — `0x76a717` (=1), `0x76a737` (=0) and
|
||||
/// `DrawLayer 0x76b3a6`'s read — so there are **two** writers; and the enable arm calls
|
||||
/// *neither* helper. It reaches the ON one only one hop away and only when the button is the
|
||||
@@ -328,8 +326,8 @@ fn set_enabled(lua: &Lua, this: &Table, on: bool) -> mlua::Result<()> {
|
||||
|
||||
/// Run one of the engine's own state edges over frame `h`, if it is a live Button.
|
||||
///
|
||||
/// The reference's edge set is exactly seven `SetButtonState` call sites and **six of them are
|
||||
/// engine-side** (wow-re `scratch/button-state-edge-set.md`, VERIFIED): Enable, Disable, the hide
|
||||
/// The reference's edge set is exactly seven `SetButtonState 0x779790` call sites and **six of
|
||||
/// them are engine-side**: Enable, Disable, the hide
|
||||
/// notify, mouse-down, mouse-up and the drag-threshold crossing. There is **no enter or leave
|
||||
/// edge** — which is why the pointer path no longer calls anything here when the cursor crosses a
|
||||
/// button's boundary (decision 2134).
|
||||
@@ -357,8 +355,8 @@ pub(super) fn edge(model: &mut Model, h: FrameHandle, f: impl FnOnce(&mut Button
|
||||
/// ```
|
||||
///
|
||||
/// — so **a DISABLED Button runs neither script**, and `0x7794b0`'s hover sound is the only thing
|
||||
/// past the branch (wow-re `scratch/button-state-edge-set.md` §3.1, VERIFIED; the base pair's
|
||||
/// script slots `+0x140`/`+0x148` are `ledger.tsv:7944/7945`). Every path that delivers a hover
|
||||
/// past the branch; the base pair's
|
||||
/// script slots are `+0x140`/`+0x148`. Every path that delivers a hover
|
||||
/// edge dispatches through the object's own vtable, so there is no way around the guard: the hover
|
||||
/// walk (`0x766218`–`0x76623c`, `[vt+0x50]` then `[vt+0x4c]`), `SetMouseFocus 0x764dc0`
|
||||
/// (`0x764ddd push 0`), and the hide/removal tail (`0x764cce mov edx,[edi]; push 1;
|
||||
@@ -370,7 +368,7 @@ pub(super) fn edge(model: &mut Model, h: FrameHandle, f: impl FnOnce(&mut Button
|
||||
/// notify — so `GetMouseFocus()` answers a disabled button; it is the scripts alone that stay
|
||||
/// quiet. This is what makes AtlasLoot's unset QuickLook buttons safe on the reference: their
|
||||
/// `<OnEnter>` guards with `if this:IsEnabled() then`, which is a NUMBER `1`/`0` and therefore
|
||||
/// always truthy (`scratch/button-enabled-state.md`), so the body would index a nil
|
||||
/// always truthy (`IsEnabled 0x7800b0`), so the body would index a nil
|
||||
/// `QuickLooks[n]` — the reference never runs it at all.
|
||||
///
|
||||
/// `None` (the cursor over no frame) and every non-Button kind notify normally.
|
||||
@@ -498,8 +496,8 @@ fn free_outgoing(lua: &Lua, outgoing: Option<RegionHandle>, incoming: RegionHand
|
||||
|
||||
/// The shared body of `Set<State>Texture(texture | "path" | nil)` / `(r, g, b [, a])`.
|
||||
///
|
||||
/// The reference forks on `lua_type(L, 2)` into four legs (`0x781970`, carved instruction by
|
||||
/// instruction in wow-re `button-state-texture-path-setter.md` §1). **Two of them never touch the
|
||||
/// The reference forks on `lua_type(L, 2)` into four legs (`0x781970`).
|
||||
/// **Two of them never touch the
|
||||
/// slot's own region at all**, so they are resolved *before* [`ensure_slot`] — which would
|
||||
/// otherwise lazily build a region for a call whose entire purpose is to replace or drop one.
|
||||
fn set_slot_texture(lua: &Lua, this: &Table, slot: Slot, args: &MultiValue) -> mlua::Result<()> {
|
||||
@@ -618,13 +616,13 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
texture_pair(lua, &m, "Disabled", Slot::Disabled)?;
|
||||
texture_pair(lua, &m, "Highlight", Slot::Highlight)?;
|
||||
|
||||
// SetText/GetText target the ButtonText fontstring (RF-28: the `text` attr routes there).
|
||||
// SetText/GetText target the ButtonText fontstring (`0x778dc0`: the `text` attr routes there).
|
||||
m.set(
|
||||
"SetText",
|
||||
lua.create_function(|lua, (this, text): (Table, Option<Value>)| {
|
||||
let text = super::binding_abi::text_arg(lua, text)?;
|
||||
// `CSimpleButton::SetText 0x778dc0` opens `if (!text) return;` (`0x778dcc`, wow-re
|
||||
// `button-label-build-and-anchor-order.md`) — a nil never reaches the label at all, so
|
||||
// `CSimpleButton::SetText 0x778dc0` opens `if (!text) return;` (`0x778dcc`) — a nil
|
||||
// never reaches the label at all, so
|
||||
// it neither clears the text nor lazily creates the FontString. That guard is the
|
||||
// BUTTON's own: a `FontString:SetText(nil)` is not a no-op, it truncates the cell
|
||||
// (`0x771d80`, decision 2110). Below the guard the button is a pass-through to
|
||||
@@ -665,11 +663,10 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
|
||||
// SetFontString(fontString) — ADOPT a caller-made FontString as this Button's label.
|
||||
//
|
||||
// Byte-carved end to end by wow-re (decision 1505,
|
||||
// `system/ui/scratch/resize-bounds-and-button-fontstring.md`
|
||||
// §5): the binding `0x780a60` is gates + a delegate to `CSimpleButton::SetFontString
|
||||
// 0x778d20`, which is the SAME function `SetText`'s lazy creation path funnels through — so
|
||||
// adopting and creating share their whole tail, and only the allocation differs.
|
||||
// End to end (decision 1505): the binding `0x780a60` is gates + a delegate to
|
||||
// `CSimpleButton::SetFontString 0x778d20`, which is the SAME function `SetText`'s lazy creation
|
||||
// path funnels through — so adopting and creating share their whole tail, and only the
|
||||
// allocation differs.
|
||||
//
|
||||
// The idiom it exists for: build the label yourself so you can style and place it, then hand
|
||||
// it to the button so `SetText` and the per-state font machinery drive it. Quiver's
|
||||
@@ -787,10 +784,9 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
})?,
|
||||
)?;
|
||||
|
||||
// GetTextWidth / GetTextHeight — the Button's OWN text-extent readers (`0x782290` / `0x782390`,
|
||||
// wow-re `system/ui/scratch/item9-firing34-merge.md` l.36 and the Button method carve in
|
||||
// `widget-api-batch-benilla.md` Q8, which lists both present on Button and `GetStringWidth`
|
||||
// absent). Both are thin forwards onto the label FontString's own extent vtable slots
|
||||
// GetTextWidth / GetTextHeight — the Button's OWN text-extent readers (`0x782290` /
|
||||
// `0x782390`); both are present on Button's own method table, `GetStringWidth` is not.
|
||||
// Both are thin forwards onto the label FontString's own extent vtable slots
|
||||
// (`0x1c` / `0x20`), which is exactly what this delegation is.
|
||||
//
|
||||
// **Who asks.** `Bagnon_Forever/database/ui.lua:61` sizes its character-switch dropdown from
|
||||
@@ -866,8 +862,7 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
})?,
|
||||
)?;
|
||||
|
||||
// SetFont(file, height [, flags]) / GetFont() — the Button's own font, `0x780880`/`0x79f3b0`
|
||||
// (wow-re `system/ui/scratch/widget-api-batch-benilla.md` Q8, §5-verified).
|
||||
// SetFont(file, height [, flags]) / GetFont() — the Button's own font, `0x780880`/`0x79f3b0`.
|
||||
// `_LazyPig/LazyPigMenu.lua:214` calls it straight on a `CreateFrame("Button", …)`, and it is
|
||||
// that addon's only blocker.
|
||||
//
|
||||
@@ -1069,8 +1064,8 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
)?;
|
||||
// IsEnabled() → the NUMBER 1 or the NUMBER 0 — never a boolean, and never nil.
|
||||
//
|
||||
// wow-re `ui/scratch/button-enabled-state.md`, VERIFIED off the Button method table at
|
||||
// `0x879d10`: `IsEnabled 0x7800b0` reads the three-valued STATE at `[obj+0x328]`
|
||||
// The Button method table at
|
||||
// `0x879d10` gives `IsEnabled 0x7800b0`, which reads the three-valued STATE at `[obj+0x328]`
|
||||
// (0 DISABLED / 1 NORMAL / 2 PUSHED), does `setne cl` for "not disabled", and pushes that as a
|
||||
// NUMBER. So the reference's own `IsEnabled() == 0` and `== 1` tests (FriendsFrame.lua l.404,
|
||||
// StaticPopup.lua l.713) are live code.
|
||||
@@ -1114,7 +1109,7 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
)?;
|
||||
|
||||
// **`SetButtonState(state[, lock]) 0x780270` — TWO arguments**, and the second is not
|
||||
// cosmetic (wow-re `scratch/binding-shape-arity-law.md` §3, VERIFIED). It reads three stack
|
||||
// cosmetic. It reads three stack
|
||||
// indices and returns none; the state maps case-insensitively over exactly
|
||||
// `{"DISABLED", "NORMAL", "PUSHED"}` (`0x780390`, `SStrCmpI`) and an unrecognised string
|
||||
// raises `Usage: %s:SetButtonState("state", lock)`; the flag is `GetBoolOrDefault` with
|
||||
@@ -1190,8 +1185,7 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
c.set(
|
||||
"SetChecked",
|
||||
lua.create_function(|lua, (this, v): (Table, Value)| {
|
||||
// Numeric coercion, NOT Lua truthiness — byte-verified (decision 0227; wow-re
|
||||
// `system/ui/scratch/button-check-and-state-texture.md`, `SetChecked 0x799bf0` →
|
||||
// Numeric coercion, NOT Lua truthiness (decision 0227; `SetChecked 0x799bf0` →
|
||||
// `0x6f1c10`): a number goes through `lua_tonumber` then a truncate-to-int (`fistp`,
|
||||
// round-toward-zero, `0x40a2b0`) and the C++ setter tests `!= 0`. So `SetChecked(0)`
|
||||
// UNchecks (0 is Lua-truthy — only a numeric read gets this right) and `SetChecked(1)`
|
||||
|
||||
@@ -12,8 +12,7 @@
|
||||
//! lookup directions are live in the corpus: by index (`GetChannelName(i)`) and by name
|
||||
//! (`GetChannelName("world")`, `GetChannelName("Trade - City")`).
|
||||
//!
|
||||
//! Signature verified against wow-5875-re `system/ui/scratch/zone-chat-channel-autojoin.md`
|
||||
//! l.374-380 — `GetChannelName = 0x4a05e0`, **three** returns:
|
||||
//! Signature verified against the reference — `GetChannelName = 0x4a05e0`, **three** returns:
|
||||
//!
|
||||
//! | # | the client's | ours |
|
||||
//! |---|---|---|
|
||||
@@ -32,11 +31,12 @@ use super::Model;
|
||||
|
||||
/// The 1-based slot of `name`, case-insensitively — `GetChannelName`'s first return.
|
||||
/// One `ChatChannels.dbc` row as the VM needs it for `JoinChannelByName` (decision 1908),
|
||||
/// `Add/RemoveChatWindowChannel` and `EnumerateServerChannels` (wow-re chat-cache-grammar.md
|
||||
/// §5-6): the id, the **Shortcut** (`General`, `Trade`, … — what every one of those verbs compares
|
||||
/// a typed name against, whole and case-folded), the name composed for the zone the player is in
|
||||
/// (`General - Elwynn Forest`; `None` while the zone text is empty, which is the verbs' nil leg),
|
||||
/// and whether the row is listed here at all (a `flags & 0x10` city row is, only in a city).
|
||||
/// `Add/RemoveChatWindowChannel` (`0x4a1000`/`0x4a1260`) and `EnumerateServerChannels`
|
||||
/// (`0x4a1790`): the id, the **Shortcut** (`General`, `Trade`, … — what every one of those verbs
|
||||
/// compares a typed name against, whole and case-folded), the name composed for the zone the
|
||||
/// player is in (`General - Elwynn Forest`; `None` while the zone text is empty, which is the
|
||||
/// verbs' nil leg), and whether the row is listed here at all (a `flags & 0x10` city row is, only
|
||||
/// in a city).
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct ZoneChannelRow {
|
||||
pub id: u32,
|
||||
@@ -213,9 +213,6 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
// and `_SetDefaults` (l.649) keep for that key. Exactly the `SHOW_TUTORIALS` shape decision
|
||||
// 2077 corrected, and this tree's own `OptionsFrame.xml` carried the wrong reading until 2115.
|
||||
//
|
||||
// Every byte fact below is wow-re `system/ui/scratch/guild-recruitment-mode.md`, a §5 round
|
||||
// dispatched for this work.
|
||||
//
|
||||
// **The state is one int** — the reference's `[0x843608]`, written only by `0x49ea70`
|
||||
// (`mov ds:0x843608,ecx`; five call sites, no address-takes) and read by the Lua getter and by
|
||||
// the chat-cache writer. Ours is `Model::guild_recruitment_mode`, seated at login and rendered
|
||||
@@ -260,8 +257,7 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
g.set(
|
||||
"SetGuildRecruitmentMode",
|
||||
// `[0x4a0060, 0x4a00c4)`. One argument at stack index 1, and **two ways to raise** — this
|
||||
// is a shape-A binding (wow-re `numeric-arg-coercion-law.md`), not one of the many that
|
||||
// swallow a nil:
|
||||
// raises on a bad argument rather than swallowing a nil, unlike many bindings:
|
||||
//
|
||||
// * `lua_isnumber 0x6f34d0` — so a NUMERIC STRING passes (`"1"` works) — else
|
||||
// `luaL_error("Usage: SetGuildRecruitmentMode(mode)")`, which longjmps.
|
||||
@@ -373,8 +369,8 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
|
||||
// GetChannelList() → slot1, name1, slot2, name2, … over every joined channel, in join order.
|
||||
//
|
||||
// **The shape is settled by two independent consumers, not by a recorded signature** — wow-re
|
||||
// has the address (`0x4a02d0`, `scratch/bindings.md` l.152) and no contract:
|
||||
// **The shape is settled by two independent consumers, not by a recorded signature** — the
|
||||
// reference gives the address (`0x4a02d0`) and no contract:
|
||||
//
|
||||
// · the reference's own `FCFDropDown_LoadChannels(...)` walks `for i=1, arg.n, 2` and reads
|
||||
// `arg[i+1]` as the NAME (FloatingChatFrame.lua l.445-455) — so the pair is (slot, name),
|
||||
@@ -422,7 +418,7 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
// whose zone substitution is empty
|
||||
//
|
||||
// `CMSG_JOIN_CHANNEL` goes out on both non-nil legs. **The third argument registers the
|
||||
// channel in that window's own list** (contract §5, `0x49ec24`–`0x49ede7`, VERIFIED):
|
||||
// channel in that window's own list** (`0x49ec24`–`0x49ede7`):
|
||||
// `frameId` is `lua_tonumber`d, truncated and decremented, and anything outside `0..10`
|
||||
// after that — a missing or non-numeric argument yields `-1` — silently skips the
|
||||
// registration; otherwise `(Shortcut, ChannelID)` for a matched row and `(name, 0)` for a
|
||||
@@ -493,7 +489,7 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
)?,
|
||||
)?;
|
||||
|
||||
// EnumerateServerChannels() — `0x4a1790` (chat-cache-grammar.md §6): the `ChatChannels.dbc`
|
||||
// EnumerateServerChannels() — `0x4a1790`: the `ChatChannels.dbc`
|
||||
// **shortcuts** in row order, a `flags & 0x10` row only when the zone is a city
|
||||
// (`AreaTable.Flags & 0x8`); 0 values while the zone is unresolvable (an empty catalog here).
|
||||
// Never the composed `<name> - <zone>`: `FCFDropDown_LoadServerChannels` shows these bare.
|
||||
@@ -509,8 +505,8 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
})?,
|
||||
)?;
|
||||
|
||||
// LeaveChannelByName(name) — `0x4a0000` → `0x49ee70`, the contract wow-re
|
||||
// `leavechannelbyname-contract.md` §11 carved (decision 2144). The only stock caller is
|
||||
// LeaveChannelByName(name) — `0x4a0000` → `0x49ee70`, the contract decision 2144 carved. The
|
||||
// only stock caller is
|
||||
// `SlashCmdList["LEAVE"]`, which passes the first whitespace token of the slash text.
|
||||
//
|
||||
// 1. a string or number, else it RAISES `Usage: LeaveChannelByName("name")`; 0 returns.
|
||||
@@ -528,7 +524,7 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
// `resolved`), so `/leave Trade` leaves `Trade - City`. The mechanism, done right, not the
|
||||
// reference's broken form of it.
|
||||
//
|
||||
// Nothing local is freed here and no event fires (§6, §10): the slot goes when the server's
|
||||
// Nothing local is freed here and no event fires: the slot goes when the server's
|
||||
// `YOU_LEFT` matches its stored name, and the mask bit with it — both the app's.
|
||||
g.set(
|
||||
"LeaveChannelByName",
|
||||
@@ -794,11 +790,11 @@ mod command_tests {
|
||||
);
|
||||
}
|
||||
|
||||
/// **`JoinChannelByName`'s third argument registers the channel in that window** (contract §5,
|
||||
/// decision 2144): `frameId - 1` indexes the ten window records, `(Shortcut, ChannelID)` for a
|
||||
/// DBC row and `(name, 0)` for a custom channel, deduplicated by name; a missing, non-numeric or
|
||||
/// out-of-range frame skips the registration and nothing else. It is what puts a `/join`ed
|
||||
/// channel back into a window a `/leave` had stripped it from.
|
||||
/// **`JoinChannelByName`'s third argument registers the channel in that window**
|
||||
/// (`0x49ec24`–`0x49ede7`; decision 2144): `frameId - 1` indexes the ten window records,
|
||||
/// `(Shortcut, ChannelID)` for a DBC row and `(name, 0)` for a custom channel, deduplicated by
|
||||
/// name; a missing, non-numeric or out-of-range frame skips the registration and nothing else.
|
||||
/// It is what puts a `/join`ed channel back into a window a `/leave` had stripped it from.
|
||||
#[test]
|
||||
fn join_channel_by_name_registers_the_channel_in_the_named_window() {
|
||||
let mut s = UiScript::new().unwrap();
|
||||
@@ -838,10 +834,11 @@ mod command_tests {
|
||||
assert_eq!(s.take_channel_commands().len(), 5);
|
||||
}
|
||||
|
||||
/// `LeaveChannelByName`'s legs (contract §11; decision 2144): a number passes through for the
|
||||
/// app's slot lookup and strips nothing; a shortcut composes for the zone and strips the
|
||||
/// window entry it was registered under, in every window; an unresolvable shortcut is a
|
||||
/// complete no-op; a custom name goes verbatim and strips verbatim. Nil raises. Zero returns.
|
||||
/// `LeaveChannelByName`'s legs (`0x4a0000` → `0x49ee70`; decision 2144): a number passes
|
||||
/// through for the app's slot lookup and strips nothing; a shortcut composes for the zone and
|
||||
/// strips the window entry it was registered under, in every window; an unresolvable shortcut
|
||||
/// is a complete no-op; a custom name goes verbatim and strips verbatim. Nil raises. Zero
|
||||
/// returns.
|
||||
#[test]
|
||||
fn leave_channel_by_name_composes_strips_and_raises_the_way_the_reference_does() {
|
||||
let mut s = UiScript::new().unwrap();
|
||||
|
||||
@@ -20,18 +20,18 @@
|
||||
//! the same bindings, and the gate was simply never exercised.)
|
||||
//!
|
||||
//! **The absent shape for a third unit is the right ANSWER for `UnitStat` and a known GAP for the
|
||||
//! resistance pair — and the reason it used to be filed as blanket-faithful was wrong.** The
|
||||
//! binary does not gate slots 1/2 on SELF at all: `UnitStat`'s are NULL + typemask bit 3 only, and
|
||||
//! it returns whatever the client's copy of `UNIT_FIELD_STAT0+i` holds (VERIFIED wow-re
|
||||
//! `ui/scratch/pet-paperdoll-stat-api.md` §4). What makes our zeros agree there is the *server's*
|
||||
//! visibility, not a client gate: `UNIT_FIELD_STAT*` is PRIVATE + OWNER_ONLY, and the only
|
||||
//! owner-visible units a 1.12 unit token can name are the two we already serve — so a stranger's
|
||||
//! copy is zero on the reference too. `UNIT_FIELD_RESISTANCES` carries a third flag,
|
||||
//! **`SPECIAL_INFO`**, which vmangos grants to the caster of `SPELL_AURA_EMPATHY` — its own comment
|
||||
//! reads `// Beast Lore` (`Player.cpp:2603-2610`, `Object.cpp:1065-1067`). So with Beast Lore up on
|
||||
//! a beast, the reference's `UnitResistance("target", i)` / `UnitArmor("target")` return that
|
||||
//! creature's real numbers through their non-SELF leg, and ours return zeros. That is a real,
|
||||
//! reachable divergence, unfed rather than decided: the app pushes snapshots for two tokens only.
|
||||
//! resistance pair — and the reason it used to be filed as blanket-faithful was wrong.** The binary
|
||||
//! does not gate slots 1/2 on SELF at all: `UnitStat`'s are NULL + typemask bit 3 only, and it
|
||||
//! returns whatever the client's copy of `UNIT_FIELD_STAT0+i` holds (`UnitStat 0x518600`). What
|
||||
//! makes our zeros agree there is the *server's* visibility, not a client gate: `UNIT_FIELD_STAT*`
|
||||
//! is PRIVATE + OWNER_ONLY, and the only owner-visible units a 1.12 unit token can name are the two
|
||||
//! we already serve — so a stranger's copy is zero on the reference too. `UNIT_FIELD_RESISTANCES`
|
||||
//! carries a third flag, **`SPECIAL_INFO`**, which vmangos grants to the caster of
|
||||
//! `SPELL_AURA_EMPATHY` — its own comment reads `// Beast Lore` (`Player.cpp:2603-2610`,
|
||||
//! `Object.cpp:1065-1067`). So with Beast Lore up on a beast, the reference's
|
||||
//! `UnitResistance("target", i)` / `UnitArmor("target")` return that creature's real numbers
|
||||
//! through their non-SELF leg, and ours return zeros. That is a real, reachable divergence, unfed
|
||||
//! rather than decided: the app pushes snapshots for two tokens only.
|
||||
//! `unit_combat_stats` already works over any store, so the missing piece is a third push.
|
||||
//!
|
||||
//! **Return shapes differ BY FAMILY, and the reference Lua's own asymmetry is the tell**
|
||||
@@ -155,19 +155,19 @@ pub struct UnitCombatStats {
|
||||
/// app resolves WHICH skill via [`weapon_subclass_skill`] (unarmed = [`SKILL_UNARMED`]) and
|
||||
/// reads the pair from `PLAYER_SKILL_INFO`; `UnitAttackBothHands` serves it verbatim.
|
||||
///
|
||||
/// **The permanent half belongs to the BASE, not to the modifier** (`0x5ea460`, VERIFIED —
|
||||
/// wow-re `ui/scratch/unitrangedattack-skill-pair.md` §3; corrected here by decision 1812).
|
||||
/// Every one of these four pairs comes out of that one reader, so all four carry the split.
|
||||
/// **The permanent half belongs to the BASE, not to the modifier** (`0x5ea460`; corrected here
|
||||
/// by decision 1812). Every one of these four pairs comes out of that one reader, so all four
|
||||
/// carry the split.
|
||||
pub main_weapon_skill: (i32, i32),
|
||||
/// The OFF hand's, read the same way. `UnitAttackBothHands` pushes both hands (§4 of wow-re's
|
||||
/// `pet-paperdoll-stat-api.md`: `0x518810` calls `[vtbl+0xb0]` twice, hand 0 then hand 1), and
|
||||
/// an empty or non-weapon off hand is Unarmed exactly as the main hand is.
|
||||
/// The OFF hand's, read the same way. `UnitAttackBothHands` pushes both hands (`0x518810`
|
||||
/// calls `[vtbl+0xb0]` twice, hand 0 then hand 1), and an empty or non-weapon off hand is
|
||||
/// Unarmed exactly as the main hand is.
|
||||
pub offhand_weapon_skill: (i32, i32),
|
||||
/// The ranged weapon's skill pair (`UnitRangedAttack`), same split.
|
||||
///
|
||||
/// **Player-only, and by a different gate than its two neighbours.** `UnitRangedAttack`
|
||||
/// `0x518b90` is a DIRECT call gated on the PLAYER typemask alone — no vtable slot and **no
|
||||
/// `CGUnit_C` fallback body** (wow-re `unitrangedattack-skill-pair.md` §4). So where
|
||||
/// `CGUnit_C` fallback body**. So where
|
||||
/// `UnitDefense` and `UnitAttackBothHands` pass a pet through SELF-OR-MINE and answer
|
||||
/// `level * 5`, this one answers `(0, 0)` for a pet and for every other player too. Mirroring
|
||||
/// [`cgunit_skill`] here would be wrong.
|
||||
@@ -277,14 +277,14 @@ pub struct InvSlotView {
|
||||
/// The instance's live durability `(current, max)` — the equipped-item tooltip's
|
||||
/// "Durability X / Y" line (see [`super::container::ContainerSlot::durability`]).
|
||||
pub durability: Option<(u32, u32)>,
|
||||
/// `ITEM_FIELD_FLAGS` (wire field 21) — the alert/broken laws read two bits (VERIFIED wow-re
|
||||
/// inventory-alert-law `0x4c7ee0`): `0x08` wrapped (a gift — never alerts, never broken),
|
||||
/// `ITEM_FIELD_FLAGS` (wire field 21) — the alert/broken laws read two bits (`0x4c7ee0`):
|
||||
/// `0x08` wrapped (a gift — never alerts, never broken),
|
||||
/// `0x10` force-red (alert status 4 regardless of durability).
|
||||
pub flags: u32,
|
||||
/// `0x5da2c0` — **the instance is runtime-bound**: `ITEM_FIELD_FLAGS & 1` (soulbound), or a
|
||||
/// live enchant slot naming a `SpellItemEnchantment` row that binds. App-resolved off the raw
|
||||
/// descriptor (the doll twin of [`super::container::ContainerSlot::already_bound`]); the
|
||||
/// tooltip's §6 bind line overrides to **Soulbound** on it (B310).
|
||||
/// tooltip's bind line overrides to **Soulbound** on it (B310).
|
||||
pub already_bound: bool,
|
||||
/// An `|Hitem:…|h[Name]|h` link once the name is known — the doll twin of
|
||||
/// `ContainerSlot::link`; carried onto the cursor payload so a world-drop `DELETE_ITEM_CONFIRM`
|
||||
@@ -395,8 +395,8 @@ const SLOT_INFO: [(&str, i64, &str); 36] = [
|
||||
("Bag12", 75, "Bag"),
|
||||
];
|
||||
|
||||
/// The durability-alert regions in the client's own slot table (`0x806eb8`, VERIFIED wow-re
|
||||
/// inventory-alert-law — 12 entries): alert index 1..=11 → the live-id equipment slot (Head,
|
||||
/// The durability-alert regions in the client's own slot table (`0x806eb8` — 12 entries): alert
|
||||
/// index 1..=11 → the live-id equipment slot (Head,
|
||||
/// Shoulders, Chest, Waist, Legs, Feet, Wrists, Hands, Weapon, Shield, Ranged — the ref
|
||||
/// FrameXML's `INVENTORY_ALERT_STATUS_SLOTS` order), index 12 → the client's low-ammo region
|
||||
/// (slot -1 in its table; FrameXML never reads it, the binding answers it). Our live-id 0 IS
|
||||
@@ -415,8 +415,8 @@ fn slot_is_broken(v: &InvSlotView) -> bool {
|
||||
}
|
||||
|
||||
/// One region's `GetInventoryAlertStatus` value — the recompute `0x4c7ee0`'s per-item
|
||||
/// classification (VERIFIED wow-re inventory-alert-law, §5-cross-checked; the enum's own name
|
||||
/// is `INV_ALERT_STATUS`): `4` (red) = broken per [`slot_is_broken`]; `3` (yellow) = tracks
|
||||
/// classification (the enum's own name is `INV_ALERT_STATUS`): `4` (red) = broken per
|
||||
/// [`slot_is_broken`]; `3` (yellow) = tracks
|
||||
/// durability and **`1..=5` points left — an ABSOLUTE count, no percentage, maxDurability is
|
||||
/// only the tracks-durability gate** (`cmp [D+0xa0],5`); `0` otherwise. Statuses 1/2 are the
|
||||
/// temp-weapon-enchant alerts (present/expiring-in-30s) — no enchant feed yet, and the 1.12
|
||||
@@ -531,7 +531,7 @@ fn with_unit_stats<T>(
|
||||
|
||||
/// The post-processing both skill-shaped bindings share: **`if (mod + base) < 0 then mod = -base`**
|
||||
/// — so the pair can never sum below zero (`0x519298` for `UnitDefense`, `0x5188a7` per hand for
|
||||
/// `UnitAttackBothHands`; wow-re `ui/scratch/pet-paperdoll-stat-api.md` §4). A debuff deeper than
|
||||
/// `UnitAttackBothHands`). A debuff deeper than
|
||||
/// the skill itself reads as "reduced to 0", never as a negative total.
|
||||
fn skill_clamped((base, modifier): (i32, i32)) -> (i64, i64) {
|
||||
let base = i64::from(base);
|
||||
@@ -544,8 +544,8 @@ fn skill_clamped((base, modifier): (i32, i32)) -> (i64, i64) {
|
||||
///
|
||||
/// It is one helper because it is one function in the client: both bindings dispatch through the
|
||||
/// resolved unit's vtable, and a `CGUnit_C` lands on a three-line body that multiplies the level by
|
||||
/// five and writes 0 to the modifier (`0x613680` / `0x6136b0`, wow-re
|
||||
/// `ui/scratch/pet-paperdoll-stat-api.md`). Nothing here is skill data — a creature has no skill
|
||||
/// five and writes 0 to the modifier (`0x613680` / `0x6136b0`). Nothing here is skill data — a
|
||||
/// creature has no skill
|
||||
/// block at all, which is exactly why the client substitutes a formula.
|
||||
fn cgunit_skill(model: &Model, token: &str) -> i64 {
|
||||
model.unit(token).map_or(0, |u| i64::from(u.level)) * 5
|
||||
@@ -725,8 +725,7 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
// **The first two returns are the same field, UNDECOMPOSED** — `UNIT_FIELD_STAT0+i` raw
|
||||
// (`fild`, `0x518689`), then that same value clamped at zero (`0x5186a3`–`0x5186b7`:
|
||||
// `setl cl; dec ecx; and ecx,eax`). Slots 3/4 are `PLAYER_FIELD_POSSTAT0+i` (`0x518712`) and
|
||||
// `NEGSTAT0+i` (`0x518772`), each behind a SELF gate. VERIFIED, wow-re
|
||||
// `ui/scratch/pet-paperdoll-stat-api.md` §4.
|
||||
// `NEGSTAT0+i` (`0x518772`), each behind a SELF gate.
|
||||
//
|
||||
// This served `effective − pos − neg` as the first return until decision 1397. Subtracting is
|
||||
// the ref Lua's *own* job — `PaperDollFrame_SetStats` writes the tooltip's base as
|
||||
@@ -882,8 +881,8 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
// hand).
|
||||
//
|
||||
// **It answered two until decision 1810**, which is 1793's shape at its quietest: `0x518810`
|
||||
// calls `[vtbl+0xb0]` TWICE, hand 0 then hand 1, and pushes `(base0, mod0, base1, mod1)`
|
||||
// (VERIFIED, wow-re `ui/scratch/pet-paperdoll-stat-api.md` §4). The reference's own
|
||||
// calls `[vtbl+0xb0]` TWICE, hand 0 then hand 1, and pushes `(base0, mod0, base1, mod1)`.
|
||||
// The reference's own
|
||||
// `PaperDollFrame_SetAttackBothHands` destructures only the first two — its next line is
|
||||
// `-- FIXME: The offhand stats aren't displayed yet.` — so putting the stock character sheet on
|
||||
// the chain could never have exposed this. An addon reading four gets two nils.
|
||||
@@ -930,12 +929,12 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
// modifier's sign itself (PaperDollFrame.lua:259-271: modifier > 0 → a green posBuff, < 0 → a
|
||||
// red negBuff).
|
||||
//
|
||||
// **The fork is a VIRTUAL CALL on the resolved unit, not a token test** (wow-re
|
||||
// `ui/scratch/pet-paperdoll-stat-api.md`, the §5 that corrected decision 1057's INTERIM):
|
||||
// **The fork is a VIRTUAL CALL on the resolved unit, not a token test** (correcting decision
|
||||
// 1057's INTERIM):
|
||||
// `[vtbl+0xac]` is either `CGPlayer_C 0x5eda20` — resolve the Defense SkillLine (`0x6de040`)
|
||||
// and read `PLAYER_SKILL_INFO` — or `CGUnit_C 0x613680`, which is three lines:
|
||||
// `*out1 = UNIT_FIELD_LEVEL * 5; *out2 = 0`. **A level-60 pet shows 300**, not the 0 that
|
||||
// 1057 shipped while the dispatch was out. The outer gate is SELF **or**
|
||||
// `*out1 = UNIT_FIELD_LEVEL * 5; *out2 = 0`. **A level-60 pet shows 300**,
|
||||
// not the 0 that 1057 shipped. The outer gate is SELF **or**
|
||||
// `UNIT_FIELD_SUMMONEDBY == my guid`, which our pet passes and no other token we serve does —
|
||||
// so `"player"` takes the skill leg, `"pet"` the level leg (a warlock's minion included: it is
|
||||
// summoned by us too), and everything else the gate-failure zeros.
|
||||
@@ -1066,9 +1065,8 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
const USAGE_GET_INVENTORY_ITEM_COUNT: &str = "Usage: GetInventoryItemCount(unit, slot)";
|
||||
const INVALID_SLOT_GET_INVENTORY_ITEM_COUNT: &str =
|
||||
"Invalid inventory slot in GetInventoryItemCount";
|
||||
// GetInventoryItemCount("player", slot) — CARVED (`0x4c8680`, wow-re
|
||||
// `system/ui/scratch/inventory-item-count-law.md`), and it is nothing like the shape every
|
||||
// secondary source describes. Four answers, in the reference's own order:
|
||||
// GetInventoryItemCount("player", slot) — CARVED (`0x4c8680`), and it is nothing like the
|
||||
// shape every secondary source describes. Four answers, in the reference's own order:
|
||||
//
|
||||
// · an EMPTY slot pushes **1**, not 0 (`0x4c8797`). Both FrameXML callers gate on
|
||||
// `GetInventoryItemTexture` first, which is why nobody ever noticed.
|
||||
@@ -1612,10 +1610,9 @@ mod tests {
|
||||
/// `UnitDefense` `0x519200` and `UnitAttackBothHands` `0x518810` gate on SELF-OR-MINE and then
|
||||
/// dispatch through the resolved unit's vtable, so a pet passes and lands on `CGUnit_C`'s
|
||||
/// `level * 5`. `UnitRangedAttack` `0x518b90` is a **direct call** gated on the PLAYER typemask
|
||||
/// alone, with no vtable slot and **no `CGUnit_C` fallback body at all** (VERIFIED, wow-re
|
||||
/// `ui/scratch/unitrangedattack-skill-pair.md` §4 — `0x612b40`/`0x5edae0`/`0x5ea460` appear as
|
||||
/// a dword zero times image-wide, against a positive control of one hit each for the four
|
||||
/// addresses that ARE in a vtable).
|
||||
/// alone, with no vtable slot and **no `CGUnit_C` fallback body at all** — `0x612b40`,
|
||||
/// `0x5edae0` and `0x5ea460` appear as a dword zero times image-wide, against a positive
|
||||
/// control of one hit each for the four addresses that ARE in a vtable.
|
||||
///
|
||||
/// So a pet's ranged attack is `(0, 0)`, not `level * 5`. The whole reason this is a test and
|
||||
/// not a comment is that the three verbs look interchangeable from the FrameXML side, and
|
||||
@@ -1657,8 +1654,7 @@ mod tests {
|
||||
/// decision 1810.**
|
||||
///
|
||||
/// The quietest shape decision 1793 describes: `0x518810` calls `[vtbl+0xb0]` twice, hand 0
|
||||
/// then hand 1, and pushes `(base0, mod0, base1, mod1)` (VERIFIED, wow-re
|
||||
/// `ui/scratch/pet-paperdoll-stat-api.md` §4). The reference's own
|
||||
/// then hand 1, and pushes `(base0, mod0, base1, mod1)`. The reference's own
|
||||
/// `PaperDollFrame_SetAttackBothHands` destructures only the first two — its very next line is
|
||||
/// `-- FIXME: The offhand stats aren't displayed yet.` — so no amount of running the stock
|
||||
/// character sheet could have exposed it. An addon reading four got two nils.
|
||||
@@ -1891,9 +1887,8 @@ mod tests {
|
||||
assert!(s.eval::<bool>("return HasWandEquipped()").unwrap());
|
||||
}
|
||||
|
||||
/// **`GetInventoryItemCount`'s container fork** (`0x4c8680`, wow-re
|
||||
/// `system/ui/scratch/inventory-item-count-law.md`). Four answers, and three of them are not
|
||||
/// the stack count:
|
||||
/// **`GetInventoryItemCount`'s container fork** (`0x4c8680`). Four answers, and three of them
|
||||
/// are not the stack count:
|
||||
///
|
||||
/// · an equipped QUIVER (its `ItemSubClass.dbc` row has `DisplayFlags & 0x4`) answers the sum
|
||||
/// of its arrows — the number that shows on the bag bar;
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
//! itself (`ui_chat/input/parse.rs`) and now drains what the reference's own Lua decided.
|
||||
//!
|
||||
//! Every verb is a queue or a flag — the engine-free seam (0068 §3): the VM never sees the wire,
|
||||
//! the app drains and sends. Registrar addresses from wow-re `ui/scratch/bindings.md`:
|
||||
//! the app drains and sends. Registrar addresses:
|
||||
//! `0x49fd30 DoEmote`, `0x48c7b0 RandomRoll`, `0x489c40 AssistByName`, `0x48a610 UninviteByName`.
|
||||
|
||||
use mlua::{Lua, MultiValue, Value};
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
//! SendChatMessage(text [, chatType [, language [, channel/target]]])
|
||||
//! ```
|
||||
//!
|
||||
//! `chatType` defaults to `"SAY"`; `language` is accepted and **ignored** (§2); the fourth
|
||||
//! `chatType` defaults to `"SAY"`; `language` is accepted and **ignored** (`0x49f1e0`); the fourth
|
||||
//! argument is the whisper target for `"WHISPER"` and the channel name or number for
|
||||
//! `"CHANNEL"`, and is unused otherwise.
|
||||
//!
|
||||
@@ -70,7 +70,7 @@ impl super::UiScript {
|
||||
/// `GetNumLaguages`/`GetLanguageByIndex` walk. The app folds it the reference's way:
|
||||
/// `0x4b25b0` stores `[languageId] = spellId` for every known spell whose `Effect_1 == 39`,
|
||||
/// and `0x5ec720` answers non-zero only when that spell's skill line is in the player's
|
||||
/// skill block (wow-re `chat-language-scramble.md` §8, C6). Fires `LANGUAGE_LIST_CHANGED`
|
||||
/// skill block. Fires `LANGUAGE_LIST_CHANGED`
|
||||
/// (`0x49b970`, event 0x102) when the list moves.
|
||||
pub fn set_known_languages(&mut self, names: Vec<String>) {
|
||||
let changed = {
|
||||
@@ -89,9 +89,8 @@ impl super::UiScript {
|
||||
}
|
||||
|
||||
pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
// GetNumLaguages() — `0x49fb30`, the binary's own spelling (wow-re
|
||||
// `bag-language-combat-action-bindings.md` §2). Walks `Languages.dbc` and counts the rows
|
||||
// `0x5ec720` answers non-zero for; **one number**.
|
||||
// GetNumLaguages() — `0x49fb30`, the binary's own spelling. Walks `Languages.dbc` and counts
|
||||
// the rows `0x5ec720` answers non-zero for; **one number**.
|
||||
lua.globals().set(
|
||||
"GetNumLaguages",
|
||||
lua.create_function(|lua, _ignored: MultiValue| {
|
||||
@@ -117,7 +116,7 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
})?,
|
||||
)?;
|
||||
// GetDefaultLanguage() → **exactly ONE value, a string** — or **zero values**, which is not
|
||||
// the same thing (`0x49fcd0`, wow-re `bag-language-combat-action-bindings.md` §2).
|
||||
// the same thing (`0x49fcd0`).
|
||||
//
|
||||
// The binding takes **no arguments** (no arg-presence check and no arg-fetch call anywhere in
|
||||
// its 94 bytes — contrast its sibling `GetLanguageByIndex 0x49fbe0`, which opens with
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
//! The reference keeps one runtime table of chat types (`0xb4e518`, stride 0x43: a 0x40-byte
|
||||
//! name and three RGB bytes), seeded at boot from the 94-entry static table at `0x804710` and
|
||||
//! then extended with ten extras `CHANNEL1`…`CHANNEL10`, each coloured from the live `CHANNEL`
|
||||
//! entry (wow-re `system/ui/scratch/chat-color-table.md`, "Seeding"). The `chat-cache.txt`
|
||||
//! entry (`0x4982c0`). The `chat-cache.txt`
|
||||
//! `COLORS` block overwrites matched entries in place at load — absent names keep the compiled
|
||||
//! defaults — and the app owns that file; here the table is state the app feeds and drains
|
||||
//! ([`super::UiScript::set_chat_colors`], [`super::UiScript::take_chat_color_changes`]).
|
||||
@@ -35,8 +35,8 @@ pub const FIXED_CHAT_TYPES: usize = 94;
|
||||
/// The extras seeded at boot: `CHANNEL1`…`CHANNEL10`, indices 95–104.
|
||||
pub const EXTRA_CHAT_TYPES: usize = 10;
|
||||
|
||||
/// `0x804710` — name and default RGB, verbatim (chat-color-table.md, "The complete default
|
||||
/// table"). The order is load-bearing: it *is* the index `GetChatTypeIndex` answers.
|
||||
/// `0x804710` — name and default RGB, verbatim. The order is load-bearing: it *is* the index
|
||||
/// `GetChatTypeIndex` answers.
|
||||
const DEFAULTS: [(&str, [u8; 3]); FIXED_CHAT_TYPES] = [
|
||||
("SAY", [255, 255, 255]),
|
||||
("PARTY", [170, 170, 255]),
|
||||
|
||||
@@ -8,12 +8,11 @@
|
||||
//!
|
||||
//! ## `GetChatWindowInfo(id)` → `name, fontSize, r, g, b, a, shown, locked, docked`
|
||||
//!
|
||||
//! A registered Lua binding in the real client (`0x4a0ba0`, wow-re `system/ui/ledger.tsv`), reading
|
||||
//! the per-window struct array at `0xb4fe50` — stride 0x98, **10 windows**, colour bytes at
|
||||
//! +0xd8..+0xdb renormalised by the f32 1/255 (`system/ui/scratch/chat-color-table.md`). Those
|
||||
//! structs are loaded from the character's `chat-cache.txt`, and a real stock one is on disk
|
||||
//! (`wow-5875-re/WoW/WTF/Account/ONE/VMaNGOS/Onepaladin/chat-cache.txt`) — every value below is
|
||||
//! quoted from it:
|
||||
//! A registered Lua binding in the real client (`0x4a0ba0`), reading the per-window struct array at
|
||||
//! `0xb4fe50` — stride 0x98, **10 windows**, colour bytes at +0xd8..+0xdb renormalised by the f32
|
||||
//! 1/255 (`0x8026c8`). Those structs are loaded from the character's `chat-cache.txt`, and a real
|
||||
//! stock one, written by the reference client itself, is on disk — every value below is quoted from
|
||||
//! it:
|
||||
//!
|
||||
//! ```text
|
||||
//! WINDOW 1 SIZE 0 COLOR 0 0 0 0 LOCKED 1 DOCKED 1 SHOWN 1
|
||||
@@ -62,15 +61,13 @@
|
||||
//! | `SetChatWindowSize(id, size)` | `0x4a1470` | the cache's `SIZE` |
|
||||
//! | `SetChatWindowLocked(id, isLocked)` | `0x4a1650` | the cache's `LOCKED` |
|
||||
//!
|
||||
//! (wow-re `system/ui/ledger.tsv:9449-9451` + `scratch/item17-frameapi-fullcarve.md` l.17-18,
|
||||
//! VERIFIED; `[0x806498]` is the 255.0 the first two multiply by.) **Bytes, not floats** — that is
|
||||
//! (`[0x806498]` is the 255.0 the first two multiply by.) **Bytes, not floats** — that is
|
||||
//! why the getter renormalises by 1/255, and why [`ChatWindowLook`] stores the colour as `u8`: a
|
||||
//! set→get round trip through the real client quantises, and a store of `f32` here would
|
||||
//! round-trip values the reference cannot hold. `SetChatWindowAlpha(1, 0.4)` answers
|
||||
//! `102/255 = 0.4`, not `0.4`.
|
||||
//!
|
||||
//! **The record's own layout**, from the §5 dispatched for this work
|
||||
//! (`system/ui/scratch/chat-window-record.md`): the colour is ONE packed `CImVector` at
|
||||
//! **The record's own layout**: the colour is ONE packed `CImVector` at
|
||||
//! **`+0x88` B, `+0x89` G, `+0x8a` R, `+0x8b` A** — note the **BGRA** order — and the font size is
|
||||
//! an `i32` at **`+0x84`**, not a byte and with no `× 255` anywhere near it. (Both correct this
|
||||
//! module's earlier `+0xd8..+0xdb`, which was the low half of the *absolute* operand
|
||||
@@ -139,14 +136,14 @@ pub(super) const NUM_CHAT_WINDOWS: usize = 7;
|
||||
/// [`Self::default`] is the stock `chat-cache.txt` row — `COLOR 0 0 0 0`, `SIZE 0`, `LOCKED 1` —
|
||||
/// i.e. a black box at alpha 0 that cannot be dragged, which is the classic "chat is text over the
|
||||
/// world until you mouse over it".
|
||||
/// `CHATMSGGROUP` — the client's 68-entry message-group table at `0x805fb0` (wow-re
|
||||
/// `system/ui/scratch/chat-cache-grammar.md` §2): `(name, defaultOn, addedVersion)`, stride 0xc,
|
||||
/// `CHATMSGGROUP` — the client's 68-entry message-group table at `0x805fb0`:
|
||||
/// `(name, defaultOn, addedVersion)`, stride 0xc,
|
||||
/// in the order that IS the record's `+0x20` flag index. **Not** the 94-entry colour table: 67
|
||||
/// names are shared, `CREATURE` exists only here, and 27 colour names (RAID, OFFICER,
|
||||
/// WHISPER_INFORM, EMOTE, TEXT_EMOTE, MONSTER_*, CHANNEL_JOIN/LEAVE/LIST/NOTICE[_USER], AFK, DND,
|
||||
/// IGNORED, BG_SYSTEM_*, RAID_LEADER/WARNING/BOSS_EMOTE, FOREIGN_TELL, FILTERED, BATTLEGROUND[_LEADER])
|
||||
/// have no group. `defaultOn` is what the boot init copies into window 2's flags (§4);
|
||||
/// `addedVersion` is the loader's back-fill for a file older than `ADDEDVERSION 2` (§3).
|
||||
/// have no group. `defaultOn` is what the boot init (`0x4982c0`) copies into window 2's flags;
|
||||
/// `addedVersion` is the loader's (`0x498a60`) back-fill for a file older than `ADDEDVERSION 2`.
|
||||
pub const MESSAGE_GROUPS: [(&str, bool, u8); 68] = [
|
||||
("SYSTEM", true, 0),
|
||||
("SAY", true, 0),
|
||||
@@ -299,13 +296,13 @@ impl ChatWindowLook {
|
||||
/// `DOCKED` position from [`WINDOW_STATE`]. The seed for the model's array and for the
|
||||
/// settings parser's per-line default, so a file that never mentions `DOCKED` keeps the
|
||||
/// shipped dock rather than silently undocking both windows.
|
||||
/// The boot init `0x4982c0` (wow-re chat-cache-grammar.md §4) — the record a client with no
|
||||
/// The boot init `0x4982c0` — the record a client with no
|
||||
/// `chat-cache.txt` runs from, and what FrameXML's own `FloatingChatFrame_Update` then docks,
|
||||
/// hides and saves: window 1 shown and undocked (it IS the dock — `FCF_DockFrame(ChatFrame1, 1)`
|
||||
/// at FloatingChatFrame.xml's file scope), groups 1–10 by a literal fill; window 2 shown and
|
||||
/// `docked 1`, every `defaultOn` group of 11–68 (the 34 the stock file lists); windows 3–10
|
||||
/// hidden with nothing enabled. Every window locked, size 0, colour 0, name empty, ten empty
|
||||
/// channel slots. The init fires no event; the loader does (§8).
|
||||
/// channel slots. The init fires no event; the loader (`0x498a60`) does.
|
||||
pub fn stock(index: usize) -> Self {
|
||||
let groups = |range: std::ops::Range<usize>, all: bool| -> Vec<String> {
|
||||
MESSAGE_GROUPS[range]
|
||||
@@ -331,8 +328,8 @@ impl ChatWindowLook {
|
||||
}
|
||||
|
||||
/// The loader's discipline over the message set — unknown names dropped, duplicates folded,
|
||||
/// **table order** — so `GetChatWindowMessages` answers the way a flag walk does (§5: "table
|
||||
/// order 0..67") whatever order a file or a caller listed them in.
|
||||
/// **table order** — so `GetChatWindowMessages` (`0x4a0d20`) answers the way a flag walk does —
|
||||
/// table order 0..67 — whatever order a file or a caller listed them in.
|
||||
pub fn normalize_messages(&mut self) {
|
||||
let mut flags = [false; MESSAGE_GROUPS.len()];
|
||||
for m in &self.messages {
|
||||
@@ -420,8 +417,8 @@ impl super::Model {
|
||||
true
|
||||
}
|
||||
|
||||
/// **Strip `key` from every window's list** — leave-by-name `0x49ee70`'s `0x49f001`–`0x49f085`
|
||||
/// (wow-re `leavechannelbyname-contract.md` §6): all ten windows, the first entry whose name
|
||||
/// **Strip `key` from every window's list** — leave-by-name `0x49ee70`'s `0x49f001`–`0x49f085`:
|
||||
/// all ten windows, the first entry whose name
|
||||
/// equals `key` case-insensitively, the cell blanked. `key` is the DBC Shortcut when the
|
||||
/// argument matched a row, else the argument verbatim; the numeric form strips nothing,
|
||||
/// because a slot name never equals a window entry. Answers whether anything was stripped.
|
||||
@@ -588,8 +585,7 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
)?;
|
||||
|
||||
// SetChatWindowLocked(id, isLocked) — `0x4a1650`, writing the record's `+0x8c` locked field
|
||||
// (wow-re `system/ui/ledger.tsv:9325` + `scratch/chat-window-record.md` §2, VERIFIED:
|
||||
// `0x4a16a2 mov [esi+0xb4fedc],eax`, initialised to **1** at `0x4984e4`, read back as
|
||||
// (`0x4a16a2 mov [esi+0xb4fedc],eax`, initialised to **1** at `0x4984e4`, read back as
|
||||
// `GetChatWindowInfo`'s 8th return at `0x4a0cbf`).
|
||||
//
|
||||
// **A `bool` is the faithful store even though the field is an `i32`**: the cache writer
|
||||
@@ -713,10 +709,10 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
// ── The rest of the 0x98 record: name, shown, message types, channels ──────────────────
|
||||
//
|
||||
// `SetChatWindowName 0x4a13f0` / `SetChatWindowShown 0x4a1730` are registrar entries beside
|
||||
// the look setters (chat-window-record.md §8); `Add/Remove/GetChatWindowMessages`
|
||||
// the look setters; `Add/Remove/GetChatWindowMessages`
|
||||
// (`0x4a0e80`/`0x4a0f40`/`0x4a0d20`) and `Add/Remove/GetChatWindowChannel(s)`
|
||||
// (`0x4a1000`/`0x4a1260`/`0x4a0dc0`) read and write the `+0x20` flags and the `+0x64`/`+0x74`
|
||||
// arrays the same census bounded (§7). Every write that moves the record queues the window
|
||||
// arrays the same census bounded. Every write that moves the record queues the window
|
||||
// for the app's persist pass, like the look setters.
|
||||
|
||||
fn with_look<T>(
|
||||
@@ -834,7 +830,7 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
})?,
|
||||
)?;
|
||||
|
||||
// AddChatWindowChannel(id, "channel") — `0x4a1000` (chat-cache-grammar.md §5): (i) the
|
||||
// AddChatWindowChannel(id, "channel") — `0x4a1000`: (i) the
|
||||
// `ChatChannels.dbc` walk compares the argument with each row's **Shortcut**, whole, case-
|
||||
// folded; no match → id 0 and the Lua string; a match with no zone text yet → **nil** (0
|
||||
// values) and nothing stored; a match → the row's id and **the DBC's own Shortcut string**,
|
||||
@@ -961,7 +957,7 @@ mod tests {
|
||||
}
|
||||
|
||||
/// Trap 2: `shown`/`docked` are `nil` where the record stores 0, because FrameXML branches
|
||||
/// on them bare and `0` is true in Lua. At the boot init (chat-cache-grammar.md §4) window 1
|
||||
/// on them bare and `0` is true in Lua. At the boot init (`0x4982c0`) window 1
|
||||
/// is shown and undocked — it is the dock, `FCF_DockFrame(ChatFrame1, 1)` at file scope —
|
||||
/// window 2 is shown with `docked 1`, and 3..7 are neither; the `DOCKED 1`/`DOCKED 2`,
|
||||
/// `SHOWN 1`/`SHOWN 0` a stock file carries are what FrameXML's own dock pass then saved.
|
||||
@@ -996,8 +992,8 @@ mod tests {
|
||||
}
|
||||
|
||||
/// Trap 3: `docked` is the dock POSITION `FCF_DockFrame(frame, index)` inserts at — the
|
||||
/// init's window 2 answers 1 (§4, `mov ds:0xb4ff78, 1`), and once FrameXML has saved its
|
||||
/// dock (`FCF_SaveDock`) the stored positions are whatever it wrote.
|
||||
/// init's window 2 answers 1 (`0x4982c0`: `mov ds:0xb4ff78, 1`), and once FrameXML has saved
|
||||
/// its dock (`FCF_SaveDock`) the stored positions are whatever it wrote.
|
||||
#[test]
|
||||
fn docked_is_a_dock_position_not_a_flag() {
|
||||
let mut s = UiScript::new().unwrap();
|
||||
@@ -1065,7 +1061,7 @@ mod tests {
|
||||
/// The three setters round-trip through the getter — and they round-trip through the
|
||||
/// reference's BYTE quantisation, not through the float they were handed. `0.4 × 255 = 102`,
|
||||
/// and `102/255` is what comes back — and `0.5` comes back `127/255`, not `128/255`, because
|
||||
/// `__ftol` truncates (§5-verified: a real client stores 127 there).
|
||||
/// `__ftol` truncates (a real client stores 127 there).
|
||||
#[test]
|
||||
fn the_setters_round_trip_through_the_engine_byte() {
|
||||
let s = UiScript::new().unwrap();
|
||||
@@ -1214,7 +1210,7 @@ mod record_tests {
|
||||
use super::{ChatWindowLook, MESSAGE_GROUPS};
|
||||
use crate::script::{UiScript, ZoneChannelRow};
|
||||
|
||||
/// The boot init (chat-cache-grammar.md §4): window 1 shown with groups 1–10, window 2 shown
|
||||
/// The boot init (`0x4982c0`): window 1 shown with groups 1–10, window 2 shown
|
||||
/// at dock index 1 with the 34 `defaultOn` groups of 11–68, the rest empty and hidden.
|
||||
#[test]
|
||||
fn the_stock_records_are_the_boot_init() {
|
||||
@@ -1308,7 +1304,7 @@ mod record_tests {
|
||||
);
|
||||
}
|
||||
|
||||
/// `AddChatWindowChannel`'s legs (chat-cache-grammar.md §5): a shortcut match stores the
|
||||
/// `AddChatWindowChannel`'s legs (`0x4a1000`): a shortcut match stores the
|
||||
/// DBC's own spelling with its id, a custom name stores as typed with 0, a shortcut with no
|
||||
/// zone text yet is nil and nothing; a duplicate answers the id and stores nothing.
|
||||
#[test]
|
||||
|
||||
@@ -42,9 +42,9 @@ const BUILD_DATE: &str = "Sep 19 2006";
|
||||
pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
let g = lua.globals();
|
||||
|
||||
// `GetLocale()` -> one string, and `IsMacClient()` -> nil, both ARITY-EXACT in the reference's
|
||||
// own shape table (`re/audit/binding-shapes.tsv`: `GetLocale 0x46ce40`/`0x48d8b0` is
|
||||
// `0 -> 1 (string?)`, `IsMacClient 0x48c980` is `0 -> 1 (nil)`, both `exact`/`agree`).
|
||||
// `GetLocale()` -> one string, and `IsMacClient()` -> nil, both ARITY-EXACT: `GetLocale
|
||||
// 0x46ce40`/`0x48d8b0` is `0 -> 1 (string?)`, `IsMacClient 0x48c980` is `0 -> 1 (nil)`, both
|
||||
// `exact`/`agree`.
|
||||
//
|
||||
// **`IsMacClient` answering nil is the whole verb, not a placeholder.** The table records the
|
||||
// return KIND as `(nil)` — this binding pushes nil on the PC build the reference was carved
|
||||
@@ -61,8 +61,8 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
)?;
|
||||
g.set("IsMacClient", lua.create_function(|_, ()| Ok(Value::Nil))?)?;
|
||||
|
||||
// **`FrameXML_Debug([v])` — the XML loader's own trace switch, get-or-set** (decision 2160,
|
||||
// wow-re `ui/scratch/framexml-debug-trace-flag.md`). `0x488440` reads the global `[0xceea30]`
|
||||
// **`FrameXML_Debug([v])` — the XML loader's own trace switch, get-or-set** (decision 2160).
|
||||
// `0x488440` reads the global `[0xceea30]`
|
||||
// through `0x6edb40`, and:
|
||||
//
|
||||
// - a **Lua-truthy** argument takes the SET arm (`0x48845d je` after `lua_toboolean 0x6f34d0`)
|
||||
@@ -71,8 +71,7 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
// - the stored value is `lua_tonumber` truncated **toward zero** (`0x40a2b0`), so `1.9` is 1
|
||||
// and a non-numeric string is 0, which is 5.0's `tonumber` coercion;
|
||||
// - an absent, nil or false argument is a pure GET and leaves the flag alone;
|
||||
// - it always returns ONE number — the flag's value *after* the call
|
||||
// (`re/audit/binding-shapes.tsv`: `argc 1 exact, returns 1, (number), agree`).
|
||||
// - it always returns ONE number — the flag's value *after* the call.
|
||||
//
|
||||
// The reference ships a call to it commented out in its own `BasicControls.xml:20`; the
|
||||
// corpus's consumer is `ImprovedErrorFrame`, which drives it off a saved `XMLDebug` CVar at
|
||||
@@ -300,7 +299,7 @@ impl super::UiScript {
|
||||
}
|
||||
// The `realmName` CVar is the SAME fact, and it is set here so it cannot drift from
|
||||
// `GetRealmName()`. It is a real 1.12 CVar (`0x83f2d0`, persisted — the client builds
|
||||
// its SavedVariables path from it, wow-re `savedvariables-protocol.md`), and it had no
|
||||
// its SavedVariables path from it), and it had no
|
||||
// value at all here: `Ace/AceState.lua:27` is
|
||||
// `ace.trim(GetCVar("realmName"))` inside `SetGameState`, which EVERY Ace addon runs at
|
||||
// PLAYER_ENTERING_WORLD, so the nil became `gsub(nil, ...)` and took the family down.
|
||||
@@ -422,7 +421,8 @@ mod tests {
|
||||
}
|
||||
|
||||
/// `GetLocale` answers one string and `IsMacClient` answers nil — the reference's own shapes
|
||||
/// (`binding-shapes.tsv`, both `exact`/`agree`), and the arity is the half a caller branches on.
|
||||
/// (`GetLocale 0x46ce40`/`0x48d8b0`, `IsMacClient 0x48c980`, both `exact`/`agree`), and the
|
||||
/// arity is the half a caller branches on.
|
||||
#[test]
|
||||
fn the_client_identity_pair_answers_its_reference_arity() {
|
||||
let s = UiScript::new().unwrap();
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
//! The `ColorSelect` method surface — the `CSimpleColorSelect` widget behavior over the kind tag
|
||||
//! (factory `0x6eef90`; LoadXML `0x78b3f0`, script-map `0x78b4f0`, RF-28
|
||||
//! `rf28-typed-widget-loadxml.md`). The widget holds one colour and fires `OnColorSelect` (its own
|
||||
//! (factory `0x6eef90`; LoadXML `0x78b3f0`, script-map `0x78b4f0`). The widget holds one colour and
|
||||
//! fires `OnColorSelect` (its own
|
||||
//! script slot, `+0x338`) when that colour is set.
|
||||
//!
|
||||
//! **Why this is engine-side and not four lines of Lua on `ColorPickerFrame`.** The corpus creates
|
||||
@@ -11,10 +11,8 @@
|
||||
//! header names. So it lives with `Slider`/`StatusBar`, in the per-kind dispatcher.
|
||||
//!
|
||||
//! **The colour law is byte-verified, and it is not what it looks like.** This module was first
|
||||
//! written against the obvious model — store three RGB bytes, hand them back — and a §5 cross-check
|
||||
//! dispatched into wow-re for exactly this question corrected it
|
||||
//! (`system/ui/scratch/colorselect-color-law.md`, 2026-08-11: three independent derivations plus an
|
||||
//! emulated Unicorn oracle over the four helpers' own bytes). Two corrections, both load-bearing:
|
||||
//! written against the obvious model — store three RGB bytes, hand them back — which is wrong.
|
||||
//! Two corrections, both load-bearing:
|
||||
//!
|
||||
//! 1. **The state is HSV `f32`, not RGB** — see [`ColorSelectState`] for the members and why.
|
||||
//! 2. **The round trip is not the identity.** Inbound and outbound use *different* quantizers
|
||||
@@ -42,8 +40,8 @@
|
||||
//! callers in the 218-addon corpus and waited for one under decision 1195; the customer that
|
||||
//! arrived is our own `ColorPickerFrame.xml`, whose four elements the XML loader installs through
|
||||
//! exactly these — two of them with **no file at all**, because the disc and the strip are pixels
|
||||
//! the client computes and the app renderer now computes too (decision 1592, and wow-re
|
||||
//! `system/ui/scratch/colorselect-drawn-appearance.md` for what it draws).
|
||||
//! the client computes and the app renderer now computes too (decision 1592 — the wheel `0x78b580`
|
||||
//! and the value strip `0x78b8a0` generate them).
|
||||
//!
|
||||
//! **NOT carried, still waiting for a customer (decision 1195):** `SetColorHSV`/`GetColorHSV`
|
||||
//! (`0x78e920`/`0x78ea00`) — zero corpus callers, though the state they would read and write is
|
||||
@@ -86,7 +84,7 @@ fn with_colorselect<T>(
|
||||
}
|
||||
|
||||
/// One of the widget's four texture sub-objects. The wheel and the strip are the two the press
|
||||
/// handler hit-tests (`[+0x318]+0x24` and `[+0x320]+0x24`, wow-re `colorselect-color-law.md` §5);
|
||||
/// handler (`0x78bf10`) hit-tests (`[+0x318]+0x24` and `[+0x320]+0x24`);
|
||||
/// the two thumbs are pure output, positioned from the HSV at extract.
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
||||
enum Slot {
|
||||
@@ -116,8 +114,7 @@ impl Slot {
|
||||
*slot = Some(rh);
|
||||
}
|
||||
|
||||
/// The layer a slot's region is born on — VERIFIED, not guessed: wow-re
|
||||
/// `colorselect-drawn-appearance.md` reads the wheel bound at layer 2 (ARTWORK) through
|
||||
/// The layer a slot's region is born on: the wheel is bound at layer 2 (ARTWORK) through
|
||||
/// `0x77fd10`, with the two markers on OVERLAY above the art they ride.
|
||||
fn layer(self) -> DrawLayer {
|
||||
match self {
|
||||
@@ -249,8 +246,8 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
|
||||
m.set(
|
||||
"SetColorRGB",
|
||||
// Shape C on r, g, b (`ColorSelect:SetColorRGB 0x78eae0`, `2=C 3=C 4=C 5=B`, wow-re
|
||||
// `numeric-arg-coercion-law.md`): bare `lua_tonumber`, nil → 0.0, no raise.
|
||||
// Shape C on r, g, b (`ColorSelect:SetColorRGB 0x78eae0`, `2=C 3=C 4=C 5=B`): bare
|
||||
// `lua_tonumber`, nil → 0.0, no raise.
|
||||
lua.create_function(|lua, (this, r, g, b): (Table, Value, Value, Value)| {
|
||||
let (r, g, b) = (
|
||||
crate::script::object::as_f64(&r),
|
||||
@@ -308,7 +305,7 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Fire `OnColorSelect(self, r, g, b)` — the widget's own script slot (`+0x338`, RF-28). Fired
|
||||
/// Fire `OnColorSelect(self, r, g, b)` — the widget's own script slot (`+0x338`). Fired
|
||||
/// outside the model borrow; a handler error goes to [`Model::errors`] rather than back to the
|
||||
/// setter's caller, the same posture as the Slider's `OnValueChanged`.
|
||||
fn fire_color_select(lua: &Lua, this: &Table, r: f64, g: f64, b: f64) -> mlua::Result<()> {
|
||||
@@ -332,8 +329,8 @@ fn fire_color_select(lua: &Lua, this: &Table, r: f64, g: f64, b: f64) -> mlua::R
|
||||
|
||||
/// The wheel's normalised coordinates for a cursor at `(x, y)`: the offset from the wheel rect's
|
||||
/// centre, scaled by its **half-extents** — so a non-square wheel normalises to a disc, not an
|
||||
/// ellipse (`0x78bdd0`..: `nx=(x−cx)/((right−left)·0.5)`, `ny=(y−cy)/((top−bottom)·0.5)`, wow-re
|
||||
/// `colorselect-color-law.md` §5). y is up, this arena's convention and the client's.
|
||||
/// ellipse (`0x78bdd0`..: `nx=(x−cx)/((right−left)·0.5)`, `ny=(y−cy)/((top−bottom)·0.5)`). y is up,
|
||||
/// this arena's convention and the client's.
|
||||
fn wheel_norm(r: Rect, x: f32, y: f32) -> (f32, f32) {
|
||||
let hw = (r.right - r.left) * 0.5;
|
||||
let hh = (r.top - r.bottom) * 0.5;
|
||||
@@ -373,7 +370,7 @@ fn strip_value(r: Rect, y: f32) -> f32 {
|
||||
/// the inverse of [`wheel_hs`], which is the whole invariant — click a pixel, the marker lands on
|
||||
/// that pixel.
|
||||
///
|
||||
/// Byte-exact to `0x78bc20` (wow-re `colorselect-drawn-appearance.md` §4), which does
|
||||
/// Byte-exact to `0x78bc20`, which does
|
||||
/// `SetPoint(CENTER, wheel, CENTER, −m·cos θ, −m·sin θ)` with `θ = H·π/180` and
|
||||
/// `m = GetWidth(wheel)·0.5·S`. Its two `fchs` are the pick law's `+π` seen from the other side:
|
||||
/// drop either sign and the marker sits diametrically opposite the colour it marks.
|
||||
@@ -544,8 +541,8 @@ mod tests {
|
||||
}
|
||||
|
||||
/// **The round trip is not the identity, and the deviation is the client's.** `(0, 1, 1)` — cyan,
|
||||
/// a two-channel tie at the maximum — comes back with green one step low, the exact witness
|
||||
/// wow-re's exhaustive sweep names. Greys are its control class and never lose.
|
||||
/// a two-channel tie at the maximum — comes back with green one step low, exactly as the
|
||||
/// reference does. Greys are its control class and never lose.
|
||||
#[test]
|
||||
fn the_round_trip_loses_exactly_one_step_on_the_witness_colour() {
|
||||
let s = UiScript::new().unwrap();
|
||||
@@ -567,7 +564,7 @@ mod tests {
|
||||
}
|
||||
|
||||
/// **And it ratchets.** The FrameXML idiom — read the colour back, hand it straight to
|
||||
/// `SetColorRGB` — re-applies the same lossy map instead of settling. wow-re's pin: `(0, 8, 132)`
|
||||
/// `SetColorRGB` — re-applies the same lossy map instead of settling. `(0, 8, 132)`
|
||||
/// walks to `(0, 0, 132)` in eight cycles. Every Ace2/Dewdrop colour option is that idiom, once
|
||||
/// per open-and-accept, so this is the shape of a real player-visible drift and it is deliberate.
|
||||
#[test]
|
||||
@@ -738,7 +735,7 @@ mod tests {
|
||||
}
|
||||
|
||||
/// **A click on the wheel picks the colour that pixel shows.** The disc's law and the pick law
|
||||
/// are inverses (wow-re `colorselect-color-law.md` §5 / `colorselect-drawn-appearance.md`), so
|
||||
/// are inverses (`0x78b580` draws it, `0x78bdd0` picks it), so
|
||||
/// this is stated at the four cardinal points, where the hue is nameable: LEFT is red, RIGHT
|
||||
/// cyan, TOP violet, BOTTOM chartreuse. A sign error anywhere in the chain mirrors one axis and
|
||||
/// two of these four flip.
|
||||
|
||||
@@ -18,8 +18,8 @@ use mlua::{Lua, Table, Value};
|
||||
use super::cursor::{self, CursorItem, CursorPayload};
|
||||
use super::Model;
|
||||
|
||||
/// The charter/petition lines an item tooltip prints under its name — wow-re
|
||||
/// `system/ui/scratch/tooltip-content-law.md`'s **line 3**: *"(petition/guild-charter items) Title /
|
||||
/// The charter/petition lines an item tooltip prints under its name — the item tooltip's own
|
||||
/// emission order, line 3 (`0x854d7c..0x854dd0`): *"(petition/guild-charter items) Title /
|
||||
/// Creator / Num-signatures (white; title wraps) — a resolved petition object; keys
|
||||
/// `PETITION_*`/`GUILD_CHARTER_*`"*.
|
||||
///
|
||||
@@ -28,11 +28,11 @@ use super::Model;
|
||||
/// plain petition reads *"Petition:"* / *"Created by"*.
|
||||
///
|
||||
/// **The third line, the signature count, is deliberately NOT here.** The law names it, but neither
|
||||
/// the carve nor the packet says where the number comes from: the petition record carries no count,
|
||||
/// and the only candidate — the item's `ITEM_FIELD_ENCHANTMENT` slot-0 *charges* dword, which
|
||||
/// vmangos's own source documents as "the on-item signature count" — has its write **commented
|
||||
/// out** server-side, so it is always zero and the guess could never be falsified here. Omitted
|
||||
/// rather than invented; the line is one field away the day someone pins the source.
|
||||
/// the reference nor the packet says where the number comes from: the petition record carries no
|
||||
/// count, and the only candidate — the item's `ITEM_FIELD_ENCHANTMENT` slot-0 *charges* dword,
|
||||
/// which vmangos's own source documents as "the on-item signature count" — has its write
|
||||
/// **commented out** server-side, so it is always zero and the guess could never be falsified here.
|
||||
/// Omitted rather than invented; the line is one field away the day someone pins the source.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Default)]
|
||||
pub struct PetitionSlotView {
|
||||
/// The record's charter bit: `true` picks the `GUILD_CHARTER_*` keys, `false` the `PETITION_*`
|
||||
@@ -68,7 +68,7 @@ pub struct ContainerSlot {
|
||||
/// Whether this item may be placed on an ACTION-BAR slot — app-resolved from the template
|
||||
/// exactly like [`Self::equip_slots`] (the engine holds no item knowledge of its own).
|
||||
/// `PlaceAction`'s only item filter, byte-read: an on-use spell OR equippable
|
||||
/// (`ItemInfo::placeable_on_action_bar`, wow-re `action-item-slot.md` §5 — decision 0666).
|
||||
/// (`ItemInfo::placeable_on_action_bar`, `PlaceAction 0x4e62e0` — decision 0666).
|
||||
pub bar_placeable: bool,
|
||||
/// The instance's live durability `(current, max)` — `ITEM_FIELD_DURABILITY`/`MAXDURABILITY`
|
||||
/// off the streamed item object; `None` for indestructible items (max 0) or while the create
|
||||
@@ -97,7 +97,7 @@ pub struct ContainerSlot {
|
||||
/// live enchant slot naming a `SpellItemEnchantment` row that binds. App-resolved off the raw
|
||||
/// descriptor ([`crate::items::already_bound`] in benilla-app — the same predicate the enchant
|
||||
/// cursor's bind question asks), because the binding half needs a DBC join and the engine
|
||||
/// holds no item knowledge of its own. The tooltip's §6 bind line overrides to **Soulbound**
|
||||
/// holds no item knowledge of its own. The tooltip's bind line overrides to **Soulbound**
|
||||
/// on it (B310); `false` on any source with no streamed item object.
|
||||
pub already_bound: bool,
|
||||
/// The instance's enchant slots, resolved by the app ([`super::EnchantView`]) and in
|
||||
@@ -122,8 +122,8 @@ pub struct ContainerSlot {
|
||||
pub petition: Option<PetitionSlotView>,
|
||||
}
|
||||
|
||||
/// One enchant slot as the tooltip renders it (wow-re `ui/scratch/tooltip-content-law.md` §E3,
|
||||
/// byte-verified; decision 0920). The app resolves the DBC row; every rule below is the engine's.
|
||||
/// One enchant slot as the tooltip renders it (`0x52c9f9`–`0x52ca23`; decision 0920). The app
|
||||
/// resolves the DBC row; every rule below is the engine's.
|
||||
#[derive(Clone, Debug, Default, PartialEq)]
|
||||
pub struct EnchantView {
|
||||
/// `ITEM_FIELD_ENCHANTMENT` slot: **0** permanent · **1** temporary · **2..6** the
|
||||
@@ -273,11 +273,10 @@ impl super::UiScript {
|
||||
/// **Arm the gift-wrap cursor** on the paper at `(bag, slot)` — the app's call when a
|
||||
/// right-click takes the reference's begin-wrap arm (`ItemInfo::begins_gift_wrap`).
|
||||
///
|
||||
/// Three acts, exactly the three `0x5edea0` performs (wow-re
|
||||
/// `object-layer/scratch/gift-wrap-law.md`): the paper's slot reads **locked**, the displayed
|
||||
/// cursor becomes **mode 2** (`Interface\Cursor\Cast.blp` — the same art the cast cursor
|
||||
/// uses, table `0x853b88` slot 2), and **nothing goes on the wire**. What completes it is the
|
||||
/// next left-click on a container slot. Decision 1934.
|
||||
/// Three acts, exactly the three `0x5edea0` performs: the paper's slot reads **locked**, the
|
||||
/// displayed cursor becomes **mode 2** (`Interface\Cursor\Cast.blp` — the same art the cast
|
||||
/// cursor uses, table `0x853b88` slot 2), and **nothing goes on the wire**. What completes it
|
||||
/// is the next left-click on a container slot. Decision 1934.
|
||||
pub fn arm_gift_wrap(&mut self, bag: i64, slot: u32) {
|
||||
let mut model = self.model_mut();
|
||||
model.pending_wrap = Some(PendingWrap { bag, slot });
|
||||
@@ -288,8 +287,8 @@ impl super::UiScript {
|
||||
|
||||
/// **Cancel an armed gift wrap and nothing else** — disarm, unlock the paper, reset the
|
||||
/// cursor. Deliberately narrower than `ClearCursor`, which also drops any held payload:
|
||||
/// what wow-re records is that a right-click "uses the item and cancels the wrap", not by
|
||||
/// which of the two routes, so this touches only the half the finding actually names.
|
||||
/// the reference records that a right-click "uses the item and cancels the wrap," not by
|
||||
/// which of the two routes, so this touches only the half it names.
|
||||
pub fn cancel_gift_wrap(&mut self) -> Option<PendingWrap> {
|
||||
let mut model = self.model_mut();
|
||||
let wrap = model.pending_wrap.take()?;
|
||||
@@ -346,7 +345,7 @@ pub struct PendingWrap {
|
||||
pub slot: u32,
|
||||
}
|
||||
|
||||
/// A **displayed-cursor override** the FrameXML cursor family arms (wow-re cursor-system.md §7): the
|
||||
/// A **displayed-cursor override** the FrameXML cursor family arms: the
|
||||
/// single "displayed mode" (`0xbe2c2c`) the real client swaps to while a UI element wants a non-base
|
||||
/// cursor, restored to the base mode by `ResetCursor`. The app maps each to the matching
|
||||
/// `Interface\Cursor\*` art over the world classifier's Point.
|
||||
@@ -380,9 +379,9 @@ pub enum UiCursorMode {
|
||||
/// empty, same-item (the wire merges), and different-item (the wire swaps: the displaced item
|
||||
/// lands where the held one came from) all alike. The displaced item never hops onto the
|
||||
/// cursor: 0216 §2 shipped that exchange off 0091's gloss; the director's eye caught it and
|
||||
/// the same-day §5 verdict refuted it at the bytes — no `SetCursorItem` on the place branch
|
||||
/// (`0x5e0c40`/`0x4f9b30`), `ClearCursor(0)`, one put-down sound (decision 0218; wow-re
|
||||
/// cursor-dragdrop-slots.md). Bag placements are server-authoritative; only the ACTION bar
|
||||
/// the bytes refuted it — no `SetCursorItem` on the place branch
|
||||
/// (`0x5e0c40`/`0x4f9b30`), `ClearCursor(0)`, one put-down sound (decision 0218). Bag
|
||||
/// placements are server-authoritative; only the ACTION bar
|
||||
/// hops its displaced payload (client-authoritative — the slice-4 note in 0218).
|
||||
/// - holding a SPLIT carry (`count: Some(n)`), a click onto an empty/unresolved/same-item
|
||||
/// destination queues the split move and clears; onto a DIFFERENT item it's a no-op (kept —
|
||||
@@ -563,8 +562,8 @@ fn queue_move(
|
||||
pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
// ContainerIDToInventoryID(containerID) → the equipment slot the bag is worn in.
|
||||
//
|
||||
// **Two linear arms, one signed compare, and nothing else** (`0x4f94e0`, 124 bytes; wow-re
|
||||
// `bag-language-combat-action-bindings.md` §1, §5-cross-checked). Written as the reference's
|
||||
// **Two linear arms, one signed compare, and nothing else** (`0x4f94e0`, 124 bytes). Written
|
||||
// as the reference's
|
||||
// own three instructions — `t = id - 1`, `if t < 4` (SIGNED), then `+20` or `+60` — rather
|
||||
// than as the two closed forms `id+19` / `id+59`, because the wrap at the `i32` edge is then
|
||||
// reproduced rather than approximated.
|
||||
@@ -604,11 +603,10 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
)?;
|
||||
|
||||
// SetBagPortaitTexture(texture, containerID) — `0x4fa4f0` (374 B), the reference's own
|
||||
// spelling, typo included, and CARVED (wow-re `system/ui/scratch/bag-portrait-and-appendtext.md`,
|
||||
// §5 four-worker round). The bag window's portrait: `ContainerFrame_GenerateFrame` sets it for
|
||||
// every container except the keyring, which takes `SetPortraitToTexture` with a fixed path one
|
||||
// line above (ContainerFrame.lua l.418-423). benilla runs that file off the player's chain
|
||||
// (1751), so this is a live call site.
|
||||
// spelling, typo included, and CARVED. The bag window's portrait:
|
||||
// `ContainerFrame_GenerateFrame` sets it for every container except the keyring, which takes
|
||||
// `SetPortraitToTexture` with a fixed path one line above (ContainerFrame.lua l.418-423).
|
||||
// benilla runs that file off the player's chain (1751), so this is a live call site.
|
||||
//
|
||||
// Three arms, and the FIRST is the one nobody would have guessed:
|
||||
//
|
||||
@@ -855,7 +853,7 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
model.item_picks.push((bag, slot));
|
||||
return Ok(false);
|
||||
}
|
||||
// Repair mode (`0x4f9c7b`, wow-re repair-machinery.md): while the repair cursor is
|
||||
// Repair mode (`0x4f9c7b`): while the repair cursor is
|
||||
// armed, the click means "repair this item" — queued for the app to send — and
|
||||
// nothing is picked up. The mode STICKS across clicks (only HideRepairCursor /
|
||||
// merchant-close clears it).
|
||||
@@ -872,7 +870,7 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
// every surface routes through — top-level there for exactly the same reason.
|
||||
|
||||
// ShowContainerSellCursor(bag, slot) — arm the pouch cursor for a sellable hover (5875
|
||||
// `0x4fa460`, wow-re cursor-system.md §7: Buy(3) only when the slot actually holds an item —
|
||||
// `0x4fa460`: Buy(3) only when the slot actually holds an item —
|
||||
// an empty slot leaves the cursor unchanged; no Unable twin, no SellPrice check — selling
|
||||
// never grays), and the reference's own `IsTargeting` bail at its first instruction.
|
||||
lua.globals().set(
|
||||
@@ -884,8 +882,8 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
.get(&bag)
|
||||
.and_then(|c| c.slots.get(&slot))
|
||||
.is_some();
|
||||
// `IsTargeting` bails at the function's FIRST instruction — an armed spell suppresses
|
||||
// the sell cursor outright (wow-re `item-target-cursor-and-dropitemonunit.md`). Now
|
||||
// `IsTargeting` bails at the function's FIRST instruction (`0x4fa469`) — an armed spell
|
||||
// suppresses the sell cursor outright. Now
|
||||
// that the mode is sticky, this gate has to be here rather than app-side: a write of
|
||||
// Buy would otherwise stamp over the cast cursor and there would be no per-frame world
|
||||
// write to put it back.
|
||||
@@ -933,7 +931,7 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
//
|
||||
// Its one shipped caller is the unit-frame hover pair (`UnitFrame_OnEnter`/`OnLeave`), which is
|
||||
// the ONLY lit/grey cursor split over a UI element in 1.12 — and it is authored in Lua, not in
|
||||
// C++ (wow-re `item-target-cursor-and-dropitemonunit.md` §4.3).
|
||||
// C++.
|
||||
lua.globals().set(
|
||||
"SetCursor",
|
||||
lua.create_function(|lua, name: Option<String>| {
|
||||
@@ -1484,7 +1482,7 @@ mod tests {
|
||||
.unwrap());
|
||||
}
|
||||
|
||||
// ── `ContainerIDToInventoryID` (wow-re `bag-language-combat-action-bindings.md` §1) ─────────
|
||||
// ── `ContainerIDToInventoryID` (`0x4f94e0`) ─────────────────────────────────────
|
||||
|
||||
/// Two linear arms and no range check. **−2 → 17 and 0 → 19 are not special cases** — they are
|
||||
/// ordinary points on the `id <= 4` line that the caller-side "keyring is −2 / backpack is 0"
|
||||
|
||||
@@ -51,8 +51,8 @@
|
||||
//! module's, not the app's (decision 1124: [`recipe_order`], the craft type's own byte-verified
|
||||
//! comparator, applied in [`UiScript::set_craft`]). `GetCraftInfo` therefore never
|
||||
//! returns the `"header"` `craftType` the ref Lua also checks for (ref l.38/77/199/252/292). The
|
||||
//! header/grouping law itself is no longer pending — decision 0446 confirmed it byte-exact (wow-re
|
||||
//! `tradeskill` TU-B) and TradeSkill grew the real tree engine ([`super::tradeskill`]'s `build_groups`)
|
||||
//! header/grouping law itself is no longer pending — decision 0446 confirmed it byte-exact
|
||||
//! (`0x4f60c0`) and TradeSkill grew the real tree engine ([`super::tradeskill`]'s `build_groups`)
|
||||
//! on it. Craft never got the same port and stays on this v1 flat render; `Expand/CollapseCraftSkillLine`
|
||||
//! are still literal no-ops, wired only so the ported XML's header-click handlers don't error
|
||||
//! (TradeSkill's `Expand/CollapseTradeSkillSubClass` precedent). 0446 also confirmed the one live case
|
||||
@@ -67,8 +67,7 @@
|
||||
//! [`super::tooltip_spell`] beside `SetTrainerService`, its structural twin: both are SELECTORS
|
||||
//! into the shared spell/item builders rather than renderers of their own. The v1 two-line
|
||||
//! "name white, description gold" render is gone — `SetCraftSpell 0x533e90` funnels into the same
|
||||
//! `0x52e610`/`0x52b650` pair every other content binding does, so two lines were eight-plus short
|
||||
//! (wow-re `ui/scratch/trainer-service-tooltip-law.md` §4.1).
|
||||
//! `0x52e610`/`0x52b650` pair every other content binding does, so two lines were eight-plus short.
|
||||
|
||||
use mlua::{Lua, MultiValue, Value, Variadic};
|
||||
|
||||
@@ -188,8 +187,7 @@ pub struct CraftState {
|
||||
const CRAFT_TYPE_BEAST_TRAINING: u32 = 1;
|
||||
|
||||
/// The Craft window's **row order** — `0x4f6920` (craft type 1, Beast Training) / `0x4f67a0` (every
|
||||
/// other type, i.e. Enchanting), byte-verified in wow-re
|
||||
/// (`system/ui/scratch/trainer-craft-list-order.md`, decision 1124). Both are the same cascade and
|
||||
/// other type, i.e. Enchanting; decision 1124). Both are the same cascade and
|
||||
/// the Beast Training one has one extra key:
|
||||
///
|
||||
/// 1. the **difficulty tier** `[+0xc]` ascending ([`super::TradeSkillDifficulty::tier`]);
|
||||
@@ -357,7 +355,7 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
// trainingPointCost, requiredLevel (ref l.37/168/289's own 7-tuple — module doc). `craftType` is
|
||||
// the difficulty color key, never "header"; `isExpanded` is always nil; the trailing two are
|
||||
// Beast Training's own fields, hardcoded 0 (out of scope, module doc). OOB → a single nil.
|
||||
// The two link verbs (wow-re `tradeskill/scratch/tradeskill-craft-item-links.md`, 1973).
|
||||
// The two link verbs (1973).
|
||||
//
|
||||
// GetCraftItemLink(index) — `0x4f72a0`: the number gate raises its Usage; then the spell's
|
||||
// Spell.dbc `castUI` decides. An Enchanting recipe (`castUI == 3` — this window's craft type,
|
||||
@@ -832,7 +830,7 @@ mod tests {
|
||||
assert_eq!(s.eval::<i64>("return GetCraftSelectionIndex()").unwrap(), 0);
|
||||
}
|
||||
|
||||
/// **Beast Training's rank order** (decision 1124), pinned against wow-re's emulated run of the
|
||||
/// **Beast Training's rank order** (decision 1124), pinned against an emulated run of the
|
||||
/// real `0x4f6920` over real `Spell.dbc` values — and the regression for the director's report
|
||||
/// that "Beast Training lists a skill's ranks out of ascending order" (ledger B229).
|
||||
///
|
||||
@@ -897,7 +895,7 @@ mod tests {
|
||||
]
|
||||
);
|
||||
|
||||
// The falsifiable control wow-re ran: the SAME rows at the Enchanting craft type select
|
||||
// The falsifiable control: the SAME rows at the Enchanting craft type select
|
||||
// `0x4f67a0`, which has no `spellLevel` key — so the ranks fall to the trailing spell-id
|
||||
// tie-break instead. That they still come out ascending here is benilla's determinism, not
|
||||
// the reference's order; what matters is that the two types differ at all.
|
||||
@@ -922,7 +920,7 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
/// The craft link pair (wow-re `tradeskill-craft-item-links.md`, 1973): an Enchanting recipe
|
||||
/// The craft link pair (`0x4f72a0`; 1973): an Enchanting recipe
|
||||
/// answers the ENCHANT link — fixed white, the spell's id and name — and cannot miss; another
|
||||
/// craft type answers zero values (no shipped CREATE_ITEM input); the reagent link is nil
|
||||
/// until its template is in the store, then the item link in its quality colour; the raises.
|
||||
|
||||
@@ -45,7 +45,7 @@ pub const EQUIPMENT_BAG: i64 = -100;
|
||||
// ─────────────────────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
/// What the cursor carries — the client's payload-mode global [0xb4d900] as a typed enum
|
||||
/// (wow-re cursor-dragdrop-payload.md §1: 1 = live item, 3 = spell, **4 = pet action**, **8 =
|
||||
/// (1 = live item, 3 = spell, **4 = pet action**, **8 =
|
||||
/// macro**, **10 = stabled pet**; our Action arm is the client's bar-slot pickup; the money arm
|
||||
/// is mode 2 ([`CursorMoney`], 1962/1965) and the preview arm stays unbuilt). One transition seam
|
||||
/// for every surface, so sounds, CURSOR_UPDATE, and lock display can't drift apart per window (decision 0216).
|
||||
@@ -65,20 +65,20 @@ pub enum CursorPayload {
|
||||
Merchant(CursorMerchantItem),
|
||||
/// A pet picked up from the stable window — **mode 10** (see [`CursorStablePet`]).
|
||||
///
|
||||
/// Mode 10 was recorded as "class/talent ability (DBC)" until wow-re's stable-master carve
|
||||
/// corrected it (`system/ui/scratch/stable-master-window.md` §9; decision 1677): `0x495020` is
|
||||
/// the stabled-pet grab, `[0xb4d900] = 10` is written at exactly one site image-wide, and it is
|
||||
/// inside it. benilla built the stable's drag frame-locally on the strength of the old note —
|
||||
/// this variant is what puts it back on the shared cursor, so a stable pet dropped on the world
|
||||
/// or another window clears through the same path as every other payload.
|
||||
/// Mode 10 was recorded as "class/talent ability (DBC)" until the bytes corrected it (decision
|
||||
/// 1677): `0x495020` is the stabled-pet grab, `[0xb4d900] = 10` is written at exactly one site
|
||||
/// image-wide, and it is inside it. benilla built the stable's drag frame-locally on the
|
||||
/// strength of that earlier reading — this variant is what puts it back on the shared cursor,
|
||||
/// so a stable pet dropped on the world or another window clears through the same path as
|
||||
/// every other payload.
|
||||
StablePet(CursorStablePet),
|
||||
/// Coins picked up off a money frame — **mode 2** (see [`CursorMoney`]).
|
||||
Money(CursorMoney),
|
||||
}
|
||||
|
||||
/// Copper held on the cursor — payload **mode 2**, the client's `[0xb4e2f0]` amount written by
|
||||
/// `0x494cc0` from `PickupPlayerMoney 0x48abc0` with `LOOTWINDOWCOINSOUND` (wow-re
|
||||
/// `cursor-dragdrop-payload.md` §1's payload table). The purse is never debited by the pickup:
|
||||
/// `0x494cc0` from `PickupPlayerMoney 0x48abc0` with `LOOTWINDOWCOINSOUND`. The purse is never
|
||||
/// debited by the pickup:
|
||||
/// the stock `MoneyTypeInfo["PLAYER"]` shows `GetMoney() - GetCursorMoney() - …`, the subtraction
|
||||
/// being how the held coins leave the display while the money stays yours (1962).
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
@@ -114,11 +114,11 @@ pub struct CursorMerchantItem {
|
||||
/// A pet held on the cursor from the stable window — payload **mode 10** (`0x495020`, the grab
|
||||
/// `0x495010`).
|
||||
///
|
||||
/// Mode 10 was recorded as "class/talent ability (DBC)" until wow-re's stable-master carve
|
||||
/// corrected it (`system/ui/scratch/stable-master-window.md` §9; decision 1677): `[0xb4d900] = 10`
|
||||
/// is written at exactly one site image-wide and it is inside the stabled-pet grab, whose id global
|
||||
/// `[0xb4e300]` holds a **stable index**, not a spell. benilla first built the stable's drag
|
||||
/// frame-locally on the strength of the old note; this puts it on the shared cursor.
|
||||
/// Mode 10 was recorded as "class/talent ability (DBC)" until the bytes corrected it (decision
|
||||
/// 1677): `[0xb4d900] = 10` is written at exactly one site image-wide and it is inside the
|
||||
/// stabled-pet grab, whose id global `[0xb4e300]` holds a **stable index**, not a spell. benilla
|
||||
/// first built the stable's drag
|
||||
/// frame-locally on the strength of that earlier reading; this puts it on the shared cursor.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct CursorStablePet {
|
||||
/// The slot the grab came from, in the Lua index space `PickupStablePet` takes (`0` = the
|
||||
@@ -221,8 +221,8 @@ pub struct CursorAction {
|
||||
/// accepted drop is a word that already existed elsewhere in client state. Nothing is encoded at
|
||||
/// drop time and nothing here needs decoding.
|
||||
///
|
||||
/// This payload is **pet-bar-only**. `PlaceAction` refuses it (wow-re `action-item-slot.md`'s
|
||||
/// payload table: pet actions and class abilities are the refused modes, macros are not), and
|
||||
/// This payload is **pet-bar-only**. `PlaceAction` refuses it (`PlaceAction 0x4e62e0`'s
|
||||
/// accept table: pet actions and class abilities are the refused modes, macros are not), and
|
||||
/// nothing else produces it — which is also why a pet bar can only ever be *rearranged*, never
|
||||
/// populated from the spellbook: its contents are the server's.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
@@ -238,8 +238,8 @@ pub struct CursorPetAction {
|
||||
}
|
||||
|
||||
/// A macro payload ([`super::macros`]'s `PickupMacro` produces it) — the client's cursor mode
|
||||
/// **8**, whose payload global is the bare macro id (`[0xb4e2fc]`, wow-re `action-item-slot.md`'s
|
||||
/// payload table, where mode 8 is the ONE non-item/non-spell payload `PlaceAction` accepts).
|
||||
/// **8**, whose payload global is the bare macro id (`[0xb4e2fc]`, the ONE non-item/non-spell
|
||||
/// payload `PlaceAction 0x4e62e0` accepts).
|
||||
/// Unlike every other arm this one carries no source slot to swap back to: a macro lives in the
|
||||
/// macro table, not in the surface it was dragged from, so a refused place just leaves it held.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
@@ -273,7 +273,7 @@ pub(crate) fn queue_cursor_update(model: &mut Model) {
|
||||
// - the ACTION bar's grid follows "is anything held that could land there" — every arm except
|
||||
// the pet one, which `PlaceAction` refuses outright, and the vendor row (mode 5): its grab
|
||||
// goes through the shared setter `0x4950f0`, whose `ACTIONBAR_SHOWGRID` branch is taken for
|
||||
// mode 7 alone (`0x495106`/`0x49513a`, wow-re `merchant-cursor-law.md` §5);
|
||||
// mode 7 alone (`0x495106`/`0x49513a`);
|
||||
// - the PET bar's grid follows the pet payload alone. The reference fires `PET_BAR_SHOWGRID`
|
||||
// from inside the pet-action pickup builder itself (`0x494f28`), not from a shared cursor
|
||||
// transition — so a spell on the cursor lights the action bar's empty slots and leaves the
|
||||
@@ -377,9 +377,8 @@ pub(crate) fn clear_cursor(model: &mut Model) {
|
||||
///
|
||||
/// Both drop the payload and fire `CURSOR_UPDATE`. `ClearCursor` also un-locks the source slot;
|
||||
/// this does **not**: `0x494b60`'s mode-1 arm is entered with `param1 = 0`, which skips `0x495420`,
|
||||
/// so the sold item's slot stays **greyed** until the server's own inventory update removes it
|
||||
/// (wow-re `object-layer/scratch/vendor-sell-on-right-click.md` §3, the §5 trio dispatched from
|
||||
/// benilla 1905). Leaving the lock set is the point: the item is gone from the cursor but not yet
|
||||
/// so the sold item's slot stays **greyed** until the server's own inventory update removes it.
|
||||
/// Leaving the lock set is the point: the item is gone from the cursor but not yet
|
||||
/// gone from the bag, and the grey is what says so.
|
||||
///
|
||||
/// Shared by the two verbs that sell off the cursor — `PickupMerchantItem`'s sell fork
|
||||
@@ -427,8 +426,8 @@ pub enum WorldPick {
|
||||
/// release both on the world (the world frame, or no frame at all where none is loaded — the
|
||||
/// caller's `over_world`, decision 2089), no drag (the byte-verified trigger, decision 0218: the
|
||||
/// client's `0x495300` runs on the WorldFrame click release only; a drag released over the world
|
||||
/// routes as a drag and keeps carrying) — routed by [`Model::world_pick`] (decisions 0571 + 0574,
|
||||
/// byte-verified wow-re cursor-dragdrop-payload.md §11):
|
||||
/// routes as a drag and keeps carrying) — routed by [`Model::world_pick`] (`0x481f60`; decisions
|
||||
/// 0571 + 0574):
|
||||
///
|
||||
/// - `Object`: NOTHING drops — the object leg (`0x492ce0`) keeps every real payload and
|
||||
/// dispatches SELECT instead.
|
||||
@@ -700,7 +699,7 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
}
|
||||
// Mode 2 — coins (1962). Reported as nothing for the same reason: the money arm
|
||||
// of `GetCursorInfo` is not carved, and `GetCursorMoney` is the reference's own
|
||||
// way of asking. Joins the carve when it lands.
|
||||
// way of asking. The same gap closes once that arm is carved.
|
||||
Some(CursorPayload::Money(_)) => {
|
||||
Ok((Value::Nil, Value::Nil, Value::Nil, Value::Nil))
|
||||
}
|
||||
@@ -951,7 +950,7 @@ mod tests {
|
||||
assert!(s.cursor_payload().is_none());
|
||||
}
|
||||
|
||||
/// The world-drop pick routing (decisions 0571 + 0574 — wow-re §11, amended by 0843): a
|
||||
/// The world-drop pick routing (decisions 0571 + 0574 — `0x481f60`, amended by 0843): a
|
||||
/// spell/action payload clears silently on BOTH empty-world legs — terrain included, the
|
||||
/// 0843 divergence (the reference keeps it on terrain; the director wants the left click to
|
||||
/// dismiss) — while an item keeps its byte-faithful popup flow.
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
//! The three BAG-SLOT verbs — `PutItemInBag`, `PutItemInBackpack`, `PickupBagFromSlot`.
|
||||
//!
|
||||
//! CARVED, not inferred (wow-re `system/ui/ui.md`, "The three bag verbs — one worker, a three-way
|
||||
//! fork, and a return value that is not 'did it place'"; `scratch/bag-verbs-law.md`, §5 trio +
|
||||
//! orchestrator byte arbitration 2026-08-11). The three findings that shape everything below:
|
||||
//! CARVED, not inferred. The three findings that shape everything below:
|
||||
//!
|
||||
//! 1 · **`PutItemInBackpack()` IS `PutItemInBag(0xFF)`.** `0x4c7ed0` is a ten-byte thunk —
|
||||
//! `mov ecx,0xff` / `jmp 0x4c7c00` — so there is one worker here too, and only the destination
|
||||
|
||||
@@ -84,7 +84,7 @@ pub(super) fn pickup_action(model: &mut Model, id: u32) -> bool {
|
||||
///
|
||||
/// Returns whether the caller should repaint.
|
||||
pub(crate) fn place_action(model: &mut Model, id: u32) -> bool {
|
||||
// The two accept filters, byte-read (wow-re `action-item-slot.md` §5 — decision 0666). Both
|
||||
// The two accept filters, byte-read (`PlaceAction 0x4e62e0` — decision 0666). Both
|
||||
// reject with a **bare return**: no store, no clear, no packet — mechanically identical to
|
||||
// clicking with an empty cursor, and the refused payload STAYS on the cursor.
|
||||
//
|
||||
@@ -115,7 +115,7 @@ pub(crate) fn place_action(model: &mut Model, id: u32) -> bool {
|
||||
CursorPayload::Item(i) => Some((ACTION_KIND_ITEM, i.item_id, i.texture.clone())),
|
||||
CursorPayload::Spell(s) => Some((ACTION_KIND_SPELL, s.spell_id, s.texture.clone())),
|
||||
// Mode 8 — the one non-item/non-spell payload the reference's `PlaceAction` accepts
|
||||
// (`action-item-slot.md` §5's payload table: pet actions and class abilities are refused,
|
||||
// (`0x4e62e0`'s accept table: pet actions and class abilities are refused,
|
||||
// macros are not). It packs the bare macro id under the MACRO tag, exactly as the SPELL
|
||||
// and ITEM arms pack theirs.
|
||||
CursorPayload::Macro(m) => Some((ACTION_KIND_MACRO, m.index, m.texture.clone())),
|
||||
|
||||
@@ -76,8 +76,7 @@ pub(crate) fn maybe_start_drag(model: &mut Model, pos: (f32, f32)) -> Option<(u3
|
||||
// un-presses (`0x779410`, guarded on `locked == 0 && state != DISABLED`) and then forwards to
|
||||
// the base notify that fires `<OnDragStart>`. So a button you drag off releases its pushed art
|
||||
// at the THRESHOLD crossing, which is here — not when the cursor leaves its rect, and not for
|
||||
// a frame that never registered for drag (wow-re `scratch/button-state-edge-set.md`;
|
||||
// decision 2134).
|
||||
// a frame that never registered for drag (decision 2134).
|
||||
super::super::button::edge(model, source, crate::widget::ButtonState::on_drag_start);
|
||||
let id = model
|
||||
.arena
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
//! The money cursor — payload mode 2 (decisions 1962, 1965): coins picked up off a money frame,
|
||||
//! and everything the reference does with them. VERIFIED at the bytes throughout — wow-re
|
||||
//! `money-cursor-law.md` (commit `a4dc2ff4`), which 1965 folded in:
|
||||
//! and everything the reference does with them. VERIFIED at the bytes throughout:
|
||||
//!
|
||||
//! * **The purse is never debited.** Nothing on this path writes `PLAYER_FIELD_COINAGE`; the
|
||||
//! backpack's figure is `GetMoney() − GetCursorMoney() − GetPlayerTradeMoney()`, FrameXML
|
||||
@@ -140,7 +139,7 @@ pub(crate) fn pickup_trade_money(model: &mut Model, amount: i32) -> bool {
|
||||
}
|
||||
|
||||
impl crate::script::UiScript {
|
||||
/// The trade window's open leg (`SetTradePartner 0x4bf4e0`, §4b): coins held on the cursor
|
||||
/// The trade window's open leg (`SetTradePartner 0x4bf4e0`): coins held on the cursor
|
||||
/// fold into the offer before anything else. Answers the new offer when they did.
|
||||
pub fn fold_cursor_money_into_trade(&mut self) -> Option<u32> {
|
||||
let mut model = self.model_mut();
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
//! The pet bar's drag (decision 1010; wow-re `ui/scratch/pet-action-bar-api.md` §10.3/§10.4/§10.7)
|
||||
//! — **one** Lua verb, `PickupPetAction`, which forks on whether the cursor is already carrying a
|
||||
//! pet payload. The reference's own bar calls it from all three gestures (`PetActionButton_OnClick`
|
||||
//! The pet bar's drag (decision 1010) — **one** Lua verb, `PickupPetAction`, which forks on
|
||||
//! whether the cursor is already carrying a pet payload. The reference's own bar calls it from all
|
||||
//! three gestures (`PetActionButton_OnClick`
|
||||
//! under shift, `OnDragStart`, and `OnReceiveDrag` — `PetActionBarFrame.lua:252-283`), so "pick up"
|
||||
//! and "drop" are the same binding seen from either side of an empty cursor.
|
||||
//!
|
||||
@@ -43,7 +43,7 @@ use super::{queue_cursor_update, CursorPayload, CursorPetAction};
|
||||
/// **The clamp lives here**, and it is a deliberate divergence from the reference's own arithmetic:
|
||||
/// its callers gate `cmp esi,0xa; jbe`, which admits index 10 (Lua argument 11), and neither
|
||||
/// `0x4bce00` nor `0x4bc9a0` bounds-checks again — so a real client writes one dword *past* the
|
||||
/// ten-slot array. Not reproduced (wow-re §10.7 says so in as many words).
|
||||
/// ten-slot array. Not reproduced.
|
||||
const PET_SLOTS: u32 = 10;
|
||||
|
||||
/// The client's slot type, masked as it masks it (`(packed >> 24) & 0x3F`).
|
||||
@@ -66,8 +66,8 @@ fn payload_word(packed: u32) -> Option<u32> {
|
||||
}
|
||||
|
||||
/// The duplicate scan's comparison key (`0x4bca44`/`0x4bca57`, mask `0x3FFFFFFF`) — everything but
|
||||
/// the two autocast bits. **Not** type-1-only: the note's earlier prose said the duplicate clear
|
||||
/// was, and the bytes say the mask covers the whole type+action field for any source type.
|
||||
/// the two autocast bits. **Not** type-1-only: the mask covers the whole type+action field for
|
||||
/// any source type.
|
||||
fn same_action(a: u32, b: u32) -> bool {
|
||||
a & 0x3FFF_FFFF == b & 0x3FFF_FFFF
|
||||
}
|
||||
@@ -80,8 +80,8 @@ fn is_blanked_spell(packed: u32) -> bool {
|
||||
}
|
||||
|
||||
/// A relocation **candidate** (`0x4bca8b`–`0x4bcab0`): not a token, and its low 16 bits zero. So an
|
||||
/// all-zero slot qualifies and so does a blanked type-1 slot — the note's earlier "token-or-empty"
|
||||
/// reading was wrong in both directions.
|
||||
/// all-zero slot qualifies and so does a blanked type-1 slot — not simply "token-or-empty" in
|
||||
/// either direction.
|
||||
fn is_relocation_candidate(packed: u32) -> bool {
|
||||
!is_token(packed) && packed & 0xFFFF == 0
|
||||
}
|
||||
@@ -277,9 +277,9 @@ fn write_slot(model: &mut Model, target: usize, source: u32, passive: bool) -> O
|
||||
Some(assigned)
|
||||
}
|
||||
|
||||
/// A **token** word's `texture` is the NAME of a global, not a path — `GetPetActionInfo`'s own
|
||||
/// convention (§2.4: the two `char[32]` tables hand back `PET_ATTACK_TEXTURE`, and the bar paints
|
||||
/// with `getglobal(texture)`). The cursor takes a PATH, so a command or reaction word that reaches
|
||||
/// A **token** word's `texture` is the NAME of a global, not a path — `GetPetActionInfo 0x4bdc50`'s
|
||||
/// own convention: the two `char[32]` tables hand back `PET_ATTACK_TEXTURE`, and the bar paints
|
||||
/// with `getglobal(texture)`. The cursor takes a PATH, so a command or reaction word that reaches
|
||||
/// the cursor has to carry one, or the drag is invisible — you pick Attack up and nothing sticks
|
||||
/// to the pointer. Resolved here, the one place a slot's view becomes a cursor payload, exactly as
|
||||
/// `PetActionBar.xml` resolves it in the one place a slot's view becomes a button.
|
||||
|
||||
@@ -132,7 +132,7 @@ pub struct VideoCaps {
|
||||
/// **A BACKEND constant, not a device capability** — `caps+0x20` is written by the two device
|
||||
/// constructors only (1 under Direct3D, 0 under OpenGL) and read by `GetVideoCaps` alone, so
|
||||
/// `hasTripleBuffering == 1` means "the backend is Direct3D". Its name is the one INFERRED
|
||||
/// label in the seven (wow-re `ui/scratch/video-options-verbs.md` §2.2).
|
||||
/// label in the seven (`GetVideoCaps 0x48db40`).
|
||||
///
|
||||
/// `false` here, and it is pushed as the **number 0**, never nil: benilla is not Direct3D and
|
||||
/// exposes no buffering knob, so check button 13 is hidden and button 6 re-seated
|
||||
@@ -522,8 +522,7 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
// the selection with `GetCurrentMultisampleFormat()`, `_Initialize` walks
|
||||
// `GetMultisampleFormats()` three varargs at a time building the menu, and the Okay handler
|
||||
// (l.240) calls `SetMultisampleFormat(UIDropDownMenu_GetSelectedID(...))`. Identities and
|
||||
// behaviour from wow-re `system/console/scratch/gxmultisample-default.md` §7 — registration
|
||||
// table `0x83de68`, records `0x83e2c0`/`0x83e2c8`/`0x83e2d0`.
|
||||
// behaviour from the registration table `0x83de68`, records `0x83e2c0`/`0x83e2c8`/`0x83e2d0`.
|
||||
lua.globals().set(
|
||||
"GetMultisampleFormats",
|
||||
lua.create_function(|lua, ()| {
|
||||
@@ -632,8 +631,7 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
/// reference's own verb writes, each named by the row it is.
|
||||
///
|
||||
/// **The reference does NOT restore registered defaults, and this list is not a guess at which it
|
||||
/// does** (wow-re `ui/scratch/video-options-verbs.md` §4.3, the round that corrected benilla's
|
||||
/// first reading). `0x48dad0` → `0x639a20` maps the already-matched `VideoHardware.dbc` row through
|
||||
/// does.** `0x48dad0` → `0x639a20` maps the already-matched `VideoHardware.dbc` row through
|
||||
/// nine lookup tables into a hardware-recommended settings struct, copies the `CGxFormat` **preset**
|
||||
/// that row names for this adapter (`[0xc4e6ac]` → `0xc518e0 + 0x38·n`) into the working device
|
||||
/// record, writes that record into twelve `gx*` CVars, performs a full synchronous `RestartGx`, and
|
||||
@@ -942,7 +940,7 @@ fn install_video_verbs(lua: &Lua) -> mlua::Result<()> {
|
||||
)?;
|
||||
// ── GetGamma / SetGamma ──────────────────────────────────────────────────────────────────
|
||||
// **The display-brightness pair** (2182) — `GetGamma 0x4891c0` and `SetGamma 0x4891f0`, carved
|
||||
// end to end in wow-re `ui/scratch/video-options-verbs.md` §3.
|
||||
// end to end.
|
||||
//
|
||||
// The one thing about them that is not obvious from the name: **the unit is the SLIDER's
|
||||
// offset, not the CVar's.** `0x4891d0` is `dc 2d`, ModRM reg field 5 = **FSUBR** (`mem − ST(0)`,
|
||||
@@ -1036,8 +1034,7 @@ pub const WORLD_DETAIL_STOPS: [u32; 3] = [16, 32, 48];
|
||||
/// row 3 is `func = "WorldDetail"`, and `OptionsFrame_Save`/`_Load` prefer `getglobal("Set"..func)`
|
||||
/// / `getglobal("Get"..func)` over `SetCVar`/`GetCVar`). Decision 2163.
|
||||
///
|
||||
/// **The setter, carved end to end** (own decode, agreeing with wow-re
|
||||
/// `cvar/scratch/registered-defaults-census.md` §8):
|
||||
/// **The setter, carved end to end** (own decode):
|
||||
///
|
||||
/// ```text
|
||||
/// 488ddf call 0x6f34d0 ; lua_isnumber(L,1)? else error 0x8423e8
|
||||
@@ -1068,8 +1065,8 @@ pub const WORLD_DETAIL_STOPS: [u32; 3] = [16, 32, 48];
|
||||
/// descending table with ascending `elseif`s, so Restore Defaults always parks the slider at 0.
|
||||
///
|
||||
/// **Where benilla diverges, and why it is not the smallCull ladder.** `SmallCull` is a dead knob
|
||||
/// in the reference — `[0x868620]` has one writer and no reader image-wide (wow-re
|
||||
/// `cvar/scratch/graphics-cost-cvar-census.md` §8) — so its *only* consumer there is this getter,
|
||||
/// in the reference — `[0x868620]` has one writer and no reader image-wide — so its *only*
|
||||
/// consumer there is this getter,
|
||||
/// which makes it storage for a stop the client keeps nowhere else. benilla keeps the stop:
|
||||
/// [`CVAR_WORLD_DETAIL`] is it, and `frillDensity` is the same knob in the reference's unit. So the
|
||||
/// getter reads the stop directly and `SmallCull` is not registered, because registering a CVar
|
||||
@@ -1157,8 +1154,7 @@ const USAGE_SET_WORLD_DETAIL: &str = "Usage: SetWorldDetail(value)";
|
||||
const RANGE_SET_WORLD_DETAIL: &str = "value must be in the range 0, 2";
|
||||
|
||||
/// **The four nameplate verbs** — `ShowNameplates 0x489450`, `HideNameplates 0x489460`,
|
||||
/// `ShowFriendNameplates 0x489470`, `HideFriendNameplates 0x489480` (wow-re
|
||||
/// `ui/scratch/party-leader-and-nameplate-verbs.md`, §5 four-worker round).
|
||||
/// `ShowFriendNameplates 0x489470`, `HideFriendNameplates 0x489480`.
|
||||
///
|
||||
/// Each is a **10-byte body** over one of two setters, and the reference's shape is worth stating
|
||||
/// because two obvious readings are wrong:
|
||||
|
||||
@@ -38,7 +38,7 @@ pub struct DeathUiState {
|
||||
/// XP_LOSS_NO_SICKNESS, ref UIParent.lua's CONFIRM_XP_LOSS arm).
|
||||
pub sickness_duration: Option<String>,
|
||||
/// What `HasSoulstone()` answers: the **label** of the self-resurrect available right now, or
|
||||
/// `None` for nil (decision 1746, whose Context carries the wow-re decode this shape comes from).
|
||||
/// `None` for nil (`HasSoulstone 0x48ac80`; decision 1746).
|
||||
///
|
||||
/// A string, not an id, because that is the whole of what the API returns: the DEATH dialog
|
||||
/// stamps it straight onto its second button (`Button2:SetText(HasSoulstone())`) and uses the
|
||||
|
||||
@@ -247,8 +247,7 @@ impl super::UiScript {
|
||||
/// and the demand-load copy classified a missing manifest entry as a script error, so an addon the
|
||||
/// reference loads (logging `Couldn't open %s` and continuing) came back as a session failure here.
|
||||
///
|
||||
/// The reference's own shape, VERIFIED (wow-re `system/ui/scratch/xml-toc-path-resolution.md` §4
|
||||
/// and `include-lua-dispatch.md` §7): a failed open is **non-fatal** — `0x6edaa0` logs
|
||||
/// The reference's own shape: a failed open is **non-fatal** — `0x6edaa0` logs
|
||||
/// `"Couldn't open %s"` (`0x846ff4`) to the log sink at severity 2 and returns null; every failure
|
||||
/// leg "reports through the sink and returns normally, with no throw, no `longjmp` and no abort",
|
||||
/// and the enclosing `.toc`/document keeps loading. Nothing on that path reaches the Lua error
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
//! The dialog engine's own verbs (decision 1963) — the bindings the stock `StaticPopup.lua`
|
||||
//! bodies call that no window of ours had ever needed, each built to wow-re's
|
||||
//! `staticpopup-dialog-bindings.md` (VERIFIED at the bytes unless a line here says INFERRED):
|
||||
//! bodies call that no window of ours had ever needed, each built to the reference's own shape
|
||||
//! (VERIFIED at the bytes unless a line here says INFERRED):
|
||||
//!
|
||||
//! | binding | args → returns | here |
|
||||
//! |---|---|---|
|
||||
@@ -10,8 +10,8 @@
|
||||
//! | `GetAreaSpiritHealerTime()` | 0 → 1 number, whole seconds | reads the app's deadline |
|
||||
//! | `AcceptBattlefieldPort(index, accept)` | 2 → 0 | raises on a non-number index, silent off 1..3; the optional-bool answer |
|
||||
//! | `CancelMeetingStoneRequest()` | 0 → 0 | queued; the app applies the leadership gate |
|
||||
//! | `IsInMeetingStoneQueue()` | 0 → the number `1` or `nil`, one value always | `[0xb72038] != 0` (wow-re `meeting-stone-status.md` §3.1, 1974) |
|
||||
//! | `GetMeetingStoneStatusText()` | 0 → string or `nil` | the cached text `[0xb7203c]`, `nil` while empty (§3.2) |
|
||||
//! | `IsInMeetingStoneQueue()` | 0 → the number `1` or `nil`, one value always | `[0xb72038] != 0` (1974) |
|
||||
//! | `GetMeetingStoneStatusText()` | 0 → string or `nil` | the cached text `[0xb7203c]`, `nil` while empty |
|
||||
//! | `CheckPetUntrainerDist()` | 0 → `1` or `nil` | the app's latch-and-range flag |
|
||||
//! | `ConfirmPetUnlearn()` | 0 → 0 | counted; the app holds the latch and the money gate |
|
||||
//!
|
||||
@@ -79,7 +79,8 @@ impl super::UiScript {
|
||||
pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
let g = lua.globals();
|
||||
|
||||
// 0 args → 1 number: `max(deadline − now, 0) / 1000`, truncated (§5).
|
||||
// 0 args → 1 number: `max(deadline − now, 0) / 1000`, truncated (`GetInstanceBootTimeRemaining
|
||||
// 0x48b620`).
|
||||
g.set(
|
||||
"GetInstanceBootTimeRemaining",
|
||||
lua.create_function(|lua, ()| {
|
||||
@@ -88,7 +89,8 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
})?,
|
||||
)?;
|
||||
|
||||
// §6: `if (cached healer == 0:0) return;` — SILENT — else `0x2E3` + the cached guid.
|
||||
// `AcceptAreaSpiritHeal 0x48df20`: `if (cached healer == 0:0) return;` — SILENT — else `0x2E3`
|
||||
// + the cached guid.
|
||||
g.set(
|
||||
"AcceptAreaSpiritHeal",
|
||||
lua.create_function(|lua, ()| {
|
||||
@@ -99,11 +101,11 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
Ok(())
|
||||
})?,
|
||||
)?;
|
||||
// §6: `0x6e7040(0xA18)` — the generic cancel-aura routine: the event fires (only for this
|
||||
// spell), then `0x136` + `u32 2584`, no guid. The routine's refusal — AttributesEx bit 13
|
||||
// set, bit 2 clear, and `0x5ee290(player)` — never reaches its third leg for spell 2584:
|
||||
// the shipped Spell.dbc fails the first two, checked against the data by the app's
|
||||
// `spell_2584_never_trips_the_cancel_gate`. So the send is unconditional here.
|
||||
// `CancelAreaSpiritHeal 0x48df30`: `0x6e7040(0xA18)` — the generic cancel-aura routine: the
|
||||
// event fires (only for this spell), then `0x136` + `u32 2584`, no guid. The routine's refusal
|
||||
// — AttributesEx bit 13 set, bit 2 clear, and `0x5ee290(player)` — never reaches its third leg
|
||||
// for spell 2584: the shipped Spell.dbc fails the first two, checked against the data by the
|
||||
// app's `spell_2584_never_trips_the_cancel_gate`. So the send is unconditional here.
|
||||
g.set(
|
||||
"CancelAreaSpiritHeal",
|
||||
lua.create_function(|lua, ()| {
|
||||
@@ -124,9 +126,9 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
})?,
|
||||
)?;
|
||||
|
||||
// §7: arg 1 must satisfy `lua_isnumber` (a number or a numeric string) or the binding
|
||||
// RAISES `Usage:`; truncated, 1-based, off 1..=3 SILENT. Arg 2 is the reference's
|
||||
// optional-boolean coercion (`0x6f1c10`, default 0), normalised to one byte.
|
||||
// `AcceptBattlefieldPort 0x4ab3b0`: arg 1 must satisfy `lua_isnumber` (a number or a numeric
|
||||
// string) or the binding RAISES `Usage:`; truncated, 1-based, off 1..=3 SILENT. Arg 2 is the
|
||||
// reference's optional-boolean coercion (`0x6f1c10`, default 0), normalised to one byte.
|
||||
g.set(
|
||||
"AcceptBattlefieldPort",
|
||||
lua.create_function(|lua, (index, accept): (Value, Value)| {
|
||||
@@ -154,8 +156,8 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
})?,
|
||||
)?;
|
||||
|
||||
// §8: `0x293`, empty; gated only on party leadership (the app's, which holds the party) —
|
||||
// it clears nothing, the server's reply does.
|
||||
// `CancelMeetingStoneRequest 0x4ca5a0`: `0x293`, empty; gated only on party leadership (the
|
||||
// app's, which holds the party) — it clears nothing, the server's reply does.
|
||||
g.set(
|
||||
"CancelMeetingStoneRequest",
|
||||
lua.create_function(|lua, ()| {
|
||||
@@ -189,8 +191,8 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
})?,
|
||||
)?;
|
||||
|
||||
// §9: `1` or `nil` — never `0`. The range test (`d² <= INTERACT_DISTANCE²`, a NaN OUT) is the
|
||||
// app's, on the latched trainer.
|
||||
// `CheckPetUntrainerDist 0x48d1c0`: `1` or `nil` — never `0`. The range test
|
||||
// (`d² <= INTERACT_DISTANCE²`, a NaN OUT) is the app's, on the latched trainer.
|
||||
g.set(
|
||||
"CheckPetUntrainerDist",
|
||||
lua.create_function(|lua, ()| {
|
||||
@@ -198,8 +200,8 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
Ok(flag(model.pet_untrainer_pending))
|
||||
})?,
|
||||
)?;
|
||||
// §9: the confirm arm — the latch's guid, the money gate (`ERR_NOT_ENOUGH_MONEY`, no packet)
|
||||
// and `0x2F0`, all the app's; here the call is counted.
|
||||
// `ConfirmPetUnlearn 0x48dca0`: the confirm arm — the latch's guid, the money gate
|
||||
// (`ERR_NOT_ENOUGH_MONEY`, no packet) and `0x2F0`, all the app's; here the call is counted.
|
||||
g.set(
|
||||
"ConfirmPetUnlearn",
|
||||
lua.create_function(|lua, ()| {
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
//!
|
||||
//! In the client the widget clones the unit's live model on `SetUnit`/`Dress` (`0x5059a0`, the
|
||||
//! attachment tree deep-copied) and `TryOn` overwrites one bodyslot or one of two hand lanes of
|
||||
//! that clone (wow-re `ui/scratch/dressup-model-equipment.md`). benilla renders no FrameXML model:
|
||||
//! that clone (`0x504350`). benilla renders no FrameXML model:
|
||||
//! every model pane is a booth bake the app composes from a look — the player's own visible items
|
||||
//! with the tried-on ones substituted in — and the VM holds neither item templates nor the
|
||||
//! player's equipment. So the verbs record *intents*, in order, and the app applies them
|
||||
@@ -18,10 +18,10 @@
|
||||
//! ## What each verb means, off the bytes
|
||||
//!
|
||||
//! - `SetUnit(unit)` / `RefreshUnit()` / `Dress()` all funnel into the same rebuild-from-the-unit
|
||||
//! worker (§1 of the note): every substitution is gone and the model is what the player shows in
|
||||
//! worker (`0x505b50`): every substitution is gone and the model is what the player shows in
|
||||
//! the world. One intent, [`DressUpIntent::Dress`].
|
||||
//! - `Undress()` → `0x504490`: clears components bodyslots `0..0xb` — every worn piece, base and
|
||||
//! tried-on alike — and touches no hand lane, so a held weapon stays (§5). [`DressUpIntent::Undress`].
|
||||
//! tried-on alike — and touches no hand lane, so a held weapon stays. [`DressUpIntent::Undress`].
|
||||
//! - `TryOn(item)`: the argument is `trunc(tonumber(arg))` (`__ftol 0x40a2b0`) handed to the item
|
||||
//! cache, so a numeric string is an id and anything else is item 0, which previews nothing. The
|
||||
//! stock `DressUpItemLink` hands it the digits it `gsub`bed out of the `|Hitem:` link. It gates
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
//! The `EditBox` runtime — keyboard focus, the text buffer + cursor + selection, editing, and the
|
||||
//! key/char dispatch (`CSimpleEditBox`, factory `0x6eec70`).
|
||||
//!
|
||||
//! Grounded in wow-5875-re's byte-verified RF-0082 (`rf82-editbox-runtime.md`):
|
||||
//! Byte-verified against the reference client:
|
||||
//!
|
||||
//! - **Focus (§1):** a single class-owned focus owner ([`Model::focused_editbox`], the client's
|
||||
//! - **Focus:** a single class-owned focus owner ([`Model::focused_editbox`], the client's
|
||||
//! `DAT_00cf4dc8`). `SetFocus` gates on effective-visibility, is a no-op if already focused, and
|
||||
//! fires `OnEditFocusLost` on the old box then `OnEditFocusGained` on the new. A LeftButton click
|
||||
//! focuses UNCONDITIONALLY — and **collapses the selection to the clicked byte index** on its way
|
||||
@@ -12,17 +12,17 @@
|
||||
//! field, and `0x77e3f6` is the only instruction image-wide that gives a box focus — so *every*
|
||||
//! focus gain, whatever triggers it, is selection-neutral.
|
||||
//!
|
||||
//! **`autoFocus` DOES focus on show** (corrected 2026-08-29, wow-re `editbox-selection-focus-law.md`
|
||||
//! §6): the OnShow override tail-jumps `SetFocus` when nothing else holds focus, and the OnHide
|
||||
//! **`autoFocus` DOES focus on show** (corrected 2026-08-29, `0x81c910` slot +0x30 `0x77a750`):
|
||||
//! the OnShow override tail-jumps `SetFocus` when nothing else holds focus, and the OnHide
|
||||
//! mirror tail-jumps `ClearFocus`. The old "verified by absence" negative came from a `call`-only
|
||||
//! census that could not see a tail-`jmp`. Both overrides are [`visibility_focus`] here, and the
|
||||
//! construction default is `true` off the bytes (see
|
||||
//! [`EditBoxState::auto_focus`](crate::widget::EditBoxState::auto_focus)). The
|
||||
//! self-acquire-on-first-key half stands beside it.
|
||||
//! - **Routing (§2):** a focused box processes and CONSUMES every key/char (`return 1` past the
|
||||
//! - **Routing:** a focused box processes and CONSUMES every key/char (`return 1` past the
|
||||
//! guard); an unfocused non-autoFocus box ignores input. The override fires ONLY the specialized
|
||||
//! scripts (Enter/Escape/Space/Tab/TextChanged/TextSet/focus), never generic `OnKeyDown`/`OnChar`.
|
||||
//! - **Text/editing (§3/§4):** every insert replaces the selection first; `numeric` aborts an insert
|
||||
//! - **Text/editing:** every insert replaces the selection first; `numeric` aborts an insert
|
||||
//! wholesale on any non-digit; caps trim from the end (`maxBytes` then `maxLetters`); `SetText`
|
||||
//! short-circuits when unchanged; `HighlightText(0,-1)` selects all with the client's clamp.
|
||||
//!
|
||||
@@ -62,9 +62,9 @@ pub(super) const REG_EDITBOX_METHODS: &str = "__benilla_editbox_methods";
|
||||
// Public entry points (called from UiScript::char_input / key_input / mouse_button)
|
||||
// ─────────────────────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
/// A typed character. Routes per §1/§2; on a focused box, Ctrl+A (delivered as the SOH control char)
|
||||
/// selects all, other C0 control chars are consumed-but-inert, and printable text is inserted. Always
|
||||
/// consumes when a box is (or becomes) focused.
|
||||
/// A typed character. Routed per the focus/routing rules above; on a focused box, Ctrl+A
|
||||
/// (delivered as the SOH control char) selects all, other C0 control chars are consumed-but-inert,
|
||||
/// and printable text is inserted. Always consumes when a box is (or becomes) focused.
|
||||
pub(super) fn char_input(lua: &Lua, text: &str) -> bool {
|
||||
let Some(h) = route(lua) else {
|
||||
return false;
|
||||
@@ -86,7 +86,7 @@ pub(super) fn char_input(lua: &Lua, text: &str) -> bool {
|
||||
}
|
||||
|
||||
/// Paste an OS-clipboard string into the focused box. The engine-free runtime can't reach the OS
|
||||
/// clipboard itself (RF-0082's stated gap), so the host reads it and hands the text here. Sanitized
|
||||
/// clipboard itself, so the host reads it and hands the text here. Sanitized
|
||||
/// the same way a typed char is gated — every C0 control char is dropped, except a newline in a
|
||||
/// `multiLine` box — then inserted as one edit (selection-replace + numeric/caps rules via [`insert`],
|
||||
/// no `OnSpacePressed` fire: a paste is not a typed space). Always consumes when a box is focused.
|
||||
@@ -104,8 +104,8 @@ pub(super) fn paste(lua: &Lua, text: &str) -> bool {
|
||||
}
|
||||
|
||||
/// A non-character key by name — only the three *box-event* keys act (their FrameXML scripts);
|
||||
/// every editing key reaches the box as a semantic [`EditAction`] via [`action`] instead. Routes
|
||||
/// per §1/§2; a focused box consumes the key even when it does nothing with it.
|
||||
/// every editing key reaches the box as a semantic [`EditAction`] via [`action`] instead. Follows
|
||||
/// the same focus/routing rules; a focused box consumes the key even when it does nothing with it.
|
||||
pub(super) fn key_input(lua: &Lua, key: &str) -> bool {
|
||||
let Some(h) = route(lua) else {
|
||||
return false;
|
||||
@@ -130,8 +130,8 @@ pub(super) fn key_input(lua: &Lua, key: &str) -> bool {
|
||||
}
|
||||
|
||||
/// One semantic editing operation — the host's per-OS keymap output (decision 0301). Same
|
||||
/// routing/consumption law as [`key_input`]: the focused (or self-acquiring per §2) box processes
|
||||
/// it; no box → not consumed.
|
||||
/// routing/consumption law as [`key_input`]: the focused (or self-acquiring if needed) box
|
||||
/// processes it; no box → not consumed.
|
||||
pub(super) fn action(lua: &Lua, a: EditAction) -> bool {
|
||||
let Some(h) = route(lua) else {
|
||||
return false;
|
||||
@@ -143,8 +143,8 @@ pub(super) fn action(lua: &Lua, a: EditAction) -> bool {
|
||||
// Anything reaching this arm was Alt-held or was not an arrow, and the reference moves
|
||||
// the caret for both.
|
||||
EditUnit::Char => move_horizontal(lua, h, !back, extend),
|
||||
// Ctrl/Option picks the word-granular cursor helper (RF-0082 §4: "char- vs
|
||||
// word-granular by the Ctrl check").
|
||||
// Ctrl/Option picks the word-granular cursor helper (the client's Ctrl check,
|
||||
// `0x41f8f0(1)`).
|
||||
EditUnit::Word => interact::move_word(lua, h, !back, extend),
|
||||
EditUnit::Edge => move_to_edge(lua, h, !back, extend),
|
||||
},
|
||||
@@ -157,12 +157,12 @@ pub(super) fn action(lua: &Lua, a: EditAction) -> bool {
|
||||
// History recall (the chat box's `historyLines`): prev = older, next = newer, live draft
|
||||
// restored past the newest. Single-line only (benilla's multiLine box has no vertical
|
||||
// caret nav — survey gap — a multiLine box consumes the step inert). The recall chords
|
||||
// are the host keymap's plain Up/Down (rf82's history controller is untraced; decision
|
||||
// 0301). The alt-arrow gate DOES cover them, upstream: UP and DOWN are two of the four
|
||||
// codes it declines, so on a flagged box — which the reference's own chat box is —
|
||||
// history recall is **Alt**+Up/Down and a plain Up/Down turns the camera. This file used
|
||||
// to say the opposite ("`ignoreArrows` does not gate them"), which followed from reading
|
||||
// the flag as consume-but-inert; the §5 corrected both halves.
|
||||
// are the host keymap's plain Up/Down (the reference's own history controller is
|
||||
// untraced; decision 0301). The alt-arrow gate DOES cover them, upstream: UP and DOWN are
|
||||
// two of the four codes it declines, so on a flagged box — which the reference's own chat
|
||||
// box is — history recall is **Alt**+Up/Down and a plain Up/Down turns the camera. This
|
||||
// file used to say the opposite ("`ignoreArrows` does not gate them"), which followed
|
||||
// from reading the flag as consume-but-inert; both halves above are now corrected.
|
||||
EditAction::HistoryPrev | EditAction::HistoryNext => {
|
||||
if !with_eb(lua, h, |eb| eb.multi_line).unwrap_or(false) {
|
||||
history_step_key(lua, h, a == EditAction::HistoryPrev);
|
||||
@@ -173,8 +173,8 @@ pub(super) fn action(lua: &Lua, a: EditAction) -> bool {
|
||||
}
|
||||
|
||||
// The mouse/selection interaction law (click→index, drag-select, clipboard, blink) — a child
|
||||
// module over this file's focus/editing primitives (RF-0082 §1/§4 + the diffed mouse leaves) —
|
||||
// and the host-facing seam (`impl UiScript`: the advance round trip + text-UI geometry).
|
||||
// module over this file's focus/editing primitives — and the host-facing seam (`impl UiScript`:
|
||||
// the advance round trip + text-UI geometry).
|
||||
mod interact;
|
||||
mod seam;
|
||||
pub(super) use interact::{
|
||||
@@ -182,10 +182,10 @@ pub(super) use interact::{
|
||||
};
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────────────────────
|
||||
// Focus model (RF-0082 §1)
|
||||
// Focus model
|
||||
// ─────────────────────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
/// Resolve the box that should process this key/char event, self-acquiring per §2 if needed.
|
||||
/// Resolve the box that should process this key/char event, self-acquiring if needed.
|
||||
///
|
||||
/// - A live, effectively-visible focused box → process it.
|
||||
/// - A focused box that is hidden (still alive) → `None`: the client's guard returns 0 and the focus
|
||||
@@ -268,9 +268,7 @@ fn set_focus_handle(lua: &Lua, h: FrameHandle) {
|
||||
/// - **Hide** (`0x77a780`): tail-jumps `ClearFocus`, whose own guard makes it per-box — hiding a
|
||||
/// box that does not hold the keyboard writes nothing and fires nothing.
|
||||
///
|
||||
/// Both were missing until 2026-08-29 (decision 1686). wow-re had published "autoFocus does NOT
|
||||
/// focus on show — VERIFIED by enumerating callers" off a census written over `call` alone, which
|
||||
/// cannot see the override's tail-`jmp`; benilla had transcribed the negative.
|
||||
/// Both were missing until 2026-08-29 (decision 1686).
|
||||
pub(super) fn visibility_focus(lua: &Lua, h: FrameHandle, visible: bool) {
|
||||
if !visible {
|
||||
// The guard lives in `clear_focus_handle`, exactly as it does in `0x77e410` — so this is
|
||||
@@ -308,7 +306,7 @@ fn clear_focus_handle(lua: &Lua, h: FrameHandle) {
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────────────────────
|
||||
// Editing primitives (RF-0082 §3/§4) — mutate state under one borrow, then sync + fire
|
||||
// Editing primitives — mutate state under one borrow, then sync + fire
|
||||
// ─────────────────────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
/// Insert `ins` at the cursor (`0x77bee0`): replace any selection first; `numeric` aborts wholesale on
|
||||
@@ -325,12 +323,12 @@ fn insert(lua: &Lua, h: FrameHandle, ins: &str, fire_space: bool) {
|
||||
let id = frame_id_of(lua, h);
|
||||
// **The EditBox DOES fire generic `OnChar`** — with the spliced string as `arg1`, from inside
|
||||
// Insert itself (`0x77c13c`, the varargs firer `0x7026f0` with fmt `"%s"`), before the dirty
|
||||
// flush gets round to `OnTextChanged`. RF-0082's "never generic `OnKeyDown`/`OnChar`" was
|
||||
// scoped to one member of a two-member fire family and missed the varargs half; the
|
||||
// `OnKeyDown` (`+0x188`) half of that claim stands (wow-re, corrected 2026-08-29). It rides
|
||||
// the one choke point every insert path goes through — typed char, the `|`→`||` escape, the
|
||||
// multiLine Enter newline, the Lua `Insert` — and so is absent from `SetText`, which is where
|
||||
// the reference has it absent too.
|
||||
// flush gets round to `OnTextChanged`. The "never generic `OnKeyDown`/`OnChar`" rule holds
|
||||
// only for `OnKeyDown` (`+0x188`, corrected 2026-08-29) — it missed `OnChar`'s varargs-firer
|
||||
// path, distinct from the sibling fixed-arity firer `0x702690`. It rides the one choke point
|
||||
// every insert path goes through — typed char, the `|`→`||` escape, the multiLine Enter
|
||||
// newline, the Lua `Insert` — and so is absent from `SetText`, which is where the reference
|
||||
// has it absent too.
|
||||
let on_char = lua
|
||||
.create_string(ins)
|
||||
.and_then(|s| event::fire_widget_handler(lua, id, "OnChar", vec![mlua::Value::String(s)]));
|
||||
@@ -413,7 +411,7 @@ fn highlight_text(lua: &Lua, h: FrameHandle, start: i64, end: i64) {
|
||||
with_eb(lua, h, |eb| eb.highlight_text(start, end));
|
||||
}
|
||||
|
||||
// ── text-region sync (the '*' mask lives here, RF-0082 §3) ───────────────────────────────────
|
||||
// ── text-region sync (the '*' mask lives here) ────────────────────────────────────────────────
|
||||
|
||||
/// Write the *display* string into the text region's [`RegionData::text`], creating the region lazily
|
||||
/// (the implicit FontString) if none exists. `password` shows one `'*'` per **character** (the mask,
|
||||
@@ -475,11 +473,12 @@ fn write_inset_anchors(lua: &Lua, h: FrameHandle, rh: RegionHandle, [l, r, t, b]
|
||||
/// The wrapper for an EditBox's **embedded** text FontString — the region its ctor built at
|
||||
/// `0x779bee` (`Arena::build_editbox_engine_regions`). `None` when `frame` is not a live EditBox.
|
||||
///
|
||||
/// The loader's special-`<FontString>` pass uses this instead of `CreateFontString`: RF-0028 lists
|
||||
/// `<FontString>` as the EditBox's *embedded* font string (its `bytes` attr writes the box's own
|
||||
/// `maxBytes`), so the element DECLARES the ctor's object rather than adding a region. Creating a
|
||||
/// second one would leave an orphan on the frame and push the authored `<Layers>` regions one place
|
||||
/// down the creation-ordered list `GetRegions` walks.
|
||||
/// The loader's special-`<FontString>` pass uses this instead of `CreateFontString`: the
|
||||
/// reference's EditBox LoadXML (`0x779fb0`) treats `<FontString>` as the box's *embedded* font
|
||||
/// string (its `bytes` attr writes the box's own `maxBytes`), so the element DECLARES the ctor's
|
||||
/// object rather than adding a region. Creating a second one would leave an orphan on the frame
|
||||
/// and push the authored `<Layers>` regions one place down the creation-ordered list `GetRegions`
|
||||
/// walks.
|
||||
pub(crate) fn editbox_text_region_wrapper(lua: &Lua, frame: &Table) -> Option<Table> {
|
||||
let h = frame_handle_of(lua, frame).ok()?;
|
||||
let rh = ensure_text_region(lua, h)?;
|
||||
@@ -569,8 +568,8 @@ pub(super) fn refresh_text_region_justify(lua: &Lua, this: &Table) -> mlua::Resu
|
||||
|
||||
/// The EditBox text-anchoring law: the box's text region lays out LEFT-justified (the client's
|
||||
/// editbox draw `0x77da80` is left-anchored at the insets rect regardless of the font string's
|
||||
/// declared justification — RF-0082's windowed draw, focused or not), from the TOP for a
|
||||
/// multiline box, vertically centered for a single-line one. Without this, a bare
|
||||
/// declared justification — focused or not), from the TOP for a multiline box, vertically
|
||||
/// centered for a single-line one. Without this, a bare
|
||||
/// `<FontString inherits="ChatFontNormal"/>` inherits the FontString CENTER/MIDDLE defaults and
|
||||
/// an empty focused box parks its caret mid-box (the mail send tab's original sin).
|
||||
fn apply_text_region_justify(data: &mut RegionData, multi_line: bool) {
|
||||
@@ -612,8 +611,7 @@ fn fire_script(lua: &Lua, id: u32, name: &str) {
|
||||
/// **The caret flush's fire — `OnCursorChanged(x, y, w, h)`** (`0x77da80`).
|
||||
///
|
||||
/// The reference recomputes the caret's anchor inside `0x77da80` and, having done so, fires this
|
||||
/// with four floats *"each value scaled by `f`, so all four are in FrameXML UI units"* (wow-re
|
||||
/// `system/ui/ui.md`, the RF-0085 caret law, VERIFIED):
|
||||
/// with four floats, each scaled by `f` so all four are in FrameXML UI units (`0x77dd5f`):
|
||||
///
|
||||
/// * `x` — the caret's advance from its line's start, the same number the paint anchors by
|
||||
/// ([`EditBoxState::caret_row_x`]).
|
||||
@@ -691,7 +689,7 @@ pub(in crate::script) fn drain_cursor_changed(lua: &Lua) {
|
||||
}
|
||||
|
||||
/// The caret's width as `OnCursorChanged` reports it — **a constant in the reference**, not a
|
||||
/// measurement (wow-re: `w ≡ 4.0`).
|
||||
/// measurement: `w ≡ 4.0` (`+0x1c` in the layout vtable).
|
||||
const CARET_WIDTH: f32 = 4.0;
|
||||
|
||||
// The Lua method surface (SetText/GetText/HighlightText/SetFocus/…, consulted before the shared
|
||||
@@ -729,7 +727,7 @@ pub(super) fn mark_text_changed(lua: &Lua, h: FrameHandle) {
|
||||
///
|
||||
/// A box that is **not effectively visible stays pending**: `Hide`
|
||||
/// splices it out of the chain the update walk follows, so its fire waits for a `Show` (verified
|
||||
/// chain mechanics; the "hidden ⇒ no OnUpdate" step is the RE's own stated inference).
|
||||
/// chain mechanics; the "hidden ⇒ no OnUpdate" step is inferred, not confirmed in the binary).
|
||||
///
|
||||
/// A handler may itself write to a box — including this one — so the list is taken before any Lua
|
||||
/// runs and anything re-marked during the sweep lands on the NEXT drain rather than extending this
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
//! The EditBox mouse/selection interaction law — click→char-index, drag-select, the clipboard
|
||||
//! pair, and the caret blink — a child module over the mother's focus/editing primitives.
|
||||
//!
|
||||
//! Byte law (RF-0082 §1/§4 + the diffed leaves quoted in wow-re's `rf82-editbox-runtime.md`):
|
||||
//! Byte law:
|
||||
//!
|
||||
//! - **Click `0x77b800`**: on a hit — blink reset, click→index (`0x77d0d0`) places the cursor and
|
||||
//! collapses the selection, the drag flag raises (`E+0x364 = 1`), then `SetFocus`
|
||||
@@ -10,7 +10,7 @@
|
||||
//! - **Drag `0x77a860`** (mouse move while `+0x364`): map x→index, extend the selection there via
|
||||
//! the same helper Shift+arrow uses (`0x77cd10`), cursor following.
|
||||
//! - **Release** (`0x77afc0` case 1): `+0x364 = 0`.
|
||||
//! - **Clipboard** (§4): Ctrl+C/X act only with a non-empty selection (`0x77e1d0`); X then
|
||||
//! - **Clipboard**: Ctrl+C/X act only with a non-empty selection (`0x77e1d0`); X then
|
||||
//! deletes = cut. A password box copies a FIXED placeholder (`0x882748`, runtime literal
|
||||
//! unresolved — flagged live-capture), never the real text; benilla stands in the mask run.
|
||||
//! - **Blink `0x77a790`**: the focused box's accumulator (`E+0x374`) advances by dt; crossing the
|
||||
@@ -88,9 +88,9 @@ pub(in crate::script) fn drag_end(lua: &Lua) {
|
||||
}
|
||||
}
|
||||
|
||||
/// Ctrl+LEFT/RIGHT — the word-granular cursor move (§4's Ctrl branch; the client walks its
|
||||
/// per-byte class array, benilla's alnum-run approximation is INFERRED): `shift` extends the
|
||||
/// selection from the fixed anchor like the char move; else the caret collapses there.
|
||||
/// Ctrl+LEFT/RIGHT — the word-granular cursor move (the client walks its per-byte class array on
|
||||
/// the Ctrl branch, `0x41f8f0(1)`; benilla's alnum-run approximation is INFERRED): `shift` extends
|
||||
/// the selection from the fixed anchor like the char move; else the caret collapses there.
|
||||
pub(in crate::script) fn move_word(lua: &Lua, h: FrameHandle, right: bool, shift: bool) {
|
||||
with_eb(lua, h, |eb| eb.move_by_word(right, shift));
|
||||
}
|
||||
|
||||
@@ -168,9 +168,8 @@ pub(in crate::script) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
m.set(
|
||||
// `GetMaxLetters 0x79929f` — the read half, which we were missing. Not published off the
|
||||
// `strings` hit alone (that is what put `SetUnit` on the wrong widget earlier today):
|
||||
// wow-re's numeric-arg round identified `0x79929f` as the GETTER of `[widget+0x340]`,
|
||||
// the same field the setter below writes, which is table-level evidence rather than a
|
||||
// name that happens to be in the image.
|
||||
// `0x79929f` is the GETTER of `[widget+0x340]`, the same field the setter below writes,
|
||||
// which is table-level evidence rather than a name that happens to be in the image.
|
||||
//
|
||||
"GetMaxLetters",
|
||||
lua.create_function(|lua, this: Table| {
|
||||
@@ -182,9 +181,8 @@ pub(in crate::script) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
// the same EXACT count gate (`0x798fbc cmp eax,2`, the second of the four `lua_gettop`
|
||||
// callers), the same raw coerce of the value, and a field of its own at `+0x33c` whose
|
||||
// no-limit value is **-1** where `maxLetters`'s is 0 (`0x799012 jle` → `0x799022`; the
|
||||
// ctor writes -1 at `0x7799df`) — wow-re `numeric-arg-coercion-law.md` and `ui.md`'s
|
||||
// CSimpleEditBox layout. So a non-positive argument is unlimited, a positive one caps
|
||||
// the buffer's BYTES. The stock StaticPopup_Show calls it for any entry carrying
|
||||
// ctor writes -1 at `0x7799df`). So a non-positive argument is unlimited, a positive one
|
||||
// caps the buffer's BYTES. The stock StaticPopup_Show calls it for any entry carrying
|
||||
// `maxBytes` — none of the reference's own 76 does, so the reach is an addon's (1960).
|
||||
"SetMaxBytes",
|
||||
lua.create_function(|lua, (this, args): (Table, mlua::MultiValue)| {
|
||||
@@ -210,9 +208,9 @@ pub(in crate::script) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
)?;
|
||||
m.set(
|
||||
// `SetMaxLetters 0x799110` — one of only FOUR widget bindings in the whole registrar that
|
||||
// calls `lua_gettop` (wow-re `numeric-arg-coercion-law.md` Q3), and its gate is EXACT:
|
||||
// `cmp eax,2`. So the count is checked and the type is not, which is the opposite of the
|
||||
// usual pairing and the reason this needs its own body:
|
||||
// calls `lua_gettop` (`0x6f3070`), and its gate is EXACT: `cmp eax,2` (`0x79919c`). So the
|
||||
// count is checked and the type is not, which is the opposite of the usual pairing and the
|
||||
// reason this needs its own body:
|
||||
//
|
||||
// SetMaxLetters() -> RAISES `Usage:` (too few)
|
||||
// SetMaxLetters(50, 60) -> RAISES `Usage:` (too MANY — an exact gate, not a minimum)
|
||||
|
||||
@@ -103,8 +103,8 @@ impl UiScript {
|
||||
}
|
||||
}
|
||||
|
||||
/// The focused EditBox's text-UI geometry for this frame (RF-0082 leaves caret/highlight
|
||||
/// geometry to the host): which Text quad is the box's (`target`), the scroll window to draw
|
||||
/// The focused EditBox's text-UI geometry for this frame (caret/highlight geometry is left to
|
||||
/// the host): which Text quad is the box's (`target`), the scroll window to draw
|
||||
/// (`display_from`), and the caret/selection x-spans within it — all advance-table-derived.
|
||||
/// Clamps the scroll window against the text region's resolved width first (the
|
||||
/// scroll-into-view invariant, `0x77da80`). `None` when nothing is focused, the box is
|
||||
@@ -198,7 +198,8 @@ impl UiScript {
|
||||
}
|
||||
|
||||
/// Ctrl/Cmd+C for the focused EditBox: the selected substring for the OS clipboard (`None` =
|
||||
/// no selection; a password box yields its mask run, never the real text — RF-0082 §4).
|
||||
/// no selection; a password box yields its mask run, never the real text — the client's fixed
|
||||
/// placeholder `0x882748`).
|
||||
pub fn editbox_copy(&mut self) -> Option<String> {
|
||||
super::copy_selection(self.lua())
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//! Rust-driven tests of the EditBox runtime (RF-0082): focus acquisition + routing, text/cursor/
|
||||
//! Rust-driven tests of the EditBox runtime: focus acquisition + routing, text/cursor/
|
||||
//! selection editing, the specialized script fires, caps, numeric/password, and text-region sync.
|
||||
//! Frames are built programmatically via `CreateFrame("EditBox", …)` and driven through the public
|
||||
//! keyboard API (`char_input`/`key_input`/`editbox_action`/`has_keyboard_focus`) and the Lua
|
||||
@@ -18,7 +18,7 @@ fn text_quad(s: &UiScript) -> Option<String> {
|
||||
})
|
||||
}
|
||||
|
||||
// ── §1/§2 focus acquisition + routing ───────────────────────────────────────────────────────
|
||||
// ── focus acquisition + routing ─────────────────────────────────────────────────────────────
|
||||
|
||||
/// **The self-acquire path**, path 2 of the two: a box that has had no show TRANSITION (created
|
||||
/// already visible, so `visibility_focus` never runs for it) still takes the keyboard on the first
|
||||
@@ -45,9 +45,8 @@ fn an_autofocus_box_self_acquires_on_the_first_event() {
|
||||
}
|
||||
|
||||
/// **An `autoFocus` box takes the keyboard when it is shown** — the OnShow vtable override
|
||||
/// (`0x81c910` slot +0x30, `0x77a750`), missing here until decision 1686 because wow-re's own
|
||||
/// "verified by absence" negative came from a `call`-only census that could not see its tail-`jmp`.
|
||||
/// Gated on nothing else holding focus, and the gate is the whole of it — no topmost/best choice.
|
||||
/// (`0x81c910` slot +0x30, `0x77a750`), missing here until decision 1686. Gated on nothing else
|
||||
/// holding focus, and the gate is the whole of it — no topmost/best choice.
|
||||
#[test]
|
||||
fn an_autofocus_box_takes_the_keyboard_when_it_is_shown() {
|
||||
let s = script();
|
||||
@@ -225,7 +224,7 @@ fn click_focuses_regardless_of_autofocus_and_transition_order_is_lost_then_gaine
|
||||
assert!(s.errors().is_empty(), "{:?}", s.errors());
|
||||
}
|
||||
|
||||
// ── §3 text buffer + editing + OnTextChanged/OnTextSet ───────────────────────────────────────
|
||||
// ── text buffer + editing + OnTextChanged/OnTextSet ─────────────────────────────────────────
|
||||
|
||||
/// **`OnTextChanged` is deferred and coalesced** (decision 1831). An edit only raises the
|
||||
/// `textChanged` dirty bit; the fire belongs to the drain (`0x77d3e0`) that the box's own OnUpdate
|
||||
@@ -325,7 +324,7 @@ fn max_letters_trims_from_the_end() {
|
||||
assert_eq!(s.eval::<i64>("return E:GetNumLetters()").unwrap(), 3);
|
||||
}
|
||||
|
||||
// ── §4 selection / highlight / keys ──────────────────────────────────────────────────────────
|
||||
// ── selection / highlight / keys ────────────────────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn highlight_all_then_typing_replaces_the_selection() {
|
||||
@@ -509,7 +508,7 @@ fn multiline_enter_inserts_a_newline_without_onspacepressed() {
|
||||
|
||||
/// **Alt-arrow mode: the two real verbs, and the flag they share with the XML attribute.**
|
||||
///
|
||||
/// The §5 (`ignorearrows-alt-arrow-gate.md`) settled four things this pins:
|
||||
/// The reference settles four things this pins:
|
||||
///
|
||||
/// · 5875 has **no `SetIgnoreArrows`** — the 48-entry EditBox method table
|
||||
/// `[0x87bb68, 0x87bce8)` carries `SetAltArrowKeyMode`/`GetAltArrowKeyMode` at 46/47 and no
|
||||
@@ -646,16 +645,15 @@ fn get_number_parses_the_text() {
|
||||
assert_eq!(s.eval::<f64>("return E:GetNumber()").unwrap(), 0.0);
|
||||
}
|
||||
|
||||
// ── the EditBox override never fires generic OnChar/OnKeyDown (§2) ────────────────────────────
|
||||
// ── the EditBox override never fires generic OnChar/OnKeyDown ─────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn typing_fires_the_generic_on_char_with_what_was_inserted() {
|
||||
// **The half of the old law that was wrong** (wow-re, corrected 2026-08-29; decision 1686).
|
||||
// **The half of the old law that was wrong** (corrected 2026-08-29; decision 1686).
|
||||
// `CSimpleEditBox` has its own input vtable which does not chain to the base — but Insert
|
||||
// itself fires the generic `OnChar` slot (`+0x180`) at `0x77c13c`, through the **varargs**
|
||||
// firer `0x7026f0` with fmt `"%s"` and the spliced string as the argument. The published
|
||||
// negative came from censusing only the fixed-arity firer `0x702690`: one member of a
|
||||
// two-member family.
|
||||
// firer `0x7026f0` with fmt `"%s"` and the spliced string as the argument; the family's
|
||||
// other member is the fixed-arity firer `0x702690`.
|
||||
let mut s = script();
|
||||
s.run(
|
||||
r#"
|
||||
@@ -687,10 +685,10 @@ fn typing_fires_the_generic_on_char_with_what_was_inserted() {
|
||||
|
||||
#[test]
|
||||
fn key_paths_never_fire_the_generic_on_key_down() {
|
||||
// The surviving half (wow-re `frame-key-script-delivery.md` §1): the box's own key-down
|
||||
// vtable (`0x77b160`) handles the event and never chains to the base, so a focused box's
|
||||
// typing does not also run a generic `OnKeyDown` bound on that same box. Only the `OnChar`
|
||||
// half of the original claim was corrected — this one was re-censused and stands.
|
||||
// The surviving half: the box's own key-down vtable (`0x77b160`) handles the event and never
|
||||
// chains to the base, so a focused box's typing does not also run a generic `OnKeyDown` bound
|
||||
// on that same box. Only the `OnChar` half of the original claim was corrected — this one
|
||||
// stands.
|
||||
let mut s = script();
|
||||
s.run(
|
||||
r#"
|
||||
@@ -827,7 +825,7 @@ fn history_off_by_default_and_up_is_still_consumed() {
|
||||
s.run(r#"E = CreateFrame("EditBox", "E"); E:SetText("t"); E:SetFocus()"#)
|
||||
.unwrap();
|
||||
// No historyLines: AddHistoryLine is a no-op, UP consumed but inert (a focused box eats every
|
||||
// key — RF-0082 §2).
|
||||
// key — both handlers `return 1` past the guard, `0x77a900`/`0x77b160`).
|
||||
s.run(r#"E:AddHistoryLine("x")"#).unwrap();
|
||||
assert!(s.editbox_action(EditAction::HistoryPrev));
|
||||
assert_eq!(s.eval::<String>("return E:GetText()").unwrap(), "t");
|
||||
@@ -1068,7 +1066,7 @@ fn ctrl_arrows_jump_words() {
|
||||
}
|
||||
|
||||
/// Copy needs a selection; cut copies then deletes; the password box yields the mask run, never
|
||||
/// the real text (RF-0082 §4's placeholder law, mask stand-in).
|
||||
/// the real text (the client's fixed placeholder `0x882748`, mask stand-in).
|
||||
#[test]
|
||||
fn copy_cut_and_the_password_placeholder() {
|
||||
let mut s = script();
|
||||
@@ -1213,7 +1211,7 @@ fn caret_blinks_on_tick_and_resets_on_edit() {
|
||||
assert!(s.focused_editbox_text_ui().unwrap().caret_on);
|
||||
}
|
||||
|
||||
// ── A hyperlink is ONE keypress (RF-0087 §6, decision 1077) ──────────────────────────────────
|
||||
// ── A hyperlink is ONE keypress (`AdvanceTokens 0x77bb30`, decision 1077) ─────────────────────
|
||||
//
|
||||
// The engine-level law lives in `markup`; these drive it the way a player does — through the
|
||||
// focused box's public keyboard API — because that is the level the reported defect lived at:
|
||||
@@ -1361,9 +1359,8 @@ fn creating_a_box_does_not_focus_it_the_way_showing_one_does() {
|
||||
}
|
||||
|
||||
/// **`SetMaxLetters` gates the COUNT and not the type** — one of four widget bindings in the whole
|
||||
/// registrar that calls `lua_gettop`, and its gate is exact (`cmp eax,2`), while the value goes
|
||||
/// through a bare `lua_tonumber` with no `isnumber` guard (wow-re `numeric-arg-coercion-law.md`
|
||||
/// Q1/Q3, VERIFIED).
|
||||
/// registrar that calls `lua_gettop` (`0x6f3070`), and its gate is exact (`cmp eax,2`), while the
|
||||
/// value goes through a bare `lua_tonumber` (`0x6f3620`) with no `isnumber` guard.
|
||||
///
|
||||
/// That pairing is the opposite of the usual one, which is why it earns a test: benilla typed the
|
||||
/// argument `i64` and so raised on `SetMaxLetters(nil)` — aux-addon's `gui/core.lua:288` writes
|
||||
@@ -1410,12 +1407,11 @@ fn set_max_letters_gates_the_argument_count_and_coerces_the_value() {
|
||||
}
|
||||
|
||||
/// **A `CSimpleEditBox` is born with FIVE regions, and `GetRegions` hands them to Lua before any
|
||||
/// authored one.** wow-re `scratch/rf85-editbox-caret.md` §1: the ctor builds the text FontString
|
||||
/// (`E+0x328`, `0x779bee`), three selection-highlight `CSimpleTexture`s (`E+0x350/0x354/0x358`,
|
||||
/// loop `0x779c41`–`0x779c72`) and the caret (`E+0x368`, `0x779c86`) — in that order. `GetRegions
|
||||
/// 0x773f60` walks `[frame+0x1b8]`, one flat creation-ordered list, oldest first, no filter
|
||||
/// (`scratch/widget-list-bindings.md`), and insertion is at the TAIL. So the authored `<Layers>`
|
||||
/// regions start at index 6.
|
||||
/// authored one.** The ctor builds the text FontString (`E+0x328`, `0x779bee`), three
|
||||
/// selection-highlight `CSimpleTexture`s (`E+0x350/0x354/0x358`, loop `0x779c41`–`0x779c72`) and
|
||||
/// the caret (`E+0x368`, `0x779c86`) — in that order. `GetRegions 0x773f60` walks
|
||||
/// `[frame+0x1b8]`, one flat creation-ordered list, oldest first, no filter, and insertion is at
|
||||
/// the TAIL. So the authored `<Layers>` regions start at index 6.
|
||||
///
|
||||
/// The report is pfUI's `skins/blizzard/friends.lua` l.379 —
|
||||
/// `local _,_,_,_,_,left,right = GuildControlPopupFrameEditBox:GetRegions()` — which skips exactly
|
||||
@@ -1444,7 +1440,8 @@ fn an_editbox_is_born_with_the_ctors_five_regions_ahead_of_its_authored_ones() {
|
||||
assert!(report.errors.is_empty(), "errors: {:?}", report.errors);
|
||||
|
||||
// Five engine regions, then the two authored textures — and NOT a sixth from the embedded
|
||||
// `<FontString>`, which declares the ctor's object rather than adding one (RF-0028).
|
||||
// `<FontString>`, which declares the ctor's object rather than adding one (the reference's
|
||||
// EditBox LoadXML, `0x779fb0`).
|
||||
assert_eq!(
|
||||
s.eval::<i64>("return Box:GetNumRegions()").unwrap(),
|
||||
7,
|
||||
@@ -1592,7 +1589,7 @@ fn set_max_bytes_caps_the_buffer_in_bytes_with_minus_one_unlimited() {
|
||||
/// `MailFrame.xml` and `HelpFrame.xml` wire `ScrollingEdit_OnCursorChanged` to, and the reason a
|
||||
/// multiline box follows its caret as you type past the bottom (decision 2141).
|
||||
///
|
||||
/// The four args are the reference's (wow-re's RF-0085 caret law, VERIFIED): `x` the caret's
|
||||
/// The four args are the reference's (`0x77dd5f`, each scaled by `f`): `x` the caret's
|
||||
/// advance along its line, `y` **negative-downward** by row (which is what
|
||||
/// `ScrollingEdit_OnCursorChanged`'s `cursorOffset = y` then `-this.cursorOffset` reads back as a
|
||||
/// positive distance), `w` the constant `4.0`, `h` the line height.
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
//! Handler firing — the FrameScript calling convention (RF-0025), for events, ticks, and
|
||||
//! Handler firing — the FrameScript calling convention, for events, ticks, and
|
||||
//! show/hide transitions.
|
||||
//!
|
||||
//! Every handler is invoked through the *same* protected path, which reproduces both calling
|
||||
//! conventions the transition-era client supported (decision 0068):
|
||||
//!
|
||||
//! - **Legacy globals (RF-0025, byte-verified):** `this` = the firing frame's wrapper, `event` = the
|
||||
//! event name (OnEvent only), `arg1..argN` = the args — each **set then restored** around the call
|
||||
//! (`luaL_ref` saves the prior value; restored after), so nested fires are safe. The handler reads
|
||||
//! its inputs as globals; the client `pcall`s with **nargs = 0**.
|
||||
//! - **Legacy globals (byte-verified):** `this` (`0x872e64`) = the firing frame's wrapper, `event`
|
||||
//! (`0x84b648`) = the event name (OnEvent only), `arg1..argN` (`0x8722dc`) = the args — each
|
||||
//! **set then restored** around the call (`luaL_ref` saves the prior value; restored after), so
|
||||
//! nested fires are safe. The handler reads its inputs as globals; the client `pcall`s with
|
||||
//! **nargs = 0**.
|
||||
//! - **Modern args:** the same values are *also* passed positionally as `(self, event, ...)` for
|
||||
//! OnEvent (`(self, elapsed)` for OnUpdate, `(self)` for OnShow/OnHide/OnLoad) — the form Era
|
||||
//! addons are written against.
|
||||
@@ -89,7 +90,7 @@ pub(super) fn fire_event_handler(
|
||||
fire(lua, id, "OnEvent", Some(event), extra)
|
||||
}
|
||||
|
||||
/// Fire a frame's `OnUpdate` (RF-0025: `this` + `arg1 = elapsed`; modern `(self, elapsed)`).
|
||||
/// Fire a frame's `OnUpdate` (`this` + `arg1 = elapsed`; modern `(self, elapsed)`).
|
||||
pub(super) fn fire_update_handler(lua: &Lua, id: u32, elapsed: f32) -> mlua::Result<()> {
|
||||
fire(
|
||||
lua,
|
||||
@@ -155,10 +156,10 @@ pub(super) fn fire_size_changes(lua: &Lua) {
|
||||
/// [`crate::widget::WidgetArena::set_shown`]/`set_parent`'s changed-list). Errors are recorded in
|
||||
/// [`Model::errors`] rather than propagated — a handler error must not abort the `Show()` call.
|
||||
///
|
||||
/// **This is also where the `toplevel` raise fires** (`effective_visible_show 0x76ae10` @`0x76aee0`,
|
||||
/// wow-re `ui/scratch/toplevel-raise.md`): the binary tests the toplevel bit and raises *after* the
|
||||
/// subtree's visibility has propagated and *before* that node's OnShow notify — which is exactly
|
||||
/// this seam, since the arena has finished propagating by the time it hands back the changed list.
|
||||
/// **This is also where the `toplevel` raise fires** (the Show trigger `0x76ae10` @`0x76aee0`):
|
||||
/// the binary tests the toplevel bit and raises *after* the subtree's visibility has propagated
|
||||
/// and *before* that node's OnShow notify — which is exactly this seam, since the arena has
|
||||
/// finished propagating by the time it hands back the changed list.
|
||||
/// Per node and in list order, so a handler reading `GetFrameLevel()` sees the raised value the way
|
||||
/// it would in the reference. It lives here rather than at the Lua `Show` binding because *every*
|
||||
/// visibility transition this engine performs runs `0x76ae10` — an arena-level show from the
|
||||
@@ -175,10 +176,9 @@ pub(super) fn fire_visibility_changes(lua: &Lua, changed: Vec<FrameHandle>) {
|
||||
})
|
||||
.collect()
|
||||
};
|
||||
// **The hover-hide law** (wow-re `ui/scratch/hover-hide-and-tooltip-owner-law.md`, §5-
|
||||
// arbitrated): hiding the hovered frame — directly or through an ancestor's cascade — fires
|
||||
// its `OnLeave` SYNCHRONOUSLY, inside the hide and **before that frame's `OnHide`**
|
||||
// (`0x764ba0`'s kind-2 tail runs mid-`effective_visible_hide`, the leave at `0x764cce`; the
|
||||
// **The hover-hide law**: hiding the hovered frame — directly or through an ancestor's
|
||||
// cascade — fires its `OnLeave` SYNCHRONOUSLY, inside the hide and **before that frame's
|
||||
// `OnHide`** (`0x764ba0`'s kind-2 tail runs mid-`0x76ad50`, the leave at `0x764cce`; the
|
||||
// only silent case is destruction). It clears the hover cache and the drag-arm on that frame
|
||||
// (`+0x100/+0x104`, the arg-1 flavor), and schedules the re-pick — the pump re-hovers
|
||||
// whatever is now topmost at the unchanged cursor next tick ([`Model::hover_repick`]).
|
||||
@@ -223,8 +223,8 @@ pub(super) fn fire_visibility_changes(lua: &Lua, changed: Vec<FrameHandle>) {
|
||||
} else {
|
||||
// **The button's HIDE edge** — `CSimpleButton` overrides the hide notify (`+0x34`,
|
||||
// `0x7791e0`) to un-press itself before tail-jumping the base, so a button hidden
|
||||
// while held does not come back up wearing its pushed art (wow-re
|
||||
// `scratch/button-state-edge-set.md`; the guard is `state != DISABLED && locked == 0`).
|
||||
// while held does not come back up wearing its pushed art (the guard is
|
||||
// `state != DISABLED && locked == 0`).
|
||||
//
|
||||
// It hangs off the VISIBILITY transition, not off the hover, which is what it is in
|
||||
// the reference — a button hidden nowhere near the cursor un-presses too, and one
|
||||
@@ -313,7 +313,7 @@ fn arg_name(i: usize) -> Cow<'static, str> {
|
||||
}
|
||||
}
|
||||
|
||||
/// The RF-0025 calling convention itself — the single home for it, shared by [`fire`] (registry
|
||||
/// The calling convention itself — the single home for it, shared by [`fire`] (registry
|
||||
/// handlers: events, OnUpdate, OnShow/OnHide) and the loader's bottom-up `OnLoad` (which holds the
|
||||
/// compiled `Function` directly). Sets the legacy `this`/`event`/`arg1..argN` globals and passes the
|
||||
/// modern `(self[, event], extra…)` positionals, saving and restoring the globals around the call
|
||||
|
||||
@@ -313,9 +313,9 @@ impl UiScript {
|
||||
state_justify = justify;
|
||||
}
|
||||
}
|
||||
// A TITLE REGION NEVER DRAWS. It is a hit rectangle, not a visual: wow-re
|
||||
// carves it as a plain Region with no textures at all
|
||||
// (`widget-api-batch-benilla.md` Q6). Falling through here would emit the
|
||||
// A TITLE REGION NEVER DRAWS. It is a hit rectangle, not a visual:
|
||||
// `CreateTitleRegion 0x773910` builds it as a plain Region with no textures at
|
||||
// all. Falling through here would emit the
|
||||
// texture quad the `else` branch below builds — invisible on screen, but the
|
||||
// render report counts quads, so every addon that makes one would read as
|
||||
// "drew something" (1246's lesson about what an instrument is told).
|
||||
@@ -341,9 +341,10 @@ impl UiScript {
|
||||
continue;
|
||||
}
|
||||
let mut data = data_ref.cloned().unwrap_or_default();
|
||||
// The single-hop draw multiply (`propagation.md`): the region's own alpha times
|
||||
// its immediate owner's — never a product up the tree, because the owner's own
|
||||
// `effective_alpha` was already overwritten by any ancestor's SetAlpha.
|
||||
// The single-hop draw multiply (region-combine `0x772180`): the region's own
|
||||
// alpha times its immediate owner's — never a product up the tree, because the
|
||||
// owner's own `effective_alpha` was already overwritten by any ancestor's
|
||||
// `SetAlpha 0x76a690`.
|
||||
let alpha = owner_frame.map(|f| f.effective_alpha).unwrap_or(1.0)
|
||||
* data.alpha.unwrap_or(1.0);
|
||||
if let Some(fo) = state_font {
|
||||
@@ -376,7 +377,7 @@ impl UiScript {
|
||||
data.outline = fo.outline;
|
||||
}
|
||||
// Test the severance MASK, which is what the sentence above claims and what
|
||||
// wow-re pinned, not `vertex_color.is_none()`. The nil-check was an
|
||||
// the reference pins, not `vertex_color.is_none()`. The nil-check was an
|
||||
// equivalent proxy for exactly as long as an explicit `SetTextColor` was the
|
||||
// only way a button label's colour could be populated at all; the moment
|
||||
// `SetTextFontObject` began linking the label to its font object (so
|
||||
@@ -407,10 +408,10 @@ impl UiScript {
|
||||
}
|
||||
// `Button:SetFont` sits BETWEEN the two: it is a local set on the button's own
|
||||
// embedded font, so it outranks the font object that font inherits (a locally
|
||||
// set axis severs inheritance and is never restored — wow-re
|
||||
// `font-object-lua-surface.md`), but it loses to a face the label FontString
|
||||
// set for *itself*, which severs one level further down. That is what
|
||||
// `font_explicit` is, so it is the gate here too.
|
||||
// set axis clears the font's `inheritMask` bit, `CSimpleFontString+0xD4`, and
|
||||
// is never restored), but it loses to a face the label FontString set for
|
||||
// *itself*, which severs one level further down. That is what `font_explicit`
|
||||
// is, so it is the gate here too.
|
||||
if let Some(bf) = button_font {
|
||||
if !data.font_explicit.face {
|
||||
data.font_path = Some(bf.path.clone());
|
||||
@@ -436,11 +437,12 @@ impl UiScript {
|
||||
// the reference draws it nowhere. The bar-fill/thumb regions keep their own
|
||||
// fraction geometry (computed off the owner rect above) — they never carry
|
||||
// anchors and skip the resolver entirely, like the reference's own bar path.
|
||||
// The bar-fill CROPS its texture (wow-re `nameplate-vkey.md`, VERIFIED): every
|
||||
// `SetValue` rewrites the region's 4-corner UV block with `u1 = fraction` and
|
||||
// recomputes the quad as `right = left + frac·width`. So the art is sliced, never
|
||||
// squeezed — a bar texture with a horizontal ramp (`UI-StatusBar` brightens 124→166
|
||||
// left-to-right) keeps its true gradient at every fill level.
|
||||
// The bar-fill CROPS its texture (`SetValue 0x7cc450`→`0x7833c0` drives
|
||||
// `0x770410`): every `SetValue` rewrites the region's 4-corner UV block with
|
||||
// `u1 = fraction` and recomputes the quad as `right = left + frac·width`. So
|
||||
// the art is sliced, never squeezed — a bar texture with a horizontal ramp
|
||||
// (`UI-StatusBar` brightens 124→166 left-to-right) keeps its true gradient at
|
||||
// every fill level.
|
||||
if let Some(sb) = bar_fill {
|
||||
data.tex_coords = Some(bar_fill_uv(data.tex_coords, sb));
|
||||
}
|
||||
@@ -480,7 +482,7 @@ impl UiScript {
|
||||
}
|
||||
} else {
|
||||
// The draw gate is the TEXTURE slot, never the colour (`0x7706e0`: `+0xcc`
|
||||
// empty -> emit NOTHING — `texture-color-composition.md` §4, VERIFIED). A
|
||||
// empty -> emit NOTHING). A
|
||||
// vertex colour is a tint on whatever texture exists; alone it is not
|
||||
// drawable content — it survives `SetTexture(nil)` by design ("a tint
|
||||
// outlives the art it was tinting") and used to leak out of here as a
|
||||
@@ -520,21 +522,22 @@ impl UiScript {
|
||||
}
|
||||
};
|
||||
// A region draws at its owner's scale — same single hop as alpha (a region
|
||||
// has no scale of its own; `propagation.md`'s product lives on frames).
|
||||
// has no scale of its own; the effective-scale product (`0x76ac90`) lives on
|
||||
// frames).
|
||||
let scale = owner_frame.map(|f| f.effective_scale).unwrap_or(1.0);
|
||||
(rect, alpha, content, clip, scale)
|
||||
}
|
||||
};
|
||||
// **A `Model` frame's scene draws out of its bucket's ARTWORK batch, last** — wow-re
|
||||
// `ui/scratch/model-frame-draw-order.md` (2026-09-04): the batch object carries a
|
||||
// third sub-array beside the quads and the text, a render-callback list, and
|
||||
// `0x76fb00` drains the three in that order; `0x76d160` registers the model's callback
|
||||
// only for `layer == 2` (`0x76d17f cmp ebx,2`). So a model is neither a separate pass
|
||||
// nor the frame's own layer-0 slot — it is ARTWORK content, after that layer's quads.
|
||||
// **A `Model` frame's scene draws out of its bucket's ARTWORK batch, last:** the batch
|
||||
// object carries a third sub-array beside the quads and the text, a render-callback
|
||||
// list, and `0x76fb00` drains the three in that order; `0x76d160` registers the model's
|
||||
// callback only for `layer == 2` (`0x76d17f cmp ebx,2`). So a model is neither a
|
||||
// separate pass nor the frame's own layer-0 slot — it is ARTWORK content, after that
|
||||
// layer's quads.
|
||||
// This is what puts the world map's player arrow over the zone overlays: both frames
|
||||
// sit at `WorldMapFrame.level + 1`, the overlays are ARTWORK quads there, and the
|
||||
// arrow's callback drains after them (the director's report, and the case the carve
|
||||
// was dispatched on). A model's own OVERLAY/HIGHLIGHT regions still draw over it. The
|
||||
// arrow's callback drains after them (the director's report). A model's own
|
||||
// OVERLAY/HIGHLIGHT regions still draw over it. The
|
||||
// CALLBACK rank (1995) is what puts the scene after that layer's font strings too,
|
||||
// whatever their link stamps — the content key alone sat at the text rank.
|
||||
let z = match &content {
|
||||
@@ -580,15 +583,14 @@ impl UiScript {
|
||||
}
|
||||
|
||||
/// The single colour a Texture region draws with: **`texel × vertexColour`**, per channel and
|
||||
/// **alpha included** (wow-re `system/ui/scratch/texture-color-composition.md`, VERIFIED — the
|
||||
/// stage-0 combine's `MODULATE(TEXTURE, DIFFUSE)` for both colour and alpha).
|
||||
/// **alpha included** (the stage-0 combine preset live at submit, `.data 0x85c250` index 1:
|
||||
/// `MODULATE(TEXTURE, DIFFUSE)` for both colour and alpha).
|
||||
///
|
||||
/// **That law is scoped to a region with no pixel shader bound** (`+0x128 == 0` — wow-re
|
||||
/// `texture-desaturate-law.md` §6.1's correction to that note). A DESATURATED region takes the
|
||||
/// fragment program instead, which supersedes the whole stage chain and reads the vertex colour's
|
||||
/// ALPHA only; its RGB never reaches the pixel. The colour computed here still travels — the
|
||||
/// renderer needs the alpha, and the RGB is simply unread on that branch (`ui_quad.wgsl`) — so
|
||||
/// nothing changes here, but the note this cites no longer says what it used to.
|
||||
/// **That law is scoped to a region with no pixel shader bound** (`+0x128 == 0`). A DESATURATED
|
||||
/// region takes the fragment program instead, which supersedes the whole stage chain and reads
|
||||
/// the vertex colour's ALPHA only; its RGB never reaches the pixel. The colour computed here
|
||||
/// still travels — the renderer needs the alpha, and the RGB is simply unread on that branch
|
||||
/// (`ui_quad.wgsl`) — so nothing changes here.
|
||||
///
|
||||
/// `fill` is the region's own solid-colour texture ([`RegionData::fill`] — the client generates a
|
||||
/// real 8×8 texel block from it), so where it is set it IS the texel and the product is the drawn
|
||||
@@ -622,7 +624,7 @@ fn bar_fill_rect(r: Rect, sb: &crate::widget::StatusBarState) -> Rect {
|
||||
/// sliced to the value fraction along the fill axis — `[left, right, top, bottom]`, 0..1, top-left
|
||||
/// origin.
|
||||
///
|
||||
/// The client CROPS rather than scales (wow-re `nameplate-vkey.md`, VERIFIED): `SetValue`
|
||||
/// The client CROPS rather than scales: `SetValue`
|
||||
/// (`0x7cc450`→`0x7833c0`) drives `0x770410`, which writes the 4-corner UV block (`+0x104..+0x120`)
|
||||
/// with `u1 = GetValue()` and recomputes `right = left + frac·width`. Horizontal is the verified
|
||||
/// case; VERTICAL mirrors it up the `v` axis (bottom-up fill ⇒ the *bottom* edge of the art is
|
||||
|
||||
@@ -20,10 +20,10 @@
|
||||
//!
|
||||
//! ## The method surface, and where it came from
|
||||
//!
|
||||
//! §5-verified in wow-re, `system/ui/scratch/font-object-lua-surface.md` (landed `0193861a`). The
|
||||
//! Font method table is `.data 0x87c7c8` with **22 entries** — the count read from `mov edx,0x16`
|
||||
//! at `0x7a10d5`, not from a run-length scan, which merges neighbouring tables and reports a bogus
|
||||
//! 54. Its lookup `0x7a1100` has **no base-class fallback**, so 22 is the entire surface:
|
||||
//! The Font method table is `.data 0x87c7c8` with **22 entries** — the count read from `mov
|
||||
//! edx,0x16` at `0x7a10d5`, not from a run-length scan, which merges neighbouring tables and
|
||||
//! reports a bogus 54. Its lookup `0x7a1100` has **no base-class fallback**, so 22 is the entire
|
||||
//! surface:
|
||||
//! `GetObjectType · IsObjectType · GetName · SetFontObject · GetFontObject · CopyFontObject ·
|
||||
//! SetFont · GetFont · SetAlpha · GetAlpha · SetTextColor · GetTextColor · SetShadowColor ·
|
||||
//! GetShadowColor · SetShadowOffset · GetShadowOffset · SetSpacing · GetSpacing · SetJustifyH ·
|
||||
@@ -134,8 +134,7 @@ fn name_of(this: &Table) -> mlua::Result<String> {
|
||||
/// `None` for the nil form (clear the link).
|
||||
///
|
||||
/// The reference's own usage string settles the shape — `.rdata 0x87c5cc` reads verbatim
|
||||
/// `Usage: %s:SetFontObject(font or "font" or nil)` (wow-re
|
||||
/// `system/ui/scratch/font-object-lua-surface.md`, §5-verified). So all three are real: the object
|
||||
/// `Usage: %s:SetFontObject(font or "font" or nil)`. So all three are real: the object
|
||||
/// (3,180 of the corpus's 3,186 sites), the name string (our own shipped `assets/ui` plus 6 corpus
|
||||
/// sites), and nil.
|
||||
///
|
||||
@@ -388,8 +387,7 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
// **An unset Font answers `(nil, 0, "")`, all three ctor-determined** — `0x783a40` writes
|
||||
// `[esi+0x48]` and `[esi+0x4c]` from a zeroed register, and its `0x41e3a0(NULL)` stores the
|
||||
// shared empty record whose `char*` is NULL, which `lua_pushstring` turns into nil. So the
|
||||
// height is the NUMBER zero, not nil (wow-re `font-object-lua-surface.md` §9.3,
|
||||
// §5-cross-checked; decision 2129).
|
||||
// height is the NUMBER zero, not nil (decision 2129).
|
||||
m.set(
|
||||
"GetFont",
|
||||
lua.create_function(|lua, this: Table| {
|
||||
@@ -403,8 +401,8 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
)?;
|
||||
|
||||
// ── colour, and the alpha that is its fourth channel ────────────────────────────────────
|
||||
// Shape C on the three channels (`SetTextColor 0x79f4d0`, `2=C 3=C 4=C 5=B`, wow-re
|
||||
// `numeric-arg-coercion-law.md`): a nil or non-number is 0.0, never a raise (1973).
|
||||
// Shape C on the three channels (`SetTextColor 0x79f4d0`, `2=C 3=C 4=C 5=B`): a nil or
|
||||
// non-number is 0.0, never a raise (1973).
|
||||
m.set(
|
||||
"SetTextColor",
|
||||
lua.create_function(
|
||||
@@ -428,11 +426,11 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
Ok((c[0], c[1], c[2], c[3]))
|
||||
})?,
|
||||
)?;
|
||||
// SetAlpha/GetAlpha on a FontInstance are its text colour's alpha channel, not a separate
|
||||
// slot: wow-re's field map gives a FontInstance exactly one colour word (`FONTINSTANCE+0x058
|
||||
// textColor`, a packed CImVector) and no alpha field of its own, so there is nowhere else for
|
||||
// an alpha to live. Zero corpus sites; implemented because a duck-typed `if f.SetAlpha` should
|
||||
// find it and because the semantics follow from the storage rather than from a guess.
|
||||
// SetAlpha/GetAlpha on a FontInstance are its text colour's alpha channel, not a separate slot:
|
||||
// a FontInstance has exactly one colour word (`FONTINSTANCE+0x058 textColor`, a packed
|
||||
// CImVector) and no alpha field of its own, so there is nowhere else for an alpha to live. Zero
|
||||
// corpus sites; implemented because a duck-typed `if f.SetAlpha` should find it and because the
|
||||
// semantics follow from the storage rather than from a guess.
|
||||
m.set(
|
||||
"SetAlpha",
|
||||
lua.create_function(|lua, (this, a): (Table, f32)| {
|
||||
@@ -453,8 +451,8 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
m.set(
|
||||
"SetShadowColor",
|
||||
lua.create_function(
|
||||
// Shape C on r, g, b (`Font:SetShadowColor 0x79f730`, `2=C 3=C 4=C 5=B`, wow-re
|
||||
// `numeric-arg-coercion-law.md`) — bare `lua_tonumber`, no gate, never raises.
|
||||
// Shape C on r, g, b (`Font:SetShadowColor 0x79f730`, `2=C 3=C 4=C 5=B`) — bare
|
||||
// `lua_tonumber`, no gate, never raises.
|
||||
|lua, (this, r, g, b, a): (Table, Value, Value, Value, Option<f32>)| {
|
||||
let (r, g, b) = (
|
||||
crate::script::object::as_f32(&r),
|
||||
|
||||
@@ -4,10 +4,9 @@
|
||||
//!
|
||||
//! ## Why this is a module and not a base class
|
||||
//!
|
||||
//! **There is no `FontInstance` class in the 1.12.1 Lua chain.** wow-re's registrar map
|
||||
//! (`system/ui/scratch/widget-api-batch-benilla.md`, and the per-table carve in
|
||||
//! `font-object-lua-surface.md` §2) shows 23 flat `{const char* name, void* fn}` `.data` tables and
|
||||
//! a per-class `vtable[+0x8]` lookup that tail-calls **exactly one** base class's lookup on a miss.
|
||||
//! **There is no `FontInstance` class in the 1.12.1 Lua chain.** The registrar map shows 23 flat
|
||||
//! `{const char* name, void* fn}` `.data` tables and a per-class `vtable[+0x8]` lookup
|
||||
//! (`0x7020b0`'s dispatch) that tail-calls **exactly one** base class's lookup on a miss.
|
||||
//! The six text-bearing types each *re-declare* the same font names in their own flat table; the
|
||||
//! sharing happens one level **down**, in C++, where every binding is a thin type-guard shim that
|
||||
//! tail-calls one shared implementation (`0x79f210` SetFont · `0x79f3b0` GetFont · `0x79f4d0` /
|
||||
@@ -49,10 +48,9 @@
|
||||
//! through. It is the number `1`, not `true`, and not zero values.
|
||||
//!
|
||||
//! The table above was read off the PE `.data` section directly (with Font's 22-entry table as a
|
||||
//! positive control) and **independently re-derived by a wow-re §5 trio**, which agrees on all 48
|
||||
//! names, all 48 binding addresses and all 16 arities —
|
||||
//! `system/ui/scratch/editbox-font-surface.md`. Its own control for the "nothing is discarded"
|
||||
//! claim is `SetMultiLine`'s binding, which *does* emit `xor eax,eax` (`0x797e9d`) and returns 0.
|
||||
//! positive control), covering all 48 names, all 48 binding addresses and all 16 arities. The
|
||||
//! control for the "nothing is discarded" claim is `SetMultiLine`'s binding, which *does*
|
||||
//! emit `xor eax,eax` (`0x797e9d`) and returns 0.
|
||||
//! Entry 47 ends at `0x87bce8`, the first byte of the string `"GetAltArrowKeyMode"` — the table
|
||||
//! abuts its own string pool, which pins the count at 48 a second way.
|
||||
//!
|
||||
@@ -72,8 +70,8 @@
|
||||
//!
|
||||
//! Each shim loads **`[this+0x324]`** and hands it to the shared implementation as the target. On a
|
||||
//! `CSimpleEditBox` that offset is the box's implicit FontString — the same field
|
||||
//! [`EditBoxState::text_region`](crate::widget::EditBoxState::text_region) models (RF-0082, and it is
|
||||
//! the EditBox's analogue of `ButtonText`). So `editBox:SetFontObject(ChatFontNormal)` really does
|
||||
//! [`EditBoxState::text_region`](crate::widget::EditBoxState::text_region) models (the EditBox's
|
||||
//! analogue of `ButtonText`). So `editBox:SetFontObject(ChatFontNormal)` really does
|
||||
//! paint *the box's font string*, which is exactly what this module writes: resolve the widget to
|
||||
//! the region its glyphs come from, then set the same [`RegionData`](super::RegionData) fields a
|
||||
//! `FontString` sets for itself.
|
||||
@@ -195,7 +193,7 @@ pub(super) fn install(
|
||||
};
|
||||
let d = model.region_data.entry(rh).or_default();
|
||||
d.font_object = Some(name);
|
||||
// The severance mask is deliberately NOT reset. §5-verified: the real "stop inheriting
|
||||
// The severance mask is deliberately NOT reset. The real "stop inheriting
|
||||
// this property" signal is a CLEARED bit in the inheritMask (`FONTINSTANCE+0x2c`),
|
||||
// cleared by each local setter and never restored — so a property the widget set for
|
||||
// itself stays severed across a later `SetFontObject`.
|
||||
@@ -274,14 +272,13 @@ pub(super) fn install(
|
||||
// is `""` when there are none — built into a zeroed static buffer at `0xceea60` — never nil.
|
||||
//
|
||||
// **The HEIGHT slot is a number even on a FontString that was never given a font**, and that is
|
||||
// not the obvious reading: §9.3 records that the FontString reads back *through* its resolved
|
||||
// `CGxFont`, so it looks as though a NULL one should nil all three. It does not. `0x7727b0`'s
|
||||
// fifth instruction is an *unconditional* `fld [esi+0xe4]`, and its `+0xe0` test sits behind a
|
||||
// branch `GetFont` never takes (`0x79d499 push 0`) — so slot 2 never touches the `CGxFont` at
|
||||
// all and always pushes a double (wow-re `font-object-lua-surface.md` §9.3a, §5-cross-checked).
|
||||
// We answer 0 there: `+0xe4` has **no constructor writer** in the reference, so its value on
|
||||
// this path is a recycled float that nothing can reproduce, and 0 is what the Font object's own
|
||||
// ctor-determined `+0x48` gives (decision 2129).
|
||||
// not the obvious reading: the FontString reads back *through* its resolved `CGxFont`, so it
|
||||
// looks as though a NULL one should nil all three. It does not. `0x7727b0`'s fifth instruction
|
||||
// is an *unconditional* `fld [esi+0xe4]`, and its `+0xe0` test sits behind a branch `GetFont`
|
||||
// never takes (`0x79d499 push 0`) — so slot 2 never touches the `CGxFont` at all and always
|
||||
// pushes a double. We answer 0 there: `+0xe4` has **no constructor writer** in the reference,
|
||||
// so its value on this path is a recycled float that nothing can reproduce, and 0 is what the
|
||||
// Font object's own ctor-determined `+0x48` gives (decision 2129).
|
||||
//
|
||||
// A nil here is not a cosmetic difference. `aux-addon/tabs/search/frame.lua:481` computes
|
||||
// `aux.select(2, child:GetFont()) + arg1*2` in its font-resize wheel handler — `aux.select`,
|
||||
@@ -307,7 +304,7 @@ pub(super) fn install(
|
||||
// SetTextColor(r, g, b [, a]) → 0 values; alpha defaults to 1.0 (`lua_isnumber(L,5)`-gated,
|
||||
// `0x3f800000`). A FontString has no texel of its own, so its vertex colour IS the colour it
|
||||
// draws — the same `+0xb8` slot `SetVertexColor` writes. The three channels are SHAPE C
|
||||
// (`FontString:SetTextColor 0x79d9c0`, `2=C 3=C 4=C 5=B`, wow-re `numeric-arg-coercion-law.md`):
|
||||
// (`FontString:SetTextColor 0x79d9c0`, `2=C 3=C 4=C 5=B`):
|
||||
// a bare `lua_tonumber`, so a nil or a non-number is 0.0 and the call never raises — the stock
|
||||
// trainer/trade-skill rows' OnLeave hands it `this.r, this.g, this.b` before anything set them
|
||||
// (1973).
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
//! pushes nothing and fires nothing — the VM keeps its last menu (or none), exactly as the
|
||||
//! reference's frame keeps its last paint (its handler returns on a cache miss without an event,
|
||||
//! and its greeting write and `GOSSIP_SHOW` are adjacent and unconditional on one success path —
|
||||
//! wow-re `gossip-npctext-law.md` §1/§4; B292, benilla decisions 1508/1994), which is why
|
||||
//! `0x4e2010`, `0x4e22b0`; B292, benilla decisions 1508/1994), which is why
|
||||
//! [`GossipMenu::greeting`] is a plain `String`.
|
||||
|
||||
use mlua::{Lua, MultiValue, Value};
|
||||
|
||||
@@ -241,8 +241,7 @@ pub struct GuildState {
|
||||
/// **This is an advisory loop bound, not the addressable range.** In the real client the three
|
||||
/// index-taking bindings all bound against the *full* member count (`[0xb73118]`) and never
|
||||
/// read the show-offline flag at all, while `GetNumGuildMembers` answers a different number
|
||||
/// entirely — so **indices past it are valid and return real offline members** (wow-re
|
||||
/// `system/ui/scratch/guild-api-carve.md`, correcting our claim A4).
|
||||
/// entirely — so **indices past it are valid and return real offline members**.
|
||||
///
|
||||
/// Collapsing the two — filtering the vector and reporting its length — is the natural design
|
||||
/// and it is wrong: it would make a selection that survives a show-offline toggle address a
|
||||
@@ -457,7 +456,7 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
// through the GENERAL unit-token resolver `0x515940`, so an *unrecognised* token raises via the
|
||||
// L-less `luaL_error 0x7040e0` at `0x515c1a` — but "a recognised-but-unresolvable token, a
|
||||
// non-player, a guildless player and a not-yet-arrived cache record ALL return (nil, nil, 0)"
|
||||
// (wow-re `system/ui` ledger, `0x4c9330`, VERIFIED, `scratch/guild-api-carve.md`).
|
||||
// (`0x4c9330`).
|
||||
//
|
||||
// Ours raised on any token the model held no unit for, which includes `"player"` before the
|
||||
// app has pushed the first snapshot. Stock `PaperDollFrame_SetGuild` calls
|
||||
@@ -951,9 +950,9 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// **`GetGuildInfo` splits three ways, not two** — wow-re `0x4c9330` (VERIFIED,
|
||||
/// `scratch/guild-api-carve.md`): only an *unrecognised* token raises; a **recognised**
|
||||
/// token that resolves to nothing answers `(nil, nil, 0)` like a guildless player does.
|
||||
/// **`GetGuildInfo` splits three ways, not two** — `0x4c9330`: only an *unrecognised* token
|
||||
/// raises; a **recognised** token that resolves to nothing answers `(nil, nil, 0)` like a
|
||||
/// guildless player does.
|
||||
///
|
||||
/// The distinction is not academic. Stock `PaperDollFrame_SetGuild` calls
|
||||
/// `GetGuildInfo("player")` unguarded from `PaperDollFrame_OnShow`, so before this split
|
||||
|
||||
@@ -43,16 +43,14 @@ use super::char_stats::InventorySlots;
|
||||
use super::Model;
|
||||
|
||||
/// The `CheckInteractDistance` threshold table — `{10², 11.1111², 10², 30²}` for the live API's
|
||||
/// `type ∈ 1..4`, read straight out of the binary (wow-re §5-VERIFIED
|
||||
/// `PRIMITIVE:check_interact_dist2` @ `0x48ba00`, built from the static `.rdata` at
|
||||
/// `type ∈ 1..4`, read straight out of the binary (`0x48ba00`, built from the static `.rdata` at
|
||||
/// `0x804498`/`0x804490`/`0x80448c`/`0x8044a4`). Type 1 is the *inspect* row's distance, and shares
|
||||
/// its 100.0 with [`super::inspect`]'s own `CanInspect` threshold.
|
||||
pub const INTERACT_DIST_SQ: [f64; 4] = [100.0, 123.45678, 100.0, 900.0];
|
||||
|
||||
/// The `CanInspect` threshold, **squared** — `DAT_00b4d918`, which the client's own writer builds by
|
||||
/// squaring the static `.rdata` `10.0` at `0x804498` (wow-re §5-VERIFIED
|
||||
/// `PRIMITIVE:caninspect_dist2` @ `0x48a1b0`, `ledger.tsv:823`). vmangos enforces the same 10 yards
|
||||
/// as `INSPECT_DISTANCE` (`ObjectDefines.h:26`), so client and server agree exactly.
|
||||
/// The `CanInspect` threshold, **squared** — `DAT_00b4d918`, which the client's own writer builds
|
||||
/// by squaring the static `.rdata` `10.0` at `0x804498` (`0x48a1b0`). vmangos enforces the same 10
|
||||
/// yards as `INSPECT_DISTANCE` (`ObjectDefines.h:26`), so client and server agree exactly.
|
||||
pub const CAN_INSPECT_DIST_SQ: f64 = 100.0;
|
||||
|
||||
/// What the app resolved about one unit token's unit this frame — the input both range predicates
|
||||
@@ -115,11 +113,11 @@ impl super::UiScript {
|
||||
/// frame. This map is re-pushed wholesale instead, and nothing keys an event off it.
|
||||
///
|
||||
/// A token absent from the map is a token the object manager holds **no unit for**, and both
|
||||
/// predicates answer `nil` there — the reference's own null-object arm (wow-re
|
||||
/// `system/ui/scratch/dist2-null-unit-arm.md`, VERIFIED: `0x48babe test ecx,ecx; je` →
|
||||
/// `lua_pushnil`). It used to read as *in range*, on the reasoning that missing data must not
|
||||
/// gray a row; the binary says the opposite, and that default was report B316 — every distance
|
||||
/// row lit up for exactly the party member who was too far away to have an object at all.
|
||||
/// predicates answer `nil` there — the reference's own null-object arm
|
||||
/// (`0x48babe test ecx,ecx; je` → `lua_pushnil`). It used to read as *in range*, on the
|
||||
/// reasoning that missing data must not gray a row; the binary says the opposite, and that
|
||||
/// default was report B316 — every distance row lit up for exactly the party member who was
|
||||
/// too far away to have an object at all.
|
||||
///
|
||||
/// **Keys are lowercase** — the resolver folds case (`_strnicmp`, 1247), so the lookup does
|
||||
/// too, exactly as `Model::unit` does for the snapshots.
|
||||
@@ -169,8 +167,8 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
let g = lua.globals();
|
||||
|
||||
// CanInspect(unit) → 1/nil: the gate the ref's `InspectFrame_Show` opens on
|
||||
// (`Blizzard_InspectUI.lua:8`). The real client's `0x48a1b0` is a d² range test — wow-re
|
||||
// §5-VERIFIED `PRIMITIVE:caninspect_dist2`: **out of range iff `threshold < d²`**, where
|
||||
// (`Blizzard_InspectUI.lua:8`). The real client's `0x48a1b0` is a d² range test: **out of range
|
||||
// iff `threshold < d²`**, where
|
||||
// `threshold = DAT_00b4d918 = 100.0` (10 yards, the same number vmangos enforces as
|
||||
// `INSPECT_DISTANCE`). The operator is theirs too: `test ah,0x41; jne` skips the out-of-range
|
||||
// action on `C0|C3` — Less, Equal, *or unordered* — so the three in-range arms below are that
|
||||
@@ -199,13 +197,13 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
})?,
|
||||
)?;
|
||||
|
||||
// CheckInteractDistance(unit, type) → 1/nil, `type ∈ 1..4` (wow-re §5-VERIFIED
|
||||
// `PRIMITIVE:check_interact_dist2` @ `0x48ba00`): **in range iff `d² < table[type-1]`** —
|
||||
// CheckInteractDistance(unit, type) → 1/nil, `type ∈ 1..4` (`0x48ba00`): **in range iff
|
||||
// `d² < table[type-1]`** —
|
||||
// note the STRICT `<` here against `CanInspect`'s non-strict gate above, which is the
|
||||
// binary's own asymmetry (`test ah,0x5; jp` takes the out path unless `d² < thr` ordered), not
|
||||
// a transcription slip. The UnitPopup rows' `dist` field indexes it.
|
||||
//
|
||||
// The three degenerate arms are the binary's too (`dist2-null-unit-arm.md`, VERIFIED), and
|
||||
// The three degenerate arms are the binary's too, and
|
||||
// they are three DIFFERENT answers rather than one permissive default:
|
||||
//
|
||||
// - **No live unit for the token** → `nil` (`0x48babe test ecx,ecx; je`). This is the party
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
//! The **shared item-template store** — one `item id → ItemTemplateView` table every item hover
|
||||
//! renders through (decision 0274 P1). In the real client every item tooltip is the same C++
|
||||
//! renderer (`0x52b650`, behind 8 of the 9 `Set*Item` bindings — wow-re
|
||||
//! `ui/scratch/tooltip-money.md`) over the same item-template cache; benilla mirrors that with
|
||||
//! renderer (`0x52b650`, behind 8 of the 9 `Set*Item` bindings) over the same item-template cache;
|
||||
//! benilla mirrors that with
|
||||
//! one engine store the app feeds from its ask-once `ITEM_QUERY` template cache, and one engine
|
||||
//! renderer ([`super::tooltip_item`]).
|
||||
//!
|
||||
@@ -150,8 +150,8 @@ pub struct ItemTemplateView {
|
||||
pub icon: Option<String>,
|
||||
/// `RandomProperty` (template `+0x1b8`) — the item CAN roll a "… of the Bear" suffix. Its one
|
||||
/// consumer is the enchant family's third arm: with no instance to read a roll from, the
|
||||
/// tooltip prints the `<Random enchantment>` placeholder instead of any per-slot line (wow-re
|
||||
/// §1-ENCHANT §E5). Decision 0920.
|
||||
/// tooltip prints the `<Random enchantment>` placeholder instead of any per-slot line
|
||||
/// (`0x52cc33`). Decision 0920.
|
||||
pub random_property: u32,
|
||||
}
|
||||
|
||||
@@ -201,10 +201,9 @@ pub(super) fn item_usable_by_id(model: &super::Model, item_id: u32) -> bool {
|
||||
})
|
||||
})
|
||||
}
|
||||
/// The client's item-usable predicate `0x5ea930(player; itemCacheRecord, &err)` — byte-read from
|
||||
/// wow-re's `ui/scratch/disasm-full.txt`. Both merchant getters call it (`GetMerchantItemInfo`
|
||||
/// `0x4fb2a3`, `GetBuybackItemInfo` `0x4fb4f7`) and push `1`/`nil` as `isUsable`; the FrameXML
|
||||
/// reds the row on `nil`. The legs, in the binary's order:
|
||||
/// The client's item-usable predicate `0x5ea930(player; itemCacheRecord, &err)`. Both merchant
|
||||
/// getters call it (`GetMerchantItemInfo` `0x4fb2a3`, `GetBuybackItemInfo` `0x4fb4f7`) and push
|
||||
/// `1`/`nil` as `isUsable`; the FrameXML reds the row on `nil`. The legs, in the binary's order:
|
||||
///
|
||||
/// 1. `requiredLevel > player level` → unusable.
|
||||
/// 2. class mask: `allowableClass & 1<<(classId−1)` clear → unusable (−1 = every bit set).
|
||||
@@ -280,7 +279,7 @@ pub fn item_usable(
|
||||
true
|
||||
}
|
||||
|
||||
/// An item set's tooltip view (the §22 SET block), app-resolved: the ItemSet.dbc row with
|
||||
/// An item set's tooltip view (`0x854b1c`'s SET block), app-resolved: the ItemSet.dbc row with
|
||||
/// member item NAMES joined from the template cache (a `None` name = the member's template is
|
||||
/// still in flight — its line waits; the app re-pushes as answers land) and the threshold
|
||||
/// bonuses' TEXT ($-substituted spell descriptions). The engine supplies the live half: the
|
||||
@@ -358,9 +357,8 @@ impl super::UiScript {
|
||||
/// Register the shared item-stats global (the P0 Lua stat-head read — still the merchant
|
||||
/// sell-cursor's source and the compat surface while call sites finish moving to the engine
|
||||
/// renderer; the tooltip itself no longer routes through it).
|
||||
/// The 1.12 item-quality palette, **byte-verified** — wow-5875-re RF-0055
|
||||
/// (`scratch/rf55-quality-color-table.md`): the seven ARGB literals the static init `0x5291d0`
|
||||
/// writes into the BGRA array at `0xc0d3c8`, and the parallel escape strings at `0x854124`.
|
||||
/// The 1.12 item-quality palette: the seven ARGB literals the static init `0x5291d0` writes into
|
||||
/// the BGRA array at `0xc0d3c8`, and the parallel escape strings at `0x854124`.
|
||||
///
|
||||
/// `Script::GetItemQualityColor 0x48dfb0` reads `[quality*4 + 0xc0d3c8]`, takes `[+2]=R`,
|
||||
/// `[+1]=G`, `[+0]=B`, multiplies each by `1/255`, and returns them with the escape string.
|
||||
@@ -375,7 +373,7 @@ const QUALITY_COLORS: [(u8, u8, u8, &str); 7] = [
|
||||
];
|
||||
|
||||
/// The client's item-link builder `0x52adb0` as the trade-skill and craft link verbs call it
|
||||
/// (wow-re `tradeskill/scratch/tradeskill-craft-item-links.md`, 1973): `|c<rrggbb>|Hitem:<id>:0:0:0|h[<name>]|h|r`
|
||||
/// (1973): `|c<rrggbb>|Hitem:<id>:0:0:0|h[<name>]|h|r`
|
||||
/// — the three tokens literal zeros at those call sites, the colour from `0x52ad90`'s table where
|
||||
/// `cmp ecx,7; jb` is UNSIGNED, so a quality of 7 or more, or a negative one, selects index 1
|
||||
/// (white): a fixed fallback, not a clamp. The `""`-suffix arm is dead, so every link ends `|h|r`.
|
||||
@@ -538,8 +536,7 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
// (grey). There is no out-of-bounds read: the unsigned compare catches it first. The value
|
||||
// stopped being academic when the loot window went to the chain — `-1` is what
|
||||
// `GetLootSlotInfo` answers for a row whose item template is not cached, and that row's text
|
||||
// colour is this table's `-1` row (wow-re `system/ui/scratch/loot-slot-record.md` §10,
|
||||
// decision 1805).
|
||||
// colour is this table's `-1` row (`0x4c2435`, decision 1805).
|
||||
lua.globals().set(
|
||||
"GetItemQualityColor",
|
||||
lua.create_function(|lua, quality: i64| {
|
||||
@@ -563,8 +560,8 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
// itemName, itemLink, itemQuality, itemMinLevel, itemType, itemSubType,
|
||||
// itemStackCount, itemEquipLoc, itemTexture
|
||||
//
|
||||
// **Nine values, and every one of them byte-verified** against the registered binding
|
||||
// `0x48e070` (wow-5875-re `system/ui/ledger.tsv:891`), which ends `mov eax,0x9; ret`. The
|
||||
// **Nine values, and every one of them confirmed** against the registered binding
|
||||
// `0x48e070`, which ends `mov eax,0x9; ret`. The
|
||||
// signature is the whole point of the verb (decision 1199): a later client inserts `itemLevel`
|
||||
// at position 4 and pushes the required level to 5, and 36 corpus addons destructure this one
|
||||
// positionally — `Informant/Informant.lua:268` reads all nine and stores position 4 as
|
||||
@@ -582,8 +579,8 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
// (that is `GetContainerItemLink`'s shape and stays there). Auctioneer's
|
||||
// `getItemInfoFromBlizzard` names the return `itemString` and feeds it
|
||||
// straight back in, which only works because both ends are this shape.
|
||||
// 3 `itemQuality` `fild [record+0x1c]` — the same dword wow-re pinned through this function
|
||||
// for the drag payload (`scratch/cursor-dragdrop-payload.md:223`).
|
||||
// 3 `itemQuality` `fild [record+0x1c]` — the same dword `0x495300` reads for the drag
|
||||
// payload.
|
||||
// 4 `itemMinLevel` `fild [record+0x3c]` — the **required** level. `ItemLevel` lives one dword
|
||||
// earlier at `+0x38` and is never pushed here. This is the trap.
|
||||
// 5 `itemType` ItemClass.dbc's localized class name, or `""` (`0x48e236`).
|
||||
@@ -1094,8 +1091,8 @@ mod get_item_info_tests {
|
||||
mod quality_color_tests {
|
||||
use crate::script::UiScript;
|
||||
|
||||
/// The seven colours, byte-verified against wow-5875-re RF-0055's own table — and the two
|
||||
/// edges, which are where every reimplementation of this goes wrong.
|
||||
/// The seven colours, confirmed against the reference's own table (`0x5291d0`) — and the
|
||||
/// two edges, which are where every reimplementation of this goes wrong.
|
||||
///
|
||||
/// The escape string matters as much as the floats: the reference returns it as the fourth
|
||||
/// value and addons splice it straight into a link (`ITEM_QUALITY_COLORS[q].hex .. name`), so
|
||||
|
||||
@@ -252,7 +252,7 @@ impl KeybindState {
|
||||
/// (neither the target's nor the incumbent's), and its one reachable falsey return is the
|
||||
/// validator at `0x4b762c`. `"runOnUp"` has exactly one reader image-wide — `0x4b7bf1`, in
|
||||
/// `RunCommand`, gating the up half. So 1.12 binds the wheel to an action button happily, and
|
||||
/// B265 was ours. wow-re `system/ui/scratch/setbinding-refusal-law.md`.
|
||||
/// B265 was ours.
|
||||
fn set_binding(&mut self, key: &str, command: Option<&str>) -> bool {
|
||||
let Some(key) = normalize_binding_key(key) else {
|
||||
return false;
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
//! never fires is the silent-drop class 1203/1205/1211 each recorded. This is the machinery that
|
||||
//! note was waiting on, so those three names are now accepted there.
|
||||
//!
|
||||
//! ## The law (wow-re `system/ui/scratch/frame-key-script-delivery.md`, §5 trio — VERIFIED)
|
||||
//! ## The law
|
||||
//!
|
||||
//! An OS key event reaches **one dispatcher per channel**, and the dispatcher — not the frame — is
|
||||
//! what walks: `0x765f10` (key-down, kind-1 buckets, vtable `+0x60`) and `0x765df0` (char, kind-0
|
||||
@@ -16,7 +16,7 @@
|
||||
//! until one returns nonzero; that one **consumes** and both loops stop. So several frames may be
|
||||
//! called, at most one consumes.
|
||||
//!
|
||||
//! **The consumption gate is EXISTENCE, not handling** (`0x76b7d0`/`0x76bba0`/`0x76b760`, §3):
|
||||
//! **The consumption gate is EXISTENCE, not handling** (`0x76b7d0`/`0x76bba0`/`0x76b760`):
|
||||
//!
|
||||
//! | slot state | key-down returns | fires |
|
||||
//! |---|---|---|
|
||||
@@ -27,9 +27,10 @@
|
||||
//! That asymmetry is real and is transcribed below: a frame carrying only an `OnKeyUp` swallows
|
||||
//! every key-down and runs no script. `OnChar` gates on its own slot alone; `OnKeyUp` on `+0x190`
|
||||
//! alone (no `OnKeyDown` fallback). **A 1.12 handler cannot signal "handled"** — the fire's return
|
||||
//! is discarded at all three call sites (§3.1) — so a Lua handler never influences consumption.
|
||||
//! is discarded at all three call sites (`0x7026f0`) — so a Lua handler never influences
|
||||
//! consumption.
|
||||
//!
|
||||
//! **Bucket membership is the keyboard-enabled flag**, never the presence of a script (§3.2):
|
||||
//! **Bucket membership is the keyboard-enabled flag**, never the presence of a script (`0x76af00`):
|
||||
//! `EnableKeyboard(true)` on a script-less frame puts it *in* the walk, where it is called and
|
||||
//! declines. XML `enableKeyboard` enables both kinds; an XML `<Scripts>` block auto-enables per
|
||||
//! handler; **Lua `SetScript` auto-enables nothing**, so a runtime-created frame needs an explicit
|
||||
@@ -42,8 +43,8 @@
|
||||
//! the base. Modelling it as a stage before or after the walk would get the order wrong whenever a
|
||||
//! keyboard frame and the focused box are in different strata — a TOOLTIP-strata frame really does
|
||||
//! pre-empt a focused HIGH-strata box, and a WORLD-strata one really does not. So [`walk`] visits
|
||||
//! the focused box **at its own strata/level**, handing it to the RF-0082 routing this crate
|
||||
//! already had ([`super::editbox`]) and taking a `true` as consumption.
|
||||
//! the focused box **at its own strata/level**, handing it to the routing this crate already had
|
||||
//! ([`super::editbox`]) and taking a `true` as consumption.
|
||||
//!
|
||||
//! What is deliberately NOT re-derived here: the box's own focus acquisition (`autoFocus`
|
||||
//! self-acquire, click-to-focus, `SetFocus`) stays entirely in `editbox`, and an event no frame
|
||||
@@ -51,7 +52,7 @@
|
||||
//!
|
||||
//! ## Not modelled, named
|
||||
//!
|
||||
//! The **sticky per-key-code key-up slot** (§2.1: `0x765fd0` routes key-up to `[root+code*4+0x84]`,
|
||||
//! The **sticky per-key-code key-up slot** (`0x765fd0` routes key-up to `[root+code*4+0x84]`,
|
||||
//! the last frame that consumed *some* down for that code, and does **not** clear it on delivery)
|
||||
//! is not built: this engine's host feeds no key-up at all today. `OnKeyUp` is therefore accepted,
|
||||
//! stored, gated in the table above — and never fired. That is stated rather than hidden, because
|
||||
@@ -71,8 +72,8 @@ enum Channel {
|
||||
KeyDown,
|
||||
}
|
||||
|
||||
/// The walk order (§2): every keyboard-enabled, effectively-visible frame, **strata descending,
|
||||
/// then level descending, then oldest registration first**.
|
||||
/// The walk order (`0x765f10`): every keyboard-enabled, effectively-visible frame, **strata
|
||||
/// descending, then level descending, then oldest registration first**.
|
||||
///
|
||||
/// Built as a sorted `Vec` per dispatch rather than kept as nine live buckets. The reference
|
||||
/// maintains the buckets incrementally because it is walking them on every key at 1.12 frame
|
||||
@@ -107,9 +108,9 @@ fn has_script(lua: &Lua, id: u32, name: &str) -> bool {
|
||||
/// **A box in the walk is asked about FOCUS, never about a script slot.** Its ctor (`0x779ce8`)
|
||||
/// registers it in the kind-0 *and* kind-1 buckets, and its vtable `0x81c910` **replaces** the base
|
||||
/// input slots — `+0x5c` → `0x77a900` (char), `+0x60` → `0x77b160` (key-down) — and never chains to
|
||||
/// them. wow-re `system/ui/scratch/frame-key-script-delivery.md` §3, at the bytes: *"There is no
|
||||
/// `call 0x76b760` anywhere in it, so the generic `OnChar` slot `+0x180` is unreachable on an
|
||||
/// editbox."* The override's own tail is the decline this predicate exists to reach:
|
||||
/// them: there is no `call 0x76b760` anywhere in it, so the generic `OnChar` slot `+0x180` is
|
||||
/// unreachable on an editbox. The override's own tail is the decline this predicate exists to
|
||||
/// reach:
|
||||
/// `0x77a952 cmp esi,eax` — I am the focus owner → insert and consume — else
|
||||
/// `0x77a956 xor eax,eax`, **return 0, and the walk continues past me**. `0x77b160` has the same
|
||||
/// shape at `0x77b1c7`/`0x77b21e`.
|
||||
@@ -131,7 +132,7 @@ fn is_editbox(lua: &Lua, h: FrameHandle) -> bool {
|
||||
///
|
||||
/// Returns whether the event was **consumed**, which is what the caller must use to suppress the
|
||||
/// keybinding system: consumption is decided by the C++ existence gate alone, never by what the
|
||||
/// Lua handler did or returned (§3.1).
|
||||
/// Lua handler did or returned (`0x7026f0`).
|
||||
fn walk(lua: &Lua, channel: Channel, arg: &str) -> bool {
|
||||
let order = {
|
||||
let model = lua.app_data_ref::<Model>().expect("model app_data");
|
||||
@@ -208,14 +209,15 @@ fn fire(lua: &Lua, id: u32, script: &str, arg: &str) {
|
||||
}
|
||||
}
|
||||
|
||||
/// The char channel (`0x765df0`) — `arg1` is the literal character, UTF-8 (§4.1: the code is
|
||||
/// UTF-8-encoded with no range guard, so a multi-byte codepoint arrives whole).
|
||||
/// The char channel (`0x765df0`) — `arg1` is the literal character, UTF-8 (`0x41abb0` encodes the
|
||||
/// code with no range guard, so a multi-byte codepoint arrives whole).
|
||||
pub(super) fn char_input(lua: &Lua, text: &str) -> bool {
|
||||
walk(lua, Channel::Char, text)
|
||||
}
|
||||
|
||||
/// The key-down channel (`0x765f10`) — `arg1` is the key NAME, with no modifier prefix, from the
|
||||
/// same table the keybinding chord uses (§4.2). The host speaks those names already
|
||||
/// same table the keybinding chord uses (`0x76b7d0`, byte-identical to the chord's own
|
||||
/// `0x4b66b0`). The host speaks those names already
|
||||
/// (`crate::script::UiScript::key_input`'s contract), so no decode happens here.
|
||||
pub(super) fn key_input(lua: &Lua, key: &str) -> bool {
|
||||
walk(lua, Channel::KeyDown, key)
|
||||
|
||||
@@ -181,8 +181,7 @@ fn region_node_hash(data: &RegionData) -> u64 {
|
||||
}
|
||||
|
||||
/// A region's **content-derived span**, in FrameXML units — the client's virtual size getters,
|
||||
/// which the resolver calls instead of reading a stored width/height (wow-re
|
||||
/// `region-size-fallback.md` §1/§2, decision 1349).
|
||||
/// which the resolver calls instead of reading a stored width/height (decision 1349).
|
||||
///
|
||||
/// `CSimpleTexture::GetWidth 0x770720` returns the authored value only when it is **not exactly
|
||||
/// `0.0`**; on `0.0` with a texture loaded it returns the texture's own texel extent, through
|
||||
@@ -211,7 +210,7 @@ pub(super) fn content_span(
|
||||
|
||||
/// [`content_span`]'s texture arm, for a caller holding a path rather than a region — the
|
||||
/// SimpleHTML flow reservation, which asks the reference's `GetHeight` override the same way
|
||||
/// (`simplehtml-markup-engine.md` §7 step 6).
|
||||
/// (`0x770790`).
|
||||
pub(super) fn texel_span(
|
||||
probe: Option<&crate::script::TextureSizeProbe>,
|
||||
path: &str,
|
||||
@@ -227,7 +226,7 @@ const SOLID_TEXTURE_TEXELS: f32 = 8.0;
|
||||
/// The floor under a **FontString's** derived span, in FrameXML units —
|
||||
/// `CSimpleFontString::GetWidth 0x772930` / `GetHeight 0x772a60` end
|
||||
/// `fld 1-unit; fcomp candidate; test ah,0x5; jp` and return the floor when the candidate is
|
||||
/// smaller (wow-re `region-size-fallback.md` §3, `tooltip-blank-line-height.md` §3). One unit,
|
||||
/// smaller. One unit,
|
||||
/// not one line: the reference's own blank tooltip spacer is `" \n"` — a real one-line string —
|
||||
/// and this floor is what a *genuinely* empty string reads back as, never `0.0`.
|
||||
pub(super) const FONTSTRING_MIN_SPAN: f32 = 1.0;
|
||||
@@ -1153,9 +1152,9 @@ impl UiScript {
|
||||
//
|
||||
// That forever-stale box is B309. Tooltip line cells are POOLED and reused across
|
||||
// hovers (`clear_content` keeps `measured` deliberately, for the hover re-enter
|
||||
// loop), so the item tooltip's two blank gold SET spacers (`render.rs` §22) landed
|
||||
// on cells that had carried real text on an earlier hover: each drew a full row
|
||||
// out of the dead measure while the plate counted it as zero
|
||||
// loop), so the item tooltip's two blank gold SET spacers (`render.rs`, `0x854b2c`)
|
||||
// landed on cells that had carried real text on an earlier hover: each drew a full
|
||||
// row out of the dead measure while the plate counted it as zero
|
||||
// (`tooltip::cell` calls empty text `(0,0)`), and the set bonuses hung two rows
|
||||
// below the backdrop. The Lua-visible getters never disagreed — they key-check
|
||||
// (`region::measured_wh`) — which is why only the drawn geometry was wrong.
|
||||
@@ -1176,9 +1175,9 @@ impl UiScript {
|
||||
// authored value when it is not exactly `0.0` and the measured extent otherwise,
|
||||
// then returns `max(candidate, ONE FrameXML unit)` — the floor sits past the
|
||||
// `jp 0x772957` that skips the measure, so it applies to BOTH legs. `0x772a60` is
|
||||
// the identical body for height. **A FontString's span is therefore never `0.0`**
|
||||
// (wow-re `region-size-fallback.md` §3), which is what makes a single-anchored
|
||||
// FontString always resolve — and what retires the zero-span collapse this sweep
|
||||
// the identical body for height. **A FontString's span is therefore never `0.0`**,
|
||||
// which is what makes a single-anchored FontString always resolve — and what
|
||||
// retires the zero-span collapse this sweep
|
||||
// used to apply in the `axis` closure below.
|
||||
if is_fontstring {
|
||||
width = width.max(FONTSTRING_MIN_SPAN);
|
||||
@@ -1222,11 +1221,10 @@ impl UiScript {
|
||||
// on a performance fix.
|
||||
(id != SCREEN).then(|| solver.rect(id)).flatten()
|
||||
});
|
||||
// **An axis resolves, or the region does not** — there is no owner-edge
|
||||
// fallback and no zero-span collapse (decision 1664, from wow-re's byte-verified
|
||||
// `region-size-fallback.md` §7). A complete memory-operand enumeration of the real
|
||||
// resolver core `[0x7671a0, 0x76761f)` finds no parent or owner pointer in it at
|
||||
// all: the only fallbacks are the object's own nine anchor slots and
|
||||
// **An axis resolves, or the region does not** — there is no owner-edge fallback
|
||||
// and no zero-span collapse (decision 1664). A complete memory-operand enumeration
|
||||
// of the real resolver core `[0x7671a0, 0x76761f)` finds no parent or owner pointer
|
||||
// in it at all: the only fallbacks are the object's own nine anchor slots and
|
||||
// `combineEdge`/`combineCenter` over its own opposite edge/centre ± its own size.
|
||||
// `assemble 0x767a20` returns 0 the moment any edge is still UNSET, and
|
||||
// `0x768d20` latches the region unresolvable.
|
||||
@@ -1243,7 +1241,7 @@ impl UiScript {
|
||||
// * **A pinned edge with a zero span.** The span is content-derived on both region
|
||||
// classes — the texture's texel extent above, the FontString's measured extent
|
||||
// floored at one unit — so `combineEdge`'s zero-span leg is nearly unreachable
|
||||
// for a region at all (`region-size-fallback.md` §4). What is left is a texture
|
||||
// for a region at all (`0x767440`). What is left is a texture
|
||||
// with no art whatever, which the reference resolves to nothing and draws as a
|
||||
// degenerate all-zero quad.
|
||||
let axis = |lo: Option<f32>, hi: Option<f32>| -> Option<(f32, f32)> {
|
||||
|
||||
@@ -55,8 +55,7 @@ pub(super) const REG_LOOTBUTTON_METHODS: &str = "__benilla_lootbutton_methods";
|
||||
|
||||
/// The **item-cache miss** quality `GetLootSlotInfo` answers for a row whose item template has not
|
||||
/// landed yet — the reference's own sentinel, and emphatically **not** a nil. `0x4c23a0` reads the
|
||||
/// item-cache record's `[rec+0x1c]` and hands back `-1` when the cache has no record for the id
|
||||
/// (wow-re `system/ui/scratch/loot-slot-record.md` §4, a §5 trio round).
|
||||
/// item-cache record's `[rec+0x1c]` and hands back `-1` when the cache has no record for the id.
|
||||
///
|
||||
/// It is why stock `UIParent.lua` builds `ITEM_QUALITY_COLORS` over **`for i = -1, 6`** (l.66):
|
||||
/// index `-1` exists *for this value*. `LootFrame_Update` indexes the table with the raw return and
|
||||
@@ -129,8 +128,7 @@ pub struct LootRow {
|
||||
/// The client keeps exactly this, at `+0x14` of its own 0x1c-byte loot record, and
|
||||
/// `SetLootItem 0x533470` copies it into the tooltip's `+0x424` — a loot slot is **not** an
|
||||
/// item object (that leg passes an all-zero item guid), so the roll is the only enchant source
|
||||
/// a loot hover can have (wow-re `loot-slot-record.md`, `tooltip-content-law.md` §E6-LOOT).
|
||||
/// [`Self::name`] already carries the suffix the same id joins on.
|
||||
/// a loot hover can have. [`Self::name`] already carries the suffix the same id joins on.
|
||||
pub random_property_id: u32,
|
||||
}
|
||||
|
||||
|
||||
@@ -36,8 +36,8 @@ use super::Model;
|
||||
const PASS: u8 = 0;
|
||||
|
||||
/// What `GetLootRollItemInfo` answers for `count` and `quality` when the item-template cache has no
|
||||
/// record — the reference's own shared miss tail `nil, nil, 1.0, 1.0, nil` at `0x4c31a3` (wow-re
|
||||
/// `system/object-layer/scratch/lootroll-chat-and-lifecycle.md` §5). `count` is a literal `1.0` on
|
||||
/// record — the reference's own shared miss tail `nil, nil, 1.0, 1.0, nil` at `0x4c31a3`.
|
||||
/// `count` is a literal `1.0` on
|
||||
/// the hit path too (`0x4c3160` — a group roll is always one stack), so only the quality is really a
|
||||
/// sentinel, and it is **`1` (Common), not `GetLootSlotInfo`'s `-1`**: each accessor carries its own,
|
||||
/// and copying one to the other would be a guess.
|
||||
|
||||
@@ -72,9 +72,9 @@
|
||||
//! ## The one known divergence, stated rather than hidden
|
||||
//!
|
||||
//! `table.insert`/`table.remove` stay on 5.1's `#t` border rather than consulting `getn`. **This
|
||||
//! is byte-confirmed as a real divergence, not a suspicion**: the RE dispatch found that 1.12's
|
||||
//! whole table library is `n`-based — `luaL_getn` is called by `insert`/`remove`/`concat`/`sort`/
|
||||
//! `foreachi` *and* base `unpack`, and `insert`/`remove` **update** the stored size. So
|
||||
//! is byte-confirmed as a real divergence, not a suspicion**: 1.12's whole table library is
|
||||
//! `n`-based — `luaL_getn 0x6f5050` is called by `insert`/`remove`/`concat`/`sort`/`foreachi` *and*
|
||||
//! base `unpack`, and `insert`/`remove` **update** the stored size (`luaL_setn 0x6f4ea0`). So
|
||||
//! `setn(t, 0)` on a non-empty table makes the next `insert` land at index 1 there and at
|
||||
//! `#t + 1` here.
|
||||
//!
|
||||
@@ -90,9 +90,8 @@
|
||||
//! ## Ground truth (decision 1196 — verified, no longer derived)
|
||||
//!
|
||||
//! This module first shipped with its member lists taken from Lua 5.0's published library
|
||||
//! registrations and cross-checked against two artifacts we hold. An RE dispatch into
|
||||
//! wow-5875-re then read the binary (`system/ui/scratch/lua-dialect.md`), and every list here is
|
||||
//! now the array in the image:
|
||||
//! registrations and cross-checked against two artifacts we hold. Every list here is now read
|
||||
//! straight off the array in the image:
|
||||
//!
|
||||
//! - **Lua 5.0**, on five independent discriminators — not just the `$Lua: Lua 5.0 …` blob at
|
||||
//! `0x811b30` but `LUA_REGISTRYINDEX = -10000` (5.1 uses −10002), a `luaT_eventname[]` pool with
|
||||
@@ -191,11 +190,7 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
//
|
||||
// `luaopen_string 0x7fd810` is six instructions, `[0x7fd810, 0x7fd827)` — `push 0;
|
||||
// push 0x822d88; mov edx,0x871938; call luaL_openlib; mov eax,1; ret` — with no
|
||||
// `createmetatable` step, and `luaL_openlib` does not reach `0x6f4020` either. (wow-re
|
||||
// `system/ui/scratch/string-metatable-closure.md`, whose §5 cross-check produced all of the
|
||||
// above and retired `lua-dialect.md` §3's INFERRED flag on it. Two of ours came from that
|
||||
// note before it was re-read: it said "four instructions" while quoting six, and it stopped at
|
||||
// "no `lua_setmetatable` call" where the reader-side argument was available.)
|
||||
// `createmetatable` step, and `luaL_openlib` does not reach `0x6f4020` either.
|
||||
//
|
||||
// So this is not a policy choice about a superset — it is the type system. Note the reference
|
||||
// also *raises* rather than no-ops if an addon tries it itself: base `setmetatable 0x702a40`'s
|
||||
@@ -223,10 +218,10 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
g.set("getn", table.get::<Value>("getn")?)?;
|
||||
|
||||
// ── 4 · what the binary said and nobody had asked (decision 1196) ─────────────────────────
|
||||
// `print` and `_VERSION` are **not in 1.12's `_G`** — the captured table says so and the RE
|
||||
// dispatch found why: the base library is a 36-entry array looped into `_G`, and neither is in
|
||||
// it (`_VERSION`'s literal is not even in the image). Both were on `reference_surface`'s
|
||||
// exception list as "inherited rather than chosen"; nothing of ours uses either.
|
||||
// `print` and `_VERSION` are **not in 1.12's `_G`** — the captured table says so, and the base
|
||||
// library's 36-entry array at `0x811e28` confirms why: neither is in it (`_VERSION`'s literal
|
||||
// is not even in the image). Both were on `reference_surface`'s exception list as "inherited
|
||||
// rather than chosen"; nothing of ours uses either.
|
||||
g.set("print", Value::Nil)?;
|
||||
g.set("_VERSION", Value::Nil)?;
|
||||
|
||||
@@ -317,9 +312,8 @@ fn install_bit(lua: &Lua) -> mlua::Result<()> {
|
||||
/// The **garbage-collector pair** — `gcinfo` answers TWO numbers and `collectgarbage` answers
|
||||
/// NONE and takes a *number* (decision 2136).
|
||||
///
|
||||
/// Both are 5.0-shaped, and 5.1 changed both in ways that are observable from Lua. Read off the
|
||||
/// image by wow-5875-re (`system/ui/scratch/lua-dialect.md` §12), and already carried in
|
||||
/// `reference/1.12-shapes.tsv` as `gcinfo … 2 exact (number,number) agree` and
|
||||
/// Both are 5.0-shaped, and 5.1 changed both in ways that are observable from Lua — already
|
||||
/// carried in `reference/1.12-shapes.tsv` as `gcinfo … 2 exact (number,number) agree` and
|
||||
/// `collectgarbage … 0 exact () agree` — rows the return-shape gate never checked, because it
|
||||
/// filters to `table_kind = global` and these two are `baselib`.
|
||||
///
|
||||
@@ -947,8 +941,7 @@ mod tests {
|
||||
/// `OPR_NOUNOPR` is 2 — a three-member enum, 5.0's, not 5.1's four-member one with
|
||||
/// `OPR_LEN`. `getbinopr` (`0x6fe0c0`) bases its switch at `'*'` (0x2A); `%` is 0x25, below
|
||||
/// the range, and reaches `OPR_NOBINOPR` = 14 — a fifteen-member `BinOpr`, again 5.0's. The
|
||||
/// metamethod-name pool at `0x871896` has neither `__len` nor `__mod`, which is where
|
||||
/// wow-5875-re saw it first (`system/ui/scratch/lua-dialect.md` §1).
|
||||
/// metamethod-name pool at `0x871896` has neither `__len` nor `__mod`.
|
||||
#[test]
|
||||
fn the_length_and_modulo_operators_are_not_in_the_grammar() {
|
||||
let s = UiScript::new().unwrap();
|
||||
@@ -1003,10 +996,11 @@ mod tests {
|
||||
/// **The six `debug*` stubs are the REFERENCE's no-ops, not ours** — and that distinction is
|
||||
/// the whole reason they are allowed to exist under real 1.12 names.
|
||||
///
|
||||
/// wow-re carved all eight (2026-08-11): `debuginfo`, `debugload`, `debugprint`, `debugdump`,
|
||||
/// `debugbreak` and `debugtimestamp` are byte-identical `xor eax,eax; ret` — three bytes, no
|
||||
/// call, no memory write, **zero Lua return values**. Only `debugprofilestart`/`stop` are real.
|
||||
/// A no-op we invented would be the "capability absent without a failure" class 1203 named;
|
||||
/// Six of the eight, `[0x7027e0, 0x702840)` — `debuginfo`, `debugload`, `debugprint`,
|
||||
/// `debugdump`, `debugbreak` and `debugtimestamp` — are byte-identical `xor eax,eax; ret` —
|
||||
/// three bytes, no call, no memory write, **zero Lua return values**. Only
|
||||
/// `debugprofilestart`/`stop` are real. A no-op we invented would be the "capability absent
|
||||
/// without a failure" class 1203 named;
|
||||
/// a no-op the client itself ships is a transcription.
|
||||
#[test]
|
||||
fn the_debug_family_is_six_stubs_and_two_real_ones() {
|
||||
@@ -1062,10 +1056,10 @@ mod tests {
|
||||
/// Stock 5.1 moved that `cc->nh++` out of the arm, pre-sized a node vector big enough that no
|
||||
/// rehash ever fires, and left all n keys in the hash — where `next` is slot order.
|
||||
///
|
||||
/// wow-5875-re `system/ui/scratch/lua-table-storage-and-next-order.md` — **executed** on
|
||||
/// `WoW.exe`'s own bytes (`lua_open` → `luaL_loadbuffer` → `lua_pcall` → `lua_next`), not
|
||||
/// derived, including the exact `Bagnon_Core.lua` below; ascending holds for n = 1..24 and
|
||||
/// regardless of the order the fields are written in.
|
||||
/// This was measured by executing `WoW.exe`'s own bytes (`lua_open` → `luaL_loadbuffer` →
|
||||
/// `lua_pcall` → `lua_next`), not merely derived from the disassembly — including the exact
|
||||
/// `Bagnon_Core.lua` below; ascending holds for n = 1..24 and regardless of the order the
|
||||
/// fields are written in.
|
||||
///
|
||||
/// It matters because every saved-variables file we write is that constructor (decision 1128's
|
||||
/// grammar, and the reference's own writer's — it never emits a bare positional entry), so
|
||||
@@ -1141,8 +1135,8 @@ mod error_quoting_tests {
|
||||
/// out of `debugstack()` with `string.find(debugstack(), "`argCheck'.-([`<].-['>])")`, whose
|
||||
/// two patterns both require the backquote; the nil it gets is then handed to a `%s`, so an
|
||||
/// argument-check *diagnostic* raises inside the error path. Closing that means rendering the
|
||||
/// traceback ourselves in the reference's own shape, which is dispatched into wow-re
|
||||
/// (`debugstack 0x703760`) rather than copied from stock 5.0's `ldblib.c`.
|
||||
/// traceback ourselves in the reference's own shape (`debugstack 0x703760`) rather than
|
||||
/// copied from stock 5.0's `ldblib.c`.
|
||||
#[test]
|
||||
fn errors_quote_program_elements_the_way_lua_5_0_does() {
|
||||
let s = UiScript::new().unwrap();
|
||||
@@ -1239,7 +1233,7 @@ mod error_quoting_tests {
|
||||
/// `mov [edx+8], esi` because 5.0 puts `metatable` at offset 8 of both structs — and returns 0
|
||||
/// without writing for every other tag. 5.0 has no `G(L)->mt[]` array for a string to have a
|
||||
/// slot in, so the type *cannot* carry one; `luaopen_string 0x7fd810`'s four instructions
|
||||
/// (wow-re `lua-dialect.md` §4) are the same fact from the other end.
|
||||
/// are the same fact from the other end.
|
||||
///
|
||||
/// The **wording** is asserted, not just the raise: it is what a player sees in a script
|
||||
/// error, and 5.0's quoting convention is already load-bearing one file over — `sandbox`'s
|
||||
|
||||
@@ -6,8 +6,8 @@
|
||||
//! anything walking the table. `luaL_getn` (`0x6f5050`) reads `t.n`, then `sizes[t]`, then counts
|
||||
//! `rawgeti` from 1 until the first nil.
|
||||
//!
|
||||
//! **And the size is not private to `getn`/`setn` — the whole table library rides it.** wow-re's
|
||||
//! exhaustive rel32 census (`system/ui/scratch/lua-dialect.md` §4) lists every call site:
|
||||
//! **And the size is not private to `getn`/`setn` — the whole table library rides it.** A
|
||||
//! whole-image call-site census lists every one:
|
||||
//! `luaL_getn` is called by `table.getn`, `table.insert`, `table.remove`, `table.concat`,
|
||||
//! `table.sort`, `table.foreachi` and **base `unpack`**; `luaL_setn` by `table.setn`,
|
||||
//! `table.insert` and `table.remove`. So `insert`/`remove` **update** the remembered size and the
|
||||
@@ -429,7 +429,7 @@ mod tests {
|
||||
}
|
||||
|
||||
/// The four readers take the remembered size, not the border: `concat`, `sort`, `foreachi`,
|
||||
/// and base `unpack` (wow-re's call-site census, this module's header).
|
||||
/// and base `unpack` (the call-site census in this module's header).
|
||||
#[test]
|
||||
fn concat_sort_foreachi_and_unpack_all_read_the_remembered_size() {
|
||||
let s = UiScript::new().unwrap();
|
||||
|
||||
@@ -23,10 +23,10 @@
|
||||
//! ## What the engine does NOT know
|
||||
//!
|
||||
//! A macro's **bound spell** — the spell whose cooldown/usability/range a macro action-bar slot
|
||||
//! reports (byte-verified: `0x4e5a50`'s macro arm returns `[rec+0x564]`, wow-re
|
||||
//! `action-spell-icon-apis.md` §2) — is the app's derivation, because resolving a name to a spell
|
||||
//! id needs the catalog and the player's book. The engine stores the body; `benilla::ui_macro`
|
||||
//! parses it. Same split as everywhere else in this crate: data and layout here, game knowledge
|
||||
//! reports (`0x4e5a50`'s macro arm returns `[rec+0x564]`) — is the app's derivation, because
|
||||
//! resolving a name to a spell id needs the catalog and the player's book. The engine stores the
|
||||
//! body; `benilla::ui_macro` parses it. Same split as everywhere else in this crate: data and
|
||||
//! layout here, game knowledge
|
||||
//! there.
|
||||
|
||||
use mlua::{Lua, MultiValue, Value};
|
||||
@@ -297,8 +297,8 @@ fn delete_macro(model: &mut Model, index: usize) -> bool {
|
||||
|
||||
/// `PickupMacro(index)` — the macro button's `OnDragStart` and the selected-macro button's
|
||||
/// `OnClick` (both in the shipped `Blizzard_MacroUI.xml`). Loads the cursor with the macro
|
||||
/// payload (the client's mode **8**, `[0xb4e2fc]` — wow-re `cursor-dragdrop-payload.md` §1), which
|
||||
/// `PlaceAction` then packs as `macroId | 0x40000000` (`action-item-slot.md`'s payload table).
|
||||
/// payload (the client's mode **8**, `[0xb4e2fc]`), which `PlaceAction 0x4e62e0` then packs as
|
||||
/// `macroId | 0x40000000`.
|
||||
///
|
||||
/// Refuses while the cursor already holds something, matching `PickupSpell`'s precedent: a macro
|
||||
/// button is a SOURCE, never a fit-checked drop target, so silently discarding the held payload
|
||||
|
||||
@@ -107,8 +107,8 @@ pub struct MailInboxRow {
|
||||
/// One auction mail's invoice — what `GetInboxInvoiceInfo(index)` hands back, already parsed.
|
||||
///
|
||||
/// The invoice is **TEXT**, not wire data: the auction house writes the numbers into the mail's
|
||||
/// subject and body and the client `sscanf`s them back out (wow-re `ui/scratch/auction-house.md`
|
||||
/// §11.1a). The app owns that parse; this is its result.
|
||||
/// subject and body and the client `sscanf`s them back out (`GetInboxInvoiceInfo 0x4af360`).
|
||||
/// The app owns that parse; this is its result.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct MailInvoice {
|
||||
/// `true` → the literal token `"seller"` (your auction sold), `false` → `"buyer"` (you won).
|
||||
@@ -246,8 +246,7 @@ impl super::UiScript {
|
||||
/// **`0x4acdc0(1)` — the client's compose-tab reset**, whole. Zeroes the send tab's attachment
|
||||
/// (`0xb6ef90/94`), money (`0xb6efa4`) and COD (`0xb6efa8`) globals and then **tail-fires its
|
||||
/// three events**, in this order: `SEND_MAIL_MONEY_CHANGED`, `SEND_MAIL_COD_CHANGED`,
|
||||
/// `MAIL_SEND_SUCCESS` (`@0x4ace14/1e/28` — wow-re `system/ui/scratch/mail-interaction.md`
|
||||
/// §1/§4).
|
||||
/// `MAIL_SEND_SUCCESS` (`@0x4ace14/1e/28`).
|
||||
///
|
||||
/// **The fire belongs to the reset, and that is the whole point of this shape.** Both call
|
||||
/// sites used to fire `MAIL_SEND_SUCCESS` themselves, and the send-result one fired it
|
||||
@@ -258,7 +257,7 @@ impl super::UiScript {
|
||||
/// left its subject and its icon sitting in the form (director's report, decision 2145).
|
||||
///
|
||||
/// `MAIL_SEND_SUCCESS` is **overloaded** — it means "the compose form is now clean", not "a
|
||||
/// mail was sent" (the anomaly wow-re verified twice: opening a mailbox fires it too).
|
||||
/// mail was sent" — opening a mailbox fires it too.
|
||||
pub fn reset_compose_tab(&mut self) {
|
||||
{
|
||||
let mut model = self.model_mut();
|
||||
@@ -423,9 +422,9 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
)?;
|
||||
|
||||
// GetInboxInvoiceInfo(index) → invoiceType, itemName, playerName, bid, buyout, deposit,
|
||||
// consignment (MailFrame.lua l.302). **SEVEN values, always** — the reference's own
|
||||
// `mov eax,7`, with the miss tail `nil, nil, nil, 0, 0, 0, 0`: three nils then four zeros
|
||||
// (wow-re `ui/scratch/auction-house.md` §11.1a). MailFrame.lua guards on the third
|
||||
// consignment (MailFrame.lua l.302). **SEVEN values, always** — the reference's own `mov eax,7`
|
||||
// (`0x4af5a1`), with the miss tail `nil, nil, nil, 0, 0, 0, 0` (`0x4af559`): three nils then
|
||||
// four zeros. MailFrame.lua guards on the third
|
||||
// (`if playerName then`), so the shape of the miss is what keeps the invoice pane hidden —
|
||||
// returning one bare nil instead would leave the four numeric destructures nil and the pane's
|
||||
// arithmetic would run on them.
|
||||
@@ -478,8 +477,8 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
(row, usable)
|
||||
};
|
||||
// **Five values on every path**, and the empty leg is `(nil, nil, 0, 0, nil)` —
|
||||
// `GetInboxItem 0x4af5d0`, the sibling of the `GetSendMailItem` block above (wow-re
|
||||
// `mail-interaction.md` §5.1; decision 2129). This answered ONE value, which a caller
|
||||
// `GetInboxItem 0x4af5d0`, the sibling of the `GetSendMailItem` block above (decision
|
||||
// 2129). This answered ONE value, which a caller
|
||||
// destructuring five reads as four nils — and the reference's own row
|
||||
// (`(nil,nil,number,number,nil) | …`) has no one-value alternative at all.
|
||||
let Some(r) = row.filter(|r| r.item_id != 0) else {
|
||||
@@ -680,9 +679,9 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
//
|
||||
// **The empty leg is `(nil, nil, 0, 0)`** — four values, and slots 3 and 4 are NUMBERS. Read at
|
||||
// `0x4ae590`: `0x4ae6d3`/`0x4ae6da` push nil, then two `push 0; push 0; lua_pushnumber` pairs
|
||||
// at `0x4ae6df`/`0x4ae6ea`, `eax = 4` (wow-re `mail-interaction.md` §5.1, §5-cross-checked;
|
||||
// decision 2129). All three of the reference's failure guards share that one block, so "nothing
|
||||
// attached" and "the item template has not loaded yet" are indistinguishable to a script.
|
||||
// at `0x4ae6df`/`0x4ae6ea`, `eax = 4` (decision 2129). All three of the reference's failure
|
||||
// guards share that one block, so "nothing attached" and "the item template has not loaded
|
||||
// yet" are indistinguishable to a script.
|
||||
//
|
||||
// The `1` this used to push in slot 3 was borrowed from the wrong binding: it is
|
||||
// `GetAuctionSellItemInfo 0x4ce590`'s empty leg (`1.0`/`-1.0`), not this one's.
|
||||
@@ -750,7 +749,7 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
})?,
|
||||
)?;
|
||||
|
||||
// ── The stationery family (wow-re `ui/scratch/stationery-bindings.md`, 1970) ──
|
||||
// ── The stationery family (1970) ──
|
||||
// The list is the app's (`Stationery.dbc` × the player's bags × the template cache); the
|
||||
// client rebuilds it from `GetNumStationeries` (`0x4ae202` → `0x4ad970`), on world enter and
|
||||
// on the last item-query answer — the app's per-frame recompute covers all three moments.
|
||||
@@ -1296,7 +1295,8 @@ mod stationery_tests {
|
||||
s
|
||||
}
|
||||
|
||||
/// The four verbs, shape by shape (wow-re `stationery-bindings.md`): the count; the info
|
||||
/// The four verbs, shape by shape (`GetNumStationeries 0x4ae1f0`, `GetStationeryInfo 0x4ae230`,
|
||||
/// `SelectStationery 0x4ae380`, `GetSelectedStationeryTexture 0x4ae3f0`): the count; the info
|
||||
/// triple with a nil cost for a carried paper and three nils past the end; a numeric string
|
||||
/// passes the number gate and a non-number raises the Usage; the selection stores the DBC id,
|
||||
/// out of range deselects, and the texture is the bare basename or nil.
|
||||
|
||||
@@ -342,9 +342,9 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
})?,
|
||||
)?;
|
||||
|
||||
// PickupMerchantItem(index) — **two verbs behind one name** (`0x4fb760`, wow-re
|
||||
// `system/ui/scratch/merchant-cursor-law.md`). `0x4fb787` calls `GetCursorItem 0x494c60`,
|
||||
// which answers for **mode 1 only**, and the result forks the whole function:
|
||||
// PickupMerchantItem(index) — **two verbs behind one name** (`0x4fb760`). `0x4fb787` calls
|
||||
// `GetCursorItem 0x494c60`, which answers for **mode 1 only**, and the result forks the whole
|
||||
// function:
|
||||
//
|
||||
// * cursor holds a real bag item → **SELL it** (`CMSG_SELL_ITEM 0x1a0`), then clear the
|
||||
// cursor. The index is IGNORED on this arm and there is no merchant-open gate on it. This
|
||||
@@ -384,7 +384,7 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
// From here every leg clears the cursor and answers nothing. Holding a mode-5 payload
|
||||
// and calling this again is the toggle-off, which is the same clear. It is the REAL
|
||||
// `ClearCursor(1,1)` (`0x4fb82d`/`0x4fb83f` on a refusal, `0x49510b` inside the grab
|
||||
// setter — wow-re `merchant-cursor-law.md` §4/§5), not a bare payload drop: a spell
|
||||
// setter), not a bare payload drop: a spell
|
||||
// or action dropped on the vendor window fires `CURSOR_UPDATE` and hides the bar's
|
||||
// grid, and an armed gift wrap is cancelled (`0x5edf10`, `ClearCursor`'s opening act).
|
||||
let held = match &model.cursor {
|
||||
@@ -451,7 +451,7 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
)?;
|
||||
|
||||
// GetBuybackItemInfo(index) → name, texture, price, quantity, numAvailable, isUsable (the
|
||||
// same 6-tuple; 0x4fb310, wow-re ui/scratch/buyback-data-path.md). `index` is 1-based
|
||||
// same 6-tuple; 0x4fb310). `index` is 1-based
|
||||
// oldest-first; an invalid index answers the fixed tuple (nil, nil, 0, 1, 0, 1). isUsable is
|
||||
// the same 0x5ea930 gate over the sold item's template (0x4fb4f7) — yes, even a just-sold
|
||||
// item reds if the seller can't use it (a mule selling a wrong-class drop).
|
||||
@@ -605,8 +605,8 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
)?;
|
||||
|
||||
// ShowMerchantSellCursor(index) (5875 `0x4fbab0`, "buying from vendor") and
|
||||
// ShowBuybackSellCursor(index) (`0x4fbbb0`, "re-buying") — the vendor-item hover cursor
|
||||
// (wow-re cursor-system.md §7). Despite the "Sell" in the names, these arm the BUY cursor with
|
||||
// ShowBuybackSellCursor(index) (`0x4fbbb0`, "re-buying") — the vendor-item hover cursor.
|
||||
// Despite the "Sell" in the names, these arm the BUY cursor with
|
||||
// an affordability gate: player coin vs the row's price → Buy(3) if `coin >= price`, else the
|
||||
// grayed UnableBuy(23). The merchant frame's OnUpdate re-arms this every frame while an item is
|
||||
// hovered (Ctrl-hover swaps to `ShowInspectCursor` instead); OnLeave `ResetCursor`s it. An
|
||||
@@ -638,11 +638,11 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
/// if the player can afford it, UnableBuy(23) otherwise. Shared by `ShowMerchantSellCursor` and
|
||||
/// `ShowBuybackSellCursor` (which differ only in which price list they read). A `None` price (no
|
||||
/// vendor open, or the 1-based index out of range) leaves the cursor untouched — the binary bails
|
||||
/// without a `CursorSetMode` (wow-re cursor-system.md §7).
|
||||
/// without a `CursorSetMode` (`0x4fbab0`/`0x4fbbb0`).
|
||||
fn arm_vendor_cursor(lua: &Lua, price_of: impl FnOnce(&MerchantState) -> Option<u32>) {
|
||||
let mut model = lua.app_data_mut::<Model>().expect("model app_data");
|
||||
// The sell-cursor family bails on `IsTargeting` at its first instruction (wow-re
|
||||
// `item-target-cursor-and-dropitemonunit.md`) — an armed spell keeps its cast cursor.
|
||||
// The sell-cursor family bails on `IsTargeting` at its first instruction (`0x6e48a0`) — an
|
||||
// armed spell keeps its cast cursor.
|
||||
if model.spell_targeting {
|
||||
return;
|
||||
}
|
||||
@@ -1013,7 +1013,7 @@ mod tests {
|
||||
assert_eq!(s.ui_cursor(), Some(UiCursorMode::Buy));
|
||||
}
|
||||
/// `PickupMerchantItem` is TWO verbs, and which one runs is decided by the cursor, never by
|
||||
/// the caller — stock FrameXML never guards it (wow-re `merchant-cursor-law.md`).
|
||||
/// the caller — stock FrameXML never guards it (`0x4fb760`).
|
||||
#[test]
|
||||
fn pickup_merchant_item_grabs_a_row_as_mode_5() {
|
||||
let mut s = UiScript::new().unwrap();
|
||||
@@ -1187,8 +1187,8 @@ mod tests {
|
||||
(n("updates"), n("shows"), n("hides"))
|
||||
}
|
||||
|
||||
/// The grab writes through the real cursor setter (`0x4950f0`, wow-re `merchant-cursor-law.md`
|
||||
/// §5): `CURSOR_UPDATE` at `0x495159`, and — mode 5 not being mode 7 — **no**
|
||||
/// The grab writes through the real cursor setter (`0x4950f0`): `CURSOR_UPDATE` at
|
||||
/// `0x495159`, and — mode 5 not being mode 7 — **no**
|
||||
/// `ACTIONBAR_SHOWGRID`: a vendor row cannot land on the bar, so its empty slots stay dark.
|
||||
#[test]
|
||||
fn a_vendor_grab_fires_cursor_update_but_not_the_bar_grid() {
|
||||
@@ -1207,7 +1207,7 @@ mod tests {
|
||||
}
|
||||
|
||||
/// Every non-sell leg opens with `ClearCursor(1,1)` (`0x4fb82d`/`0x4fb83f` on a refusal,
|
||||
/// `0x49510b` inside the grab — `merchant-cursor-law.md` §4/§5), so a spell held over the
|
||||
/// `0x49510b` inside the grab), so a spell held over the
|
||||
/// vendor window and dropped there — stock `MerchantFrame.xml`'s `OnMouseUp`,
|
||||
/// `PickupMerchantItem(0)` — is a real clear: the bar's grid hides and `CURSOR_UPDATE` fires.
|
||||
#[test]
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
//! The two message-frame widget classes, and the display machinery they share.
|
||||
//!
|
||||
//! 1.12 ships **two** of them, and msgframe-runtime.md is emphatic that they are *siblings, not
|
||||
//! parent and child* (different ctors, different vtables, different Lua type tags, offsets that do
|
||||
//! not transfer):
|
||||
//! 1.12 ships **two** of them, and they are *siblings, not parent and child* (different ctors,
|
||||
//! different vtables, different Lua type tags, offsets that do not transfer):
|
||||
//!
|
||||
//! - [`scrolling`] — `CSimpleMessageScrollFrame` (ctor `0x787670`), the chat window's class: a true
|
||||
//! ring of `maxLines`, a scrollback cursor, `AddMessage(text[,r,g,b[,id]])` with alpha forced
|
||||
@@ -36,9 +35,8 @@ pub(super) use plain::REG_MESSAGEFRAME_METHODS;
|
||||
pub(super) use scrolling::REG_SCROLLINGMESSAGEFRAME_METHODS;
|
||||
|
||||
/// Install both classes' method tables (and the chat input globals the scrolling one carries).
|
||||
/// **`AddMessage`'s text argument, and the three ways it silently swallows the whole call**
|
||||
/// (wow-5875-re `system/ui/scratch/addmessage-text-gate-silent-skip.md`, 4-worker cross-check
|
||||
/// 2026-08-31). `None` means *do nothing at all* — no line, no record, no error.
|
||||
/// **`AddMessage`'s text argument, and the three ways it silently swallows the whole call.**
|
||||
/// `None` means *do nothing at all* — no line, no record, no error.
|
||||
///
|
||||
/// `CSimpleMessageFrame::AddMessage 0x795590` fetches Lua index 2 through `lua_isstring 0x6f3510`
|
||||
/// and then `lua_tostring 0x6f3690`, and **three** results jump to the function's own epilogue at
|
||||
@@ -430,9 +428,9 @@ impl UiScript {
|
||||
/// height. The pitch is the **font height itself** — the client's own line-step law
|
||||
/// (`LayoutLines` 0x5cdc20: step = px(size) + spacing, spacing 0), the same law the app's text
|
||||
/// renderer lays wrapped rows at, so band grid and glyph rows coincide by construction. (The
|
||||
/// msgframe's own relayout `0x788750/0x788c00` is only partially read in wow-re — if the look
|
||||
/// pass ever shows the ref spacing chat lines wider than the font height, that residual is the
|
||||
/// place to pin.)
|
||||
/// msgframe's own relayout `0x788750/0x788c00` is not fully traced — if the look pass ever
|
||||
/// shows the ref spacing chat lines wider than the font height, that residual is the place to
|
||||
/// pin.)
|
||||
///
|
||||
/// **Which edge the stack grows from is the one place the two classes diverge visibly.** A
|
||||
/// ScrollingMessageFrame always stacks bottom-up with `scroll_offset` picking which message
|
||||
@@ -440,8 +438,8 @@ impl UiScript {
|
||||
/// BOTTOM (the ctor default) is the same bottom-up stack, TOP hangs the newest message off the
|
||||
/// frame's top edge with older ones stepping *down* — the shape `UIErrorsFrame.xml:4` asks for,
|
||||
/// and the one every corpus `SetInsertMode` caller asks for. The growth *anchor* per mode is
|
||||
/// wow-re's own named residual (it lives in the unwalked wrap/layout pass), so this is the
|
||||
/// INFERRED half: it is the reading that reproduces both shipped usages, and the place to
|
||||
/// not settled at the bytes, so this is the INFERRED half: it is the reading that reproduces
|
||||
/// both shipped usages, and the place to
|
||||
/// re-pin if a reference A/B ever disagrees.
|
||||
///
|
||||
/// A message that only partially fits at the far edge still draws — clipped to the frame rect,
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
//! The `MessageFrame` method surface — `CSimpleMessageFrame` (ctor `0x785640`), the class
|
||||
//! `UIErrorsFrame` is and the one `CreateFrame("MessageFrame")` makes.
|
||||
//!
|
||||
//! Grounded in wow-re's byte-verified reads (msgframe-runtime.md's §5 pair, re-confirmed at the
|
||||
//! registrar tables in `widget-api-batch-benilla.md` Q4). Three facts decide this whole file, and
|
||||
//! each is one an implementation would otherwise get wrong in a way nothing would catch:
|
||||
//! Three facts decide this whole file, and each is one an implementation would otherwise get wrong
|
||||
//! in a way nothing would catch:
|
||||
//!
|
||||
//! 1. **`AddMessage(text [, r, g, b [, a]])` — the fifth argument is ALPHA and there is no sixth.**
|
||||
//! Three corpus addons pass six (`EasyCopy.lua:12`, `QuestHistory.lua:1678`, `QuestItem.lua:300`,
|
||||
@@ -161,8 +160,8 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
with_mf(lua, &this, |mf| mf.fading_enabled = on)
|
||||
})?,
|
||||
)?;
|
||||
// 1/nil, the reference's predicate shape — `binding-shapes.tsv` has this row as
|
||||
// `(nil) | (number)`, like every other 1.12 predicate (decision 2118).
|
||||
// 1/nil, the reference's predicate shape (`0x795170`): `(nil) | (number)`, like every other
|
||||
// 1.12 predicate (decision 2118).
|
||||
m.set(
|
||||
"GetFading",
|
||||
lua.create_function(|lua, this: Table| {
|
||||
@@ -195,11 +194,12 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
// ── the shared font block ───────────────────────────────────────────────────────────────
|
||||
//
|
||||
// `Set/GetFontObject · Set/GetFont · Set/GetTextColor · Set/GetShadowColor ·
|
||||
// Set/GetShadowOffset` are real entries on this class's table, not a courtesy. wow-re's
|
||||
// registrar carve is explicit about the membership — *"Exposed on: FontString, Font object,
|
||||
// EditBox, MessageFrame, ScrollingMessageFrame, SimpleHTML. NOT on Button"* — and names this
|
||||
// class's own shims calling the shared implementations (`GetShadowColor 0x794810`). All six
|
||||
// carry the block now — `SimpleHTML`'s is its own copy rather than `font_block::install`'s
|
||||
// Set/GetShadowOffset` are real entries on this class's table, not a courtesy. The
|
||||
// registrar map is explicit about the membership — *"Exposed on: FontString, Font object,
|
||||
// EditBox, MessageFrame, ScrollingMessageFrame, SimpleHTML. NOT on Button"* (table
|
||||
// `0x879d00` has none) — and names this class's own shims calling the shared
|
||||
// implementations (`GetShadowColor 0x794810`). All six carry the block now —
|
||||
// `SimpleHTML`'s is its own copy rather than `font_block::install`'s
|
||||
// (`script/simplehtml/mod.rs`), because that class computes its own inter-block step.
|
||||
//
|
||||
// Demand is observed, not counted: `BigWigs/Plugins/Messages.lua:212` is
|
||||
@@ -505,8 +505,8 @@ mod tests {
|
||||
assert_eq!(num_messages(&s, "MF"), 0);
|
||||
}
|
||||
|
||||
/// The ctor defaults, straight off msgframe-runtime.md's shared-defaults section, plus the
|
||||
/// setters' round trip.
|
||||
/// The ctor defaults (`0x81cc2c`/`0x81cc30`: timeVisible, fadeDuration), plus the setters'
|
||||
/// round trip.
|
||||
#[test]
|
||||
fn ctor_defaults_and_the_fade_accessors() {
|
||||
let s = UiScript::new().unwrap();
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
//! The `ScrollingMessageFrame` method surface — the chat window's per-kind widget behavior
|
||||
//! (`CSimpleMessageScrollFrame`, ctor `0x787670`).
|
||||
//!
|
||||
//! Grounded in wow-re's byte-verified runtime model (msgframe-runtime.md, §5 pair): a **true ring**
|
||||
//! of `maxLines` (drop-oldest, `SetMaxLines` destructive), `AddMessage(text[,r,g,b[,id]])` with the
|
||||
//! `trunc(x*255+0.5)` color quantization + forced-opaque alpha, per-line fade snapshots ticked only
|
||||
//! while **AtBottom** (scrolled up freezes every alpha), 1-slot scrollback, and — the other half of
|
||||
//! that fade, `msgframe-fade-rearm-law.md` — **every scroll entry re-arming the displayed lines**,
|
||||
//! which is what brings a faded-out chat back. The heavy lifting
|
||||
//! A **true ring** of `maxLines` (drop-oldest, `SetMaxLines` destructive),
|
||||
//! `AddMessage(text[,r,g,b[,id]])` with the `trunc(x*255+0.5)` color quantization + forced-opaque
|
||||
//! alpha, per-line fade snapshots ticked only while **AtBottom** (scrolled up freezes every alpha),
|
||||
//! 1-slot scrollback, and — the other half of that fade — **every scroll entry re-arming the
|
||||
//! displayed lines** (`0x788b80` direct when the scroll is refused at an end, `0x788af0` via the
|
||||
//! relayout when it moves), which is what brings a faded-out chat back. The heavy lifting
|
||||
//! (the ring, the fade phases, the scroll clamps) lives on [`ScrollingMessageState`]
|
||||
//! (`crate::widget`), unit-tested there; this module is the thin Lua binding over it, plus the
|
||||
//! host-facing [`UiScript::add_chat_message`]/fade advance the app drives.
|
||||
@@ -107,7 +107,7 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
// quantizes round-half-up and forces the line opaque, then the fade drives its alpha.
|
||||
//
|
||||
// **The rgb gate is three separate `lua_isnumber` calls on indices 3/4/5, and the id's index
|
||||
// is LEG-DEPENDENT** (decision 2125; wow-re `login-chat-colour-pipeline.md`): 6 when rgb are
|
||||
// is LEG-DEPENDENT** (decision 2125): 6 when rgb are
|
||||
// present (`0x792b13 mov edx,6`), **3** when they are absent (`0x792b48 mov edx,3`) — which is
|
||||
// what makes the documented `AddMessage(text, id)` shorthand work. So `AceConsole-2.0`'s
|
||||
// `AddMessage(text, nil, nil, nil, nil, 5)` fails the R gate at index 3, takes the absent leg,
|
||||
@@ -214,7 +214,7 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
lua.create_function(|lua, this: Table| with_smf(lua, &this, |smf| smf.at_bottom()))?,
|
||||
)?;
|
||||
|
||||
// SetMaxLines is destructive (msgframe-runtime.md) — the state handles the wipe.
|
||||
// SetMaxLines is destructive (`0x7938a0`) — the state handles the wipe.
|
||||
m.set(
|
||||
"SetMaxLines",
|
||||
lua.create_function(|lua, (this, n): (Table, i64)| {
|
||||
@@ -237,8 +237,8 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
with_smf(lua, &this, |smf| smf.fading_enabled = on)
|
||||
})?,
|
||||
)?;
|
||||
// 1/nil, the reference's predicate shape — `binding-shapes.tsv` has this row as
|
||||
// `(nil) | (number)`, like every other 1.12 predicate (decision 2118).
|
||||
// 1/nil, the reference's predicate shape (`0x793a40`): `(nil) | (number)`, like every other
|
||||
// 1.12 predicate (decision 2118).
|
||||
m.set(
|
||||
"GetFading",
|
||||
lua.create_function(|lua, this: Table| {
|
||||
@@ -248,7 +248,7 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
})?,
|
||||
)?;
|
||||
// The XML attr is `displayDuration`; the Lua accessors call the same field `TimeVisible`
|
||||
// (msgframe-runtime.md).
|
||||
// (`0x788090`).
|
||||
m.set(
|
||||
"SetTimeVisible",
|
||||
lua.create_function(|lua, (this, s): (Table, f32)| {
|
||||
@@ -273,11 +273,12 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
// ── the shared font block ───────────────────────────────────────────────────────────────
|
||||
//
|
||||
// `Set/GetFontObject · Set/GetFont · Set/GetTextColor · Set/GetShadowColor ·
|
||||
// Set/GetShadowOffset` are real entries on this class's table, not a courtesy. wow-re's
|
||||
// registrar carve is explicit about the membership — *"Exposed on: FontString, Font object,
|
||||
// EditBox, MessageFrame, ScrollingMessageFrame, SimpleHTML. NOT on Button"* — and names this
|
||||
// class's own shims calling the shared implementations (`GetShadowColor 0x792240`). All six
|
||||
// carry the block now — `SimpleHTML`'s is its own copy rather than `font_block::install`'s
|
||||
// Set/GetShadowOffset` are real entries on this class's table, not a courtesy. The
|
||||
// registrar map is explicit about the membership — *"Exposed on: FontString, Font object,
|
||||
// EditBox, MessageFrame, ScrollingMessageFrame, SimpleHTML. NOT on Button"* (table
|
||||
// `0x879d00` has none) — and names this class's own shims calling the shared
|
||||
// implementations (`GetShadowColor 0x792240`). All six carry the block now —
|
||||
// `SimpleHTML`'s is its own copy rather than `font_block::install`'s
|
||||
// (`script/simplehtml/mod.rs`), because that class computes its own inter-block step.
|
||||
//
|
||||
// Demand is observed, not counted: `BigWigs/Plugins/Messages.lua:212` is
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
//! The `Minimap` method surface — the zoom API the FrameXML zoom buttons drive
|
||||
//! (`MinimapZoomIn`/`MinimapZoomOut` → `Minimap:SetZoom(Minimap:GetZoom() ± 1)`).
|
||||
//!
|
||||
//! Grounded in wow-re's byte-verified minimap node: `get_zoom_levels` (`0x6da9a0`) returns the
|
||||
//! constant 6, `set_zoom` clamps at 5 and marks the tile grid dirty, and the zoom index feeds the
|
||||
//! `zoom_to_scale` tables (`0x6da9b0`). The engine core carries only the index
|
||||
//! `get_zoom_levels` (`0x6da9a0`) returns the constant 6, `set_zoom` clamps at 5 and marks the tile
|
||||
//! grid dirty, and the zoom index feeds the zoom-to-scale tables (`0x6da9b0`). The engine core
|
||||
//! carries only the index
|
||||
//! ([`MinimapState`]); the app renderer maps it to a world radius and draws the tiles
|
||||
//! (decision 0203). The two model attrs (`minimapArrowModel=`/`minimapPlayerModel=`) are modeled
|
||||
//! only as far as the Lua surface can see them: [`apply_model_attrs`] names the nine engine
|
||||
@@ -182,7 +182,7 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
// the value MOVE, which is the whole point: the ping is pinned to the world, not to the
|
||||
// map.
|
||||
//
|
||||
// **Two numbers, always** (`0x4eefd0`, wow-re `minimap-ping-law.md`): the reference
|
||||
// **Two numbers, always** (`0x4eefd0`): the reference
|
||||
// recomputes from statics nothing ever clears, and the stock `Minimap_OnUpdate` feeds the
|
||||
// answer straight into `x * Minimap:GetWidth()` for the whole of its 5 s timer — a nil
|
||||
// here is a Lua error on every frame of that window (1974).
|
||||
|
||||
@@ -6,11 +6,11 @@
|
||||
//!
|
||||
//! ## Ground truth
|
||||
//!
|
||||
//! - **RF-0023** (`wow-5875-re/system/ui/ui.md` §"The Lua frame object model"): a frame's Lua value
|
||||
//! - **The frame object model** (`0x701bd0`/`0x7020b0`): a frame's Lua value
|
||||
//! is a table `T` with `T[0] = lightuserdata(handle)`, one *shared* metatable
|
||||
//! `__framescript_meta` whose `__index` dispatches by method name; named frames auto-publish to
|
||||
//! `_G` non-overwriting. See [`object`].
|
||||
//! - **RF-0025** (`ui.md` §"FrameScript handler-firing context"): a handler is fired via `pcall`
|
||||
//! - **The handler-firing context** (`0x704d50`): a handler is fired via `pcall`
|
||||
//! with `this`/`event`/`arg1..argN` set as **globals**, saved-then-restored around the call
|
||||
//! (nesting-safe). We also pass the *modern* `(self, event, ...)` arguments the Era addons expect
|
||||
//! (the transition-era client did both). See [`event`].
|
||||
@@ -29,12 +29,13 @@
|
||||
//!
|
||||
//! ## Identity: ids, not handle bits
|
||||
//!
|
||||
//! RF-0023 stores the `CScriptObject*` in the lightuserdata. Benilla's [`crate::widget::FrameHandle`]
|
||||
//! is an opaque generational handle with private fields (no bit accessor), so we mint a stable `u32`
|
||||
//! **id** per handle and store *that* in the lightuserdata (`id as *mut c_void`); [`Model`] owns the
|
||||
//! `id ↔ handle` bijection. The id doubles as the [`crate::layout::Handle`] used by the layout graph
|
||||
//! (frames only; the screen root is the reserved id [`SCREEN`]). This is faithful to RF-0023's intent
|
||||
//! (an opaque identity in a lightuserdata) — only the *encoding* differs.
|
||||
//! `0x701bd0` stores the `CScriptObject*` in the lightuserdata. Benilla's
|
||||
//! [`crate::widget::FrameHandle`] is an opaque generational handle with private fields (no bit
|
||||
//! accessor), so we mint a stable `u32` **id** per handle and store *that* in the lightuserdata
|
||||
//! (`id as *mut c_void`); [`Model`] owns the `id ↔ handle` bijection. The id doubles as the
|
||||
//! [`crate::layout::Handle`] used by the layout graph (frames only; the screen root is the reserved
|
||||
//! id [`SCREEN`]). This is faithful to the reference's intent (an opaque identity in a
|
||||
//! lightuserdata) — only the *encoding* differs.
|
||||
|
||||
mod action;
|
||||
mod action_bar_toggles;
|
||||
@@ -322,14 +323,14 @@ const REG_FONTSTRING_META: &str = "__benilla_fontstring_meta";
|
||||
const REG_DEFAULT_ERRORHANDLER: &str = "__benilla_default_errorhandler";
|
||||
|
||||
/// The **Region method map** (`0xcf54b4`) — the 19 names every region leaf reaches through its own
|
||||
/// lookup's fallback, carved in wow-re `system/ui/scratch/font-object-lua-surface.md` and asserted
|
||||
/// as a SET by `tests::reference_surface`. Named here because two things need the same list: that
|
||||
/// test, and the title region's narrower method table (1250 §5).
|
||||
/// Names on **both** region leaves — and each leaf registers its own copy, so these are NOT on the
|
||||
/// Region map and must not be hoisted into it (wow-re
|
||||
/// `system/ui/scratch/texture-fontstring-method-split.md`, stated there as a trap in as many words).
|
||||
/// lookup's fallback, fixed and asserted as a SET by `tests::reference_surface`. Named here because
|
||||
/// two things need the same list: that test, and the title region's narrower method table (1250
|
||||
/// §5).
|
||||
/// Names on **both** region leaves — and each leaf registers its own copy (Texture's own
|
||||
/// `SetAlpha` `0x79b580`, FontString's `0x79cb70`), so these are NOT on the Region map and must
|
||||
/// not be hoisted into it.
|
||||
/// `GetDrawLayer` is the setter's pair on both leaves (Texture `0x79a6c0`, FontString `0x79c660`)
|
||||
/// and is here now — it was carved as absent, which was true and was a gap, not a decision; pfUI's
|
||||
/// and is here now — it was found absent, which was true and was a gap, not a decision; pfUI's
|
||||
/// `GetNoNameObject` reads it off every child of a frame it reskins.
|
||||
pub(crate) const REGION_LEAF_SHARED: [&str; 9] = [
|
||||
"SetDrawLayer",
|
||||
@@ -344,7 +345,7 @@ pub(crate) const REGION_LEAF_SHARED: [&str; 9] = [
|
||||
];
|
||||
|
||||
/// **Texture-only.** Note `GetVertexColor` sits here while `SetVertexColor` is shared — an asymmetry
|
||||
/// no reasonable partition invents, and the carve calls it out. Also note the client's Texture map
|
||||
/// no reasonable partition invents, and the bytes call it out. Also note the client's Texture map
|
||||
/// has `SetGradientAlpha` where FontString has `SetAlphaGradient`: a near-miss pair, and we install
|
||||
/// only the FontString one.
|
||||
///
|
||||
@@ -441,17 +442,18 @@ pub const SCREEN: crate::layout::Handle = 0;
|
||||
/// six mouse handlers are driven by the hit-testing API in [`pointer`] ([`UiScript::mouse_move`] /
|
||||
/// [`mouse_button`](UiScript::mouse_button) / [`mouse_wheel`](UiScript::mouse_wheel)) — the app-side
|
||||
/// event feed (net/window → these calls) is the Bevy side's job (decision 0068). `OnValueChanged`
|
||||
/// is shared by the StatusBar (RF-28 `+0x32c`) and the Slider (`+0x330`; decision 0250) — one name,
|
||||
/// is shared by the StatusBar (`+0x32c`) and the Slider (`+0x330`; decision 0250) — one name,
|
||||
/// each kind dispatching to its own value-changed slot. The eight
|
||||
/// `On*Pressed`/text/focus slots are the EditBox's specialized scripts (RF-0082 §2): a focused EditBox
|
||||
/// fires ONLY these, never generic `OnKeyDown`/`OnChar` (its C++ override replaces those slots).
|
||||
/// `On*Pressed`/text/focus slots are the EditBox's specialized scripts (vtable `0x81c910`): a
|
||||
/// focused EditBox fires ONLY these, never generic `OnKeyDown`/`OnChar` (its C++ override replaces
|
||||
/// those slots).
|
||||
/// `OnHorizontalScroll`/`OnVerticalScroll`/`OnScrollRangeChanged` are the ScrollFrame's own slots
|
||||
/// (decision 0112; the reference's `[+0x32c]`/`[+0x334]`/`[+0x33c]`, script-name map `0x786c40`).
|
||||
/// `OnHorizontalScroll` joined the other two with the horizontal offset pair — it is fired by
|
||||
/// `SetHorizontalScroll`, which is what earns it the row below.
|
||||
/// `OnDragStart`/`OnDragStop`/`OnReceiveDrag` are the drag trio (decision 0216 §3) — driven by
|
||||
/// `RegisterForDrag` + the same mouse path as the six mouse handlers above, not a separate one.
|
||||
/// `OnColorSelect` is the ColorSelect's own slot (RF-28 `+0x338`), fired by its `SetColorRGB`.
|
||||
/// `OnColorSelect` is the ColorSelect's own slot (`+0x338`), fired by its `SetColorRGB`.
|
||||
///
|
||||
/// **Every name here is FIRED by something.** A kind that the engine can accept but never raise is
|
||||
/// strictly worse than the `SetScript: unsupported script` error it replaces — the addon's handler
|
||||
@@ -460,7 +462,7 @@ pub const SCREEN: crate::layout::Handle = 0;
|
||||
/// names the reference has that we do NOT fire stay OUT, with the reason recorded at
|
||||
/// [`crate::script::object::events_regions::set_script`].
|
||||
///
|
||||
/// **This list is FLAT; the reference's set is per widget type** (RF-0028's script-name→slot
|
||||
/// **This list is FLAT; the reference's set is per widget type** (script-name→slot
|
||||
/// resolvers: base map `0x76a0d0` + the type's own additions — a `<Frame>` has no `OnClick`). That
|
||||
/// divergence is deliberate and measured, and the 1751 migration has been shrinking the debt it
|
||||
/// covers. Our own FrameXML used to rely on it in 9 places; seven of them were
|
||||
@@ -518,14 +520,13 @@ const SCRIPT_KINDS: [&str; 39] = [
|
||||
"OnTooltipAddMoney",
|
||||
"OnTooltipCleared",
|
||||
"OnTooltipSetDefaultAnchor",
|
||||
// The ColorSelect's own slot (RF-28 `0x78b4f0` script-map, `+0x338`): `OnColorSelect(r, g, b)`
|
||||
// The ColorSelect's own slot (`0x78b4f0` script-map, `+0x338`): `OnColorSelect(r, g, b)`
|
||||
// — how the colour picker paints its preview swatch, and how TipBuddy's two private
|
||||
// `<ColorSelect>` frames learn a colour changed.
|
||||
"OnColorSelect",
|
||||
// The Button/CheckButton double click (RF-28 script-map `0x778c50`, `+0x4d4`) —
|
||||
// The Button/CheckButton double click (script-map `0x778c50`, `+0x4d4`) —
|
||||
// `OnDoubleClick(self, button)`, fired by [`pointer`]'s release-edge detector *instead of* the
|
||||
// second `OnClick`, 300 ms (wow-re `ui/scratch/button-doubleclick-law.md`, a §5 cross-check
|
||||
// dispatched from this work). **The corpus's
|
||||
// second `OnClick`, 300 ms (`0x77937b`). **The corpus's
|
||||
// single biggest script gap**: 250 call sites across 85 addons, and the *only* thing behind the
|
||||
// harness's entire `SetScript: unsupported script` blocker row — 8 addons, 5 dying at load and
|
||||
// 3 at session start, every one of them a FuBar plugin or a Titan panel button
|
||||
@@ -535,8 +536,8 @@ const SCRIPT_KINDS: [&str; 39] = [
|
||||
// [`crate::layout::size_changed`]'s byte-verified epsilon test — see
|
||||
// [`UiScript::resolve_layout`]. `OnSizeChanged(self, width, height)`.
|
||||
"OnSizeChanged",
|
||||
// The three KEY channels, unblocked by [`keyboard`]'s walk (wow-re
|
||||
// `scratch/frame-key-script-delivery.md`, VERIFIED). They were the standing exception in
|
||||
// The three KEY channels, unblocked by [`keyboard`]'s walk (`0x765f10`). They were the
|
||||
// standing exception in
|
||||
// [`object::events_regions`]'s note — accepted only once something fired them, which is that
|
||||
// module's whole rule. `OnKeyUp` rides in with the other two deliberately: it is *gated* today
|
||||
// (a frame carrying only an OnKeyUp consumes every key-down and runs nothing — the reference's
|
||||
@@ -951,8 +952,8 @@ impl UiScript {
|
||||
|
||||
/// Seed every Minimap widget's two zoom indices from the persisted levels — the client's
|
||||
/// minimap reset path copying each CVar object's parsed int into its live index
|
||||
/// (`[0x86f698] ← [[0xb4b410]+0x28]`, `[0x86f69c] ← [[0xb4d90c]+0x28]`; wow-re
|
||||
/// `wmo-interior-minimap.md`, VERIFIED). Called **once**, when the in-game UI materializes and
|
||||
/// (`[0x86f698] ← [[0xb4b410]+0x28]`, `[0x86f69c] ← [[0xb4d90c]+0x28]`). Called **once**, when
|
||||
/// the in-game UI materializes and
|
||||
/// the widget exists — not per frame: from then on the widget's index is the live truth and
|
||||
/// `Minimap:SetZoom` keeps the CVar following it, so a repeated push would fight the +/- buttons.
|
||||
/// Both indices clamp into `[0, MINIMAP_ZOOM_LEVELS)` exactly like `set_zoom`, so a hand-edited
|
||||
@@ -975,8 +976,8 @@ impl UiScript {
|
||||
|
||||
/// [`UiScript::run`] over a chunk that came off disk, which is **bytes** (decision 1193).
|
||||
///
|
||||
/// The reference slurps the file and hands the buffer to `luaL_loadbuffer` with no conversion
|
||||
/// (wow-5875-re `system/ui/ui.md`), and Lua 5.0 strings are byte strings — so a cp1252 locale
|
||||
/// The reference slurps the file (`0x704bc0`) and hands the buffer to `luaL_loadbuffer`
|
||||
/// (`0x6f5690`) with no conversion, and Lua 5.0 strings are byte strings — so a cp1252 locale
|
||||
/// file runs there and its literals carry the raw bytes. Reading such a file as `String` is
|
||||
/// what made 76 of a real corpus's `.lua` files read as *absent* rather than as text with an
|
||||
/// odd glyph. The two front-door transforms the reference's own compiler applies — the UTF-8
|
||||
@@ -1117,11 +1118,12 @@ impl UiScript {
|
||||
}
|
||||
}
|
||||
|
||||
/// Invoke a compiled handler `func` under the RF-0025 frame-globals convention (`this`/`self` =
|
||||
/// `wrapper`), the same set/restore path registry handlers use. For the [`crate::loader`], which
|
||||
/// holds the `OnLoad` `Function` directly (to fire it bottom-up) rather than through the registry:
|
||||
/// this keeps the convention in one home instead of duplicating it. Errors are returned so the
|
||||
/// caller routes them (the loader records them in its own report).
|
||||
/// Invoke a compiled handler `func` under the `0x704d50` frame-globals convention
|
||||
/// (`this`/`self` = `wrapper`), the same set/restore path registry handlers use. For the
|
||||
/// [`crate::loader`], which holds the `OnLoad` `Function` directly (to fire it bottom-up)
|
||||
/// rather than through the registry: this keeps the convention in one home instead of
|
||||
/// duplicating it. Errors are returned so the caller routes them (the loader records them in
|
||||
/// its own report).
|
||||
/// Whether the frame with this global name is effectively visible — shown, with every ancestor
|
||||
/// shown (`IsVisible()`'s answer, read host-side). `false` for a name no live frame carries.
|
||||
/// The read side of a window for a host that keeps state per window: the dressing-room feed
|
||||
@@ -1323,14 +1325,13 @@ impl UiScript {
|
||||
// list, and the binary says otherwise: `[CButton+0x334]` has exactly three writers
|
||||
// image-wide (the ctor, the fired-double zero, the fired-single stamp) and none of them is
|
||||
// a hide, a disable, or a mouse-leave — so a half-finished double click really does survive
|
||||
// the cursor leaving the window and coming back inside the 300 ms (wow-re
|
||||
// `ui/scratch/button-doubleclick-law.md`, "state hygiene").
|
||||
// the cursor leaving the window and coming back inside the 300 ms.
|
||||
// A thumb drag in progress when the pointer leaves is abandoned too (decision 0250 §5) —
|
||||
// the release that would end it is never fed, same leak as the drag gesture above.
|
||||
model.slider_drag = None;
|
||||
}
|
||||
|
||||
// ── Keyboard entry (RF-0082 §1/§2: the EditBox focus + key/char routing) ─────────────────────
|
||||
// ── Keyboard entry (the EditBox focus + key/char routing law below) ──────────────────────────
|
||||
//
|
||||
// benilla speaks *key names*, not scancodes — the host maps its window keycodes to these. The
|
||||
// routing is the client's exactly: if a box is focused it processes and CONSUMES every event; if
|
||||
@@ -1340,9 +1341,9 @@ impl UiScript {
|
||||
// `autoFocus` box focusing itself on SHOW, which we do not implement yet (the flag's own doc
|
||||
// carries the correction and what it waits on).
|
||||
|
||||
/// A typed character (may be multi-byte UTF-8) arriving from the host. Routes per §1/§2 and, on a
|
||||
/// focused box, inserts it (numeric/cap/password rules apply) or — for the Ctrl+A control code —
|
||||
/// selects all. Returns `true` if consumed (a focused box consumes every char).
|
||||
/// A typed character (may be multi-byte UTF-8) arriving from the host. Routes per that law and,
|
||||
/// on a focused box, inserts it (numeric/cap/password rules apply) or — for the Ctrl+A control
|
||||
/// code — selects all. Returns `true` if consumed (a focused box consumes every char).
|
||||
pub fn char_input(&mut self, text: &str) -> bool {
|
||||
// The frame walk first ([`keyboard`]): the focused box is a PARTICIPANT in it, at its own
|
||||
// strata/level, so this is not "frames before boxes" — it is the reference's one dispatcher
|
||||
@@ -1362,7 +1363,7 @@ impl UiScript {
|
||||
/// A non-character key press arriving from the host, by name — the three *box-event* keys
|
||||
/// (`"ENTER"`, `"ESCAPE"`, `"TAB"`) fire their FrameXML scripts; editing keys arrive as
|
||||
/// semantic [`EditAction`]s via [`Self::editbox_action`] instead (the host's per-OS keymap
|
||||
/// owns which chord means what). Routes per §1/§2; a focused box consumes the key even when
|
||||
/// owns which chord means what). Routes per that law; a focused box consumes the key even when
|
||||
/// it does nothing with it. Returns `true` if consumed.
|
||||
pub fn key_input(&mut self, key: &str) -> bool {
|
||||
// Same two-stage shape as `char_input` — see its note.
|
||||
@@ -1373,7 +1374,7 @@ impl UiScript {
|
||||
/// (BACKSPACE, DELETE, the arrows, HOME, END), offered to the **keyboard frames** first.
|
||||
///
|
||||
/// Returns `true` if a frame consumed it, in which case the caller must NOT also dispatch the
|
||||
/// chord — and the key's binding must not fire either (consumption suppresses it, wow-re §3).
|
||||
/// chord — and the key's binding must not fire either (consumption suppresses it, `0x76b7d0`).
|
||||
/// A `false` means either nothing wanted it or the focused box owns it; the caller proceeds
|
||||
/// exactly as it did before this entry point existed. See [`keyboard::frame_key_input`] for
|
||||
/// why declining at the box is the faithful answer rather than skipping it.
|
||||
@@ -1412,7 +1413,7 @@ impl UiScript {
|
||||
}
|
||||
|
||||
/// Whether an EditBox currently holds keyboard focus (and is effectively visible) — the app gates
|
||||
/// world/player key input on this, matching the client's `DAT_00cf4dc8 != 0` test (RF-0082 §1).
|
||||
/// world/player key input on this, matching the client's `DAT_00cf4dc8 != 0` test.
|
||||
pub fn has_keyboard_focus(&self) -> bool {
|
||||
let model = self.model_ref();
|
||||
model
|
||||
@@ -1559,8 +1560,8 @@ impl UiScript {
|
||||
model.arena.region(h).map(|r| match r.kind {
|
||||
crate::widget::RegionKind::Texture => "Texture",
|
||||
crate::widget::RegionKind::FontString => "FontString",
|
||||
// A title region is a plain Region and says so (Q6) — and it is unreachable by name
|
||||
// anyway: `CreateTitleRegion` takes no name argument at all.
|
||||
// A title region is a plain Region and says so (`0x76c440`) — and it is unreachable by
|
||||
// name anyway: `CreateTitleRegion` takes no name argument at all.
|
||||
crate::widget::RegionKind::Title => "Region",
|
||||
})
|
||||
}
|
||||
@@ -1607,7 +1608,7 @@ impl UiScript {
|
||||
}
|
||||
|
||||
/// Hand every queued script error to the Lua-side error handler — the reference's own shape:
|
||||
/// `seterrorhandler`/`geterrorhandler` are engine globals (wow-re `scratch/lua-dialect.md`,
|
||||
/// `seterrorhandler`/`geterrorhandler` are engine globals (`0x702900`/`0x702950`,
|
||||
/// the captured `_G`), the engine invokes the registered handler on a caught script error
|
||||
/// (that is the pair's contract — a handler nothing invokes would be two dead globals), and
|
||||
/// FrameXML answers with `_ERRORMESSAGE` → the red ScriptErrors dialog (decision 1305).
|
||||
|
||||
@@ -66,8 +66,7 @@ pub(crate) struct Model {
|
||||
pub(crate) addons: Vec<super::addon::AddOnInfo>,
|
||||
/// **The Lua index space** — positions into [`Self::addons`], `## Title`-sorted and
|
||||
/// hidden-filtered (decision 2175). NOT the registry: the reference keeps two structures and
|
||||
/// they are different permutations of different sets (wow-re
|
||||
/// `system/ui/scratch/addon-registry-scan-and-order.md` §7).
|
||||
/// they are different permutations of different sets (`0x51da70` rebuilds this one).
|
||||
pub(crate) addon_index: Vec<usize>,
|
||||
/// The lowercased names `SMSG_ADDON_INFO` marked `status = 2`, or **`None` when no reply has
|
||||
/// arrived this session** — and `None` is why [`Self::addon_index`] can be legitimately empty
|
||||
@@ -87,8 +86,8 @@ pub(crate) struct Model {
|
||||
pub(crate) measurer: Option<Box<dyn super::TextMeasure>>,
|
||||
/// The host's texture-path oracle ([`super::UiScript::set_texture_probe`]): does this sprite
|
||||
/// reference resolve to a file — patch chain or loose addon folder? What lets the path form of
|
||||
/// `SetTexture` return the reference's **1 | nil** load verdict inline (wow-re
|
||||
/// `widget-api-batch-benilla.md` Q1 — Atlas branches on it to pick its map art). `None` in an
|
||||
/// `SetTexture` return the reference's **1 | nil** load verdict inline (`0x79bb40` — Atlas
|
||||
/// branches on it to pick its map art). `None` in an
|
||||
/// engine-less VM (tests, the addon harness), where the path form keeps answering nil: no
|
||||
/// backend, nothing loads — which is also exactly what those tests always saw.
|
||||
pub(crate) texture_probe: Option<TextureProbe>,
|
||||
@@ -99,8 +98,8 @@ pub(crate) struct Model {
|
||||
/// CONTENT, the way the real client's size getters do — `CSimpleTexture::GetWidth 0x770720` /
|
||||
/// `GetHeight 0x770790` return the authored value only when it is not `0.0`, and otherwise read
|
||||
/// the loaded texture's `[tex+0x144]`/`[tex+0x148]` through the same converter `<AbsDimension>`
|
||||
/// uses, so **one texel is one FrameXML unit** (wow-re `region-size-fallback.md` §2, decision
|
||||
/// 1349). The host answers off the same candidate walk the renderer decodes with and memoises,
|
||||
/// uses, so **one texel is one FrameXML unit** (decision 1349). The host answers off the same
|
||||
/// candidate walk the renderer decodes with and memoises,
|
||||
/// so the size layout resolves with is the size the screen shows.
|
||||
///
|
||||
/// `None` in an engine-less VM (tests, the addon harness), where a zero-size texture keeps the
|
||||
@@ -108,8 +107,8 @@ pub(crate) struct Model {
|
||||
pub(crate) texture_size_probe: Option<TextureSizeProbe>,
|
||||
/// The host's font-path oracle ([`super::UiScript::set_font_probe`]): does this font reference
|
||||
/// load — patch chain or loose addon folder? What lets `SetFont`'s path form answer the
|
||||
/// reference's **1 | nil**, whose nil is a *load failure* and not an argument error (wow-re
|
||||
/// `system/ui/ui.md`: `0x79f345`/`0x79f361`, originating at `0x5c1ae0` under the `0x44d040`
|
||||
/// reference's **1 | nil**, whose nil is a *load failure* and not an argument error
|
||||
/// (`0x79f345`/`0x79f361`, originating at `0x5c1ae0` under the `0x44d040`
|
||||
/// font-factory cache) — `!OmniCC/main.lua:41`'s `if not Font:SetFont(saved, size) then
|
||||
/// revert end` is exactly that probe.
|
||||
///
|
||||
@@ -125,7 +124,7 @@ pub(crate) struct Model {
|
||||
pub(crate) addons_saved_account: Option<std::path::PathBuf>,
|
||||
pub(crate) addons_saved_character: Option<std::path::PathBuf>,
|
||||
/// The FrameXML **template registry**, persisted across [`crate::loader::load`] calls — the
|
||||
/// client's template table is global (`0x6ee500`, rf24), so a file may `inherits=` a template
|
||||
/// client's template table is global (`0x6ee500`), so a file may `inherits=` a template
|
||||
/// an *earlier file* registered (the real MerchantFrame.xml inherits
|
||||
/// CharacterFrameTemplates.xml's tab template). Register-before-use in load order, exactly
|
||||
/// the client's rule; a per-document registry silently dropped every cross-file inherit.
|
||||
@@ -321,7 +320,7 @@ pub(crate) struct Model {
|
||||
/// The named `<Font>` objects, **keyed by the ASCII-LOWERCASED name**.
|
||||
///
|
||||
/// 1.12's font registry hashes the name and compares keys with `SStrCmpI` — case-INSENSITIVE
|
||||
/// (`0x783870`/`0x7838c7`, wow-re `system/ui/scratch/font-object-lua-surface.md`: *"Font names
|
||||
/// (`0x783870`/`0x7838c7`: *"Font names
|
||||
/// are matched case-insensitively"*), and a name string handed to `SetFontObject` folds the
|
||||
/// same way. `Recap/RecapOptions.xml:32` inherits `GameFontHighLightSmall` — the shipped font
|
||||
/// is `GameFontHighlightSmall`, one letter's case apart — and on the real client that resolves.
|
||||
@@ -412,7 +411,7 @@ pub(crate) struct Model {
|
||||
pub(crate) all_event_frames: Vec<FrameHandle>,
|
||||
|
||||
/// The EditBox that currently owns keyboard focus — the engine's twin of the client's
|
||||
/// class-owned focus global `DAT_00cf4dc8` (`CSimpleEditBox* E`, 0 = none; RF-0082 §1). A focused
|
||||
/// class-owned focus global `DAT_00cf4dc8` (`CSimpleEditBox* E`, 0 = none). A focused
|
||||
/// box consumes every key/char; `None` lets an `autoFocus` box self-acquire on the first event.
|
||||
/// Gated on effective-visibility at read time (a box hidden while focused stops taking input).
|
||||
pub(crate) focused_editbox: Option<FrameHandle>,
|
||||
@@ -425,8 +424,7 @@ pub(crate) struct Model {
|
||||
/// be the topmost under the cursor. The reference keeps exactly this flag on the frame
|
||||
/// manager (`[root+0x1100]`) and its per-tick pump re-runs the hover walk at the **saved**
|
||||
/// cursor position, explicitly bypassing the didn't-move coalesce — so the newly exposed
|
||||
/// frame gets `OnEnter` with no physical mouse move (wow-re
|
||||
/// `ui/scratch/hover-hide-and-tooltip-owner-law.md`: writers `0x764cbb`/`0x764b8d`, pump
|
||||
/// frame gets `OnEnter` with no physical mouse move (writers `0x764cbb`/`0x764b8d`, pump
|
||||
/// tail `0x7657a1` → `0x7660d0` self-alias). [`UiScript::tick`] drains it the same way.
|
||||
pub(crate) hover_repick: bool,
|
||||
/// Per-button, the frame a mouse-down last captured (`button name → frame`), for the `OnClick`
|
||||
@@ -445,7 +443,7 @@ pub(crate) struct Model {
|
||||
pub(crate) mouse_capture: Option<FrameHandle>,
|
||||
/// Per **frame**, the [`UiScript::now`] second at which its last single `OnClick` fired — the
|
||||
/// double-click detector's entire state, and the faithful stand-in for the client's per-widget
|
||||
/// timestamp `[CButton+0x334]` (wow-re `ui/scratch/button-doubleclick-law.md`).
|
||||
/// timestamp `[CButton+0x334]`.
|
||||
///
|
||||
/// Three properties of that field are load-bearing, and are why this is keyed the way it is.
|
||||
/// It lives on the **widget**, so two frames can never pair with each other. It carries **no
|
||||
@@ -502,8 +500,7 @@ pub(crate) struct Model {
|
||||
///
|
||||
/// Read it through [`Model::unit`], never directly: that is where the fold lives. The field was
|
||||
/// called `units` until the fold landed, and it was renamed precisely so that every existing
|
||||
/// reader had to come through here rather than be trusted to remember (wow-re
|
||||
/// `system/ui/scratch/unit-token-grammar.md`).
|
||||
/// reader had to come through here rather than be trusted to remember.
|
||||
pub(crate) units_by_lower: HashMap<String, UnitState>,
|
||||
/// Per-unit-token aura list, **in display order**, pushed by the app's aura feed each frame and
|
||||
/// read by the `UnitAura` family ([`super::aura`]). The order is the app's decision, not the
|
||||
@@ -583,7 +580,7 @@ pub(crate) struct Model {
|
||||
pub(crate) social_requests: Vec<social::SocialRequest>,
|
||||
/// The client's three LFG slot words (`[0xbc70a0]`) and its comment (`[0xbc6e98]`, a
|
||||
/// 0x80-byte buffer), the BSS `SetLookingForGroup` writes and `GetLookingForGroup` reads
|
||||
/// back — never the server's (wow-re `lfg-set-get-law.md`, 1961). The slots are always zero
|
||||
/// back — never the server's (1961). The slots are always zero
|
||||
/// in the reference: see [`social`].
|
||||
pub(crate) lfg_slots: [u32; 3],
|
||||
pub(crate) lfg_comment: String,
|
||||
@@ -644,8 +641,8 @@ pub(crate) struct Model {
|
||||
/// `GetGuildRecruitmentMode` returns and `SetGuildRecruitmentMode` writes (decision 2115).
|
||||
///
|
||||
/// `0` = STANDARD, `1` = AUTO, and those two words are literally what the per-character chat
|
||||
/// cache stores it as (`OPTION_GUILD_RECRUITMENT_CHANNEL STANDARD|AUTO`; wow-re
|
||||
/// `system/ui/scratch/chat-cache-grammar.md` — the reader maps `STANDARD` to 0 and anything
|
||||
/// cache stores it as (`OPTION_GUILD_RECRUITMENT_CHANNEL STANDARD|AUTO`; `0x49ea70` maps
|
||||
/// `STANDARD` to 0 and anything
|
||||
/// else, `AUTO` included, to 1). It boots at **1**: every one of the 33 `chat-cache.txt` files
|
||||
/// the reference client itself wrote in this repo's install says `AUTO`, on characters that
|
||||
/// never opened the option.
|
||||
@@ -740,7 +737,7 @@ pub(crate) struct Model {
|
||||
pub(crate) worn_display_toggles: Vec<super::worn_display::WornDisplay>,
|
||||
|
||||
/// The server's `PLAYER_FIELD_BYTES` **byte 2** — which of the four extra action bars are on
|
||||
/// ([`action_bar_toggles`], wow-re `system/ui/scratch/action-bar-toggles.md`). `None` until the
|
||||
/// ([`action_bar_toggles`], descriptor `+0x102a`). `None` until the
|
||||
/// app pushes it ([`super::UiScript::set_action_bar_toggles`]), which is the ONLY thing that
|
||||
/// moves it: the real client never writes this cell, so `SetActionBarToggles` deliberately
|
||||
/// leaves it alone and the value lags the setter by a round trip. That is the opposite
|
||||
@@ -906,8 +903,8 @@ pub(crate) struct Model {
|
||||
/// spell rather than the pet bar's slot ([`Self::pet_autocast_toggles`] is the bar's).
|
||||
pub(crate) pet_spell_autocasts: Vec<u32>,
|
||||
/// Whether the app's own cast lifecycle holds something `SpellStopCasting()` can stop — a
|
||||
/// running auto-repeat or an in-flight cast; a channel is NOT stoppable there (wow-re
|
||||
/// `esc-stopcasting.md`; pushed each frame by the app's cast feed,
|
||||
/// running auto-repeat or an in-flight cast; a channel is NOT stoppable there (`0x6e6e80`;
|
||||
/// pushed each frame by the app's cast feed,
|
||||
/// [`super::UiScript::set_casting`]). The 1/nil return is load-bearing:
|
||||
/// `ToggleGameMenu`'s ESC chain (`UIParent.lua:1489`) only falls through to
|
||||
/// `CloseAllWindows()` on nil.
|
||||
@@ -950,7 +947,7 @@ pub(crate) struct Model {
|
||||
/// from OnUpdate and hides itself when it goes false; the binder question's twin, and the same
|
||||
/// range gate stands behind both (decision 1580).
|
||||
pub(crate) talent_master_pending: bool,
|
||||
// ── The dialog engine's verbs (decision 1963; wow-re `staticpopup-dialog-bindings.md`) ──
|
||||
// ── The dialog engine's verbs (decision 1963; `0x48dca0` is the first below) ──
|
||||
/// `ConfirmPetUnlearn()` calls since the app's last drain — the pet trainer's twin of
|
||||
/// [`Self::talent_wipe_confirms`]; the app holds the latched trainer and the money gate.
|
||||
pub(crate) pet_unlearn_confirms: u32,
|
||||
@@ -1035,7 +1032,7 @@ pub(crate) struct Model {
|
||||
pub(crate) pet_orders: Vec<u32>,
|
||||
/// `HasFullControl`'s flag — the reference's `[0xb4b3e4]`, written by
|
||||
/// `SMSG_CLIENT_CONTROL_UPDATE` naming the local player and read as "if zero, refuse" by
|
||||
/// every cast, item and cursor gate (wow-re `control-loss-and-restore.md`). Boot value 1.
|
||||
/// every cast, item and cursor gate. Boot value 1.
|
||||
pub(crate) player_control: bool,
|
||||
/// Pet bar writes queued by the drag ([`cursor::pet`], decision 1010) — **one entry per
|
||||
/// `CMSG_PET_SET_ACTION`**, each holding the one or two `(0-based position, packed word)` pairs
|
||||
@@ -1082,8 +1079,7 @@ pub(crate) struct Model {
|
||||
/// The **pet** payload arm is excluded (decision 1010): `PlaceAction` refuses it, so lighting
|
||||
/// the action bar's empty slots for a payload that cannot land there would be an invitation to
|
||||
/// a no-op. It drives [`Self::pet_grid_shown`] instead. So is the **vendor row** (mode 5): its
|
||||
/// grab setter `0x4950f0` fires `ACTIONBAR_SHOWGRID` for mode 7 alone (wow-re
|
||||
/// `merchant-cursor-law.md` §5).
|
||||
/// grab setter `0x4950f0` fires `ACTIONBAR_SHOWGRID` for mode 7 alone.
|
||||
pub(crate) cursor_grid_shown: bool,
|
||||
/// The same mirror for the PET bar's grid — `PET_BAR_SHOWGRID`/`PET_BAR_HIDEGRID`, which the
|
||||
/// reference fires from inside the pet-action pickup builder itself (`0x494f28`) rather than
|
||||
@@ -1269,32 +1265,32 @@ pub(crate) struct Model {
|
||||
/// [`super::UiScript::take_trade_skill_close`] drain.
|
||||
pub(crate) trade_skill_close: bool,
|
||||
/// The recipe groups the player has collapsed, by group key `(ItemClass, ItemSubClass)`
|
||||
/// (wow-re `tradeskill` TU-B) — a group's recipes hide, its header stays (the
|
||||
/// (`0x55ba30` resolves the key) — a group's recipes hide, its header stays (the
|
||||
/// trainer/skills-pane precedent, [`Model::trainer_collapsed`]/[`Model::skills_collapsed`]).
|
||||
/// Survives a `set_trade_skill` content re-push (pruned to the groups the fresh recipes still
|
||||
/// produce) AND a same-profession close→reopen (wow-re `tradeskill` TU-G §6 — the collapse
|
||||
/// produce) AND a same-profession close→reopen (the collapse
|
||||
/// mask `0x84dd68` round-trips the rebuild by header key); reset on a profession switch
|
||||
/// ([`Model::trade_skill_last_line`]).
|
||||
pub(crate) trade_skill_collapsed: HashSet<(u32, u32)>,
|
||||
/// Group keys the SubClass filter dropdown has hidden (empty = "All Subclasses"). Keyed like
|
||||
/// [`Model::trade_skill_collapsed`] — the real client's per-header shown flag (`header+0xc`)
|
||||
/// survives a rebuild by exactly this `(ItemClass, ItemSubClass)` key match (`0x4fca20`'s
|
||||
/// save→restore loop; the position mask `0x84dd60` is the derived form — wow-re `tradeskill`
|
||||
/// TU-G). Persists across a same-profession close/reopen; reset on a profession switch
|
||||
/// save→restore loop; the position mask `0x84dd60` is the derived form). Persists across a
|
||||
/// same-profession close/reopen; reset on a profession switch
|
||||
/// ([`Model::trade_skill_last_line`]).
|
||||
pub(crate) trade_skill_subclass_hidden: HashSet<(u32, u32)>,
|
||||
/// The InvSlot filter mask (`0x84dd64`, wow-re `tradeskill` TU-G): **bit set = slot shown**,
|
||||
/// The InvSlot filter mask (`0x84dd64`): **bit set = slot shown**,
|
||||
/// all-ones = "All Slots". The real client's build never touches this static — it is NOT
|
||||
/// pruned on a re-push, and an exclusive pick therefore keeps every OTHER bit clear even for
|
||||
/// slots that appear later. Reset (all-ones) only on a profession switch.
|
||||
pub(crate) trade_skill_invslot_mask: u32,
|
||||
/// The skill line the tradeskill state was last built for (`0xbde064`, wow-re TU-G's cache
|
||||
/// The skill line the tradeskill state was last built for (`0xbde064`, the cache
|
||||
/// key): a `set_trade_skill` push for a DIFFERENT line resets the two filters, the collapse
|
||||
/// set, and the selection; the same line (including a close→reopen round trip) keeps them.
|
||||
pub(crate) trade_skill_last_line: u32,
|
||||
/// The selected recipe's SPELL ID (`0xbde044` — the real client stores the selection by spell
|
||||
/// id, not index), shadowing [`Model::trade_skill_selection`]'s flat position so the selection
|
||||
/// survives a same-profession close→reopen (TU-G: untouched on that path) and remaps across a
|
||||
/// survives a same-profession close→reopen (untouched on that path) and remaps across a
|
||||
/// re-push.
|
||||
pub(crate) trade_skill_selected_spell: u32,
|
||||
/// Set by the engine-side mutators the real client answers with a `TRADE_SKILL_UPDATE` event
|
||||
@@ -1385,14 +1381,14 @@ pub(crate) struct Model {
|
||||
pub(crate) mail_stationery: u32,
|
||||
/// `HasNewMail()` — login-scoped (survives the mailbox window closing, unlike [`Self::mail`]
|
||||
/// above): the app's `MSG_QUERY_NEXT_MAIL_TIME`/`SMSG_RECEIVED_MAIL`-fed countdown reduced to
|
||||
/// one flag (decision 0544 P3, wow-re §5 `mail-interaction.md`). The reference minimap icon
|
||||
/// one flag (decision 0544 P3, `0x4afea0`). The reference minimap icon
|
||||
/// (`MiniMapMailFrame`) reads this on `UPDATE_PENDING_MAIL`.
|
||||
pub(crate) has_new_mail: bool,
|
||||
|
||||
/// The open auction house's snapshot the app pushes (`None` = no auctioneer session) and the
|
||||
/// intents the app drains — the auction seam ([`auction`], decision 1511). `auction_selected`
|
||||
/// is engine-local per list ([`auction::LIST`]/`BIDDER`/`OWNER`) and holds the selected
|
||||
/// **auction id**, not a row position (wow-re §5 TU-5): an id follows its row through a
|
||||
/// **auction id**, not a row position (`0x4cfda0`/`0x4cfec0`): an id follows its row through a
|
||||
/// re-sort, where an index would quietly come to mean the auction that took its place. `0` =
|
||||
/// nothing selected. It is engine-local because the reference reads the selection back
|
||||
/// synchronously inside the same handler that sets it, so it cannot be a round trip. `auction_can_query` is the app's 5 s browse
|
||||
@@ -1469,8 +1465,8 @@ pub(crate) struct Model {
|
||||
/// Item ids the renderer asked for that the store lacks — drained by the app
|
||||
/// ([`UiScript::take_item_stat_asks`]), answered via [`UiScript::set_item_template`].
|
||||
pub(crate) item_stat_asks: HashSet<u32>,
|
||||
/// The item-set store (`set id → §22 SET-block view`) + its ask-once misses — the same
|
||||
/// push/ask flow as the templates ([`item_stats`] module doc).
|
||||
/// The item-set store (`set id → SET-block view`, the block `0x854b1c` heads) + its ask-once
|
||||
/// misses — the same push/ask flow as the templates ([`item_stats`] module doc).
|
||||
pub(crate) item_sets: HashMap<u32, super::ItemSetView>,
|
||||
pub(crate) item_set_asks: HashSet<u32>,
|
||||
/// The **random-suffix roll** store (`ItemRandomProperties` id → its resolved view) — pushed
|
||||
@@ -1479,7 +1475,7 @@ pub(crate) struct Model {
|
||||
/// loop to repaint on a late answer), so the ask-once shape the template store uses would
|
||||
/// leave a first click showing an item with no lines. This mirrors the reference exactly: the
|
||||
/// builder resolves its `+0x424` against the loaded DBC store `0xc0dbd4` at draw time, and
|
||||
/// every source supplies only the id (wow-re `tooltip-content-law.md` §1-ENCHANT §E5).
|
||||
/// every source supplies only the id (the ENCHANT family, `0x52c991`).
|
||||
pub(crate) random_properties: HashMap<u32, super::RandomPropertyView>,
|
||||
/// The red-line law's player state (level/class/race/skills — [`item_stats`] module doc).
|
||||
pub(crate) player_req: PlayerReqState,
|
||||
@@ -1575,7 +1571,7 @@ pub(crate) struct Model {
|
||||
pub(crate) in_cinematic: bool,
|
||||
/// Exhaustion.dbc as the rest bindings consume it — rest-state byte → (localized name,
|
||||
/// factor), the table `GetRestState` indexes directly and whose row 1 scales
|
||||
/// `GetXPExhaustion` (wow-re rested-xp-bindings.md; decision 1087). Seeded with the shipped
|
||||
/// `GetXPExhaustion` (`0x48d3f0`; decision 1087). Seeded with the shipped
|
||||
/// 5875 enUS rows so the engine tests and a failed DBC read behave like the shipped client
|
||||
/// (the GlobalStrings-fallback posture); the app overwrites it with the install's real —
|
||||
/// localized — rows at startup ([`UiScript::set_exhaustion_rows`]).
|
||||
@@ -1714,8 +1710,8 @@ pub(crate) struct Model {
|
||||
/// side, x right / y up — the `MINIMAP_PING` event's arg2/arg3 space), republished by the app
|
||||
/// every frame from the stored world point against the player's live position. Behind
|
||||
/// `Minimap:GetPingPosition()`, which answers **two numbers always** — the reference
|
||||
/// recomputes them per call from a pair of statics nothing ever clears (wow-re
|
||||
/// `minimap-ping-law.md`), and the stock `Minimap_OnUpdate` multiplies the answer without a
|
||||
/// recomputes them per call from a pair of statics nothing ever clears (`0x4eefd0`), and the
|
||||
/// stock `Minimap_OnUpdate` multiplies the answer without a
|
||||
/// nil test for the whole of its 5 s timer (1974; 1596's nil answer stood until then). There
|
||||
/// is exactly one ping, so it lives here rather than on each Minimap widget's
|
||||
/// [`KindState`](crate::widget::KindState) — one write, one read, no arena walk.
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
//!
|
||||
//! 1.12 registers **four** model-pane types, and each has its own Lua method table that **never
|
||||
//! repeats its base's entries**; a derived pane reaches its base's verbs through the miss leg of
|
||||
//! `vtable+0x8` (wow-re `ui/scratch/model-pane-method-tables.md`, byte-enumerated 2026-08-30):
|
||||
//! `vtable+0x8` (`__index` meta `0x7020b0`):
|
||||
//!
|
||||
//! ```text
|
||||
//! CSimpleFrame 0x778590
|
||||
@@ -51,22 +51,18 @@
|
||||
//! single pooled string `0x84f22c` is referenced by **two** method-table entries in two different
|
||||
//! tables (`PlayerModel 0x84f1fc[0]` and `GameTooltip 0x854290`). All three are `PlayerModel`'s.
|
||||
//!
|
||||
//! The question a `strings` hit *does* settle, and the reason it was reached for: wow-re's
|
||||
//! registrar-dump tool silently missed six of the 23 widget tables — including this whole family —
|
||||
//! so the "not in wow-re's scan" half of the old header was a tooling defect, not a fidelity fact.
|
||||
//! The enumeration recipe that replaces both is §5.1 of the note above: census `(call|jmp)
|
||||
//! 0x701d80`, read the count from the registering `mov edx`, read the pairs at
|
||||
//! `base + 8*i`, and settle "which table owns method M" by counting image-wide dword references to
|
||||
//! M's name VA.
|
||||
//! Ownership is settled by the enumeration recipe, not a `strings` hit: census `(call|jmp)
|
||||
//! 0x701d80`, read the count from the registering `mov edx`, read the pairs at `base + 8*i`, and
|
||||
//! settle "which table owns method M" by counting image-wide dword references to M's name VA.
|
||||
//!
|
||||
//! ## The clock is the engine's; the pixels are the host's (decision 2007)
|
||||
//!
|
||||
//! A pane's animation state is not "a sequence index the host interprets": the reference widget
|
||||
//! owns a private `CM2Scene` whose clock its own `OnUpdate` advances, arms sequences through
|
||||
//! `0x7121a0` with an anchor the sampler re-reads, fires `OnUpdateModel` at the top of every
|
||||
//! paint and `OnAnimFinished` from the completion callback (wow-re
|
||||
//! `ui/scratch/modelframe-render-law.md` §4). All of that is Lua-observable — the shipped
|
||||
//! cooldown is nothing but those two handlers scrubbing `SetSequenceTime` — so it lives here:
|
||||
//! paint (`0x76d1bc`) and `OnAnimFinished` from the completion callback (`0x76cdc0`). All of that
|
||||
//! is Lua-observable — the shipped cooldown is nothing but those two handlers scrubbing
|
||||
//! `SetSequenceTime` — so it lives here:
|
||||
//! [`ModelState::clock_ms`] / [`ModelState::armed`], the tick's model pass, and the bindings
|
||||
//! below. What the engine does NOT have is the file: which ids it owns, how long each sequence
|
||||
//! runs, whether it loops. Those are the file's **facts** ([`crate::widget::ModelFileFacts`]),
|
||||
@@ -75,7 +71,7 @@
|
||||
//!
|
||||
//! ## What is deliberately NOT here
|
||||
//!
|
||||
//! Nothing of `Model`'s own 23 — the fog near/far/clear set was the last hold-out and 2027 carved
|
||||
//! Nothing of `Model`'s own 23 — the fog near/far/clear set was the last hold-out and 2027 closed
|
||||
//! it (`ClearFog 0x76f540` is `76f5c5 and [edi+0x3a4],-2`, bit 0 and nothing else, so the guess
|
||||
//! that kept it out is gone; see the install below).
|
||||
//!
|
||||
@@ -97,7 +93,7 @@ use crate::widget::{model_key, FrameHandle, KindState, ModelFileFacts, ModelLigh
|
||||
|
||||
impl Model {
|
||||
/// FrameXML units per **layout unit** — `768 · √(a²+1)` for the screen's aspect `a`
|
||||
/// (render law §3; the `G48 = 1/√(a²+1)` root scale against the `768`-tall FrameXML
|
||||
/// (`G48` [`0x832a48`] = `1/√(a²+1)`, the root scale against the `768`-tall FrameXML
|
||||
/// space). `SetPosition`'s space, and the unit a size-less pane's implicit rect is measured
|
||||
/// in. `4/3` before a screen exists.
|
||||
pub(crate) fn layout_unit(&self) -> f32 {
|
||||
@@ -262,7 +258,7 @@ enum SetLight {
|
||||
Set(ModelLight),
|
||||
}
|
||||
|
||||
/// `Model:SetLight` `0x76e1e0`'s argument walk (render law §5.3), over the arguments **after
|
||||
/// `Model:SetLight` `0x76e1e0`'s argument walk, over the arguments **after
|
||||
/// `self`** — so `a[0]` is the reference's Lua index 2.
|
||||
///
|
||||
/// The binding builds a **local** `CGLight` from `0x71b4a0` — type 1, every colour ZERO, which is
|
||||
@@ -416,7 +412,7 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
// with `ms = 0` (`0x76dec0` → `0x76cf50`), `SetSequenceTime` with the caller's `ms`
|
||||
// (`0x76dfc0` → `0x76cf80`). The arm interrupts whatever plays, resolves the id through the
|
||||
// file's `animationLookup`, and bakes the anchor `cursor_lo = sceneClock − trunc(ms)` that
|
||||
// the sampler re-reads every frame (render law §4.2). The id is an `AnimationData` id, not a
|
||||
// the sampler re-reads every frame (`0x71273d`). The id is an `AnimationData` id, not a
|
||||
// file slot; one the file does not own stops the playing track and arms nothing.
|
||||
m.set(
|
||||
"SetSequence",
|
||||
@@ -491,8 +487,8 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
|
||||
// ── The scene: light and fog ────────────────────────────────────────────────────────────
|
||||
//
|
||||
// Typed since decision 2027, off the render law's §5.1-§5.4: the widget's embedded `CGLight`
|
||||
// and the four fog fields, with `SetLight`'s argument walk and both of its traps.
|
||||
// Typed since decision 2027, after the reference: the widget's embedded `CGLight` and the four
|
||||
// fog fields, with `SetLight`'s argument walk (`0x76e1e0`) and both of its traps.
|
||||
m.set(
|
||||
"SetLight",
|
||||
lua.create_function(|lua, args: MultiValue| {
|
||||
@@ -687,7 +683,8 @@ fn playermodel_install(lua: &Lua) -> mlua::Result<()> {
|
||||
"RefreshUnit",
|
||||
lua.create_function(|lua, this: Table| {
|
||||
with_model(lua, &this, |_| ())?;
|
||||
super::dressup::redress_if_dressup(lua, &this) // the same worker as SetUnit's (§1)
|
||||
// the same worker as SetUnit's (`0x505b50`)
|
||||
super::dressup::redress_if_dressup(lua, &this)
|
||||
})?,
|
||||
)?;
|
||||
|
||||
@@ -699,8 +696,8 @@ fn playermodel_install(lua: &Lua) -> mlua::Result<()> {
|
||||
// What `0x505bb0` does BESIDES the yaw write is deliberately not modeled, and is worth naming
|
||||
// because it is real: it picks a turn animation from the sign of the change (`0xc`
|
||||
// ShuffleRight when the current facing is **<** the argument, `0xb` ShuffleLeft when **>**,
|
||||
// `0` Stand on equality or NaN — the mapping wow-re CORRECTED on 2026-08-23 after publishing
|
||||
// it inverted), plays it unless that id is already armed on bone slot 0, and then
|
||||
// `0` Stand on equality or NaN), plays it unless that id is already armed on bone slot 0,
|
||||
// and then
|
||||
// UNCONDITIONALLY sets `[+0x3e8] = 1` and `[+0x3ec] = now_ms + 100` — a 100 ms turn hold that
|
||||
// the per-paint `0x505c50` expires. Every one of those is invisible to Lua (no getter reads
|
||||
// them). The app's `<Model>` renderer (`benilla-app` `ui_models`, 2013/2019/2027) draws its
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
//! **The nameplate widgets** — `CGNamePlateFrame` as the frame-system object it actually is
|
||||
//! (decision 2148; wow-re `system/ui/scratch/nameplate-lua-surface.md`, §5 eight-worker round +
|
||||
//! orchestrator byte arbitration 2026-09-09, benilla dispatch).
|
||||
//! (decision 2148).
|
||||
//!
|
||||
//! benilla drew V-plates for a year as quads pushed straight into the UI pass, which made them
|
||||
//! invisible to Lua: a live run's `WorldFrame:GetChildren()` returned the two named FrameXML
|
||||
@@ -36,9 +35,7 @@
|
||||
//! level 0**. Since the draw walk's layer loop is *outer* and its frame walk *inner* (`0x765920`:
|
||||
//! layer counter `0x76593c`, frame walk `0x7659d0`–`0x7659f8`, `cmp eax,5` at `0x765a63`), the
|
||||
//! whole bar frame drains before any plate layer — which is why the border's bevels cap the
|
||||
//! fill's ends. The director pinned that look off a reference crop while `nameplate-vkey.md` §7
|
||||
//! recorded the opposite order; the §5 round settled it in the director's favour and corrected
|
||||
//! the note.
|
||||
//! fill's ends.
|
||||
//! - **Layers** (`GetDrawLayer 0x79a6c0`, table `0x811a80`): Border ARTWORK, Glow **HIGHLIGHT**,
|
||||
//! Name/Level/Skull OVERLAY (the ctor re-layers Name and Level off ARTWORK at `0x7cb438`/
|
||||
//! `0x7cb4ea`), RaidIcon ARTWORK, BarFill ARTWORK on the bar. **No sub-level exists in 5875**;
|
||||
@@ -150,7 +147,7 @@ pub struct PlateState {
|
||||
/// which is why the pool is keyed by this rather than indexed by the driver's sort order.
|
||||
pub key: u64,
|
||||
/// Where the plate's **TOP-CENTRE** lands, in FrameXML units from the screen's bottom-left —
|
||||
/// the reference's own seat (`nameplate-vkey.md` §8.5: the plate hangs below the point).
|
||||
/// the reference's own seat (`0x509ec0`: the plate hangs below the point).
|
||||
pub top_centre: (f32, f32),
|
||||
/// `UNIT_FIELD_HEALTH` and `UNIT_FIELD_MAXHEALTH`, **raw**: `healthbar:GetValue()` is
|
||||
/// `[bar+0x320]` (`0x78f5d0`), not a fraction, and `GetMinMaxValues()` answers `(0, max)`.
|
||||
@@ -353,7 +350,7 @@ impl UiScript {
|
||||
/// The reference's predicate is `IsTargeting() && TargetingWantsLocation(flag & 0x60) &&
|
||||
/// !0x6e6180()`. The caller supplies the first two verbatim — benilla's `TargetingWants::
|
||||
/// Location` **is** that `& 0x60` mask. The third is another mask predicate over the same
|
||||
/// pending-spell word (`& 0x878e`, per wow-re `item-target-cursor-and-dropitemonunit.md`) whose
|
||||
/// pending-spell word (`& 0x878e`) whose
|
||||
/// meaning is not settled; a word that reaches our targeting cursor with location bits is a
|
||||
/// pure ground target (the resolver binds or refuses a unit word before then), so it is left
|
||||
/// unmodelled rather than guessed at.
|
||||
@@ -515,10 +512,9 @@ impl Plate {
|
||||
.collect();
|
||||
}
|
||||
|
||||
// The blend modes are the ctor's own, one `0x7703f0` call per texture (wow-re
|
||||
// `nameplate-vkey.md` §8.1, byte-arbitrated): everything is BLEND(2) except the **glow,
|
||||
// which is ADD(3)** (`push 3` at `0x7cb36a` → `0x7cb374`). Getting that one wrong is not a
|
||||
// subtlety — see [`is_unpainted_glow`].
|
||||
// The blend modes are the ctor's own, one `0x7703f0` call per texture: everything is
|
||||
// BLEND(2) except the **glow, which is ADD(3)** (`push 3` at `0x7cb36a` → `0x7cb374`).
|
||||
// Getting that one wrong is not a subtlety — see [`is_unpainted_glow`].
|
||||
let border = texture(
|
||||
model,
|
||||
frame,
|
||||
@@ -562,7 +558,7 @@ impl Plate {
|
||||
let fill_data = model.region_data.entry(fill).or_default();
|
||||
fill_data.texture = Some(BAR_FILL_TEXTURE.to_string());
|
||||
// Not overridden by the ctor — the `CSimpleTexture` ctor's own `[+0xd0] = 2` stands
|
||||
// (`0x76fc40`; vkey §8.1). Written explicitly because the default is a FACT here, not an
|
||||
// (`0x76fc40`). Written explicitly because the default is a FACT here, not an
|
||||
// omission.
|
||||
fill_data.blend = BlendMode::Blend;
|
||||
if let Some(KindState::StatusBar(sb)) =
|
||||
@@ -933,10 +929,9 @@ pub(super) fn is_world_seated(model: &Model, frame: FrameHandle) -> bool {
|
||||
/// 20-26 that only reads as a glow when it is *added*. Blitted with straight `BLEND` it is
|
||||
/// `dst·(1−1) + black·1` — **an opaque black rectangle over the whole plate**, which is precisely
|
||||
/// what shipped when the widget port left the mode at the ctor default (director report,
|
||||
/// 2026-09-10; wow-re `nameplate-vkey.md` §8.1 had already named this exact bug and its cause in
|
||||
/// July). So `GetBlendMode()` answers `"ADD"` truthfully, an addon that re-textures the region
|
||||
/// gets its own art painted normally, and only the reference's own glow art on an ADD region is
|
||||
/// the one thing this engine declines to draw.
|
||||
/// 2026-09-10). So `GetBlendMode()` answers `"ADD"` truthfully, an addon that re-textures the
|
||||
/// region gets its own art painted normally, and only the reference's own glow art on an ADD
|
||||
/// region is the one thing this engine declines to draw.
|
||||
pub(super) fn is_unpainted_glow(data: &RegionData) -> bool {
|
||||
data.blend == BlendMode::Add && data.texture.as_deref() == Some(GLOW_TEXTURE)
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
//! `GetNetStats()` — the connection-telemetry read the main bar's performance ("ping") meter polls.
|
||||
//!
|
||||
//! One global over one pushed number. The reference binding (`0x48b8b0`, wow-re
|
||||
//! `system/ui/scratch/bindings.md`) returns `bandwidthIn, bandwidthOut, latency`; its latency is
|
||||
//! **not** the last round trip but the average over the connection's RTT ring — wow-re net W1
|
||||
//! records `HandlePong 0x537d60` as "RTT into ring, head/tail wrap 16" feeding
|
||||
//! `0x537f20 throughput/avg-RTT math (host telemetry; no wire bytes)". The app owns that ring
|
||||
//! One global over one pushed number. The reference binding (`0x48b8b0`) returns `bandwidthIn,
|
||||
//! bandwidthOut, latency`; its latency is **not** the last round trip but the average over the
|
||||
//! connection's RTT ring — `HandlePong 0x537d60` puts the RTT into that ring (head/tail wrap 16),
|
||||
//! feeding `0x537f20`'s throughput/avg-RTT math (host telemetry; no wire bytes). The app owns
|
||||
//! that ring
|
||||
//! ([`crate`]'s consumer keeps it beside its ping clock) and pushes the average here; the engine
|
||||
//! stays free of ECS/net reach (decision 0068 §3), exactly as [`super::unit`]'s `GetMoney` does.
|
||||
//!
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
//! The FrameScript object model, frame side (RF-0023) — `CreateFrame`, the frame metatable +
|
||||
//! The FrameScript object model, frame side — `CreateFrame`, the frame metatable +
|
||||
//! wrapper cache, and the shared frame method surface (decision 0068 v1). The region side lives in
|
||||
//! [`super::region`]; per-kind frame methods in their own modules ([`super::statusbar`], …),
|
||||
//! dispatched by [`kind_method_registries`] before the shared table.
|
||||
//!
|
||||
//! A frame's Lua value is a table `T` with `T[0] = lightuserdata(id)` (the id encoding stands in for
|
||||
//! RF-0023's `CScriptObject*`; see the module docs on ids) and one **shared** metatable whose
|
||||
//! `__index` is a Rust function that dispatches a method name to its implementation. Wrappers are
|
||||
//! cached in a Lua-side registry table keyed by id, so `GetParent()` (etc.) returns the *same* table
|
||||
//! every time (stable identity). Named frames auto-publish to `_G`, non-overwriting (RF-0023). Region
|
||||
//! the client's `CScriptObject*`; `0x701bd0` writes `T[0] = lightuserdata(this)` — see the module
|
||||
//! docs on ids) and one **shared** metatable whose `__index` is a Rust function that dispatches a
|
||||
//! method name to its implementation. Wrappers are cached in a Lua-side registry table keyed by id,
|
||||
//! so `GetParent()` (etc.) returns the *same* table every time (stable identity). Named frames
|
||||
//! auto-publish to `_G`, non-overwriting (`0x701bd0`). Region
|
||||
//! leaves (`Texture`/`FontString`) use the same `T[0]` scheme but a *distinct* metatable (the "tag"
|
||||
//! that separates their method surface from frames').
|
||||
//!
|
||||
@@ -44,7 +45,7 @@ pub(super) fn id_to_lud(id: u32) -> LightUserData {
|
||||
LightUserData(id as usize as *mut c_void)
|
||||
}
|
||||
|
||||
/// Read the id out of a wrapper table's `T[0]` lightuserdata (RF-0023).
|
||||
/// Read the id out of a wrapper table's `T[0]` lightuserdata (`0x701bd0` writes it).
|
||||
pub(crate) fn decode_id(this: &Table) -> mlua::Result<u32> {
|
||||
match this.raw_get::<Value>(0)? {
|
||||
Value::LightUserData(l) => Ok(l.0 as usize as u32),
|
||||
@@ -66,7 +67,7 @@ pub(super) fn frame_handle_of(lua: &Lua, this: &Table) -> mlua::Result<FrameHand
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────────────────────
|
||||
// Wrapper cache (RF-0023: lazy bind + named _G publish, non-overwriting)
|
||||
// Wrapper cache (`0x701bd0`: lazy bind + named _G publish, non-overwriting)
|
||||
// ─────────────────────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
/// Get-or-create the wrapper table for a frame id, caching it and (if the frame is named and `_G`
|
||||
@@ -101,7 +102,7 @@ pub(super) fn frame_wrapper(lua: &Lua, id: u32) -> mlua::Result<Table> {
|
||||
Ok(t)
|
||||
}
|
||||
|
||||
/// Publish a wrapper to `_G[name]`, non-overwriting (RF-0023).
|
||||
/// Publish a wrapper to `_G[name]`, non-overwriting (`0x701bd0`).
|
||||
pub(super) fn publish_global(lua: &Lua, name: &str, wrapper: &Table) -> mlua::Result<()> {
|
||||
let g = lua.globals();
|
||||
if g.get::<Value>(name)?.is_nil() {
|
||||
@@ -122,8 +123,7 @@ pub(super) fn publish_global(lua: &Lua, name: &str, wrapper: &Table) -> mlua::Re
|
||||
/// expands a leading `$parent` (`0x76c5b0`) and then calls `0x76c760`. There is no engine-side
|
||||
/// name→frame map behind either: for frames and regions a name's only effects are the copy into
|
||||
/// `[widget+0x98]` and that `_G` publish. (`CSimpleFont` names are the one exception — they key a
|
||||
/// Storm hash at `0xcf4e78` — and fonts are not anchor targets.) `wow-5875-re`
|
||||
/// `system/ui/scratch/name-string-widget-resolution.md`; `system/ui/ledger.tsv` rows
|
||||
/// Storm hash at `0xcf4e78` — and fonts are not anchor targets.) Full addresses:
|
||||
/// `0x76c5b0`/`0x76c700`/`0x76c760`/`0x7a2540`.
|
||||
///
|
||||
/// So the name is not a *frame name* — it is a **global**, and a frame's published name is only the
|
||||
@@ -194,7 +194,7 @@ pub(crate) fn resolve_named_target(model: &Model, target: &NamedTarget) -> Optio
|
||||
|
||||
/// The name a leading `$parent` expands to: the first widget at or above `start` with a non-empty
|
||||
/// name (`0x76c5b0`'s `+0x9c` walk, skipping an empty `GetName`), and
|
||||
/// [`crate::framexml::DEFAULT_PARENT_NAME`] (`"Top"`) when there is none (rf27 rules 3/5).
|
||||
/// [`crate::framexml::DEFAULT_PARENT_NAME`] (`"Top"`) when there is none (`0x76c5dd`).
|
||||
///
|
||||
/// `start` is the anchoring widget's **parent**: a frame's own anchors say `$parent` of its
|
||||
/// enclosing frame, and a region's say `$parent` of its owner — which is that region's `+0x9c`.
|
||||
@@ -266,7 +266,7 @@ fn enum_token(s: &str) -> String {
|
||||
|
||||
/// The reference's strata NAME table (`0x8119f8`) has eight rows, `BACKGROUND`..`TOOLTIP`; stratum
|
||||
/// 0 (`WORLD`) has no name and no XML or Lua can put a frame there — the WorldFrame's constructor
|
||||
/// is its only writer (decision 1984, wow-re `worldframe-widget.md` §4).
|
||||
/// is its only writer (decision 1984, `0x481aff`).
|
||||
pub(crate) fn strata_from_str(s: &str) -> Option<Strata> {
|
||||
Some(match enum_token(s).as_str() {
|
||||
"BACKGROUND" => Strata::Background,
|
||||
@@ -322,8 +322,8 @@ pub fn frame_kind_from_tag(s: &str) -> Option<FrameKind> {
|
||||
///
|
||||
/// One function because the two doors disagree only in what a MISS does, never in what a miss is
|
||||
/// (decision 2191): the Lua binding raises ([`create_frame`]), the XML loader logs
|
||||
/// `"Unknown frame type: %s"` and skips the node (`crate::loader`), and wow-re's
|
||||
/// `taxiroute-widget-type.md`/`lootbutton-widget-type.md` verify both legs off the same `0x6ee280`.
|
||||
/// `"Unknown frame type: %s"` and skips the node (`crate::loader`) — both legs off the same
|
||||
/// `0x6ee280`.
|
||||
pub(crate) fn registered_frame_kind(lua: &Lua, kind: &str) -> Option<FrameKind> {
|
||||
let frame_kind = frame_kind_from_str(kind)?;
|
||||
let one_shot_spent = frame_kind == FrameKind::WorldFrame
|
||||
@@ -341,8 +341,8 @@ fn frame_kind_from_str(s: &str) -> Option<FrameKind> {
|
||||
// batch, the one row passing `1` as its third argument).
|
||||
"WORLDFRAME" => FrameKind::WorldFrame,
|
||||
// `TaxiRouteFrame` — a registered `CreateFrame` type that is a `CSimpleFrame` and NOTHING
|
||||
// else, so it maps to `Frame` rather than earning a kind (decision 1828; wow-re
|
||||
// `ui/scratch/taxiroute-widget-type.md`). Factory `0x495ba0` allocates `0x314`, the same
|
||||
// else, so it maps to `Frame` rather than earning a kind (decision 1828). Factory
|
||||
// `0x495ba0` allocates `0x314`, the same
|
||||
// size the plain-`<Frame>` factory `0x6eec10` allocates for the same base ctor `0x769090`,
|
||||
// and the ctor `0x506950` adds no field. Its vtables are the base's length exactly (36 + 11
|
||||
// slots), so it declares no new virtual; it overrides four slots, of which two are lifetime
|
||||
@@ -430,10 +430,10 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
lua.set_named_registry_value(REG_KIND_METAS, lua.create_table()?)?;
|
||||
let frame_meta = frame_meta_for(lua, None)?;
|
||||
lua.set_named_registry_value(REG_FRAME_META, frame_meta.clone())?;
|
||||
// RF-0023 publishes the shared metatable as _G["__framescript_meta"].
|
||||
// FrameScript init (`0x7039f7`) publishes the shared metatable as _G["__framescript_meta"].
|
||||
lua.globals().set("__framescript_meta", frame_meta)?;
|
||||
|
||||
// CreateFrame(kind, name?, parent?, inherits?) — a global (RF-0024's factory is the loader's;
|
||||
// CreateFrame(kind, name?, parent?, inherits?) — a global (the loader's factory is `0x6ee280`;
|
||||
// this is the runtime one). `inherits` (4th) applies a registered template to the frame this
|
||||
// call just made, through `crate::loader::apply_template`.
|
||||
let create_frame = lua.create_function(create_frame)?;
|
||||
@@ -457,8 +457,7 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
)?;
|
||||
|
||||
// SetupFullscreenScale(frame) — the registered binding `0x48c270` the stock WorldMapFrame.xml,
|
||||
// CinematicFrame.xml and UIOptionsFrame.xml call from OnShow (decision 1980; wow-re
|
||||
// `system/ui/scratch/setup-fullscreen-scale.md`, VERIFIED at the bytes): the frame's scale
|
||||
// CinematicFrame.xml and UIOptionsFrame.xml call from OnShow (decision 1980): the frame's scale
|
||||
// becomes `min(0.75 · a, 1.0)` for the CONFIGURED aspect `a` — the `gxResolution` width over
|
||||
// height (the `widescreen` CVar's default of 1 selects it; at 0 the aspect is 4:3 and the
|
||||
// scale 1) — and nothing else is written: no anchor, size or position, and `UIParent`'s
|
||||
@@ -585,8 +584,8 @@ fn frame_meta_for(lua: &Lua, kind: Option<FrameKind>) -> mlua::Result<Table> {
|
||||
}
|
||||
|
||||
/// The registry keys of a frame's kind-specific method tables, **in resolution order** — the
|
||||
/// client's own class chain (CheckButton resolves through Button's map, RF-28). Empty for a plain
|
||||
/// kind, and for a wrapper whose frame is not live.
|
||||
/// client's own class chain (CheckButton's lookup `0x79a5d0` tail-calls Button's). Empty for a
|
||||
/// plain kind, and for a wrapper whose frame is not live.
|
||||
///
|
||||
/// Every chain here is *its own table followed by its base's whole chain*, which is what lets
|
||||
/// [`frame_meta_for`] realize it as a metatable chain instead of a search: `CheckButton`'s table
|
||||
@@ -679,8 +678,7 @@ pub(super) fn create_frame(
|
||||
.ok_or_else(|| mlua::Error::runtime(format!("CreateFrame: unknown frame type '{kind}'")))?;
|
||||
// **`name` and `inherits` are `lua_tostring` positions, and a NUMBER is a string to it.**
|
||||
// `0x7060b0` reads both through `0x6f3690` with no type guard at all, so `CreateFrame("Frame",
|
||||
// 5)` names the frame `"5"` — a `Value::String`-only match drops it (wow-re
|
||||
// `ui/scratch/xml-template-name-lookup.md` §5.2).
|
||||
// 5)` names the frame `"5"` — a `Value::String`-only match drops it.
|
||||
//
|
||||
// The fourth argument's quirk, which is real and reads like a bug in the client: `lua_tostring`
|
||||
// runs at `0x70613f` **before** the `cmp 4` gate, and `0x6f7cb1 mov dword ptr [esi],0x4`
|
||||
@@ -696,10 +694,10 @@ pub(super) fn create_frame(
|
||||
//
|
||||
// **The name-string arm is OURS, and the reference RAISES on it** — `0x7060b0` asks
|
||||
// `lua_type` three times and `cmp eax,5`: a table is the parent, nil/absent means no parent,
|
||||
// and a string takes the error leg (`xml-template-name-lookup.md` §5.2's per-position table).
|
||||
// and a string takes the error leg.
|
||||
//
|
||||
// It is **parked, not pruned**, on the same footing as `TEXTURE_ONLY_METHODS`' tail three: the
|
||||
// §5 established *that* it raises but did not quote the message, and inventing the text of a
|
||||
// It is **parked, not pruned**, on the same footing as `TEXTURE_ONLY_METHODS`' tail three: it
|
||||
// is known to raise, but the exact message was never quoted, and inventing the text of a
|
||||
// client error is exactly what decision 1721 was written about — a plausible string in the
|
||||
// voice of a finding. Measured meanwhile: **zero** call sites pass a string parent, in the
|
||||
// 109-folder corpus or in our own FrameXML, so the superset is unreachable in practice and
|
||||
@@ -747,7 +745,7 @@ pub(super) fn create_frame(
|
||||
// addon then runs on a frame with none of the art, regions or scripts it asked for and fails
|
||||
// later, somewhere unrelated.
|
||||
//
|
||||
// Ordering is the carved part, not a detail: the miss branch precedes the synthetic-node build
|
||||
// Ordering is the pinned part, not a detail: the miss branch precedes the synthetic-node build
|
||||
// (`0x706208`) and the `name` store (`0x70622d`), so a miss leaves no partial widget and no
|
||||
// orphan global behind. Doing this after construction would leave both.
|
||||
if let Some(t) = template.as_deref().filter(|t| !t.is_empty()) {
|
||||
@@ -769,8 +767,7 @@ pub(super) fn create_frame(
|
||||
// (`0x706225 push 0x838090 "name"` → `0x70622d` SetAttribute) alongside `parent=` and
|
||||
// `inherits=`, and hands it to the XML frame builder `0x6ee280` with the parent object in
|
||||
// `edx` — so the name reaches `CScriptRegion::SetName 0x76c650` by exactly the route an XML
|
||||
// `name=` does, and `0x76c691` is one of the expander's two call sites (wow-re
|
||||
// `name-string-widget-resolution.md` §5/§6).
|
||||
// `name=` does, and `0x76c691` is one of the expander's two call sites.
|
||||
//
|
||||
// The base is the frame's **parent's** first named ancestor — the same walk `$parent` in a
|
||||
// `relativeTo` takes, one link higher than the anchoring frame's own.
|
||||
|
||||
@@ -2,9 +2,8 @@
|
||||
//!
|
||||
//! The two bindings are `CScriptRegion` methods registered **once**, at `.data 0x87ca38` and
|
||||
//! `0x87ca40` inside the 19-record table `[0x87c9b8, 0x87ca50)` whose registrar `0x7a2e70` supplies
|
||||
//! base and count — so Frame, Texture, FontString and Model all run the *same* function (wow-re
|
||||
//! `system/ui/ui.md` §(b2), `scratch/name-string-widget-resolution.md` §3-4, ledger rows
|
||||
//! `0x7a2540`/`0x7a2940`). This module is that single function's argument half; what each caller
|
||||
//! base and count — so Frame, Texture, FontString and Model all run the *same* function
|
||||
//! (`0x7a2540`/`0x7a2940`). This module is that single function's argument half; what each caller
|
||||
//! keeps for itself is only the thing the reference reads off the receiver — the layout parent's
|
||||
//! default id, and where the anchor is stored.
|
||||
//!
|
||||
|
||||
@@ -180,8 +180,7 @@ pub(super) fn install(lua: &Lua, m: &Table) -> mlua::Result<()> {
|
||||
// (CustomNameplates:437, a four-argument later-client form). Accepted rather than skipped
|
||||
// because dropping it silently is the very class this fixes, and one real site is still a site.
|
||||
m.set(
|
||||
// **The three argument positions are NOT one rule, and the fourth is the odd one.** Verified
|
||||
// together in wow-re `ui/scratch/xml-template-name-lookup.md` §5.2:
|
||||
// **The three argument positions are NOT one rule, and the fourth is the odd one:**
|
||||
//
|
||||
// · `name` and `layer` go through `0x6f3510` (is-number-**or**-string) then `0x6f3690`, and
|
||||
// **the result is never tested** — so a table is simply absent (unnamed / the pre-staged
|
||||
@@ -211,8 +210,8 @@ pub(super) fn install(lua: &Lua, m: &Table) -> mlua::Result<()> {
|
||||
)?;
|
||||
// ── The title region: Frame:CreateTitleRegion() / GetTitleRegion() ──────────────────────────
|
||||
//
|
||||
// wow-re `system/ui/scratch/widget-api-batch-benilla.md` Q6 (`0x773910` / `0x773820`). Four
|
||||
// details, each one a coin-flip a reimplementation loses:
|
||||
// The byte read (`0x773910` / `0x773820`). Four details, each one a coin-flip a
|
||||
// reimplementation loses:
|
||||
//
|
||||
// · **It reads NO argument at all** — nothing in `0x773910`-`0x773a1f` touches Lua index 2. So
|
||||
// `CustomNameplates/options.lua:73`'s `CreateTitleRegion(optionsFrame)` is harmless and
|
||||
@@ -224,10 +223,10 @@ pub(super) fn install(lua: &Lua, m: &Table) -> mlua::Result<()> {
|
||||
// · **A fresh one has NO anchors** and does nothing until `SetPoint`/`SetAllPoints`. Both
|
||||
// corpus consumers immediately call `SetAllPoints`, which is the whole-window drag idiom.
|
||||
// · **`GetTitleRegion` answers 1 value (nil)** when there is none — note the asymmetry with
|
||||
// `GetBackdrop`, which answers 0 values. Q6 flags it explicitly; both converged
|
||||
// independently there, so both are reproduced here.
|
||||
// **ONE NAMED DIVERGENCE, because it is a superset and 1189 is what a superset costs.** Q6 says
|
||||
// the object answers *exactly* the 19 Region methods — no Show/Hide, no scripts, no textures.
|
||||
// `GetBackdrop`, which answers 0 values; both are reproduced here.
|
||||
// **ONE NAMED DIVERGENCE, because it is a superset and 1189 is what a superset costs.** The
|
||||
// object answers *exactly* the 19 Region methods (`0x7a2ea0`, no own table) — no Show/Hide, no
|
||||
// scripts, no textures.
|
||||
// Ours answers the whole shared region table, because Texture/FontString/Title use one
|
||||
// metatable here, so `titleRegion:SetTexture(…)` is accepted where the reference would raise
|
||||
// `attempt to call method`. It is inert rather than wrong — a title region never draws (the
|
||||
@@ -299,7 +298,7 @@ pub(super) fn install(lua: &Lua, m: &Table) -> mlua::Result<()> {
|
||||
// Ignoring it is 1203's class exactly: the addon asks for a font, we accept the call, and the
|
||||
// string comes out in whatever the default is with no failure anywhere.
|
||||
//
|
||||
// The resolution order is carved (`0x773c30`): the FONT-object registry FIRST (`0x773d39`,
|
||||
// The resolution order is fixed (`0x773c30`): the FONT-object registry FIRST (`0x773d39`,
|
||||
// create=0), and only on a font miss the template registry (`0x773d47`). This routes through
|
||||
// the `SetFontObject` binding rather than reaching into the model, which is the same call the
|
||||
// XML `<FontString inherits=>` path makes — one implementation of "apply a font object", not
|
||||
@@ -333,9 +332,8 @@ pub(super) fn install(lua: &Lua, m: &Table) -> mlua::Result<()> {
|
||||
/// the exact call. So the rule for this list is one line long: **a name is accepted only once
|
||||
/// something fires it.**
|
||||
///
|
||||
/// The 1.12 script set is fully carved (wow-re `system/ui/scratch/rf28-typed-widget-loadxml.md`
|
||||
/// l.10-18 for the base map `0x76a0d0`, the per-type sections for each widget's additions;
|
||||
/// `system/ui/ui.md` l.544-556 summarises it), so what is missing here is never a mystery — it is a
|
||||
/// The 1.12 script set is fully mapped (the base table `0x76a0d0`, plus each widget's own
|
||||
/// per-type additions), so what is missing here is never a mystery — it is a
|
||||
/// deliberate not-yet. What the corpus actually asks for, and why each answer is what it is:
|
||||
///
|
||||
/// * **`OnKeyDown` / `OnKeyUp` / `OnChar`** (14 + 1 + 4 corpus sites over 13 addons) — **accepted**
|
||||
@@ -345,7 +343,7 @@ pub(super) fn install(lua: &Lua, m: &Table) -> mlua::Result<()> {
|
||||
/// above let them in the moment something fired them.
|
||||
/// * **`OnCursorChanged`** (4 sites over 3 addons, plus two in the SHIPPED FrameXML — all of them
|
||||
/// the `ScrollingEdit_OnCursorChanged` auto-scroll idiom) — **accepted since decision 2141**,
|
||||
/// which built the edge this entry said was missing. The EditBox's own slot (RF-28 `+0x428`),
|
||||
/// which built the edge this entry said was missing. The EditBox's own slot (`+0x428`),
|
||||
/// fired by the caret flush `0x77da80` with four float caret-position args in UI units;
|
||||
/// `editbox::drain_cursor_changed` is our counterpart, on the tick, gated on the caret having
|
||||
/// actually moved. The stock `MailFrame.xml` and `HelpFrame.xml` declare it, so until 2141 the
|
||||
@@ -440,7 +438,7 @@ fn get_script(lua: &Lua, this: &Table, name: &str) -> mlua::Result<Value> {
|
||||
}
|
||||
}
|
||||
|
||||
/// `CreateTexture`/`CreateFontString`'s third argument — `inheritsFrom` — in the carved order.
|
||||
/// `CreateTexture`/`CreateFontString`'s third argument — `inheritsFrom` — in the fixed order.
|
||||
///
|
||||
/// **Font-object registry first, template registry second** (`0x773d39` then `0x773d47`). That
|
||||
/// order is not cosmetic: `inherits=` is one attribute over two namespaces, and every corpus caller
|
||||
@@ -524,9 +522,8 @@ fn create_region(
|
||||
// 0x773a20` and `CreateFontString 0x773c30` build a synthetic node carrying
|
||||
// `name=<the Lua string>` and read it back through the same `GetAttribute(node, "name")` →
|
||||
// `CScriptRegion::SetName 0x76c650` the XML path uses, and `0x76c691` is one of that
|
||||
// expander's two call sites (wow-re `name-string-widget-resolution.md` §5/§6, whose
|
||||
// `name=` reader census names `0x773ba1` and `0x773dc4` — the addresses *inside* these two
|
||||
// bindings). A region's `$parent` is its OWNER frame, so the walk starts there.
|
||||
// expander's two call sites — `0x773ba1` and `0x773dc4` are the addresses *inside* these
|
||||
// two bindings. A region's `$parent` is its OWNER frame, so the walk starts there.
|
||||
//
|
||||
// `pfQuest/browser.lua:723` is `pfBrowser.input:CreateTexture("$parentSearchIcon",
|
||||
// "OVERLAY")`, and every named widget FonzAppraiser builds is this idiom.
|
||||
|
||||
@@ -85,7 +85,7 @@ pub(super) fn install(lua: &Lua, m: &Table) -> mlua::Result<()> {
|
||||
Ok(())
|
||||
})?,
|
||||
)?;
|
||||
// ── GetObjectType / IsObjectType, frame side (wow-re `widget-type-identity.md` §6) ──────────
|
||||
// ── GetObjectType / IsObjectType, frame side (`0x7a11d0`/`0x7a1290`) ─────────────────────────
|
||||
//
|
||||
// The region side landed first because a FontString found it; `_Nameplates` is why this half
|
||||
// exists too — `_Nameplates.lua:164` tests `Nameplate:GetObjectType() ~= "Button"` and `:191`
|
||||
@@ -125,7 +125,7 @@ pub(super) fn install(lua: &Lua, m: &Table) -> mlua::Result<()> {
|
||||
FrameKind::Model => &["Model", "Frame", "Region"],
|
||||
// 4 deep: `PlayerModel` derives from `Model`, and `DressUpModel` (1969) and
|
||||
// `TabardModel` (1977) derive from it in turn, for the roster's maximum depth of 5
|
||||
// (wow-re `ui/scratch/widget-type-identity.md` §6).
|
||||
// (the reference's full 23-class roster).
|
||||
FrameKind::PlayerModel => &["PlayerModel", "Model", "Frame", "Region"],
|
||||
FrameKind::DressUpModel => &["DressUpModel", "PlayerModel", "Model", "Frame", "Region"],
|
||||
FrameKind::TabardModel => &["TabardModel", "PlayerModel", "Model", "Frame", "Region"],
|
||||
@@ -191,9 +191,8 @@ pub(super) fn install(lua: &Lua, m: &Table) -> mlua::Result<()> {
|
||||
// 1.12 registers the type-identity pair **twice, under two names**. The Region script
|
||||
// (`CScriptRegion`) publishes `GetObjectType`/`IsObjectType` — that is the pair above, and its
|
||||
// `IsObjectType` binding is `0x7a1290`. The *frame* script (`CSimpleFrameScript.cpp`,
|
||||
// `__FILE__` `0x879504`) publishes `GetFrameType 0x773640` and `IsFrameType 0x773700` as well,
|
||||
// and wow-5875-re carved both (`system/ui/scratch/item17-frameapi-batch1.md`, 76-entry
|
||||
// registrar table verified at the bytes). `GetObjectType`/`IsObjectType` are what LATER
|
||||
// `__FILE__` `0x879504`) publishes `GetFrameType 0x773640` and `IsFrameType 0x773700` as well.
|
||||
// `GetObjectType`/`IsObjectType` are what LATER
|
||||
// clients kept; `GetFrameType`/`IsFrameType` are 1.12's own, and we shipped only the first
|
||||
// pair. That is 1189's superset argument inverted — not an extra name we invented, a real one
|
||||
// we were missing — and it cost the world map:
|
||||
@@ -212,12 +211,12 @@ pub(super) fn install(lua: &Lua, m: &Table) -> mlua::Result<()> {
|
||||
// `[eax+0x18]`, the same case-insensitive whole-string compare, and answers the NUMBER 1 or
|
||||
// nil.
|
||||
//
|
||||
// **One edge is INFERRED and named rather than guessed silently:** wow-re's note glosses
|
||||
// `IsFrameType`'s *absent-argument* branch as "pushes the frame's own typename (`call [eax+4]`
|
||||
// GetName)", which is self-contradictory — a typename and a name are different slots — so the
|
||||
// branch is not settled. We take the sibling's behaviour (the `Usage:` raise) pending a byte
|
||||
// read. No caller in the 219-addon corpus, the director's AddOns, or the shipped FrameXML ever
|
||||
// omits the argument: `JIM_toolbox/Config2/Pulse_Config.lua:118` is the corpus's only
|
||||
// **One edge is INFERRED and named rather than guessed silently:** the one description of
|
||||
// `IsFrameType`'s *absent-argument* branch reads "pushes the frame's own typename (`call
|
||||
// [eax+4]` GetName)", which is self-contradictory — a typename and a name are different slots —
|
||||
// so the branch is not settled. We take the sibling's behaviour (the `Usage:` raise) pending a
|
||||
// byte read. No caller in the 219-addon corpus, the director's AddOns, or the shipped FrameXML
|
||||
// ever omits the argument: `JIM_toolbox/Config2/Pulse_Config.lua:118` is the corpus's only
|
||||
// `IsFrameType` site and it passes `"Slider"`.
|
||||
m.set(
|
||||
"GetFrameType",
|
||||
@@ -272,8 +271,8 @@ pub(super) fn install(lua: &Lua, m: &Table) -> mlua::Result<()> {
|
||||
// ── The four structure queries: GetChildren / GetNumChildren / GetRegions / GetNumRegions ──
|
||||
//
|
||||
// Registered bindings in 1.12 (`GetNumRegions 0x773e60`, `GetRegions 0x773f60`,
|
||||
// `GetNumChildren 0x774080`, `GetChildren 0x774180` — wow-re `ui/ledger.tsv`, classified
|
||||
// ORCHESTRATION: each marshals and delegates, with no fidelity math of its own).
|
||||
// `GetNumChildren 0x774080`, `GetChildren 0x774180`): each marshals and delegates, with no
|
||||
// fidelity math of its own).
|
||||
//
|
||||
// **Why these four are worth more than their size.** They are how an addon walks a frame it did
|
||||
// not build, which is the whole basis of the "reskin the default UI" genre — and that genre is
|
||||
@@ -295,23 +294,21 @@ pub(super) fn install(lua: &Lua, m: &Table) -> mlua::Result<()> {
|
||||
// **A detached region is not in the list.** `Region:SetParent(nil)` sets `Region::detached`
|
||||
// rather than removing the entry, because the owner's `Vec` is what `WidgetArena::destroy`
|
||||
// frees from — but the client's re-link virtual `0x77fd10` with a null parent genuinely
|
||||
// **unlinks from the old parent's draw layer and region list** (wow-re
|
||||
// `widget-api-batch-benilla.md` Q7, VERIFIED), so a detached region must be invisible to both
|
||||
// region verbs. Our flag is a representation choice; the list these report is the client's.
|
||||
// **unlinks from the old parent's draw layer and region list**, so a detached region must be
|
||||
// invisible to both region verbs. Our flag is a representation choice; the list these
|
||||
// report is the client's.
|
||||
//
|
||||
// **The count and the list come from ONE function** ([`regions_of`] / [`children_of`]), never
|
||||
// two walks with the same filter written twice. `GetNumRegions` disagreeing with
|
||||
// `#{GetRegions()}` is a bug an addon would hit as an off-by-one deep inside a loop it did not
|
||||
// write, and the only way to make it impossible is to have one of them BE the other.
|
||||
//
|
||||
// **Settled by a wow-re §5 quartet** (`ui/scratch/widget-list-bindings.md`), which confirmed two
|
||||
// of these choices and corrected a third:
|
||||
// **The order and inclusion rules below — one of them a correction:**
|
||||
//
|
||||
// · **The order is link order, oldest first, and no reversal.** `CSimpleFrame` embeds a punned
|
||||
// `TSList` header whose ctor sets `tail = &header`, `head = (&header)|1`, and BOTH linkers
|
||||
// append at the TAIL (`0x76a750` for regions, `0x76aa20` for children — one caller each).
|
||||
// Our insertion-order `Vec` is the same order. (wow-re's own `draw-order-law.md` said
|
||||
// "tail→head"; that was a direction mislabel and was corrected in the same round.)
|
||||
// Our insertion-order `Vec` is the same order.
|
||||
// · **Hidden children and regions are returned and counted** — a VERIFIED negative: the
|
||||
// counting loops never load the visibility word at all.
|
||||
// · **The TITLE REGION IS NOT RETURNED**, and that one corrected us. Both creation paths
|
||||
@@ -327,7 +324,7 @@ pub(super) fn install(lua: &Lua, m: &Table) -> mlua::Result<()> {
|
||||
// empty, never nil.
|
||||
//
|
||||
// The one thing still out of reach is not these bindings': Questie reads
|
||||
// `({Minimap:GetChildren()})[9]` for the player arrow, and the §5 found why that index works —
|
||||
// `({Minimap:GetChildren()})[9]` for the player arrow; here is why that index works —
|
||||
// `CMinimap`'s ctor `0x4edbc0` builds NINE engine-owned `Model` children before the XML
|
||||
// `<Frames>` descent, the ninth (`[Minimap+0x338]`) being the arrow `SetPlayerFacing 0x4eb8e0`
|
||||
// writes. We create none of them, so index 9 is nil here until the minimap grows its engine
|
||||
@@ -364,8 +361,7 @@ pub(super) fn install(lua: &Lua, m: &Table) -> mlua::Result<()> {
|
||||
"GetNumRegions",
|
||||
lua.create_function(|lua, this: Table| Ok(regions_of(lua, &this)?.len()))?,
|
||||
)?;
|
||||
// SetParent — the runtime reparent, per the byte-verified law (wow-re
|
||||
// `ui/scratch/setparent-runtime-strata-level.md`; decision 1323). The binding half
|
||||
// SetParent — the runtime reparent (decision 1323). The binding half
|
||||
// (`0x7a1550`): the parent argument is a frame table, a NAME string (`0x76c760`), or an
|
||||
// explicit nil — an ABSENT argument is NOT the nil path (`0x6f3400` returns −1) and raises
|
||||
// like an unresolvable name; the cycle guard is the binding's own inline ancestor walk and
|
||||
@@ -518,7 +514,7 @@ pub(super) fn install(lua: &Lua, m: &Table) -> mlua::Result<()> {
|
||||
let h = frame_handle_of(lua, &this)?;
|
||||
let lvl = level.clamp(0, i64::from(u16::MAX)) as u16;
|
||||
// **A script level change carries no children** — the binding `0x774560` calls
|
||||
// `set_frame_level 0x76a4f0` with `propagate=0` (wow-re `ui/ui.md`, default levels).
|
||||
// `set_frame_level 0x76a4f0` with `propagate=0`.
|
||||
// Only the toplevel raise shifts a subtree. Stock `BonusActionButtonTemplate` is written
|
||||
// for this: it raises the button +2 and then its cooldown +2 by hand, landing the sweep
|
||||
// one level over the button — carrying the children made it three, over the
|
||||
@@ -586,9 +582,10 @@ pub(super) fn install(lua: &Lua, m: &Table) -> mlua::Result<()> {
|
||||
Ok(model.arena.frame(h).map(|f| f.alpha).unwrap_or(1.0))
|
||||
})?,
|
||||
)?;
|
||||
// Backdrop (backdrop-mechanism.md): SetBackdrop(table|nil) installs (or, with nil, tears down)
|
||||
// the frame's tiled bg + 8-piece border plate. The two color setters tint the bg / all 8 border
|
||||
// pieces (never the reverse — spec §4). The Lua-table SetBackdrop defaults both colors to WHITE
|
||||
// Backdrop (`0x7776e0` → `0x76a5d0`): SetBackdrop(table|nil) installs (or, with nil, tears
|
||||
// down) the frame's tiled bg + 8-piece border plate. The two color setters tint the bg / all 8
|
||||
// border pieces (never the reverse — `0x77f410`/`0x77f440`). The Lua-table SetBackdrop defaults
|
||||
// both colors to WHITE
|
||||
// (the ctor), so a caller must SetBackdropColor after to tint (the tooltip's OnLoad does).
|
||||
m.set(
|
||||
"SetBackdrop",
|
||||
@@ -616,7 +613,7 @@ pub(super) fn install(lua: &Lua, m: &Table) -> mlua::Result<()> {
|
||||
Ok(())
|
||||
})?,
|
||||
)?;
|
||||
// `GetBackdrop()` (`0x777370`; wow-re `ui/scratch/widget-api-batch-benilla.md` Q5) — five things
|
||||
// `GetBackdrop()` (`0x777370`) — five things
|
||||
// a plausible implementation gets wrong, so each is spelled out with its reason:
|
||||
//
|
||||
// 1. It is a **reconstruction from the struct**, never the caller's table. The reference stores
|
||||
@@ -770,8 +767,8 @@ pub(super) fn install(lua: &Lua, m: &Table) -> mlua::Result<()> {
|
||||
))
|
||||
})?,
|
||||
)?;
|
||||
// EnableKeyboard(flag) / IsKeyboardEnabled() — `0x776f90` / `0x776ff0`, real Frame entries in
|
||||
// wow-re's frame-API carve, beside the mouse pair above.
|
||||
// EnableKeyboard(flag) / IsKeyboardEnabled() — `0x776f90` / `0x776ff0`, real Frame entries,
|
||||
// beside the mouse pair above.
|
||||
//
|
||||
// 8 corpus addons call this and were RAISING on it (`ColorPickerPlus:258`,
|
||||
// `Dewdrop-2.0.lua:2021-2022` — which is in ~65 addons — `AckisRecipeList/ARLFrame.lua:1650`).
|
||||
@@ -782,9 +779,9 @@ pub(super) fn install(lua: &Lua, m: &Table) -> mlua::Result<()> {
|
||||
// three times (1203/1205/1211): the majority of the corpus calls pass **false**, and for those
|
||||
// our behaviour is already the reference's — we deliver no keys to arbitrary frames either. The
|
||||
// `true` callers want delivery we do not do, but they did not get it before this landed; they
|
||||
// got a raise that killed the enclosing function. And per
|
||||
// `frame-key-script-delivery.md` §3.2 being enabled genuinely is separable from having a
|
||||
// handler, so the flag is a real piece of the model, not a placeholder for one.
|
||||
// got a raise that killed the enclosing function. And being enabled genuinely is separable
|
||||
// from having a handler (`0x76af00` never touches the handler slots), so the flag is a real
|
||||
// piece of the model, not a placeholder for one.
|
||||
m.set(
|
||||
"EnableKeyboard",
|
||||
lua.create_function(|lua, (this, enable): (Table, bool)| {
|
||||
@@ -874,7 +871,7 @@ pub(super) fn install(lua: &Lua, m: &Table) -> mlua::Result<()> {
|
||||
))
|
||||
})?,
|
||||
)?;
|
||||
// Clamp-to-screen (`0x776c00`/`0x776cb0`, geometry flags bit4 — layout.md): the layout resolve
|
||||
// Clamp-to-screen (`0x776c00`/`0x776cb0`, geometry flags bit4): the layout resolve
|
||||
// keeps the frame's assembled rect inside the window, size preserved. GameTooltip frames
|
||||
// default true by construction (widget::Frame::clamped_to_screen — decision 0352).
|
||||
m.set(
|
||||
@@ -929,7 +926,7 @@ pub(super) fn install(lua: &Lua, m: &Table) -> mlua::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Parse a Lua `SetBackdrop` table into a [`Backdrop`] (backdrop-mechanism.md §1). The keys read —
|
||||
/// Parse a Lua `SetBackdrop` table into a [`Backdrop`] (`0x7776e0`'s key reads). The keys read —
|
||||
/// exactly the compiled reader's set — are `bgFile`/`edgeFile` (strings), `tile` (boolean, default
|
||||
/// false), `tileSize`/`edgeSize` (numbers; a missing number leaves the ctor default: tileSize 0,
|
||||
/// edgeSize 32), and `insets{left,right,top,bottom}` (numbers, each default 0). A non-string file or
|
||||
@@ -1015,8 +1012,7 @@ fn children_of(lua: &Lua, this: &Table) -> mlua::Result<Vec<u32>> {
|
||||
/// This frame's live regions as stable ids, in creation order — **the title region and detached
|
||||
/// ones excluded** — the one walk `GetRegions` and `GetNumRegions` share.
|
||||
///
|
||||
/// Both exclusions are the client's list, not tidiness, and both are VERIFIED (wow-re
|
||||
/// `ui/scratch/widget-list-bindings.md`, §5 quartet):
|
||||
/// Both exclusions are the client's list, not tidiness:
|
||||
///
|
||||
/// - **Detached.** `SetParent(nil)` reaches `0x77fd55` → the removal virtual `0x76a7f0`, which
|
||||
/// unlinks the node and frees it, and then skips every re-link. We keep the entry only so the
|
||||
@@ -1051,8 +1047,7 @@ fn regions_of(lua: &Lua, this: &Table) -> mlua::Result<Vec<u32>> {
|
||||
}
|
||||
|
||||
/// One backdrop colour channel, the reference's own conversion (`SetBackdropColor 0x777d30` /
|
||||
/// `SetBackdropBorderColor 0x7780d0` — instruction-identical bar the delegate; wow-re
|
||||
/// `scratch/numeric-arg-coercion-law.md` Q4, VERIFIED).
|
||||
/// `SetBackdropBorderColor 0x7780d0` — instruction-identical bar the delegate).
|
||||
///
|
||||
/// **r/g/b are UNGATED and alpha is not**, and that asymmetry is the whole of this function:
|
||||
///
|
||||
@@ -1066,7 +1061,8 @@ fn regions_of(lua: &Lua, this: &Table) -> mlua::Result<Vec<u32>> {
|
||||
///
|
||||
/// Then the reference clamps to `[0,1]` with **NaN landing on 1.0** (the compare's unordered arm
|
||||
/// takes the high clamp), and quantizes `×255 + 0.5` through `__ftol` — round-half-up, not the
|
||||
/// bare truncate `chat-window-record.md`'s colour path uses. The field is a packed `0xAARRGGBB`
|
||||
/// bare truncate `SetChatWindowColor 0x4a14f0`'s colour path uses. The field is a packed
|
||||
/// `0xAARRGGBB`
|
||||
/// byte quad, so nothing finer survives the store and we quantize on the way in rather than
|
||||
/// pretend to a precision `GetBackdropColor` could not read back.
|
||||
fn backdrop_channel(lua: &Lua, v: Option<Value>, gated: bool) -> f32 {
|
||||
|
||||
@@ -115,7 +115,7 @@ pub(super) fn install(lua: &Lua, m: &Table) -> mlua::Result<()> {
|
||||
.map_or(0, |i| i.anchors.len() as i64))
|
||||
})?;
|
||||
// SetAllPoints([relativeTo]) — pin TOPLEFT+BOTTOMRIGHT to the target (default: the parent),
|
||||
// the XML `setAllPoints="true"` behavior as a method (rf24 `0x767800`'s SetAllPoints path).
|
||||
// the XML `setAllPoints="true"` behavior as a method (`0x767800`'s SetAllPoints path).
|
||||
set_shared(
|
||||
lua,
|
||||
m,
|
||||
|
||||
@@ -14,20 +14,20 @@
|
||||
//! ```
|
||||
//!
|
||||
//! and that layering is the reference's own: `RegisterForDrag 0x776d60` + the threshold-gated
|
||||
//! `OnDragStart`/`OnDragStop` pair (wow-re `cursor-dragdrop-payload.md` §9, our decision 0216 §3 —
|
||||
//! `OnDragStart`/`OnDragStop` pair (`0x76bf70`/`0x76c040`, our decision 0216 §3 —
|
||||
//! [`crate::script::cursor`], dispatched from [`crate::script::pointer`]) are a **separate**
|
||||
//! system from the engine move below. The gesture decides *when* the handlers run; this module
|
||||
//! owns only what happens between `StartMoving` and `StopMovingOrSizing`.
|
||||
//!
|
||||
//! ## The mechanism (wow-re `system/ui`, VERIFIED unless said otherwise)
|
||||
//! ## The mechanism
|
||||
//!
|
||||
//! The three flags are bits of the frame's flag word `[frame+0xb4]`, all written by the one
|
||||
//! generic setter `0x76a3c0` (`if (v) flags |= mask; else flags &= ~mask;` — no calls, no side
|
||||
//! effects): **movable `0x100`** (`SetMovable 0x776420` / `IsMovable 0x7764d0`), **resizable
|
||||
//! `0x200`** (`SetResizable 0x776590` / `IsResizable 0x776640`), **userPlaced `0x1000`**
|
||||
//! (`SetUserPlaced 0x776a50` / `IsUserPlaced 0x776b40`). The XML attributes land on the same
|
||||
//! setter (`movable="true"` → `0x76a3c0(0x100)`, `resizable="true"` → `0x200` — wow-re
|
||||
//! `rf24-framexml-loader.md`), which is why the loader now *calls* these instead of warning.
|
||||
//! setter (`movable="true"` → `0x76a3c0(0x100)`, `resizable="true"` → `0x200`), which is why the
|
||||
//! loader now *calls* these instead of warning.
|
||||
//!
|
||||
//! `StartMoving 0x776700` tests the movable bit and raises when it is clear, then enters the drag
|
||||
//! at `0x7652b0`, which `Raise()`s the frame, **sets the userPlaced bit itself**, and records the
|
||||
@@ -40,10 +40,10 @@
|
||||
//! Movement is a **pump**, run from the mouse-move handler: `0x7655b0` (a diffed-bit-exact
|
||||
//! PRIMITIVE) takes `dx = x − root+0xd08`, `dy = y − root+0xd0c`, and — only if either is
|
||||
//! non-zero — applies them and re-centers the sample. The application (`0x76a660` → the 9-case
|
||||
//! `geo_768710`, also a diffed-bit-exact PRIMITIVE) selects on the 3×3 region the drag grabbed:
|
||||
//! `0x768710`, also a diffed-bit-exact PRIMITIVE) selects on the 3×3 region the drag grabbed:
|
||||
//! the eight edge/corner cases resize, and **case 4, the plain move, accumulates the scaled delta
|
||||
//! straight into the anchor's offsets in place** — `xOffset += dx/scale`, `yOffset += dy/scale`
|
||||
//! (`CAnchor+0x4`/`+0x8`, wow-re `frame-model.md`) — then invalidates the layout (`0x7680e0`).
|
||||
//! (`CAnchor+0x4`/`+0x8`) — then invalidates the layout (`0x7680e0`).
|
||||
//!
|
||||
//! Two consequences worth stating, because both are easy to get wrong:
|
||||
//!
|
||||
@@ -59,13 +59,13 @@
|
||||
//!
|
||||
//! ## What benilla does NOT take from that, and why
|
||||
//!
|
||||
//! - **All anchors translate, not just one.** wow-re's note reads the case-4 accumulate as hitting
|
||||
//! a single anchor record; it is their composition of verified pieces, not a recorded finding,
|
||||
//! and a one-anchor translation would *deform* a frame stretched between two anchors instead of
|
||||
//! moving it. Every movable frame in practice carries exactly one anchor, where the two readings
|
||||
//! are identical, so translating the whole set is the same behaviour everywhere it is observable
|
||||
//! and the sane behaviour where it is not.
|
||||
//! - **No clamp rebate.** `geo_768710` rebates the residual delta when the clamp pushes back
|
||||
//! - **All anchors translate, not just one.** The case-4 accumulate could be read as hitting a
|
||||
//! single anchor record; that reading is a composition of verified pieces, not a recorded
|
||||
//! finding, and a one-anchor translation would *deform* a frame stretched between two anchors
|
||||
//! instead of moving it. Every movable frame in practice carries exactly one anchor, where the
|
||||
//! two readings are identical, so translating the whole set is the same behaviour everywhere it
|
||||
//! is observable and the sane behaviour where it is not.
|
||||
//! - **No clamp rebate.** `0x768710` rebates the residual delta when the clamp pushes back
|
||||
//! (`SetClampedToScreen`); we do not, so dragging a clamped frame into the screen edge and back
|
||||
//! out can lag the cursor. Named, not hidden — the rebate is one of the two halves of that
|
||||
//! primitive we have not transcribed (the other is the eight resize cases).
|
||||
@@ -105,8 +105,8 @@ pub(crate) struct FrameMove {
|
||||
///
|
||||
/// **The reference's drag MODE, reduced to the one bit that is observable from Lua.** The
|
||||
/// mouse-up handler `0x766420` auto-cancels modes 1 (modifier-drag) and 2 (title region) and
|
||||
/// leaves mode 3 (`StartMoving`) running until `StopMovingOrSizing`
|
||||
/// (wow-re `widget-api-batch-benilla.md` Q6). So a title drag ends on release while a scripted
|
||||
/// leaves mode 3 (`StartMoving`) running until `StopMovingOrSizing`. So a title drag ends on
|
||||
/// release while a scripted
|
||||
/// one outlives the button — which is exactly the distinction this module's own doc already
|
||||
/// records for the drag/move split.
|
||||
pub(crate) auto_stop: bool,
|
||||
@@ -129,7 +129,7 @@ pub(crate) struct FrameSizing {
|
||||
pub(super) fn install(lua: &Lua, m: &Table) -> mlua::Result<()> {
|
||||
// SetMovable(flag) / IsMovable() — flag word bit 0x100 (`0x776420`/`0x7764d0` through the
|
||||
// generic setter `0x76a3c0`). A pure flag write: it moves nothing, and clearing it mid-drag
|
||||
// stops nothing either (the setter has no side effects — wow-re ledger `0x76a3c0`); it is the
|
||||
// stops nothing either (the setter has no side effects — `0x76a3c0`); it is the
|
||||
// guard `StartMoving` tests, and the XML `movable="true"` attribute writes the same bit.
|
||||
// mlua's bool conversion is Lua truthiness, so the corpus's `SetMovable(1)` reads as true —
|
||||
// matching the reference binding's own `toboolean` marshal.
|
||||
@@ -158,9 +158,8 @@ pub(super) fn install(lua: &Lua, m: &Table) -> mlua::Result<()> {
|
||||
// ── The resize-bounds quad — `Set/GetMinResize`, `Set/GetMaxResize` ────────────────────────
|
||||
//
|
||||
// Frame method table `0x878ec0`: `GetMinResize 0x775f20` · `SetMinResize 0x776020` ·
|
||||
// `GetMaxResize 0x7761a0` · `SetMaxResize 0x7762a0`. Byte-carved end to end by wow-re
|
||||
// (`system/ui/scratch/resize-bounds-and-button-fontstring.md` §2), and every clause below is
|
||||
// from that read rather than from the shape of the name:
|
||||
// `GetMaxResize 0x7761a0` · `SetMaxResize 0x7762a0`. Every clause below is from the bytes,
|
||||
// not from the shape of the name:
|
||||
//
|
||||
// · **The argument gate is `lua_isnumber`, not a strict number** (`0x6f34d0` on BOTH slots 2
|
||||
// and 3), so a numeric STRING passes and is coerced — while a missing argument, an explicit
|
||||
@@ -289,17 +288,17 @@ pub(super) fn install(lua: &Lua, m: &Table) -> mlua::Result<()> {
|
||||
start_moving(&mut model, h)
|
||||
})?,
|
||||
)?;
|
||||
// StartSizing(point) — begin a resize drag from a named grip (`0x776830`, verified in wow-re's
|
||||
// ledger; the reference's own caller is `FloatingChatFrame.lua:600`,
|
||||
// StartSizing(point) — begin a resize drag from a named grip (`0x776830`; the reference's own
|
||||
// caller is `FloatingChatFrame.lua:600`,
|
||||
// `this:GetParent():StartSizing(anchorPoint)`).
|
||||
//
|
||||
// **What is verified and what is read, kept apart on purpose.** Verified: the verb exists, it
|
||||
// takes the grip name, it returns nothing, and `StopMovingOrSizing` ends it (the same slot
|
||||
// clear as a move). NOT recorded anywhere in wow-re — the ledger has it as ORCHESTRATION with
|
||||
// no inline math — is WHICH EDGES a given grip moves. Taken here as the plain meaning of an
|
||||
// anchor point, which is how the reference's own caller uses it (its resize grips pass the
|
||||
// corner they sit in): the named edges follow the cursor and the opposite ones stay put. If an
|
||||
// RE pass ever contradicts that, this comment is where to correct it.
|
||||
// clear as a move). Not settled anywhere — the verb only orchestrates, with no inline math — is
|
||||
// WHICH EDGES a given grip moves. Taken here as the plain meaning of an anchor point, which is
|
||||
// how the reference's own caller uses it (its resize grips pass the corner they sit in): the
|
||||
// named edges follow the cursor and the opposite ones stay put. If the bytes ever contradict
|
||||
// that, this comment is where to correct it.
|
||||
//
|
||||
// Four corpus addons reach it through ONE line — `FuBar_Panel.lua:980`, replicated into
|
||||
// FuBar_CorkFu, FuBar_FuXPFu, FuBar_SpellStatusFu and oRA2 — which is 1207's rule and why the
|
||||
@@ -329,7 +328,7 @@ pub(super) fn install(lua: &Lua, m: &Table) -> mlua::Result<()> {
|
||||
// caller error. The pump's switch is on the anchor point-id, and case 4 (CENTER) is the
|
||||
// one arm that never touches width or height: it shifts the frame's anchor offsets and
|
||||
// returns before the clamp is ever reached, so a CENTER grip is unbounded by
|
||||
// construction (wow-re `resize-bounds-and-button-fontstring.md` §3.6). This file used
|
||||
// construction (`0x768bfb`). This file used
|
||||
// to say *"a grip naming no edge would resize nothing; the reference has no such call
|
||||
// and we refuse rather than invent one"* — the bytes have since answered that, and the
|
||||
// answer is the translate below, which is exactly what [`advance_move`] already does.
|
||||
@@ -421,8 +420,8 @@ fn get_flag(lua: &Lua, this: &Table, which: Flag) -> mlua::Result<Value> {
|
||||
/// The family's refusal, named frame and all — the reference's own shape (it formats the frame's
|
||||
/// `GetName()`, substituting a literal when the frame is anonymous).
|
||||
///
|
||||
/// **The text is OURS.** wow-re records the guards and the three `.rdata` addresses
|
||||
/// (`0x879810` StartMoving, `0x879828` StartSizing, `0x879844` SetUserPlaced) but not the strings
|
||||
/// **The text is OURS.** The three `.rdata` addresses are known (`0x879810` StartMoving,
|
||||
/// `0x879828` StartSizing, `0x879844` SetUserPlaced) but not the strings
|
||||
/// at them — only the truncated IDA symbol stubs `aFrameSIsNotMov` / `aFrameSIsNotRes` /
|
||||
/// `aFrameSIsNotM_0`, which is what this wording is shaped after. Nothing should match on it.
|
||||
fn not_flagged(model: &Model, h: FrameHandle, want: &str) -> mlua::Error {
|
||||
@@ -463,13 +462,13 @@ fn start_moving(model: &mut Model, h: FrameHandle) -> mlua::Result<()> {
|
||||
}
|
||||
|
||||
/// Begin a **title-region** move — mode 2, the drag a mouse-down inside `frame:GetTitleRegion()`
|
||||
/// starts (wow-re Q6, `0x7662c0` → `0x765320(frame, mode=2, …)` → `0x7652b0`).
|
||||
/// starts (`0x7662c0` → `0x765320(frame, mode=2, …)` → `0x7652b0`).
|
||||
///
|
||||
/// [`start_moving`]'s body **minus the movable gate**, and that omission is the carved part rather
|
||||
/// than a shortcut: the movable bit is `frame+0xb4 & 0x100`, tested by `StartMoving` (`0x77678b`,
|
||||
/// else `"Frame %s is not movable"`) and by the modifier-drag path — and **not read anywhere** on
|
||||
/// `0x7662c0`→`0x765320`→`0x7652b0`→`0x768430`. Q6 marks the no-gate reading VERIFIED and the
|
||||
/// observable claim INFERRED, because both FrameXML users happen to be `movable="true"`; a title
|
||||
/// [`start_moving`]'s body **minus the movable gate**, and that omission is the settled part
|
||||
/// rather than a shortcut: the movable bit is `frame+0xb4 & 0x100`, tested by `StartMoving`
|
||||
/// (`0x77678b`, else `"Frame %s is not movable"`) and by the modifier-drag path — and **not read
|
||||
/// anywhere** on `0x7662c0`→`0x765320`→`0x7652b0`→`0x768430`. The no-gate reading is settled; the
|
||||
/// observable claim is INFERRED, because both FrameXML users happen to be `movable="true"`; a title
|
||||
/// region on a non-movable frame is the case that would tell them apart, and it drags here.
|
||||
///
|
||||
/// Returns whether a move actually started — `false` when one was already in flight, which is
|
||||
@@ -515,10 +514,9 @@ fn mark_user_placed_change(model: &mut Model, h: FrameHandle) {
|
||||
|
||||
/// Apply a frame's `SetMinResize`/`SetMaxResize` bounds to a proposed size, per axis.
|
||||
///
|
||||
/// The reference's predicate, transcribed (decision 1505; wow-re
|
||||
/// `system/ui/scratch/resize-bounds-and-button-fontstring.md` §3.5 — decoded from `geo_768710`'s
|
||||
/// **emitted** `jcc`, not from the FPU status mask, and deliberately not from the dead `geo_768550`
|
||||
/// copy whose operand order differs):
|
||||
/// The reference's predicate, transcribed (decision 1505 — decoded from `0x768710`'s **emitted**
|
||||
/// `jcc`, not from the FPU status mask, and deliberately not from the dead `0x768550` copy whose
|
||||
/// operand order differs):
|
||||
///
|
||||
/// ```text
|
||||
/// if (min != 0.0) and (v < min): v = min
|
||||
@@ -594,8 +592,8 @@ pub(crate) fn advance_size(model: &mut Model, pos: (f32, f32)) {
|
||||
return;
|
||||
};
|
||||
// The frame's own `SetMinResize`/`SetMaxResize` bounds — read before the mutable borrow, and
|
||||
// the only path in the client that consults them at all (`clamp_resize`, and wow-re's §3.1
|
||||
// reachability census behind it).
|
||||
// the only path in the client that consults them at all (`clamp_resize`, reached only through
|
||||
// `0x76a660`).
|
||||
let (new_w, new_h) = clamp_resize(model, sz.frame, w0 + dw, h0 + dh);
|
||||
// **The rebate.** A saturated clamp is not a bare `max`/`min` in the reference: the residual
|
||||
// it swallowed is subtracted back out of the drag delta before the delta reaches the anchors
|
||||
@@ -609,7 +607,7 @@ pub(crate) fn advance_size(model: &mut Model, pos: (f32, f32)) {
|
||||
// rule. The sign follows the grip: a growing edge (`dw = +dx`) adds the residual, a shrinking
|
||||
// one (`dw = -dx`) subtracts it, so the delta always agrees with the size the clamp allowed.
|
||||
// An axis nobody gripped is left alone rather than rebated against a bound the frame was
|
||||
// already outside of — that frame stays put until a drag touches its axis, which is §3.8.
|
||||
// already outside of — that frame stays put until a drag touches its axis.
|
||||
let rebate = |d: f32, want: f32, got: f32, grew: bool, gripped: bool| {
|
||||
if !gripped {
|
||||
return d;
|
||||
@@ -632,11 +630,11 @@ pub(crate) fn advance_size(model: &mut Model, pos: (f32, f32)) {
|
||||
// The planted edge: a frame anchored by its LEFT that is gripped on the LEFT has to move too,
|
||||
// or the resize would push the right edge instead. Only the gripped axis shifts.
|
||||
//
|
||||
// **Which edges travel is OURS, and the byte carve does not settle it** — stated rather than
|
||||
// **Which edges travel is OURS, and the bytes do not settle it** — stated rather than
|
||||
// quietly aligned. The reference collapses the frame to a single planted TOPLEFT anchor at
|
||||
// `StartSizing` (`0x768430`), so *its* travelling edges are necessarily LEFT and TOP; we keep
|
||||
// the authored anchor set intact and shift every anchor's offsets, which is a different model
|
||||
// with a different answer. The rebate above is the part the carve does settle, and it applies
|
||||
// with a different answer. The rebate above is the part the bytes do settle, and it applies
|
||||
// either way.
|
||||
if !input.anchors.is_empty() && (sz.left || sz.bottom) {
|
||||
for a in &mut input.anchors {
|
||||
@@ -686,7 +684,7 @@ pub(crate) fn advance_move(model: &mut Model, pos: (f32, f32)) {
|
||||
}
|
||||
// Offsets are LOCAL units — the resolver multiplies them by the frame's own layoutScale
|
||||
// (`anchor_resolve_x`), so the cursor's screen delta divides by it on the way in, exactly the
|
||||
// `dx/scale` of `geo_768710`'s case 4.
|
||||
// `dx/scale` of `0x768710`'s case 4.
|
||||
let inv = 1.0 / eff_scale(model, mv.frame);
|
||||
let (dx, dy) = (dx * inv, dy * inv);
|
||||
if let Some(input) = model.layout_inputs.get_mut(&mv.frame) {
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
//! frames. Until this module it was parsed and thrown away with a warn-once, so a benilla dialog
|
||||
//! opened behind another one stayed behind it.
|
||||
//!
|
||||
//! ## The law (wow-re `system/ui/scratch/toplevel-raise.md`, §5 fan-out 2026-07-21 — VERIFIED)
|
||||
//! ## The law
|
||||
//!
|
||||
//! The flag is **`[frame+0xb4]` bit `0x1`**, written by the same pure bit-setter `0x76a3c0` as
|
||||
//! `movable` (`0x100`) and `resizable` (`0x200`): `if (v) flags |= mask; else flags &= ~mask;`, no
|
||||
@@ -23,11 +23,11 @@
|
||||
//! *ancestor* of the one that was clicked or shown.
|
||||
//! 2. **`force != 0` ⇒ recompute the OVERLAPPED bit** (`0x76513e..0x765290`): resolve the frame's
|
||||
//! pending layout, then scan its stratum bucket from **its own level upward** for a frame that is
|
||||
//! not itself, is not one of its descendants (`is_descendant 0x767010`), and whose screen rect
|
||||
//! not itself, is not one of its descendants (`0x767010`), and whose screen rect
|
||||
//! intersects non-emptily. The answer is stored as `+0xb4` bit `0x10`.
|
||||
//! 3. **The raise, gated on that bit** (`0x7651ac..0x7651e7`): `level_compact 0x764eb0` squeezes the
|
||||
//! free levels out of the stratum — over **every live frame of it, hidden included**
|
||||
//! (`ui/scratch/level-compact-law.md`) — then `set_frame_level(bucket->count, propagate = 1)`.
|
||||
//! 3. **The raise, gated on that bit** (`0x7651ac..0x7651e7`): level compaction (`0x764eb0`)
|
||||
//! squeezes the free levels out of the stratum — over **every live frame of it, hidden
|
||||
//! included** — then `set_frame_level(bucket->count, propagate = 1)`.
|
||||
//!
|
||||
//! Three consequences the tests pin: a raise is **`level := top occupied level + 1`**, not a list
|
||||
//! reshuffle; it is **occlusion-gated** — `Raise()` on a frame that overlaps nothing changes nothing
|
||||
@@ -51,10 +51,10 @@
|
||||
//! term *above* the link stamp in our key, so a bump lifts the frame over every same-strata frame
|
||||
//! regardless of when each was last shown. The arithmetic is pinned in
|
||||
//! `tests/toplevel.rs::a_raise_is_top_occupied_level_plus_one_after_compaction`.
|
||||
//! - **`level_compact`** is [`crate::widget::WidgetArena::compact_levels`] — an order-preserving
|
||||
//! - **Level compaction** is [`crate::widget::WidgetArena::compact_levels`] — an order-preserving
|
||||
//! renumber of **every frame** in one stratum, hidden ones included: `0x764eb0` walks the client's
|
||||
//! master frame list filtering on strata and level alone, and never reads `+0xd4`
|
||||
//! (`ui/scratch/level-compact-law.md`, decision 2104). It changes no draw order by itself; it is
|
||||
//! (decision 2104). It changes no draw order by itself; it is
|
||||
//! what keeps `level := max + 1` from ratcheting upward for the length of a session. Renumbering
|
||||
//! only the *bucket* (the visible half) is the one shape it may not have: the raise's `propagate`
|
||||
//! delta is computed in the new numbering and applied to levels that are still in the old one, so
|
||||
@@ -84,12 +84,12 @@
|
||||
//!
|
||||
//! ## Triggers — the census, and which ones benilla wires
|
||||
//!
|
||||
//! wow-re's complete rel32 + absolute-dword census finds six call sites. Ours:
|
||||
//! A complete rel32 + absolute-dword census finds six call sites. Ours:
|
||||
//!
|
||||
//! | site | what it is | benilla |
|
||||
//! |---|---|---|
|
||||
//! | `0x775b0a` (Lua `Frame:Raise()`) | the explicit script call | [`install`] |
|
||||
//! | `0x76aeec` in `effective_visible_show 0x76ae10` | **the Show trigger** | [`raise_on_show`], run from [`crate::script::event::fire_visibility_changes`] |
|
||||
//! | `0x76aeec` in `0x76ae10` | **the Show trigger** | [`raise_on_show`], run from [`crate::script::event::fire_visibility_changes`] |
|
||||
//! | `0x7652d7` in begin-move `0x7652b0` | drag start | [`super::movable`]'s `start_moving` |
|
||||
//! | `0x766392` in the mouse-down handler `0x7662c0` | **the click trigger** | [`crate::script::UiScript::mouse_button`]'s down arm, first thing — capture-else-hover, unguarded, ahead of the title-region swallow |
|
||||
//! | `0x764a4c` (`force = 0`) | re-applies an already-computed bit `0x10` | no counterpart (see the OVERLAPPED note above) |
|
||||
@@ -185,7 +185,7 @@ pub(in crate::script) fn raise(model: &mut Model, frame: FrameHandle) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
/// The Show trigger — `effective_visible_show 0x76ae10` @`0x76aee0`: on a genuine `+0xd4`
|
||||
/// The Show trigger — `0x76ae10` @`0x76aee0`: on a genuine `+0xd4`
|
||||
/// false→true transition, **test the frame's own toplevel bit** and raise if it is set.
|
||||
///
|
||||
/// The own-bit test is load-bearing and is why this is not just [`raise`]: the worker would walk up
|
||||
@@ -228,7 +228,7 @@ fn nearest_toplevel(model: &Model, frame: FrameHandle) -> Option<FrameHandle> {
|
||||
/// Every clause matters, and each is a separate test:
|
||||
/// - **`level >= t.level` only.** A toplevel frame that overlaps solely something *below* it is
|
||||
/// already on top of it, so there is nothing to raise over.
|
||||
/// - **`is_descendant 0x767010` exclusion.** A window overlaps its own children by construction;
|
||||
/// - **Excludes descendants (`0x767010`).** A window overlaps its own children by construction;
|
||||
/// without this every toplevel frame with content would raise on every trigger.
|
||||
/// - **non-empty intersection.** Frames that merely share an edge do not overlap — the intersection
|
||||
/// has to have positive width *and* height, so a zero-area touch is not an occlusion.
|
||||
|
||||
@@ -65,8 +65,7 @@ pub struct SavedInstanceInfo {
|
||||
}
|
||||
|
||||
/// One **raid roster** row — `GetRaidRosterInfo`'s nine returns, as the app resolved them
|
||||
/// (wow-re `ui/scratch/raid-roster-bindings.md` §2, §5-cross-checked with orchestrator
|
||||
/// byte-arbitration). Field order is the push order of the reference's success tuple.
|
||||
/// (`0x4bb560`). Field order is the push order of the reference's success tuple.
|
||||
///
|
||||
/// The row is the *record*, not the answer: two of the nine are computed at the binding
|
||||
/// ([`Self::subgroup`]'s 1-based exposure, [`Self::zone`]'s offline substitution), because both
|
||||
@@ -86,8 +85,8 @@ pub struct RaidMemberInfo {
|
||||
/// false at its entry).
|
||||
pub guid: u64,
|
||||
/// Return 2 — the rank, **exposed exactly as stored** (`0x4bb607 fild [edi+0xc]`, no
|
||||
/// adjustment). wow-re could not derive the numeric convention from the binding, and does not
|
||||
/// claim one; the corpus does, consistently: `ChatLog.lua:351-353` prints `@` for `2` and `*`
|
||||
/// adjustment). The numeric convention is not derivable from the binding alone; the corpus
|
||||
/// does, consistently: `ChatLog.lua:351-353` prints `@` for `2` and `*`
|
||||
/// for `1`, i.e. **0 member · 1 assistant · 2 leader**, which is what the app fills.
|
||||
pub rank: u32,
|
||||
/// Return 3 — the subgroup **as stored, 0-based**. The binding exposes `subgroup + 1`
|
||||
@@ -110,11 +109,11 @@ pub struct RaidMemberInfo {
|
||||
/// reference: return 8 is `1` on exactly the two arms that produced a real zone and `nil` on
|
||||
/// the `PLAYER_OFFLINE` one.
|
||||
pub online: bool,
|
||||
/// Return 9 — **deliberately unlabelled.** wow-re verified the mechanism and refused the
|
||||
/// name: a streamed member answers `1` iff `[obj+0x110 +0x40] <= 0` (UNIT_FIELD_HEALTH by the
|
||||
/// descriptor line this repo already anchors), and an unstreamed one iff the roster record's
|
||||
/// `[+0x18]` carries **both** bits `0x4` and `0x1`. Two independent workers guessed "isDead"
|
||||
/// positionally and the note declined to adopt it (§5 Open).
|
||||
/// Return 9 — **deliberately unlabelled.** The mechanism is confirmed: a streamed member
|
||||
/// answers `1` iff `[obj+0x110 +0x40] <= 0` (UNIT_FIELD_HEALTH by the descriptor line this
|
||||
/// repo already anchors), and an unstreamed one iff the roster record's `[+0x18]` carries
|
||||
/// **both** bits `0x4` and `0x1`. The name itself is a guess, not settled: two independent
|
||||
/// reads proposed "isDead" positionally, but it is not adopted here.
|
||||
///
|
||||
/// Three things corroborate that guess without settling it, recorded here so whoever settles
|
||||
/// it starts ahead: the whole corpus destructures position 9 as `isDead`
|
||||
@@ -153,7 +152,7 @@ pub struct PartyState {
|
||||
/// `UnitIsPartyLeader 0x516210` does not. So an unresolvable-but-non-raising argument (none,
|
||||
/// `nil`, `""`, `"target"` with no target) resolves to `0:0`, matches the zeroed leader, and
|
||||
/// the reference answers **`1` while solo**. A client that answers `nil` there is not
|
||||
/// reproducing 1.12 (wow-re `ui/scratch/party-leader-and-nameplate-verbs.md`).
|
||||
/// reproducing 1.12.
|
||||
pub leader_guid: u64,
|
||||
/// **The active player's own GUID** — the reference's `0x468550` read (`[0xb41414]+0xc0`),
|
||||
/// `0` out of world. Fed by the app beside the leader; the pair is the leader gate the
|
||||
@@ -284,8 +283,8 @@ impl super::UiScript {
|
||||
}
|
||||
|
||||
/// One member's answer, forwarded to the leader (`{guid, status}`, decisions 1989/1997).
|
||||
/// The handler's leader arm (`0x4ba360`, wow-re `ui/scratch/readycheck-response-store.md`):
|
||||
/// the record's guid is matched against the roster entry in full (`0x4ba3f9`/`0x4ba405`),
|
||||
/// The handler's leader arm (`0x4ba360`): the record's guid is matched against the roster
|
||||
/// entry in full (`0x4ba3f9`/`0x4ba405`),
|
||||
/// and a match stores **the constant 0** into the "has not answered" flag whatever the status
|
||||
/// byte says (`0x4ba40e`; `ecx` is zeroed before the loop and never written) — so any answer,
|
||||
/// ready or not, clears the member. The status byte gates one thing: a `0` prints
|
||||
@@ -349,9 +348,8 @@ impl super::UiScript {
|
||||
/// against the local player's own GUID (`0x468550`). `IsPartyLeader 0x4e9130` opens
|
||||
/// `mov eax,[0xbc75fc]` · `mov esi,[0xbc75f8]` and compares **the same two globals** against the
|
||||
/// same GUID; its only extra instruction is an `or ecx,eax; je` short-circuit on a `0:0` cached
|
||||
/// leader, which is behaviourally inert (a zero GUID can never equal a live player's). wow-re's
|
||||
/// orchestrator re-read both prologues and they are byte-identical in the globals they load
|
||||
/// (`raid-roster-bindings.md` §3, VERIFIED).
|
||||
/// leader, which is behaviourally inert (a zero GUID can never equal a live player's), and both
|
||||
/// prologues are byte-identical in the globals they load.
|
||||
///
|
||||
/// **So `IsRaidLeader()` is TRUE for an ordinary 5-man party leader**, and its body reads no
|
||||
/// raid-vs-party flag and never touches the roster array/count the other two raid bindings use.
|
||||
@@ -662,9 +660,9 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
// IsRaidOfficer() → nil, always: a 1.12 PARTY has no officer rank (the assistant flag is a
|
||||
// raid concept). The popup's leader-or-assistant gates read this and fall back to the leader
|
||||
// half. The roster now carries the assistant rank ([`RaidMemberInfo::rank`], `1`), so the
|
||||
// *data* to answer this exists — what does not is a carve of `IsRaidOfficer`'s own body, and
|
||||
// "it is probably rank ≥ 1 for the player's row" is a guess, not a mechanism. Left nil until
|
||||
// someone reads the bytes.
|
||||
// *data* to answer this exists — what does not is a reading of `IsRaidOfficer`'s own body,
|
||||
// and "it is probably rank ≥ 1 for the player's row" is a guess, not a mechanism. Left nil
|
||||
// until someone reads the bytes.
|
||||
g.set(
|
||||
"IsRaidOfficer",
|
||||
lua.create_function(|_, ()| Ok(Value::Nil))?,
|
||||
@@ -672,12 +670,11 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
|
||||
// ── The raid-management verbs (decision 1549) ────────────────────────────────────────────
|
||||
//
|
||||
// All nine are `ORCHESTRATION` in wow-re's own classification (`ui/scratch/bindings.md`:
|
||||
// `ConvertToRaid 0x4bbc90`, `SetRaidSubgroup 0x4bb990`, `SwapRaidSubgroup 0x4bbb00`,
|
||||
// `PromoteToAssistant 0x4bbd20`, `RequestRaidInfo 0x4a1850`, `GetSavedInstanceInfo 0x4a1920`;
|
||||
// `UninviteFromRaid 0x48a580` and `SetRaidRosterSelection 0x4bb820` in
|
||||
// `item17-frameapi-fullcarve.md`) — "marshals + delegates to a C++ method/net-send; no inline
|
||||
// fidelity math". So the *binding* has no law of its own to reproduce: the law is the wire's,
|
||||
// All nine share the same shape (`ConvertToRaid 0x4bbc90`, `SetRaidSubgroup 0x4bb990`,
|
||||
// `SwapRaidSubgroup 0x4bbb00`, `PromoteToAssistant 0x4bbd20`, `RequestRaidInfo 0x4a1850`,
|
||||
// `GetSavedInstanceInfo 0x4a1920`, `UninviteFromRaid 0x48a580`, `SetRaidRosterSelection
|
||||
// 0x4bb820`): each marshals and delegates to a C++ method or net-send, with no inline
|
||||
// fidelity math. So the *binding* has no law of its own to reproduce: the law is the wire's,
|
||||
// which `benilla-protocol`'s `group` family already carries byte-golden, and the marshalling
|
||||
// is the queue below. Nothing here is a guess about a body nobody has read.
|
||||
//
|
||||
@@ -781,8 +778,7 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
// unanswered, and prints `RAID_MEMBERS_AFK` with the `", "`-joined names — or
|
||||
// `READY_CHECK_NO_AFK` — as a `CHAT_MSG_SYSTEM` line. Both keys are read raw off `_G`
|
||||
// (`0x704350`: `lua_gettable` on GLOBALSINDEX, empty-string default), never through
|
||||
// `GetText`. Sends no packet, raises nothing, returns nothing (decision 1989; wow-re
|
||||
// `ui/scratch/uiparent-onupdate-engine-verbs.md` §3).
|
||||
// `GetText`. Sends no packet, raises nothing, returns nothing (decision 1989).
|
||||
g.set(
|
||||
"CheckReadyCheckTime",
|
||||
lua.create_function(|lua, ()| {
|
||||
@@ -1041,7 +1037,7 @@ mod tests {
|
||||
assert_eq!(master, None);
|
||||
}
|
||||
|
||||
// ── The raid trio (wow-re `ui/scratch/raid-roster-bindings.md`) ─────────────────────────────
|
||||
// ── The raid trio (`UnitInRaid`, `GetRaidRosterInfo`, `IsRaidLeader`) ────────────────────
|
||||
//
|
||||
// One test per return-shape trap, in `item_stats`'s `get_item_info_tests` style: the ARITY
|
||||
// assertion is half of each, because a signature regression is the failure every individual
|
||||
@@ -1210,11 +1206,9 @@ mod tests {
|
||||
assert!(s
|
||||
.eval::<bool>(r#"return UnitInRaid("mouseover") == nil"#)
|
||||
.unwrap());
|
||||
// **The input partition, and it is NOT "never raises".** This test used to assert that a
|
||||
// missing, wrong-typed or unknown token is all nil, on a wow-re finding that published "no
|
||||
// error path". That claim was refuted by a later §5 cross-check and corrected at the source
|
||||
// (`raid-roster-bindings.md` §1: *"The grammar is now enumerated and it settles the other
|
||||
// way"*), which named `UnitInRaid` as one of exactly three verbs carrying the wrong claim.
|
||||
// **The input partition, and it is NOT "never raises".** A quiet nil covers a missing,
|
||||
// wrong-typed, or a recognised-but-unmatched token; a token matching none of the
|
||||
// resolver's prefixes raises instead.
|
||||
//
|
||||
// Quiet nil: `0x6f3690` returns NULL for a missing or uncoercible argument, `0x515970` maps
|
||||
// NULL/empty to GUID `0:0`, and `0x4baee0` short-circuits `0:0` to false at entry.
|
||||
@@ -1237,7 +1231,7 @@ mod tests {
|
||||
|
||||
/// **`IsRaidLeader()` is true for an ordinary 5-man party leader.** Its body reads the same
|
||||
/// two leader-GUID globals `IsPartyLeader` does and carries no raid-vs-party flag at all
|
||||
/// (`0x4bb8c0` vs `0x4e9130`, both prologues re-read by wow-re's orchestrator). Answering nil
|
||||
/// (`0x4bb8c0` vs `0x4e9130`). Answering nil
|
||||
/// "because there is no raid" would be the divergence.
|
||||
#[test]
|
||||
fn is_raid_leader_is_true_for_a_party_leader() {
|
||||
@@ -1362,9 +1356,9 @@ mod tests {
|
||||
}
|
||||
|
||||
/// `IsRaidOfficer()` still answers nil, and the raid arc landing (decision 1549) did NOT
|
||||
/// change that — it is not waiting on a feature, it is waiting on a carve of `0x4bb910`'s own
|
||||
/// body. The binding's doc carries the refusal; this pins the behaviour so a later "surely it
|
||||
/// is just rank >= 1" edit has to argue with a test.
|
||||
/// change that — it is not waiting on a feature, it is waiting on a reading of `0x4bb910`'s
|
||||
/// own body. The binding's doc carries the refusal; this pins the behaviour so a later
|
||||
/// "surely it is just rank >= 1" edit has to argue with a test.
|
||||
///
|
||||
/// What it costs today, stated where someone will find it: `RaidFrameAddMemberButton` is
|
||||
/// enabled for the raid LEADER only, where the reference also enables it for an assistant.
|
||||
@@ -1759,7 +1753,7 @@ mod tests {
|
||||
|
||||
/// A "not ready" answer prints `RAID_MEMBER_NOT_READY` for that member the moment it arrives —
|
||||
/// and clears their flag all the same, because the leader arm stores the constant 0 whatever
|
||||
/// the byte says (`0x4ba40e`, wow-re `readycheck-response-store.md`). A guid the roster does
|
||||
/// the byte says (`0x4ba40e`). A guid the roster does
|
||||
/// not hold writes nothing.
|
||||
#[test]
|
||||
fn a_not_ready_answer_prints_its_line_and_still_clears_the_member() {
|
||||
|
||||
@@ -102,14 +102,14 @@ pub(crate) struct StoredPetAction {
|
||||
}
|
||||
|
||||
/// The hunter-pet stat block behind `GetPetHappiness`/`GetPetLoyalty`/`GetPetTrainingPoints`/
|
||||
/// `GetPetExperience` and `HasPetUI`'s second return (decision 1005; wow-re §11b), plus the two
|
||||
/// `GetPetExperience` and `HasPetUI`'s second return (`0x4be670`; decision 1005), plus the two
|
||||
/// **family**-derived answers `UnitCreatureFamily("pet")` and `GetPetFoodTypes()` (decision 1062).
|
||||
///
|
||||
/// **The four stat bindings share one gate** — `0x6116e0(pet)`, "is this a hunter's pet" — which is
|
||||
/// why they share one pushed struct: a warlock's imp resolves perfectly well and still answers
|
||||
/// nothing, because happiness, loyalty and training points are hunter machinery.
|
||||
///
|
||||
/// **The two family fields sit on OPPOSITE sides of that gate, and the split is carved.**
|
||||
/// **The two family fields sit on OPPOSITE sides of that gate.**
|
||||
/// [`Self::family`] is outside it — `UnitCreatureFamily 0x51a310` has no class test, so a warlock
|
||||
/// minion shows "Imp" and gating it would blank a line the reference fills. [`Self::food_types`] is
|
||||
/// inside it — `GetPetFoodTypes 0x4bea10` shares `0x6116e0` with the four stats.
|
||||
@@ -155,8 +155,8 @@ pub struct PetStats {
|
||||
/// four lookup-miss paths as [`Self::family`], which is why it sits beside it.
|
||||
///
|
||||
/// **Outside the hunter gate, like [`Self::family`] and unlike [`Self::food_types`] — and that
|
||||
/// placement is INFERRED, not carved** (decision 1676). `GetPetIcon 0x4beb10` is registered
|
||||
/// adjacent to both in the same table (wow-re §11b's neighbour list) and has not been read.
|
||||
/// placement is INFERRED** (decision 1676). `GetPetIcon 0x4beb10` is registered adjacent to
|
||||
/// both in the same table and has not been read.
|
||||
/// It is grouped with the family word because it is a pure family-row lookup like that one,
|
||||
/// where the diet's gate is shared with the four *stat* bindings. The choice is unobservable
|
||||
/// in the reference's own call sites — every one of them (`PetStable.lua:51`, `161-162`) sits
|
||||
@@ -522,7 +522,7 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
)?;
|
||||
|
||||
// UnitCreatureFamily(unit) → the localized family word, or NIL. Exactly ONE return on every
|
||||
// path (`0x51a310`, wow-re-VERIFIED) — decision 1062.
|
||||
// path (`0x51a310`) — decision 1062.
|
||||
//
|
||||
// **Scoped to the `"pet"` token, and that narrowing is stated rather than hidden.** The real
|
||||
// binding resolves any unit and reads `[[unit+0xb30]+0x1c]` off its cached creature-query
|
||||
@@ -533,9 +533,9 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
// feed resolves it explicitly and the other tokens wait for a second consumer — at which point
|
||||
// this moves onto `UnitState` beside `creature_type_name`.
|
||||
//
|
||||
// Note there is NO class gate here: a warlock's imp answers "Imp". That is the carved shape,
|
||||
// and it is what makes the family word and `GetPetFoodTypes` below behave differently for the
|
||||
// same pet.
|
||||
// Note there is NO class gate here: a warlock's imp answers "Imp". That is the shape, and it
|
||||
// is what makes the family word and `GetPetFoodTypes` below behave differently for the same
|
||||
// pet.
|
||||
//
|
||||
// All four of the reference's nil paths arrive as one pushed `None` — no cached record, id 0,
|
||||
// id past the table, and a null row (ids 10/13/14/18/22 are absent from the shipped file). A
|
||||
@@ -595,10 +595,10 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
)?;
|
||||
|
||||
// PetStopAttack() — queue the call-off. No argument: the wire carries only the pet's guid.
|
||||
// The six one-shot orders (`0x4be450`…`0x4be4a0`, wow-re `pet-action-bar-api.md` §10.9,
|
||||
// carved 2026-09-03): each writes the literal slot word the bar's own reaction or command
|
||||
// slot carries — `0x06000000`/1/2 PASSIVE/DEFENSIVE/AGGRESSIVE, `0x07000000`/1/2
|
||||
// WAIT/FOLLOW/ATTACK — and re-enters `0x4bd1d0`, the identical call a `CastPetAction` press
|
||||
// The six one-shot orders (`0x4be450`…`0x4be4a0`): each writes the literal slot word the
|
||||
// bar's own reaction or command slot carries — `0x06000000`/1/2 PASSIVE/DEFENSIVE/AGGRESSIVE,
|
||||
// `0x07000000`/1/2 WAIT/FOLLOW/ATTACK — and re-enters `0x4bd1d0`, the identical call a
|
||||
// `CastPetAction` press
|
||||
// makes, PetAttack with the current selection's guid as a bar ATTACK click would. No
|
||||
// arguments, no returns; the no-pet gate is silent, so a session without a pet bar has
|
||||
// nothing to order and drops it, as the bar's own press would (1958; §10.9 confirmed the
|
||||
@@ -686,11 +686,11 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
// PetRename(name) — the PETRENAMECONFIRM popup's OnAccept, carrying the text the RENAME_PET
|
||||
// edit box collected.
|
||||
//
|
||||
// The argument reaches `lua_tostring`, so a NUMBER coerces (wow-re §11c) — `mlua::String`
|
||||
// accepts one the same way. What the reference does with the RESULT is the app's business and
|
||||
// is queued for it rather than dropped here: an empty name raises `ERR_NULL_PETNAME` and an
|
||||
// over-long one is truncated, both at the send. Only a missing or unconvertible argument dies
|
||||
// here, because there is nothing to queue.
|
||||
// The argument reaches `lua_tostring 0x6f3690`, which coerces a NUMBER via `0x6f7c80` —
|
||||
// `mlua::String` accepts one the same way. What the reference does with the RESULT is the
|
||||
// app's business and is queued for it rather than dropped here: an empty name raises
|
||||
// `ERR_NULL_PETNAME` and an over-long one is truncated, both at the send. Only a missing or
|
||||
// unconvertible argument dies here, because there is nothing to queue.
|
||||
g.set(
|
||||
"PetRename",
|
||||
lua.create_function(|lua, name: Option<mlua::String>| {
|
||||
@@ -1109,9 +1109,9 @@ mod tests {
|
||||
.unwrap());
|
||||
}
|
||||
|
||||
/// **Bucket 0 is a number, not nil**, and this is the trap the RE calls out by name: the
|
||||
/// shipped `PetFrame.lua` branches on 1/2/3 and hides only on `not happiness`, so a 0 must
|
||||
/// leave the icon up. Collapsing it into the failure path hides a frame the reference shows.
|
||||
/// **Bucket 0 is a number, not nil** (`GetPetHappiness 0x4be900`): the shipped `PetFrame.lua`
|
||||
/// branches on 1/2/3 and hides only on `not happiness`, so a 0 must leave the icon up.
|
||||
/// Collapsing it into the failure path hides a frame the reference shows.
|
||||
#[test]
|
||||
fn happiness_bucket_zero_is_not_the_failure_case() {
|
||||
let mut s = UiScript::new().unwrap();
|
||||
|
||||
@@ -8,10 +8,8 @@
|
||||
//! that plain data; every verb queues a [`PetitionRequest`] the app drains
|
||||
//! ([`UiScript::take_petition_requests`]). No ECS or net reach from the engine (decision 0068 §3).
|
||||
//!
|
||||
//! Every contract below is **VERIFIED at the bytes** — wow-re
|
||||
//! `system/ui/scratch/petition-charter-api.md`, a §5 round commissioned by this slice, which carved
|
||||
//! the whole `PetitionInfo.cpp` TU (`0x84cfb8`). Where it corrected what this file first shipped,
|
||||
//! the correction is named on the binding.
|
||||
//! Every contract below is byte-verified against the whole `PetitionInfo.cpp` TU (`0x84cfb8`).
|
||||
//! Where it corrected what this file first shipped, the correction is named on the binding.
|
||||
//!
|
||||
//! **The snapshot mirrors the module's own `.data` state, and that is why it looks split.** The
|
||||
//! real client keeps the signature list (`[0xbdce20]` + the `0x10`-stride signer array) and the
|
||||
@@ -185,7 +183,7 @@ impl super::UiScript {
|
||||
/// GlobalStrings key of the line to show, and **no packet is built**.
|
||||
///
|
||||
/// **Partial, deliberately, and here is exactly how far it goes.** The code space belongs to
|
||||
/// `0x6c9b70`, which that round did not carve, so only the checks whose meaning is unambiguous from
|
||||
/// `0x6c9b70`, which is not read in full, so only the checks whose meaning is unambiguous from
|
||||
/// the message keys are implemented — empty, a leading or trailing space, and consecutive spaces.
|
||||
/// The three that need data we do not have (`ERR_GUILD_NAME_TOO_SHORT`'s minimum,
|
||||
/// `ERR_GUILD_NAME_PROFANE`'s word list, `ERR_GUILD_NAME_MIXED_LANGUAGES`' script rules) **pass**
|
||||
@@ -194,7 +192,7 @@ impl super::UiScript {
|
||||
/// sending blindly is the case the reference makes loudest and the server answers with silence —
|
||||
/// clicking Purchase with an empty box.
|
||||
///
|
||||
/// The full table, from the note, with what each maps to:
|
||||
/// The reference's full table of codes, with what each maps to:
|
||||
///
|
||||
/// | code | key | implemented |
|
||||
/// |---|---|---|
|
||||
@@ -651,8 +649,8 @@ mod tests {
|
||||
Err("ERR_GUILD_NAME_NAME_CONSECUTIVE_SPACES")
|
||||
);
|
||||
// Passed through on purpose: the minimum length, the profanity list and the script rules
|
||||
// all live inside a function that round did not carve, and refusing a name the server would
|
||||
// accept is the worse failure. The server re-checks every one of them.
|
||||
// all live inside `0x6c9b70`, which is not read in full, and refusing a name the server
|
||||
// would accept is the worse failure. The server re-checks every one of them.
|
||||
for ok in ["Legacy of Steel", "A", "Ab", "Éclair", "x y z"] {
|
||||
assert_eq!(
|
||||
validate_guild_name(ok),
|
||||
|
||||
@@ -6,10 +6,11 @@
|
||||
//! effective-visible frame whose resolved rect (and effective ScrollFrame clip, decision 0112
|
||||
//! §5) contains the point. Pure query; fires nothing, mutates nothing.
|
||||
//! - [`UiScript::mouse_move`] hit-tests every move and, on a captured-frame **change**, fires the
|
||||
//! `OnLeave`/`OnEnter` pair (RF-0025's `motion=true` hover boundary) — and, before that,
|
||||
//! advances an armed drag gesture, an in-flight **Slider thumb drag**, and an in-flight
|
||||
//! **frame move** (`StartMoving`, [`super::object`]'s `movable` cluster). All three move within
|
||||
//! one frame, crossing no hover boundary, so they must run before the boundary test.
|
||||
//! `OnLeave`/`OnEnter` pair (the reference's own `motion=true` hover-boundary argument) — and,
|
||||
//! before that, advances an armed drag gesture, an in-flight **Slider thumb drag**, and an
|
||||
//! in-flight **frame move** (`StartMoving`, [`super::object`]'s `movable` cluster). All three
|
||||
//! move within one frame, crossing no hover boundary, so they must run before the boundary
|
||||
//! test.
|
||||
//! - [`UiScript::mouse_button`] resolves `OnMouseDown`/`OnMouseUp`, the `OnClick`
|
||||
//! press/release-registration rules ([`button::wants_click`]), the **drag trio** + **world
|
||||
//! drop** below, the **Slider thumb-drag** capture (a left press on a thumb, decision 0250 §5),
|
||||
@@ -55,8 +56,7 @@ use super::{button, cursor, editbox, event, Model, UiScript};
|
||||
|
||||
/// The double-click interval — **300 ms, VERIFIED off the bytes**, a hardcoded instruction
|
||||
/// immediate: `0x77937b 81 f9 2c 01 00 00 cmp ecx, 0x12c`, comparing `now − [this+0x334]` inside
|
||||
/// the Button mouse-UP dispatcher `0x7792d0` (wow-re `ui/scratch/button-doubleclick-law.md`, a §5
|
||||
/// cross-check dispatched from here).
|
||||
/// the Button mouse-UP dispatcher `0x7792d0`.
|
||||
///
|
||||
/// The units are verified too, not assumed: `now` comes from `0x42c010` → `0x42b790`, whose
|
||||
/// scale is stored as `1.0/freq × 1000.0` and whose counter is `KERNEL32!GetTickCount` (import
|
||||
@@ -67,8 +67,8 @@ use super::{button, cursor, editbox, event, Model, UiScript};
|
||||
/// pass `style = 0x20` = `CS_OWNDC` alone — and `GetDoubleClickTime` is not imported), it is not a
|
||||
/// CVar (no such `.rdata` string; nothing near the compare reaches `CVar::Register 0x63db90`), and
|
||||
/// there is **no position gate** (the `0x766122`–`0x766140` "double-click detector" this was first
|
||||
/// chased through is really the zero-motion cursor-move coalescing filter — that gloss in
|
||||
/// `world-click-drag-arbitration.md` l.238 is refuted and corrected). The only spatial constraint
|
||||
/// chased through is really the zero-motion cursor-move coalescing filter). The only spatial
|
||||
/// constraint
|
||||
/// is that each release hit-tests inside the frame, applied independently; the two clicks may land
|
||||
/// arbitrarily far apart.
|
||||
///
|
||||
@@ -77,10 +77,10 @@ pub(super) const DOUBLE_CLICK_SECS: f64 = 0.300;
|
||||
|
||||
// ── Input / hit-testing (decision 0068; spec-faithful, not byte-pinned) ─────────────────────
|
||||
//
|
||||
// The mouse-focus model (`propagation.md`): focus walks frames top-down in reverse draw order;
|
||||
// the first mouse-enabled, effective-visible frame whose resolved rect contains the cursor
|
||||
// The reference's mouse-focus model: focus walks frames top-down in reverse draw order; the
|
||||
// first mouse-enabled, effective-visible frame whose resolved rect contains the cursor
|
||||
// captures. This is faithful to the *documented model*, **not** bit-exact — the real hit-test
|
||||
// leaf is a flagged RE-time item, not byte-pinned (`anchors.md`; see [`crate::order::hit_test`]).
|
||||
// leaf is not byte-pinned (see [`crate::order::hit_test`]).
|
||||
// Coordinates are WoW UI space (y-up, pixels) — the same space [`resolve`]/[`extract`] use.
|
||||
// Call [`resolve`] first so rects are populated; a frame with no resolved rect never captures.
|
||||
|
||||
@@ -129,10 +129,10 @@ pub(super) fn install(lua: &mlua::Lua) -> mlua::Result<()> {
|
||||
}
|
||||
|
||||
/// Whether `h` is the world frame — the one frame whose mouse hits belong to the 3D world
|
||||
/// (decisions 1983/1984, `worldframe-widget.md`: "the hover walk probes strata 8→0 and the world
|
||||
/// frame wins exactly when nothing else mouse-enabled is under the cursor; the press runs the
|
||||
/// frame's Lua `OnMouseDown` (never consuming), then the `BUTTON1`/`BUTTON2` binding — which
|
||||
/// **is** the world click; the release mirrors it").
|
||||
/// (decisions 1983/1984: the hover walk `0x7661cd` inside `0x7660d0` probes strata 8→0, and the
|
||||
/// world frame wins exactly when nothing else mouse-enabled is under the cursor; the press runs
|
||||
/// the frame's Lua `OnMouseDown` — never consuming (`0x483c40`) — then the `BUTTON1`/`BUTTON2`
|
||||
/// binding, which **is** the world click; the release mirrors it).
|
||||
fn is_world_handle(model: &Model, h: FrameHandle) -> bool {
|
||||
model
|
||||
.arena
|
||||
@@ -202,7 +202,7 @@ impl UiScript {
|
||||
// `ChatFrame1` is never in the hit-test index — instead the engine synthesises one
|
||||
// mouse-enabled `CSimpleHyperlinkButton` child PER SPAN (`0x7a3240`, born through the
|
||||
// `CSimpleButton` ctor) and routes the three hyperlink scripts back to the parent
|
||||
// (wow-re `mouse-enable-law.md` + the follow-up round `68987021`).
|
||||
// through vtable slots `+0x90`/`+0x94`/`+0x98`.
|
||||
//
|
||||
// We hold the spans as rects on the parent rather than as child frames, so the same
|
||||
// law expresses as a disjunct here. What it buys is everything the reference gets from
|
||||
@@ -235,10 +235,11 @@ impl UiScript {
|
||||
let scroll_sources = scroll_clip_sources(&model);
|
||||
let fh = order::hit_test(&sorted, |fh| {
|
||||
// **The wheel gates on the handler and CONTINUES** — the one genuine fall-through in
|
||||
// the engine (wow-re `ui/scratch/hittest-no-fallthrough-law.md`). Down/up deliberately
|
||||
// do not use the idiom; the wheel does. So this asks for the WHEEL flag alone and lets
|
||||
// the sweep walk past a frame that merely takes the mouse, which is what puts the wheel
|
||||
// through a scroll pane's chrome and into the pane.
|
||||
// the engine (`0x76c180`, gated on `[frame+0x160]`'s OnMouseWheel chunk being
|
||||
// present). Down/up deliberately do not use the idiom; the wheel does. So this asks
|
||||
// for the WHEEL flag alone and lets the sweep walk past a frame that merely takes the
|
||||
// mouse, which is what puts the wheel through a scroll pane's chrome and into the
|
||||
// pane.
|
||||
//
|
||||
// It used to also accept `is_mouse_enabled` as a stand-in, because the loader did not
|
||||
// arm the wheel kind and no frame ever carried the flag. The loader arms it now.
|
||||
@@ -328,8 +329,7 @@ impl UiScript {
|
||||
// used to sit here claimed the opposite off two mis-attributed addresses:
|
||||
// `0x7793f0` is the drag-start override, not a leave, and `0x7791e0` is the hide
|
||||
// override, not an enter. The real enter/leave notifies `0x779490`/`0x7794e0`
|
||||
// read `[+0x328]` only as a DISABLED guard and never write it (wow-re
|
||||
// `scratch/button-state-edge-set.md`, a §5 census of every store to the field).
|
||||
// read `[+0x328]` only as a DISABLED guard and never write it.
|
||||
//
|
||||
// So a press held over a button and walked off it stays PUSHED in the reference —
|
||||
// it is the *drag threshold* (below) that un-presses one, and only for a frame
|
||||
@@ -398,7 +398,8 @@ impl UiScript {
|
||||
///
|
||||
/// **False whenever the region has no resolved rect**, which is the common case and the correct
|
||||
/// one: a freshly created title region has NO anchors at all and does nothing until
|
||||
/// `SetPoint`/`SetAllPoints` (Q6). Both corpus consumers call `SetAllPoints` immediately, so
|
||||
/// `SetPoint`/`SetAllPoints` (`CreateTitleRegion 0x773910`). Both corpus consumers call
|
||||
/// `SetAllPoints` immediately, so
|
||||
/// theirs covers the whole window — the whole-window-as-drag-handle idiom.
|
||||
fn title_region_hit(&self, h: crate::widget::FrameHandle, x: f32, y: f32) -> bool {
|
||||
let model = self.model_ref();
|
||||
@@ -434,8 +435,8 @@ impl UiScript {
|
||||
/// ([`cursor::world_drop_click`]) fires ONLY on a completed left CLICK over the world —
|
||||
/// press AND release both [`over_world`], no started drag (the byte-verified trigger: the
|
||||
/// client's `0x495300` runs on the WorldFrame click release; a drag release routes as a
|
||||
/// drag, never a click — wow-re cursor-dragdrop-payload.md, and the director's report it
|
||||
/// confirmed) — routed by the app-fed pick (decisions 0571 + 0574): over an object nothing
|
||||
/// drag, never a click — and the director's report confirmed it) — routed by the app-fed
|
||||
/// pick (decisions 0571 + 0574): over an object nothing
|
||||
/// drops (the reference keeps an item payload and selects); over terrain an item fires
|
||||
/// `DELETE_ITEM_CONFIRM` and stays held while a spell/action survives; over nothing the
|
||||
/// item pops and any other payload clears silently. A drag released over nothing keeps its
|
||||
@@ -448,11 +449,10 @@ impl UiScript {
|
||||
let hit_id = self.hit_test(x, y);
|
||||
// ── The RAISE, and it happens FIRST ─────────────────────────────────────────────────────
|
||||
//
|
||||
// wow-re ledger `0x7662c0` (VERIFIED, the `toplevel-raise.md` §5 fan-out): the
|
||||
// mouse-button-DOWN handler "resolves target = root+0x80 (existing capture) else
|
||||
// root+0x7c (hover frame), calls `0x76a5b0` Raise() **UNGUARDED** @`0x766392` (the
|
||||
// toplevel gate is inside `0x7650f0`, so the raised frame may be an ancestor of the
|
||||
// clicked one), then title-region drag or capture+OnMouseDown".
|
||||
// `0x7662c0`: the mouse-button-DOWN handler resolves target = root+0x80 (existing
|
||||
// capture) else root+0x7c (hover frame), calls `0x76a5b0` Raise() **UNGUARDED**
|
||||
// @`0x766392` (the toplevel gate is inside `0x7650f0`, so the raised frame may be an
|
||||
// ancestor of the clicked one), then title-region drag or capture+OnMouseDown.
|
||||
//
|
||||
// Four clauses, each load-bearing and each visible:
|
||||
//
|
||||
@@ -483,7 +483,7 @@ impl UiScript {
|
||||
// ordinary path: the reference's `0x7662c0` tests `titleRegion+0x24` point-in-rect FIRST
|
||||
// and only a MISS falls through to `0x7663e6`, which sets the captured frame and dispatches
|
||||
// `vtable[0x68]` — the OnMouseDown script. So a hit takes `OnMouseDown`/`OnMouseUp` and
|
||||
// `RegisterForDrag`'s `OnDragStart` with it (wow-re `widget-api-batch-benilla.md` Q6).
|
||||
// `RegisterForDrag`'s `OnDragStart` with it.
|
||||
//
|
||||
// Returning early is what implements that swallow: no OnMouseDown, no drag arm, no OnClick,
|
||||
// no double-click bookkeeping. It reports the event CONSUMED, because it was.
|
||||
@@ -674,8 +674,8 @@ impl UiScript {
|
||||
// ── The double-click adjudication — on the RELEASE edge, on the click that just
|
||||
// qualified, and STRICTLY EXCLUSIVE with it.
|
||||
//
|
||||
// All three of those are byte-verified (wow-re `ui/scratch/button-doubleclick-law.md`):
|
||||
// the only site that fires the `+0x4d4` slot is `0x77938d`, inside the mouse-UP
|
||||
// All three of those are byte-verified: the only site that fires the `+0x4d4`
|
||||
// slot is `0x77938d`, inside the mouse-UP
|
||||
// dispatcher `0x7792d0` — the mouse-DOWN dispatcher `0x779210` has no double-click
|
||||
// leg at all — and `0x77939d jmp 0x7793b5` skips *past* the single leg's
|
||||
// `call [edx+0x94]`, so **`OnDoubleClick` REPLACES the second `OnClick`; it is not
|
||||
@@ -736,13 +736,11 @@ impl UiScript {
|
||||
// ── `OnMouseUp` goes to the frame that took the PRESS, not to whatever is under the
|
||||
// cursor now — the mouse CAPTURE (`root+0x80`).
|
||||
//
|
||||
// The client's mouse-DOWN handler `0x7662c0` takes the pointer: "resolves target =
|
||||
// The client's mouse-DOWN handler `0x7662c0` takes the pointer: resolves target =
|
||||
// root+0x80 (existing capture) else root+0x7c (hover frame) … then title-region drag or
|
||||
// **capture**+OnMouseDown" (wow-re ledger `0x7662c0`).
|
||||
// **capture**+OnMouseDown (`0x7662c0`).
|
||||
//
|
||||
// The **release** half is `0x766420`, and it is VERIFIED — wow-re
|
||||
// `system/ui/scratch/mouseup-dispatch-law.md`, a §5 dispatched from this repo for exactly
|
||||
// this line after it first landed on an extrapolation (decision 1599). It reads the capture
|
||||
// The **release** half is `0x766420` (decision 1599). It reads the capture
|
||||
// and **nothing else**: `[mgr+0x80]` is snapshotted into `ebx` at entry (`0x76642b`),
|
||||
// before anything, and `[mgr+0x7c]` — the hover frame — is **never read** anywhere in
|
||||
// `[0x766420, 0x7664f0)`. One dispatch, `0x7664a4 call [eax+0x6c]`.
|
||||
@@ -767,7 +765,7 @@ impl UiScript {
|
||||
// was a divergence that let a press over open space deliver a release to whatever the
|
||||
// cursor had wandered onto.
|
||||
//
|
||||
// (Two things the same §5 settled that are invisible from here: the capture is cleared at
|
||||
// (Two more facts about this dispatch, invisible from here: the capture is cleared at
|
||||
// `0x7664bb`, **after** the dispatch — so a C++ consumer inside `OnMouseUp` still sees it,
|
||||
// though no Lua binding reads it, `GetMouseFocus 0x48df40` being `+0x7c`; and the clear is
|
||||
// gated on `evt+0x18 == 0`, the button mask *after* the event, so a **chorded** release
|
||||
@@ -904,8 +902,8 @@ impl UiScript {
|
||||
let target: Option<u32> = {
|
||||
// **Wheel-enabled OR mouse-enabled**, not mouse-enabled alone. The reference's wheel
|
||||
// dispatcher `0x7664f0` walks its OWN index and calls the hit probe directly, ignoring
|
||||
// hover, capture and the mouse bit — `EnableMouse` is not a prerequisite
|
||||
// (wow-re `mouse-enable-law.md` §6). Ours went through the mouse hit test, which
|
||||
// hover, capture and the mouse bit — `EnableMouse` is not a prerequisite. Ours went
|
||||
// through the mouse hit test, which
|
||||
// coupled the two: correcting the ctor list to the reference's (ScrollFrame is inert
|
||||
// on the mouse bit) silently stopped every scroll pane from taking the wheel.
|
||||
let hit = self.hit_test_wheel(x, y);
|
||||
@@ -941,7 +939,7 @@ impl UiScript {
|
||||
/// Point-in-rect for hit-testing, in WoW UI coords (**y-up**): the cursor `(x, y)` is inside `r` when
|
||||
/// it lies within `[left, right] × [bottom, top]` (edges inclusive). This is the same space
|
||||
/// [`UiScript::resolve`]/[`UiScript::extract`] produce rects in — the verified `point_in_rect
|
||||
/// 0x76b020` primitive (`propagation.md`), modeled on our [`Rect`] convention.
|
||||
/// 0x76b020` primitive, modeled on our [`Rect`] convention.
|
||||
pub(super) fn point_in_rect(r: Rect, x: f32, y: f32) -> bool {
|
||||
x >= r.left && x <= r.right && y >= r.bottom && y <= r.top
|
||||
}
|
||||
|
||||
@@ -45,7 +45,8 @@
|
||||
//! −4; the *binding* subtracts 5 instead of negating, so it gives rank 1 → **−4** and rank 4 →
|
||||
//! **−1**. They genuinely disagree, and the client is the authority for what the client's binding
|
||||
//! returns — [`visual_rank`] implements the binding. Nothing here reads the server's form, and a
|
||||
//! reader "fixing" this back to `-rank` would be reverting a §5 cross-check.
|
||||
//! reader "fixing" this back to `-rank` would be undoing a verified read of [`visual_rank`]'s own
|
||||
//! bytes.
|
||||
//!
|
||||
//! Conflating the two scales puts Sergeant's badge on a Corporal, so [`visual_rank`] is the one
|
||||
//! place the conversion happens and every caller goes through it. The trap is that both numbers
|
||||
@@ -91,9 +92,9 @@
|
||||
//! `GetPVPRankInfo` calls the first. **A client that gendered the pane's title would be more
|
||||
//! "correct" than the reference and would diverge from it**, which is not what we build.
|
||||
//!
|
||||
//! ## The counter-intuitive four (wow-re `system/ui/scratch/honor-panel-law.md`, §5, 2026-08-21)
|
||||
//! ## The counter-intuitive four
|
||||
//!
|
||||
//! Everything below is byte law from that carve, and every one of them looks like a bug:
|
||||
//! Everything below is byte law, and every one of them looks like a bug:
|
||||
//!
|
||||
//! 1. `GetPVPRankProgress` **multiplies by a slightly-wrong f32 reciprocal** and does not clamp —
|
||||
//! [`rank_progress`].
|
||||
@@ -184,7 +185,7 @@ pub struct InspectHonorData {
|
||||
/// The reply's `highestRank` byte — `GetInspectHonorData`'s twelfth return, which the
|
||||
/// reference names `lifetimeRank`. Internal scale, like every other rank here.
|
||||
///
|
||||
/// **Passed through, where its self-side twin is suppressed below 5.** The carve pins
|
||||
/// **Passed through, where its self-side twin is suppressed below 5.** This pins
|
||||
/// `GetPVPLifetimeStats`'s `cmp al,5; jb` at `0x51a843` and records `0x4c9620` as twelve plain
|
||||
/// pushes with no such gate, so this one is not filtered here. That is a *recorded absence*
|
||||
/// rather than an asserted negative: if a re-read of `0x4c9620` ever turns up the same
|
||||
@@ -257,7 +258,7 @@ impl super::UiScript {
|
||||
///
|
||||
/// **Gendered is right *here* and wrong in the pane**, which is the whole reason this is not
|
||||
/// simply `GetPVPRankInfo`'s lookup: the credit formatter resolves through `0x612bf0`
|
||||
/// (§4.3-VERIFIED) while `GetPVPRankInfo` passes gender 0. It shares [`rank_title_gendered`]
|
||||
/// while `GetPVPRankInfo` passes gender 0. It shares [`rank_title_gendered`]
|
||||
/// with `UnitPVPName`, the engine's other gendered caller.
|
||||
///
|
||||
/// The number `SMSG_PVP_CREDIT` carries is the **INTERNAL** rank — the GlobalString index
|
||||
@@ -265,8 +266,8 @@ impl super::UiScript {
|
||||
/// badge indexes; see the module doc's "Two ranks". Unlike the pane's binding this path applies
|
||||
/// **no range check**, so rank 19 legitimately names "Leader" here.
|
||||
///
|
||||
/// Two facts the caller owns, both from the same carve, because neither is expressible in this
|
||||
/// signature: the gender is the **local player's** while the team is the **victim's**
|
||||
/// Two facts the caller owns, neither expressible in this signature: the gender is the
|
||||
/// **local player's** while the team is the **victim's**
|
||||
/// (`0x625374` hands `0x612bf0` the entry `this`, still the local player), and a victim whose
|
||||
/// team resolves to **−1 emits no line at all** (`0x625321 js` bails the whole formatter) —
|
||||
/// which is a `u8` here precisely so that decision cannot be smuggled into a key that misses.
|
||||
@@ -306,8 +307,8 @@ const RANK_BAR_SCALE: f64 = f32::from_bits(0x3B80_8081) as f64;
|
||||
/// The rank progress bar's byte → the fraction the reference feeds straight to a `StatusBar` whose
|
||||
/// `minValue`/`maxValue` are `0`/`1`.
|
||||
///
|
||||
/// **A multiply by [`RANK_BAR_SCALE`], not a division by 255, and no clamp** — both halves are the
|
||||
/// §5 verdict correcting the natural reading, and both are deliberate:
|
||||
/// **A multiply by [`RANK_BAR_SCALE`], not a division by 255, and no clamp** — both halves are
|
||||
/// verified at the bytes, correcting the natural reading, and both are deliberate:
|
||||
///
|
||||
/// - `0x51aace` is `fild DWORD` then `fmul DWORD PTR ds:0x8026c8`. Under the client's PC_53 x87
|
||||
/// invariant `(double)b * (double)K` and `(double)b / 255.0` differ for **255 of the 256** byte
|
||||
|
||||
@@ -2,9 +2,8 @@
|
||||
//!
|
||||
//! Several of these pin facts that look like bugs — the multiply-not-divide rank bar, the
|
||||
//! backwards negative visual ranks, rank 19's rejection, the ungendered pane title, the suppressed
|
||||
//! sub-5 lifetime rank. They are wow-re `system/ui/scratch/honor-panel-law.md` (§5, 2026-08-21)
|
||||
//! and each one would pass just as happily against the wrong reading if it were written loosely,
|
||||
//! so they are written tightly on purpose.
|
||||
//! sub-5 lifetime rank. Each is verified at the bytes, and each one would pass just as happily
|
||||
//! against the wrong reading if it were written loosely, so they are written tightly on purpose.
|
||||
|
||||
use super::*;
|
||||
use crate::script::{UiScript, UnitState};
|
||||
@@ -936,7 +935,7 @@ fn has_inspect_honor_data_latches_on_the_push_and_clears_with_it() {
|
||||
assert!(s.eval::<bool>(held()).unwrap());
|
||||
|
||||
// `0x4c95e0` pushes the NUMBER 1, not a boolean — one value either way, and the truthy arm is
|
||||
// `1` rather than `true`. (The carve says `lua_pushnumber(1.0)`; in a VM whose numbers are all
|
||||
// `1` rather than `true`. (It is `lua_pushnumber(1.0)`; in a VM whose numbers are all
|
||||
// doubles there is no observable integer/float distinction, and `tostring` proves it.)
|
||||
assert_eq!(
|
||||
s.eval::<String>("return type(HasInspectHonorData())")
|
||||
@@ -1007,7 +1006,7 @@ fn get_inspect_honor_data_answers_twelve_zeros_when_no_reply_is_held() {
|
||||
|
||||
// And after the app drops the reply it is zeros again — our `Option` is the latch AND the
|
||||
// data, where the engine keeps them apart and would answer the previous target's numbers
|
||||
// here (§5.1/§5.2: re-keying zeroes the two flags and leaves the sixteen slots alone).
|
||||
// here (`0x4c6f70` re-keying zeroes the two flags and leaves the sixteen slots alone).
|
||||
s.set_inspect_honor(Some(inspect_state()));
|
||||
s.set_inspect_honor(None);
|
||||
assert_eq!(
|
||||
|
||||
@@ -74,7 +74,7 @@ pub struct QuestItemView {
|
||||
/// the name and icon `GetRewardSpell` / `GetQuestLogRewardSpell` answer (stock
|
||||
/// `QuestFrameItems_Update` counts it as one more reward slot, `rewardType = "spell"`, and the
|
||||
/// slot's hover is `GameTooltip:SetQuestRewardSpell()`). `tradeskill` is the third return the
|
||||
/// reference derives from the spell itself (1944 — the derivation is wow-re's to pin).
|
||||
/// reference derives from the spell itself (1944 — the derivation is not yet pinned).
|
||||
#[derive(Clone, Debug, Default, PartialEq)]
|
||||
pub struct QuestRewardSpell {
|
||||
pub spell_id: u32,
|
||||
@@ -114,7 +114,7 @@ pub struct QuestState {
|
||||
pub reward_spell: Option<QuestRewardSpell>,
|
||||
/// The panel's background material (`GetQuestBackgroundMaterial` — nil is the reference's own
|
||||
/// "Parchment" fallback in `QuestFrame_GetMaterial`). What fills it on the wire is 1944's
|
||||
/// wow-re question; until the binary says, nothing does, and the verb answers nil honestly.
|
||||
/// open question; until the binary says, nothing does, and the verb answers nil honestly.
|
||||
pub background_material: Option<String>,
|
||||
}
|
||||
|
||||
@@ -172,11 +172,11 @@ pub enum QuestAction {
|
||||
/// greeting's Goodbye (`HideUIPanel(QuestFrame)`), a UIPanel eviction. Its binding `0x501a10`
|
||||
/// calls the teardown `0x501130(0,1)` and **nothing else**; that routine's one send is
|
||||
/// `MSG_QUEST_PUSH_RESULT{DECLINE}` for a PLAYER source, so closing an NPC's window is
|
||||
/// network-silent (wow-re `system/ui/scratch/quest-share-flow.md` §4.1 caller table, §6).
|
||||
/// network-silent.
|
||||
///
|
||||
/// **Two actions, because the reference has two routines.** 1738 merged this into
|
||||
/// `DeclineQuest`'s action on the reading that both verbs are one routine; the note's caller
|
||||
/// table shows they share only the teardown, and the HELLO re-open lives in `DeclineQuest`'s
|
||||
/// `DeclineQuest`'s action on the reading that both verbs are one routine; the reference's
|
||||
/// callers show they share only the teardown, and the HELLO re-open lives in `DeclineQuest`'s
|
||||
/// fork alone. The merge made ESC on an NPC re-open its list — a multi-quest greeting could
|
||||
/// not be closed.
|
||||
Close,
|
||||
@@ -448,7 +448,7 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
// QuestChooseRewardError() — 0x5021a0, 0 args, 0 returns: the Complete button pressed with
|
||||
// choices on offer and none picked (stock `QuestRewardCompleteButton_OnClick`,
|
||||
// QuestFrame.lua:96-100). Sixteen bytes — `push 0x98; call 0x496720` — the game-error row
|
||||
// `ERR_QUEST_MUST_CHOOSE` (wow-re quest-material-reward-spell-bindings.md §2): the row's sound
|
||||
// `ERR_QUEST_MUST_CHOOSE`: the row's sound
|
||||
// cue `igQuestFailed` plays first, then kind 2 routes the GlobalStrings text through
|
||||
// `UI_ERROR_MESSAGE` — synchronously, as SignalEvent is — and UIErrorsFrame prints it. The
|
||||
// text is read from the same global the FrameXML shows, so a localised chain shows its own
|
||||
@@ -757,7 +757,7 @@ mod tests {
|
||||
|
||||
/// `DeclineQuest` and `CloseQuest` are TWO intents — two reference routines (`0x5013f0` and
|
||||
/// `0x501a10`) that share only the teardown `0x501130`; the giver re-open is `DeclineQuest`'s
|
||||
/// alone (wow-re `quest-share-flow.md` §4.1/§6).
|
||||
/// alone.
|
||||
#[test]
|
||||
fn decline_and_close_are_two_intents() {
|
||||
let mut s = UiScript::new().unwrap();
|
||||
|
||||
@@ -105,7 +105,7 @@ pub struct QuestLogEntryView {
|
||||
/// This row's detail pane — description, money, rewards, reward spell. **Per row, not per
|
||||
/// selection** (decision 2247): the reference's detail bindings read `ds:0xbb7480` (what
|
||||
/// `SelectQuestLogEntry 0x4dfae0` wrote, synchronously) and then PEEK the quest cache in the
|
||||
/// same call (`0x4e1130` -> `0xc0e1b0`/`0x562a40`, wow-re `ui/ledger.tsv`), so a
|
||||
/// same call (`0x4e1130` -> `0xc0e1b0`/`0x562a40`), so a
|
||||
/// select-then-read pair inside ONE frame answers about the row just selected. Carrying one
|
||||
/// detail for "the selection" instead made `SelectQuestLogEntry` inert until the next push:
|
||||
/// every entry of an addon's log walk answered with whichever row the snapshot happened to be
|
||||
@@ -174,8 +174,8 @@ pub struct QuestLogState {
|
||||
/// The ids of the quests folded under a collapsed header — in the log, absent from
|
||||
/// [`Self::entries`]. No getter indexes them; they exist for the **watch prune** alone, which
|
||||
/// in the reference scans the whole row array, hidden rows included (`0x4de7a7`–`0x4de80f`;
|
||||
/// a collapsed-group quest is still a row there, sorted past the visible window — wow-re
|
||||
/// `system/ui/scratch/questlog-list-rebuild.md` §7). Without them a collapse would read as the
|
||||
/// a collapsed-group quest is still a row there, sorted past the visible window). Without them
|
||||
/// a collapse would read as the
|
||||
/// quest leaving the log and silently drop its watch.
|
||||
pub hidden_quest_ids: Vec<u32>,
|
||||
}
|
||||
@@ -263,8 +263,7 @@ impl super::UiScript {
|
||||
}
|
||||
|
||||
/// The signed seconds remaining on a quest-log row's timer, or `None` when the row has no timer at
|
||||
/// all. **This is the reference's own formula**, byte-verified (wow-re
|
||||
/// `system/ui/scratch/quest-timer-law.md`, the 2026-08-09 §5 trio; decision 1154):
|
||||
/// all. **This is the reference's own formula**, byte-verified (decision 1154):
|
||||
///
|
||||
/// ```text
|
||||
/// value = slot.timer + G − now − 1
|
||||
@@ -327,8 +326,8 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
// `tag` is the bare word (`Elite`, `Raid`, …) or nil — the ref's Lua wraps it in the
|
||||
// parentheses itself (`"("..questTag..")"`, ref l.195). isComplete is 1 / -1 / nil.
|
||||
//
|
||||
// **The arity is always SIX, and the two failure shapes are different** (wow-re
|
||||
// `ui/scratch/questlog-title-tag.md`, §5-verified at `0x4df930`): a MISSING or non-number
|
||||
// **The arity is always SIX, and the two failure shapes are different** (`0x4df930`): a
|
||||
// MISSING or non-number
|
||||
// argument raises `Usage:` (shape A — [`number_arg`], truncating toward zero like the
|
||||
// reference's `_ftol`), while an out-of-range NUMBER is not an error at all — it returns
|
||||
// `nil, 0, nil, nil, nil, nil` off `mov eax,6` at all three exits. Return 2 is the NUMBER
|
||||
@@ -366,8 +365,7 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
0 => Value::Nil,
|
||||
c => Value::Integer(i64::from(c.signum())),
|
||||
};
|
||||
// **Returns 4 and 5 are `1`/`nil`, NOT booleans** (wow-re
|
||||
// `ui/scratch/questlog-title-tag.md` §7's table): `isHeader` is `1` on a header row
|
||||
// **Returns 4 and 5 are `1`/`nil`, NOT booleans**: `isHeader` is `1` on a header row
|
||||
// and `nil` on a quest; `isCollapsed` is `1` only for a header whose bit in
|
||||
// `[0xbb748c]` is clear — a quest row and an EXPANDED header both answer `nil`. We
|
||||
// pushed `true`/`false`, which every `if ( isHeader )` in FrameXML reads the same and
|
||||
@@ -984,8 +982,8 @@ mod tests {
|
||||
.unwrap());
|
||||
}
|
||||
|
||||
/// The §5-verified arity contract (wow-re `ui/scratch/questlog-title-tag.md`, `0x4df930`):
|
||||
/// six values ALWAYS, an out-of-range number is not an error, and return 2 is the number `0`
|
||||
/// The arity contract (`0x4df930`): six values ALWAYS, an out-of-range number is not an
|
||||
/// error, and return 2 is the number `0`
|
||||
/// — while a missing or non-number argument raises `Usage:`.
|
||||
#[test]
|
||||
fn out_of_range_is_six_values_with_a_zero_level_and_a_bad_arg_raises() {
|
||||
@@ -1027,8 +1025,8 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
/// §7's table for returns 4 and 5: `1` on a header, `nil` on a quest; `isCollapsed` is `1`
|
||||
/// only for a COLLAPSED header — an expanded one and every quest row answer `nil`.
|
||||
/// `isHeader`/`isCollapsed`, returns 4 and 5: `1` on a header, `nil` on a quest; `isCollapsed`
|
||||
/// is `1` only for a COLLAPSED header — an expanded one and every quest row answer `nil`.
|
||||
#[test]
|
||||
fn is_header_and_is_collapsed_are_one_or_nil_never_booleans() {
|
||||
let mut s = UiScript::new().unwrap();
|
||||
@@ -1313,7 +1311,7 @@ mod tests {
|
||||
/// the watch prune counts them — the reference's prune (`0x4de7a7`–`0x4de80f`) scans the whole
|
||||
/// row array for a non-header row with the watched id, and a collapsed-group quest is still a
|
||||
/// row there, just sorted past the visible window; the collapse itself (`0x4ded30`) re-sorts
|
||||
/// and recounts only (wow-re `system/ui/scratch/questlog-list-rebuild.md` §7/§8). So watch →
|
||||
/// and recounts only. So watch →
|
||||
/// collapse → expand keeps the watch, and a quest that genuinely LEFT the log still drops it.
|
||||
#[test]
|
||||
fn a_collapsed_header_keeps_its_quests_watched() {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
//! The **region** side of the object model (RF-0023's distinct-tag leaves): the `Texture`/
|
||||
//! `FontString` wrapper cache, their shared metatable, and the region method surface. Split from
|
||||
//! [`super::object`] (which keeps the frame side + `CreateFrame`) so each grows along its own
|
||||
//! axis — frames grow per-kind method tables ([`super::statusbar`], [`super::button`]), regions
|
||||
//! grow paint/coords methods here.
|
||||
//! The **region** side of the object model (each leaf carries the reference's own type tag): the
|
||||
//! `Texture`/`FontString` wrapper cache, their shared metatable, and the region method surface.
|
||||
//! Split from [`super::object`] (which keeps the frame side + `CreateFrame`) so each grows along
|
||||
//! its own axis — frames grow per-kind method tables ([`super::statusbar`], [`super::button`]),
|
||||
//! regions grow paint/coords methods here.
|
||||
|
||||
use mlua::{Lua, MultiValue, Table, Value};
|
||||
|
||||
@@ -78,11 +78,11 @@ pub(super) fn region_wrapper(lua: &Lua, id: u32) -> mlua::Result<Table> {
|
||||
}
|
||||
let t = lua.create_table()?;
|
||||
t.raw_set(0, Value::LightUserData(id_to_lud(id)))?;
|
||||
// **A title region gets a NARROWER metatable, chosen here rather than per lookup.** wow-re Q6
|
||||
// carves the object as answering *exactly* the 19 Region methods — no Show/Hide, no textures,
|
||||
// no text — and this cache is created once per region, so picking the table at construction
|
||||
// costs nothing on the call path (dispatching inside `__index` would put a model borrow and a
|
||||
// kind lookup in front of EVERY region method call in the UI).
|
||||
// **A title region gets a NARROWER metatable, chosen here rather than per lookup.**
|
||||
// `CreateTitleRegion 0x773910`'s object answers *exactly* the 19 Region methods — no
|
||||
// Show/Hide, no textures, no text — and this cache is created once per region, so picking the
|
||||
// table at construction costs nothing on the call path (dispatching inside `__index` would
|
||||
// put a model borrow and a kind lookup in front of EVERY region method call in the UI).
|
||||
let kind = {
|
||||
let model = lua.app_data_ref::<Model>().expect("model");
|
||||
model
|
||||
@@ -160,9 +160,9 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
// client's own file calls this global inside our VM.**
|
||||
//
|
||||
// A NAME, not a region handle — strictly what the reference's callers are attested to pass.
|
||||
// Whether the real binding also accepts a texture object is not carved, so it is not accepted
|
||||
// here: inventing the wider signature is how a superset starts (1189), and nothing needs it —
|
||||
// both of our own callers already hold the name.
|
||||
// Whether the real binding also accepts a texture object has not been read from the bytes, so
|
||||
// it is not accepted here: inventing the wider signature is how a superset starts (1189), and
|
||||
// nothing needs it — both of our own callers already hold the name.
|
||||
//
|
||||
// The behaviour is the crop it was always: set the texture AND mark the region a portrait, so
|
||||
// it draws masked to its inscribed circle. The client's portraits are circular and the frame
|
||||
@@ -255,8 +255,7 @@ fn install_region_methods(lua: &Lua) -> mlua::Result<()> {
|
||||
//
|
||||
// 1244 shipped four of the six missing Region-map members and deliberately left these two
|
||||
// DISPATCHED rather than guessed, because every interesting detail is one a plausible
|
||||
// implementation gets wrong. wow-re answered (§5 trio + byte cross-check,
|
||||
// `system/ui/scratch/widget-type-identity.md`), and every one of those details is below.
|
||||
// implementation gets wrong. Every one of those details is below, verified at the bytes.
|
||||
//
|
||||
// `GetObjectType` is a per-class `.data` `const char*` read through `vtable[+0x1c]` — Texture
|
||||
// `0x773480` → `"Texture"`, FontString `0x7735d0` → `"FontString"` — pushed with
|
||||
@@ -322,9 +321,8 @@ fn install_region_methods(lua: &Lua) -> mlua::Result<()> {
|
||||
|
||||
// SetParent(frame) — **a Texture/FontString really does have this**, and we were the ones
|
||||
// missing it. `SetParent` lives in the REGION method table (`0x7a1550`); Texture's class lookup
|
||||
// falls back to Region's at `0x79c650` and FontString's at `0x79ee50`, so both reach it (wow-re
|
||||
// `system/ui/scratch/widget-api-batch-benilla.md` Q7, §5-verified). `FuBar_FuXPFu.lua:210`'s
|
||||
// `self.Spark:SetParent(self.XPBar)` is not a broken addon.
|
||||
// falls back to Region's at `0x79c650` and FontString's at `0x79ee50`, so both reach it.
|
||||
// `FuBar_FuXPFu.lua:210`'s `self.Spark:SetParent(self.XPBar)` is not a broken addon.
|
||||
//
|
||||
// Four contract traps, each spelled out because each is a plausible implementation's silent
|
||||
// divergence:
|
||||
@@ -481,15 +479,15 @@ fn install_region_methods(lua: &Lua) -> mlua::Result<()> {
|
||||
// **The Texture/FontString superset is NOT closed here, deliberately.** They still share one
|
||||
// table, so a Texture answers `SetText` and a FontString answers `SetTexture`. Splitting those
|
||||
// needs the per-table membership facts, and the naive partition is WRONG: `paint.rs` installs
|
||||
// `SetDrawLayer`/`SetVertexColor`/`SetAlpha`/`SetAlphaGradient`, all of which the font carve
|
||||
// says a FontString legitimately has. A wrong split REMOVES verbs addons use, which is worse
|
||||
// than the superset it fixes — so that half waits for the membership read (1238's shape).
|
||||
// `SetDrawLayer`/`SetVertexColor`/`SetAlpha`/`SetAlphaGradient`, all of which FontString's own
|
||||
// method table (`0xcf5400`) legitimately has. A wrong split REMOVES verbs addons use, which is
|
||||
// worse than the superset it fixes — so that half waits for the membership read (1238's shape).
|
||||
let title = lua.create_table()?;
|
||||
for name in super::REGION_MAP_METHODS {
|
||||
let f: Value = m.get(name)?;
|
||||
title.set(name, f)?;
|
||||
}
|
||||
// ── The two LEAF tables (wow-re `texture-fontstring-method-split.md`) ───────────────────────
|
||||
// ── The two LEAF tables ──────────────────────────────────────────────────────────────────
|
||||
//
|
||||
// Texture's map is `0x87c128` (22 entries, lookup `0x79c620`), FontString's is `0xcf5400` (32,
|
||||
// lookup `0x79ee20`); both tail-call the Region map and stop there — no third table. Until now
|
||||
@@ -505,8 +503,9 @@ fn install_region_methods(lua: &Lua) -> mlua::Result<()> {
|
||||
// are the client's own lists now.
|
||||
//
|
||||
// Copied out of the full table rather than installed twice, so one implementation stands behind
|
||||
// both visibilities — and note the carve's warning that the shared names use the IDENTICAL
|
||||
// `const char*` in the client's two tables, so de-duplicating by name would drop one side.
|
||||
// both visibilities — and note that the shared names use the IDENTICAL `const char*` in the
|
||||
// client's two tables (e.g. `GetDrawLayer` at `0x87c41c`), so de-duplicating by name would
|
||||
// drop one side.
|
||||
for (key, extra) in [
|
||||
(REG_TEXTURE_METHODS, &super::TEXTURE_ONLY_METHODS[..]),
|
||||
(REG_FONTSTRING_METHODS, &super::FONTSTRING_ONLY_METHODS[..]),
|
||||
@@ -583,7 +582,8 @@ pub(super) fn region_type_name(model: &Model, rh: RegionHandle) -> &'static str
|
||||
match model.arena.region(rh).map(|r| r.kind) {
|
||||
Some(RegionKind::Texture) => "Texture",
|
||||
Some(RegionKind::FontString) => "FontString",
|
||||
// The title region's own type name — it is a Region and nothing more (Q6).
|
||||
// The title region's own type name — it is a Region and nothing more (`CreateTitleRegion
|
||||
// 0x773910`).
|
||||
Some(RegionKind::Title) => "Region",
|
||||
None => "Region",
|
||||
}
|
||||
@@ -625,13 +625,12 @@ pub(super) fn region_owner_id(model: &mut Model, rh: RegionHandle) -> u32 {
|
||||
}
|
||||
}
|
||||
|
||||
/// The client's **creation-path implicit anchor** (wow-re `system/ui/scratch/`
|
||||
/// `region-implicit-anchor.md`, §5 VERIFIED; decision 1310): a per-region-type post-step the real
|
||||
/// engine runs immediately after a region's LoadXML returns (`0x7701c0` texture / `0x771480`
|
||||
/// fontstring — the same two fire from the Button state-texture and ButtonText paths, and from Lua
|
||||
/// `CreateTexture`/`CreateFontString` only on a template-registry hit). Condition: the region has a
|
||||
/// parent AND every one of its nine anchor slots is empty — any anchor from any source suppresses
|
||||
/// it. Then:
|
||||
/// The client's **creation-path implicit anchor** (decision 1310): a per-region-type post-step
|
||||
/// the real engine runs immediately after a region's LoadXML returns (`0x7701c0` texture /
|
||||
/// `0x771480` fontstring — the same two fire from the Button state-texture and ButtonText paths,
|
||||
/// and from Lua `CreateTexture`/`CreateFontString` only on a template-registry hit). Condition:
|
||||
/// the region has a parent AND every one of its nine anchor slots is empty — any anchor from any
|
||||
/// source suppresses it. Then:
|
||||
///
|
||||
/// - a **Texture** gets `SetAllPoints(parent)` — two corner anchors, TOPLEFT→TOPLEFT and
|
||||
/// BOTTOMRIGHT→BOTTOMRIGHT at (0,0). Two opposing corners pin all four edges, so an authored
|
||||
@@ -809,7 +808,7 @@ pub(super) fn region_set_point(lua: &Lua, this: &Table, args: &MultiValue) -> ml
|
||||
/// `Region:GetWidth()`/`GetHeight()` — **the virtual size getters**, per region class.
|
||||
///
|
||||
/// The Lua bindings are not field reads. `GetWidth 0x7a1e00` ends `ff 52 1c` and
|
||||
/// `GetHeight 0x7a2030` ends `ff 52 20` (wow-re `minimap-ping-law.md`): both dispatch through the
|
||||
/// `GetHeight 0x7a2030` ends `ff 52 20`: both dispatch through the
|
||||
/// receiver's own **geometry vtable** — slots `+0x1c`/`+0x20` — which is the identical call the
|
||||
/// rect resolver makes (`0x767579`). So a region's Lua-visible size and the size its rect is built
|
||||
/// from are one number BY CONSTRUCTION on the reference, and this function is how they are one
|
||||
@@ -834,7 +833,7 @@ pub(super) fn region_set_point(lua: &Lua, this: &Table, args: &MultiValue) -> ml
|
||||
/// wrap constraint, the very cell `GetStringWidth 0x79e510` returns. The *height* is the other
|
||||
/// cell, `0x7729b0`'s `[fs+0x100]`, which IS the wrapped line count × line height. So a wrapped
|
||||
/// string reports a width wider than its own box and a height taller than one line, and that
|
||||
/// asymmetry is the client's (wow-re `fontstring-overflow.md` "The measurement echo").
|
||||
/// asymmetry is the client's.
|
||||
pub(super) fn measured_wh(lua: &Lua, this: &Table) -> mlua::Result<(f32, f32)> {
|
||||
let rh = region_handle_of(lua, this)?;
|
||||
// Same-tick measure when a host font engine is installed — see `region::text`'s `natural_w`.
|
||||
|
||||
@@ -172,7 +172,7 @@ pub(super) fn install(lua: &Lua, m: &Table) -> mlua::Result<()> {
|
||||
},
|
||||
)?;
|
||||
|
||||
// ── The rest of the Region map `0xcf54b4` (wow-re `font-object-lua-surface.md`) ──────────────
|
||||
// ── The rest of the Region map `0xcf54b4` ───────────────────────────────────────────────────
|
||||
//
|
||||
// These four landed together because the MAP is the unit, not the name. `SetParent` shipped
|
||||
// alone when one addon line named it, and its own getter stayed missing for months — which is
|
||||
|
||||
@@ -45,7 +45,6 @@ pub(super) fn install(lua: &Lua, m: &Table) -> mlua::Result<()> {
|
||||
// was present — when it was not, it copies byte 3 of the read-back colour over the packed
|
||||
// alpha (`0x79adfe`/`0x79ae0a`) before the store. So `SetVertexColor(r, g, b)` on 1.12.1 is
|
||||
// `SetVertexColor(r, g, b, currentAlpha)`, and a three-argument call CANNOT restore opacity.
|
||||
// wow-re `system/ui/scratch/button-state-texture-path-setter.md` §7.
|
||||
//
|
||||
// Why it still says 1.0: the fix is one line, but its blast radius is every three-argument
|
||||
// call in FrameXML and in the addon corpus, and it only bites where a region already carries
|
||||
@@ -55,8 +54,8 @@ pub(super) fn install(lua: &Lua, m: &Table) -> mlua::Result<()> {
|
||||
// fourth argument explicitly (`SetVertexColor(1.0, 1.0, 1.0, 0.5)`), so closing this cannot
|
||||
// silently change that file.
|
||||
//
|
||||
// **Shape C on r, g, b** (`Texture:SetVertexColor `0x79abd0``, `2=C 3=C 4=C 5=B`, wow-re
|
||||
// `numeric-arg-coercion-law.md`): a bare `lua_tonumber` with no `lua_isnumber` gate, so a nil,
|
||||
// **Shape C on r, g, b** (`Texture:SetVertexColor `0x79abd0``, `2=C 3=C 4=C 5=B`): a bare
|
||||
// `lua_tonumber` with no `lua_isnumber` gate, so a nil,
|
||||
// a table or a string is **0.0** and the call never raises. Taking them as `f32` made mlua's
|
||||
// converter the gate instead — the 2176 class — and the stock
|
||||
// `QuestLogFrame.lua:337` idiom hands three nils (`titleButton.r/g/b` are only assigned in
|
||||
@@ -78,9 +77,9 @@ pub(super) fn install(lua: &Lua, m: &Table) -> mlua::Result<()> {
|
||||
)?,
|
||||
)?;
|
||||
|
||||
// GetVertexColor — the setter's own pair, a real 5875 binding (`0x79aa50`, wow-re
|
||||
// `system/ui/ledger.tsv`; it sits directly above `SetVertexColor 0x79abd0` in the same region
|
||||
// method family). Never set = the untinted white every region draws at by default.
|
||||
// GetVertexColor — the setter's own pair, a real 5875 binding (`0x79aa50`; it sits directly
|
||||
// above `SetVertexColor 0x79abd0` in the same region method family). Never set = the untinted
|
||||
// white every region draws at by default.
|
||||
m.set(
|
||||
"GetVertexColor",
|
||||
lua.create_function(|lua, this: Table| {
|
||||
@@ -160,8 +159,8 @@ pub(super) fn install(lua: &Lua, m: &Table) -> mlua::Result<()> {
|
||||
// reached us only once the survey started seating the addon registry — the whole point of
|
||||
// that instrument fix. The stray `true` is meaningless in 1.12 and the addon author
|
||||
// presumably meant a later client's second parameter; either way the client shrugs.
|
||||
// The RETURN is part of the contract (wow-re `widget-api-batch-benilla.md` Q1, VERIFIED):
|
||||
// the path form answers **1 | nil — nil meaning the file did not load** — and the colour
|
||||
// The RETURN is part of the contract (`SetTexture 0x79bb40`): the path form answers
|
||||
// **1 | nil — nil meaning the file did not load** — and the colour
|
||||
// and clear forms answer 1. Atlas is the load-bearing caller: `Atlas_Refresh` does
|
||||
// `local builtIn = AtlasMap:SetTexture("…\Images\Maps\"..zoneID)` and walks its plugin
|
||||
// fallback chain on nil, so a binding that returns nothing draws no map at all whatever
|
||||
@@ -182,9 +181,9 @@ pub(super) fn install(lua: &Lua, m: &Table) -> mlua::Result<()> {
|
||||
let data = model.region_data.entry(rh).or_default();
|
||||
data.circular = false;
|
||||
data.portrait_unit = None;
|
||||
// **`SetTexture` CLEARS the desaturation** (wow-re `texture-desaturate-law.md` §2.3,
|
||||
// VERIFIED): `+0x128` is a `CGxShader*`, and `CSimpleTexture::SetTexture`
|
||||
// (`0x770200`) writes it from its 4th stack arg, for which the Lua binding
|
||||
// **`SetTexture` CLEARS the desaturation**: `+0x128` is a `CGxShader*`, and
|
||||
// `CSimpleTexture::SetTexture` (`0x770200`) writes it from its 4th stack arg,
|
||||
// for which the Lua binding
|
||||
// (`0x79bb40`) pushes slot 0 — permanently NULL — on both of its legs. A
|
||||
// re-implementation that keeps a desaturate boolean independent of the texture
|
||||
// handle diverges on every `icon:SetDesaturated(1)` followed by `icon:SetTexture(t)`.
|
||||
@@ -232,12 +231,12 @@ pub(super) fn install(lua: &Lua, m: &Table) -> mlua::Result<()> {
|
||||
// **Ask the probe BEFORE writing the slot.** The reference's load-FAILURE
|
||||
// arm (`cmp [ebp-4],2; jl` at `0x770288` → `0x77028e`–`0x7702b2`) releases
|
||||
// the handle it just built and returns 0 **leaving `+0xcc` and `+0x128` as
|
||||
// they were** — "the widget keeps whatever texture it already had"
|
||||
// (wow-re `texture-service-name-resolution.md` §161-169). Storing the path
|
||||
// first and letting the verdict drive only the *return value* meant a
|
||||
// mistyped or not-yet-shipped path ERASED the art it failed to replace,
|
||||
// and did it silently: `GetTexture()` echoed the missing path and the quad
|
||||
// was dropped, so the region went blank with nothing said (decision 2124).
|
||||
// they were** — the widget keeps whatever texture it already had. Storing
|
||||
// the path first and letting the verdict drive only the *return value*
|
||||
// meant a mistyped or not-yet-shipped path ERASED the art it failed to
|
||||
// replace, and did it silently: `GetTexture()` echoed the missing path and
|
||||
// the quad was dropped, so the region went blank with nothing said
|
||||
// (decision 2124).
|
||||
//
|
||||
// The store and the return ask deliberately different questions. A VM with
|
||||
// **no probe installed** has no backend to ask, so it stores — that is
|
||||
@@ -281,7 +280,8 @@ pub(super) fn install(lua: &Lua, m: &Table) -> mlua::Result<()> {
|
||||
true
|
||||
}
|
||||
// SetTexture(nil) clears (the live API's blank-the-region form); a cleared
|
||||
// texture region draws nothing. Returns 1 (Q1's `SetTexture(nil) / ()` row).
|
||||
// texture region draws nothing. Returns 1 (`SetTexture 0x79bb40`'s
|
||||
// `SetTexture(nil) / ()` row).
|
||||
Value::Nil => {
|
||||
data.texture = None;
|
||||
data.fill = None;
|
||||
@@ -309,8 +309,8 @@ pub(super) fn install(lua: &Lua, m: &Table) -> mlua::Result<()> {
|
||||
)?,
|
||||
)?;
|
||||
|
||||
// SetDesaturated(flag) -> shaderSupported — Texture only (`0x79c1e0`, wow-re ledger; the
|
||||
// reference's own `ItemButtonTemplate.lua:69` is `local shaderSupported =
|
||||
// SetDesaturated(flag) -> shaderSupported — Texture only (`0x79c1e0`; the reference's own
|
||||
// `ItemButtonTemplate.lua:69` is `local shaderSupported =
|
||||
// icon:SetDesaturated(desaturated)`).
|
||||
//
|
||||
// **The RETURN is the whole design, and it is the half a plausible implementation drops.**
|
||||
@@ -334,8 +334,8 @@ pub(super) fn install(lua: &Lua, m: &Table) -> mlua::Result<()> {
|
||||
// static scan costed this at 61 addons and it was declined; the use-probe costed it at 98 the
|
||||
// moment anyone right-clicks.
|
||||
//
|
||||
// **The argument's truth table is not `if flag then`** (wow-re `texture-desaturate-law.md` §1.1,
|
||||
// VERIFIED — `0x6f1c10(L, 2, default=1)` dispatched through the jump table at `0x6f1ce8`). Two
|
||||
// **The argument's truth table is not `if flag then`** (`0x6f1c10(L, 2, default=1)`,
|
||||
// dispatched through the jump table at `0x6f1ce8`). Two
|
||||
// of its arms are the opposite of the obvious reading, and both are reachable:
|
||||
// · **no argument at all is ON**, not off — `LUA_TNONE` takes the `ja` default arm, so
|
||||
// `tex:SetDesaturated()` greys. This is why the flag arrives as a `MultiValue`: mlua hands a
|
||||
@@ -370,8 +370,7 @@ pub(super) fn install(lua: &Lua, m: &Table) -> mlua::Result<()> {
|
||||
})?,
|
||||
)?;
|
||||
|
||||
// GetTexture() — Texture only (`0x79ba70`), verified in wow-re's widget-method batch
|
||||
// (`system/ui/scratch/widget-api-batch-benilla.md`). Three contract details are each the kind a
|
||||
// GetTexture() — Texture only (`0x79ba70`). Three contract details are each the kind a
|
||||
// plausible implementation gets silently wrong, so each is spelled out:
|
||||
//
|
||||
// · **Exactly ONE return value**, never a multi-return.
|
||||
@@ -469,7 +468,7 @@ pub(super) fn install(lua: &Lua, m: &Table) -> mlua::Result<()> {
|
||||
// Lua boolean).
|
||||
//
|
||||
// **The flag's GEOMETRY effect is not wired, and that is stated rather than implied.** In the
|
||||
// reference the flag gates a rect-recompute leg (`ui.md:4619`, `0x770462`): with it set, a
|
||||
// reference the flag gates a rect-recompute leg (`0x770462`): with it set, a
|
||||
// `SetTexCoord` re-derives the region's own rect from the UV quad instead of leaving the rect
|
||||
// where the anchors put it and resampling inside it. Wiring that means the region resolve in
|
||||
// `region::layout` reading this flag and taking the rect from `RegionData::tex_coords` — a
|
||||
@@ -554,8 +553,8 @@ pub(super) fn install(lua: &Lua, m: &Table) -> mlua::Result<()> {
|
||||
|
||||
// `SetRotation` WAS here, on the Texture leaf, and is GONE. 1.12 registers the name once, in
|
||||
// the **PlayerModel** table `0x84f1fc` (`0x505f00`, argc 2 — the paper doll's rotate arrows),
|
||||
// which we already answer through `modelframe`; it is in neither region map, and the carve
|
||||
// above lists it among the five names that are in NEITHER (`texture-fontstring-method-split.md`).
|
||||
// which we already answer through `modelframe`; it is in neither region map, and the complete
|
||||
// membership census (`0x87c128`/`0xcf5400`) lists it among the five names that are in NEITHER.
|
||||
// Ours was a later-era Texture verb shipped early for the world-map player arrow's stand-in
|
||||
// rotation — and that arrow has since become a real Model frame driven by `ModelState::facing`
|
||||
// (`script::worldmap_arrow`), so the verb's own reason went with it.
|
||||
@@ -577,8 +576,8 @@ pub(super) fn install(lua: &Lua, m: &Table) -> mlua::Result<()> {
|
||||
// corner in the renderer's screen winding.
|
||||
m.set(
|
||||
"SetTexCoord",
|
||||
// Every coordinate is **shape C** (`Texture:SetTexCoord 0x79beb0`, all positions C,
|
||||
// wow-re `numeric-arg-coercion-law.md`): bare `lua_tonumber`, nil/table/string → 0.0. Only
|
||||
// Every coordinate is **shape C** (`Texture:SetTexCoord 0x79beb0`, all positions C): bare
|
||||
// `lua_tonumber`, nil/table/string → 0.0. Only
|
||||
// the ARITY raises (`0x79bf5d dec eax ; cmp eax,4 ; je ; cmp eax,8 ; je`), which is the
|
||||
// match below. `Variadic<f32>` made mlua the per-coordinate gate; it is not one.
|
||||
lua.create_function(|lua, (this, args): (Table, mlua::Variadic<Value>)| {
|
||||
@@ -676,8 +675,8 @@ impl crate::script::UiScript {
|
||||
|
||||
/// Install the host's texture **texel-size** oracle — what lets a region with an authored size
|
||||
/// of `0` on an axis take that span from its art, as the client's virtual size getters do
|
||||
/// ([`Model::texture_size_probe`], decision 1349 / wow-re `region-size-fallback.md` §2). A VM
|
||||
/// that never gets one leaves such a region exactly where it was.
|
||||
/// ([`Model::texture_size_probe`], decision 1349; `CSimpleTexture::GetWidth 0x770720`'s
|
||||
/// native-texel fallback). A VM that never gets one leaves such a region exactly where it was.
|
||||
pub fn set_texture_size_probe(&mut self, probe: crate::script::TextureSizeProbe) {
|
||||
self.model_mut().texture_size_probe = Some(probe);
|
||||
}
|
||||
|
||||
@@ -51,8 +51,7 @@ pub(super) fn install(lua: &Lua, m: &Table) -> mlua::Result<()> {
|
||||
// it behaved inside this shim.
|
||||
|
||||
// GetText — **an EMPTY string comes back as `nil`, and that substitution is the getter's own**
|
||||
// (`FontString:GetText 0x79d690`, wow-re
|
||||
// `system/ui/scratch/fontstring-text-cell-and-gettext-contract.md`, §5 five-worker round):
|
||||
// (`FontString:GetText 0x79d690`):
|
||||
//
|
||||
// ```text
|
||||
// 79d72f mov eax,[eax+0xf0] ; the text cell
|
||||
@@ -108,7 +107,7 @@ pub(super) fn install(lua: &Lua, m: &Table) -> mlua::Result<()> {
|
||||
// (`0x7a2030` falls through to the same cached measurement), so it was pruned rather than
|
||||
// reimplemented (1251).
|
||||
// round-trip ([`super::UiScript::set_measured_text`]) — the client asks its font engine for the
|
||||
// laid-out string's metrics exactly here (`fontstring.md`), and the tooltip's auto-size sums
|
||||
// laid-out string's metrics exactly here, and the tooltip's auto-size sums
|
||||
// these to fit its lines. `0` until the string has been measured (a frame's latency; converges).
|
||||
// The stored measure only counts while its key matches the CURRENT text/font/wrap
|
||||
// ([`RegionData::measure_key`]): after a SetText the old string's width is not this string's
|
||||
@@ -121,8 +120,8 @@ pub(super) fn install(lua: &Lua, m: &Table) -> mlua::Result<()> {
|
||||
// the one cell `[fs+0xfc]` (`0x772890` is `0x79e510`'s callee). So the two agree exactly where
|
||||
// the reference makes them agree, and differ exactly where a width was declared.
|
||||
// GetStringWidth is the **natural, unwrapped** extent — never the declared box, and never the
|
||||
// wrapped one (wow-re `fontstring-overflow.md`, "The measurement echo": the reference's getter
|
||||
// re-measures the raw text with NO wrap constraint). Unlike `GetWidth` below it deliberately
|
||||
// wrapped one (the reference's getter re-measures the raw text with NO wrap constraint).
|
||||
// Unlike `GetWidth` below it deliberately
|
||||
// does NOT fall back to a declared `SetWidth`: that width is the very thing a caller
|
||||
// asks this to be independent of. A kit that sizes a box from this number and then sets a width
|
||||
// on the string — which is what the reference's own `PanelTemplates_TabResize` does — would
|
||||
@@ -207,7 +206,7 @@ pub(super) fn install(lua: &Lua, m: &Table) -> mlua::Result<()> {
|
||||
)?;
|
||||
|
||||
// GetJustifyH()/GetJustifyV() → **1 string**. Real entries on the reference's FontString table
|
||||
// (`0x79e5f0` / `0x79e7f0`, the FontString column of wow-re's two-column accessor table) that
|
||||
// (`0x79e5f0` / `0x79e7f0`, the FontString column of a two-column accessor table) that
|
||||
// this side had simply never grown: a FontString could set its justification and not read it
|
||||
// back, while the `<Font>` object — the same law, transcribed separately — could do both.
|
||||
//
|
||||
@@ -233,7 +232,7 @@ pub(super) fn install(lua: &Lua, m: &Table) -> mlua::Result<()> {
|
||||
|
||||
// SetNonSpaceWrap(enable) / CanNonSpaceWrap() — FontString only (`0x79e9f0` / `0x79ead0`).
|
||||
//
|
||||
// Two contract details from wow-re's batch, both easy to get wrong:
|
||||
// Two contract details, both easy to get wrong:
|
||||
// · the getter is **`CanNonSpaceWrap`**, not `GetNonSpaceWrap`, and it answers **`1` or nil**,
|
||||
// not a boolean — 1.12 predates that convention and an addon may compare against 1.
|
||||
// · a **no-argument call ENABLES** it (the default is on), rather than being a query.
|
||||
@@ -268,9 +267,9 @@ pub(super) fn install(lua: &Lua, m: &Table) -> mlua::Result<()> {
|
||||
})?,
|
||||
)?;
|
||||
|
||||
// SetTextHeight(height) — switch the FontString to the scaled-string regime (§5-verified,
|
||||
// wow-re `fontstring-overflow.md`: `0x771600` is the ONLY clearer of the one-to-one bit
|
||||
// `0x200`; the literal size then flows through UNCAPPED, magnified from the raster). Stored
|
||||
// SetTextHeight(height) — switch the FontString to the scaled-string regime (`0x771600` is the
|
||||
// ONLY clearer of the one-to-one bit `0x200`; the literal size then flows through UNCAPPED,
|
||||
// magnified from the raster). Stored
|
||||
// as the distinct [`RegionData::text_height`] — the font object is untouched, so GetFont
|
||||
// keeps reporting the face's own height like the real API.
|
||||
m.set(
|
||||
|
||||
@@ -5,8 +5,7 @@
|
||||
//!
|
||||
//! 1.12.1's widget inheritance is not a Lua metatable chain: every class owns a flat
|
||||
//! `{name, lua_CFunction}` `.data` table, and its lookup probes that table and, on a miss,
|
||||
//! **tail-calls exactly one base class's lookup** (wow-re
|
||||
//! `system/ui/scratch/widget-api-batch-benilla.md`):
|
||||
//! **tail-calls exactly one base class's lookup**:
|
||||
//!
|
||||
//! ```text
|
||||
//! Region 0x87c9b8 (19) lookup 0x7a2ea0 → TERMINAL (root)
|
||||
@@ -51,11 +50,11 @@
|
||||
//! way the binary's does — `WorldFrame.GetHeight == someTexture.GetHeight` is true here too.
|
||||
//!
|
||||
//! **Exactly the 19, and no more.** `Show`/`Hide`/`IsShown`/`IsVisible`/`SetAlpha`/`GetAlpha` look
|
||||
//! like they belong and do not: Frame and Texture each register their *own*, at different
|
||||
//! addresses (`texture-fontstring-method-split.md` §3), so `WorldFrame.Show(someTexture)` fails on
|
||||
//! the real client and must keep failing here. (`SetSize` used to sit outside the 19 for the
|
||||
//! opposite reason — in neither table because 1.12 has no such verb at all; decision 2142 removed
|
||||
//! it rather than filing it.) The map is the unit.
|
||||
//! like they belong and do not: Frame and Texture each register their *own* — `SetAlpha`, for
|
||||
//! instance, is `0x774e90` on Frame and `0x79b580` on Texture — so `WorldFrame.Show(someTexture)`
|
||||
//! fails on the real client and must keep failing here. (`SetSize` used to sit outside the 19 for
|
||||
//! the opposite reason — in neither table because 1.12 has no such verb at all; decision 2142
|
||||
//! removed it rather than filing it.) The map is the unit.
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::rc::Rc;
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
//! The reputation-pane bindings — the twelve `ReputationFrame` globals, driving a faithful port of
|
||||
//! the real 1.12 Reputation tab (reference FrameXML `ReputationFrame.{xml,lua}`).
|
||||
//!
|
||||
//! **The display law here is byte-pinned**, not inferred: wow-5875-re
|
||||
//! `system/ui/scratch/reputation-panel-law.md`, carved from the client's whole reputation TU
|
||||
//! (`[0x4d5200, 0x4d6d80)`) by a §5 cross-check. Addresses below are that node's.
|
||||
//! **The display law here is byte-pinned**, not inferred, across the client's whole reputation TU
|
||||
//! (`[0x4d5200, 0x4d6d80)`).
|
||||
//!
|
||||
//! Same split as [`super::skills`]: the app pushes a flat, unordered snapshot
|
||||
//! ([`UiScript::set_reputation`], each row already resolved from `Faction.dbc` + the player's live
|
||||
@@ -448,8 +447,8 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
// **Both fire `UPDATE_FACTION`, and that is the engine's own doing, not a convenience.**
|
||||
// `CollapseFactionHeader 0x4d6a50` and `ExpandFactionHeader 0x4d6aa0` both reach `0x4d6400`,
|
||||
// which tail-jumps to `0x4d5c40` — and `0x4d5dab`, inside that function, is one of the FOUR
|
||||
// whole-image fire sites of `UPDATE_FACTION` (wow-re `reputation-panel-law.md` §7.5 and §8,
|
||||
// both VERIFIED). The stock pane has no other repaint path after a fold: its header `<OnClick>`
|
||||
// whole-image fire sites of `UPDATE_FACTION`. The stock pane has no other repaint path after a
|
||||
// fold: its header `<OnClick>`
|
||||
// calls the binding and nothing else (ReputationFrame.xml:9-15), and `ReputationFrame_OnEvent`
|
||||
// repaints on `UPDATE_FACTION` alone. Our own retired file repainted itself, which is why this
|
||||
// was invisible until the pane went stock (1875).
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
//! **Saved variables** — the Lua-level settings that survive a restart (decision 1128).
|
||||
//!
|
||||
//! The reference client has *two* mechanisms for this, byte-verified in wow-re
|
||||
//! (`system/ui/scratch/savedvariables-protocol.md`):
|
||||
//! The reference client has *two* mechanisms for this, byte-verified:
|
||||
//!
|
||||
//! 1. **`RegisterForSave("NAME")`** — the Lua API FrameXML uses for its **38** globals
|
||||
//! (`LOCK_ACTIONBAR`, `CHAT_LOCKED`, the eleven `COMBAT_TEXT_*`, …), written to the flat
|
||||
@@ -46,9 +45,9 @@ impl super::UiScript {
|
||||
/// A file that fails as a chunk (a hand edit with a typo; a database past Lua's 262,143
|
||||
/// constants per chunk) leaves its globals at the defaults the code assigned, and the write at
|
||||
/// logout would then replace the player's whole file with those defaults. The reference does
|
||||
/// exactly that — it discards an unparseable file and writes whole from live values (wow-re
|
||||
/// `system/ui/scratch/savedvariables-protocol.md`) — and both load sites here already promised
|
||||
/// otherwise ("left on disk untouched"); this is what makes the promise true. The cost is this
|
||||
/// exactly that — it discards an unparseable file and writes whole from live values
|
||||
/// (`0x51f865`–`0x51f970`) — and both load sites here already promised otherwise ("left on
|
||||
/// disk untouched"); this is what makes the promise true. The cost is this
|
||||
/// session's changes to that one file, which is the trade the promise named.
|
||||
pub fn hold_saved_file(&self, path: &std::path::Path) {
|
||||
let mut model = self.model_mut();
|
||||
@@ -136,7 +135,8 @@ fn serialize(v: &Value, depth: usize, seen: &mut HashSet<*const c_void>) -> Opti
|
||||
Value::String(s) => Some(quote(&s.as_bytes())),
|
||||
Value::Table(t) => table(t, depth, seen),
|
||||
// Functions, threads, userdata (every widget reference is one — a frame's Lua value is a
|
||||
// table whose `[0]` is a lightuserdata handle, RF-0023) cannot be written as a literal.
|
||||
// table whose `[0]` is a lightuserdata handle, written at `0x701bd0`) cannot be written as
|
||||
// a literal.
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
@@ -177,8 +177,9 @@ fn quote(s: &[u8]) -> Vec<u8> {
|
||||
|
||||
/// A table constructor: `{\n<tabs>[key] = value,\n<tabs-1>}`. Keys are always bracketed (the
|
||||
/// reference's shape — its writer emits `[key] = value` for **every** table shape and never a bare
|
||||
/// positional entry, byte-verified in wow-re `system/ui/scratch/lua-table-storage-and-next-order.md`
|
||||
/// §Q5 — and it sidesteps every reserved-word and non-identifier question); entries are **sorted**
|
||||
/// positional entry, byte-verified (no branch in `[0x704607, 0x704989)` writes a value without a
|
||||
/// bracketed key, for any table shape — and it sidesteps every reserved-word and non-identifier
|
||||
/// question); entries are **sorted**
|
||||
/// — integer keys ascending, then strings alphabetically — so the file is stable across runs
|
||||
/// instead of following Lua's hash order.
|
||||
///
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
//! ScrollFrame bindings are four symmetric pairs plus `UpdateScrollChildRect`, and the C++ under
|
||||
//! them is literally cloned: `0x786d30` (horizontal) is the byte-clone of `0x786db0` (vertical)
|
||||
//! against `[+0x324]`/`[+0x32c]` instead of `[+0x328]`/`[+0x334]`, and one `0x786e30` computes
|
||||
//! both ranges out of one subtree union (wow-re `scrollframe-offset-and-range-law.md`). This module
|
||||
//! both ranges out of one subtree union. This module
|
||||
//! is written that way too — one helper per concept, taken on the axis asked for. That the
|
||||
//! horizontal half went unwritten until the corpus asked for it (`aux-addon` reads and writes both
|
||||
//! axes in one function) is why this doc used to say horizontal scroll "is out of scope: no 1.12
|
||||
@@ -76,8 +76,7 @@ enum Axis {
|
||||
/// cached: the scroll offset always clamps against the current layout, not a stale snapshot.
|
||||
///
|
||||
/// **The content extent is the scroll child's whole SUBTREE, not the child frame's own height**
|
||||
/// (wow-re `system/ui/scratch/simplehtml-markup-engine.md` §4.5, byte-identified during the
|
||||
/// SimpleHTML RE; decision 1338 corrects what 0112 shipped). `0x786e30` seeds a bounding box
|
||||
/// (decision 1338 corrects what 0112 shipped). `0x786e30` seeds a bounding box
|
||||
/// `{FLT_MAX, FLT_MAX, 0, 0}` and calls the **recursive** `0x786f80(scrollChild, &bbox)`, which
|
||||
/// unions the frame's REGION list (head `+0x1b4`, link `+0x1b8`, guard `[entry+8]+0xc4`) and
|
||||
/// re-enters itself for every CHILD FRAME (head `+0x2fc`, link `+0x300`, guard `[entry+8]+0xd0`);
|
||||
@@ -297,8 +296,7 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
// ── The horizontal axis: the same three verbs on `[+0x324]`/`[+0x31c]` ─────────────────────
|
||||
//
|
||||
// `SetHorizontalScroll(px)` `0x7912f0` → `0x786d30`, which is the BYTE-CLONE of
|
||||
// `SetVerticalScroll`'s `0x786db0` against the horizontal fields (wow-re
|
||||
// `scrollframe-offset-and-range-law.md` §2.2; `item9-firing34-glue786.md` identified the pair).
|
||||
// `SetVerticalScroll`'s `0x786db0` against the horizontal fields.
|
||||
// So every law stated above holds verbatim on this axis: no clamp, the same 2⁻²² change-gate
|
||||
// against the OLD offset alone, the same re-anchor, and `OnHorizontalScroll(self, offset)`
|
||||
// fired only on a real change. Nothing here is a horizontal special case.
|
||||
@@ -386,10 +384,9 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
event::fire_size_changes(lua);
|
||||
// `OnScrollRangeChanged(self, xRange, yRange)` — **horizontal first**, and that order is
|
||||
// byte-derived rather than assumed: at `0x786eff` the vertical result is on top of the
|
||||
// x87 stack, is `fstp`'d first and therefore pushed DEEPEST, so it lands as `arg2`
|
||||
// (wow-re `scrollframe-offset-and-range-law.md` §3.6, where two of the round's own
|
||||
// workers published it the other way round). It is why stock
|
||||
// `UIPanelScrollFrameTemplate` hands `arg2` to `ScrollFrame_OnScrollRangeChanged`.
|
||||
// x87 stack, is `fstp`'d first and therefore pushed DEEPEST, so it lands as `arg2`. It
|
||||
// is why stock `UIPanelScrollFrameTemplate` hands `arg2` to
|
||||
// `ScrollFrame_OnScrollRangeChanged`.
|
||||
// `arg1` was a hardcoded `0.0` here for as long as there was no horizontal range to
|
||||
// put in it.
|
||||
if let Err(e) = event::fire_widget_handler(
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
//! What each one means, and where the reference puts it:
|
||||
//!
|
||||
//! - `Logout()` — ask to leave the world (`CMSG_LOGOUT_REQUEST`; the client's own
|
||||
//! `ClientServices::SendLogout`, wow-re `system/net/ledger.tsv` 0x5ab000). The **server** decides
|
||||
//! `ClientServices::SendLogout 0x5ab000`). The **server** decides
|
||||
//! whether that is instant or a 20-second countdown, and says so in `SMSG_LOGOUT_RESPONSE`; the
|
||||
//! client's whole job is to narrate the answer, which is the CAMP dialog.
|
||||
//! - `Quit()` — the same request, plus a standing intent to end the *process* rather than return to
|
||||
@@ -40,8 +40,7 @@ pub enum SessionRequest {
|
||||
ForceQuit,
|
||||
/// `ForceLogout()` — the session dispatcher with `force = 1`: `CMSG_PLAYER_LOGOUT` (`0x4A`,
|
||||
/// empty) instead of `Logout`'s `0x4B`, the pending-logout latch bypassed, and nothing at all
|
||||
/// without a live in-world session (decision 1963; wow-re `staticpopup-dialog-bindings.md`
|
||||
/// §4).
|
||||
/// without a live in-world session (decision 1963; `ForceLogout 0x48ab50`'s `0x5ab020` gate).
|
||||
ForceLogout,
|
||||
/// `ReloadUI()` — tear this VM down and build a fresh one, without leaving the world.
|
||||
///
|
||||
@@ -50,7 +49,7 @@ pub enum SessionRequest {
|
||||
/// (`0x490bd0` → `0x48fbf0`) on the NEXT frame — a deferral that doubles as the reentrancy
|
||||
/// guard: the Lua state being destroyed is never the one mid-way through executing the call.
|
||||
/// Queuing an intent the app runs outside any VM call is that same guard in this engine's
|
||||
/// shape (wow-re `system/ui/scratch/savedvariables-protocol.md` §2, byte-verified).
|
||||
/// shape.
|
||||
ReloadUi,
|
||||
/// `StopCinematic()` — skip the cinematic that is playing.
|
||||
///
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
//! `CastShapeshiftForm`, consumed by `BonusActionBarFrame.lua`'s `ShapeshiftBar_*` family) over
|
||||
//! an app-pushed form list, the [`super::spellbook`]/[`super::action`] two-way shape: the app
|
||||
//! resolves everything (which known spells are forms, their icon/name/active/castable/cooldown —
|
||||
//! the wow-re-pinned mechanism lives app-side), pushes a snapshot
|
||||
//! the mechanism lives app-side), pushes a snapshot
|
||||
//! ([`super::UiScript::set_shapeshift_forms`]), and drains the click intents
|
||||
//! ([`super::UiScript::take_shapeshift_casts`]) onto the wire. The engine holds no form
|
||||
//! KNOWLEDGE — a form is "a spell id, a texture, a name, two bits, and a cooldown triple".
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
//! The **`SimpleHTML` widget** (`CSimpleHTML`, class size `0x374`, ctor `0x789dd0`) — its markup
|
||||
//! engine, its block layout, its four element fonts, and its 19-entry Lua method table.
|
||||
//!
|
||||
//! Ground truth: wow-5875-re `system/ui/scratch/simplehtml-markup-engine.md` (a §5 trio plus the
|
||||
//! orchestrator's own byte read and arbitration). The parse itself is [`parse`]; this module is
|
||||
//! Ground truth: byte-verified against `WoW.exe`. The parse itself is [`parse`]; this module is
|
||||
//! the half that touches the model — turning a block list into real FontString/Texture regions
|
||||
//! anchored bottom-to-top, and exposing the widget's Lua surface.
|
||||
//!
|
||||
@@ -29,10 +28,11 @@
|
||||
//! `ItemTextFrame` render, not an approximation of it.
|
||||
//! - **`GetContentHeight` does not exist in 1.12.1.** The method table has no height getter and
|
||||
//! nothing in the TU aggregates one; the hosting ScrollFrame measures the subtree generically
|
||||
//! instead (§4.5), which is what ours does too — the blocks are real regions of the SimpleHTML,
|
||||
//! so `GetVerticalScrollRange` sees them without SimpleHTML publishing anything.
|
||||
//! instead (`0x786e30`'s subtree union), which is what ours does too — the blocks are real
|
||||
//! regions of the SimpleHTML, so `GetVerticalScrollRange` sees them without SimpleHTML
|
||||
//! publishing anything.
|
||||
//!
|
||||
//! ## Where this diverges from the note, deliberately
|
||||
//! ## Where this diverges from the reference, deliberately
|
||||
//!
|
||||
//! - **`nextYOffset` is not pixel-snapped.** The reference stores
|
||||
//! `−pixelSnap(spacing)` (`0x766750`, quantising to whole *device* pixels). Our layout is in
|
||||
@@ -50,8 +50,9 @@
|
||||
//!
|
||||
//! ## The Lua table, and the one name that is NOT on it
|
||||
//!
|
||||
//! §5.1 dumps `.data 0x87ba80` as **19 `{name, fn}` pairs**, and all 19 are installed here. The
|
||||
//! first sixteen take an **optional leading element-name string** resolved by `0x795d80`; the last
|
||||
//! The method table `.data 0x87ba80` dumps as **19 `{name, fn}` pairs**, and all 19 are installed
|
||||
//! here. The first sixteen take an **optional leading element-name string** resolved by
|
||||
//! `0x795d80`; the last
|
||||
//! three (`SetText`, `SetHyperlinkFormat`, `GetHyperlinkFormat`) do not.
|
||||
//!
|
||||
//! **There is no `GetText`.** Later clients grew one; build 5875's table does not have it, and
|
||||
@@ -216,9 +217,9 @@ fn resolve_font(model: &Model, ef: &ElementFont) -> BlockPaint {
|
||||
///
|
||||
/// Returns `usedMarkup` (`0x78a519`'s `al`) — the Lua shim discards it, our tests do not.
|
||||
pub(crate) fn set_text(model: &mut Model, fh: FrameHandle, raw: &str) -> bool {
|
||||
// §10 step 1. The reference pool-frees the previous parse's widgets; ours must too, or a
|
||||
// second `SetText` leaves the first one's FontStrings standing behind the new ones — the
|
||||
// failure a book with a "next page" button hits on its very first turn.
|
||||
// `0x78a3a0`'s first step. The reference pool-frees the previous parse's widgets; ours must
|
||||
// too, or a second `SetText` leaves the first one's FontStrings standing behind the new
|
||||
// ones — the failure a book with a "next page" button hits on its very first turn.
|
||||
let old = model
|
||||
.simple_html
|
||||
.get_mut(&fh)
|
||||
@@ -252,13 +253,14 @@ pub(crate) fn set_text(model: &mut Model, fh: FrameHandle, raw: &str) -> bool {
|
||||
parsed.used_markup
|
||||
}
|
||||
|
||||
/// §10's `ADD_BLOCK`/`ADD_IMAGE` loop — the anchor chain, the fonts, and the `nextY` bookkeeping.
|
||||
/// `AddTextBlock 0x78adb0`/the `<IMG>` handler `0x78ab40`'s loop — the anchor chain, the fonts,
|
||||
/// and the `nextY` bookkeeping.
|
||||
fn build(model: &mut Model, fh: FrameHandle, blocks: &[Block]) {
|
||||
let frame_id = model.frame_id(fh);
|
||||
// `0x78ae19 call [CLayoutFrame vtbl + 0x1c]` = `0x768420`, `fld [ecx+0x50]` — the frame's
|
||||
// **declared** width, not its resolved rect. Every block is exactly that wide, which is what
|
||||
// makes each one word-wrap at the frame width; §4.6's corollary is that a later resize does
|
||||
// NOT re-wrap, because each block snapshotted the width at creation.
|
||||
// makes each one word-wrap at the frame width; `OnRectChanged 0x78a320`'s corollary is that a
|
||||
// later resize does NOT re-wrap, because each block snapshotted the width at creation.
|
||||
let width = model
|
||||
.layout_inputs
|
||||
.get(&fh)
|
||||
@@ -272,9 +274,10 @@ fn build(model: &mut Model, fh: FrameHandle, blocks: &[Block]) {
|
||||
for block in blocks {
|
||||
match block {
|
||||
Block::Text { text, elem, align } => {
|
||||
// §5.3, the empty-path fallback: `elementFont[elem]`, unless its resolved path is
|
||||
// empty, in which case `elementFont[0]` — so a declaration that supplies only
|
||||
// `<FontString>` supplies the font for every element, headers included.
|
||||
// The empty-path fallback (`0x78ae29`–`0x78ae54`): `elementFont[elem]`, unless its
|
||||
// resolved path is empty, in which case `elementFont[0]` — so a declaration that
|
||||
// supplies only `<FontString>` supplies the font for every element, headers
|
||||
// included.
|
||||
let paint = {
|
||||
let st = model.simple_html.entry(fh).or_default().clone();
|
||||
let own = resolve_font(model, &st.fonts[*elem]);
|
||||
@@ -312,7 +315,7 @@ fn build(model: &mut Model, fh: FrameHandle, blocks: &[Block]) {
|
||||
font_shadow: paint.shadow,
|
||||
..RegionData::default()
|
||||
};
|
||||
// §5.4: `block->+0x120 = (block->+0x120 & ~7) | (align & 7)` (`0x78ae78`), written
|
||||
// `block->+0x120 = (block->+0x120 & ~7) | (align & 7)` (`0x78ae78`), written
|
||||
// AFTER `SetFontObject`, so the tag's `align` always wins over the element font's
|
||||
// own justifyH. justifyV is left alone and keeps whatever the element font
|
||||
// supplied (ctor MIDDLE) — visually inert here, because the block's rect height IS
|
||||
@@ -349,7 +352,7 @@ fn build(model: &mut Model, fh: FrameHandle, blocks: &[Block]) {
|
||||
// the image path): the id is what the resolve seats the region's rect under, and
|
||||
// what `free_block` unmaps on the next `SetText`.
|
||||
model.region_id(rh);
|
||||
// §7 step 4 — the anchor corner is selected by `align`, and 8/16/32 get **no
|
||||
// The anchor corner is selected by `align`, and 8/16/32 get **no
|
||||
// `SetPoint` at all** (`0x78ac61 jne 0x78ace8`). An anchorless region never
|
||||
// resolves here either, so it draws nothing, which is the same outcome.
|
||||
let point = match *align {
|
||||
@@ -370,10 +373,10 @@ fn build(model: &mut Model, fh: FrameHandle, blocks: &[Block]) {
|
||||
..RegionData::default()
|
||||
},
|
||||
);
|
||||
// §7 step 6/7: an unfloated image reserves its own height in the flow, and
|
||||
// `prevBlock` is **never** written by the image path (three reads of `+0x348` in
|
||||
// the whole function, zero writes) — the next block still hangs off the last TEXT
|
||||
// block, only lower.
|
||||
// An unfloated image reserves its own height in the flow (`0x78ad07`–`0x78ad1b`),
|
||||
// and `prevBlock` is **never** written by the image path (three reads of `+0x348`
|
||||
// in the whole function, zero writes) — the next block still hangs off the last
|
||||
// TEXT block, only lower.
|
||||
//
|
||||
// The height it reserves is `texture.GetHeight()`, which is `CSimpleTexture`'s
|
||||
// **override**: a `<IMG>` with no `height=` reserves its art's own texel height,
|
||||
@@ -401,8 +404,8 @@ fn build(model: &mut Model, fh: FrameHandle, blocks: &[Block]) {
|
||||
model.touch_layout();
|
||||
}
|
||||
|
||||
/// The two anchor forms of §4.2 step 2: block 0 pins to the frame, block *N* to its predecessor's
|
||||
/// bottom edge at `nextYOffset` (negative, i.e. downward).
|
||||
/// The two anchor forms of `AddTextBlock 0x78adb0`'s `SetPoint 0x767c70` step: block 0 pins to the
|
||||
/// frame, block *N* to its predecessor's bottom edge at `nextYOffset` (negative, i.e. downward).
|
||||
fn anchor_for(prev_block: Option<u32>, frame_id: u32, point: Point, next_y: f32) -> Anchor {
|
||||
match prev_block {
|
||||
None => Anchor::new(point, frame_id, point, 0.0, 0.0),
|
||||
@@ -566,7 +569,7 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
// ── 0/1 · the font object, and the live link to it ──────────────────────────────────────
|
||||
// `SetFontObject([element,] font | "font" | nil)` → 0 values. Unlike a region's, this cannot
|
||||
// repaint anything already on screen: the element fonts are prototypes, and only the next
|
||||
// `SetText` copies them into blocks (§4.6 — a built block is never re-fonted).
|
||||
// `SetText` copies them into blocks (a built block is never re-fonted, `0x795d3e`).
|
||||
m.set(
|
||||
"SetFontObject",
|
||||
lua.create_function(|lua, (this, mut rest): (Table, MultiValue)| {
|
||||
@@ -607,8 +610,8 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
justify_v: true,
|
||||
};
|
||||
}
|
||||
// The severance mask is deliberately **not** reset on a re-point. §5-verified for
|
||||
// the region side (`script::font_block`'s `SetFontObject`): the real "stop
|
||||
// The severance mask is deliberately **not** reset on a re-point — true for
|
||||
// the region side too (`script::font_block`'s `SetFontObject`): the real "stop
|
||||
// inheriting this property" signal is a CLEARED bit in the inheritMask
|
||||
// (`FONTINSTANCE+0x2c`), cleared by each local setter and never restored — so a
|
||||
// property this element set for itself stays severed across a later
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
//! The **markup parse** half of `CSimpleHTML::SetText` — `&str` in, a block list out.
|
||||
//!
|
||||
//! Pure: no Lua, no model, no arena. Everything here is one transcription of
|
||||
//! wow-5875-re `system/ui/scratch/simplehtml-markup-engine.md` §10's algorithm and the §0–§7
|
||||
//! derivations behind it; [`super`] does the materialization (regions, anchors, fonts).
|
||||
//! Pure: no Lua, no model, no arena. Everything here is one transcription of the reference's
|
||||
//! `SetText`/`WALK_BODY`/`ADD_PARAGRAPH` algorithm and the byte derivations behind it; [`super`]
|
||||
//! does the materialization (regions, anchors, fonts).
|
||||
//!
|
||||
//! ## The one fact everything else follows from
|
||||
//!
|
||||
@@ -10,25 +10,25 @@
|
||||
//! `XMLTree::Parse 0x6f2a30`, which is embedded **expat 1.95.5** (version string `0x882094`,
|
||||
//! wrapper `__FILE__` `…\FrameXML\XMLTree.cpp` at `0x8715ec`). So an unclosed `<BR>`, a `</p>`
|
||||
//! closing a `<P>` (XML open/close matching is case-SENSITIVE, even though *SimpleHTML's own*
|
||||
//! name compares are not — §1.3, `SStrCmpI 0x414310`), a bare `&`, a non-predefined entity such
|
||||
//! name compares are not — `SStrCmpI 0x414310`), a bare `&`, a non-predefined entity such
|
||||
//! as ` `, a duplicate attribute, or any non-whitespace after `</HTML>` all fail the parse
|
||||
//! outright, and the widget renders the **raw** string instead (§3).
|
||||
//! outright, and the widget renders the **raw** string instead (`0x78a501`).
|
||||
//!
|
||||
//! [`roxmltree`] stands in for expat here, and it is the right stand-in rather than a convenient
|
||||
//! one: it is a well-formedness-checking XML parser with exactly the five XML predefined entities
|
||||
//! plus numeric character references, and it was checked case by case against the note's error
|
||||
//! table before this module was written — plain prose, a bare `&`, a bare `<`, an unclosed `<BR>`,
|
||||
//! a case-mismatched close tag, ` `, junk after the root and a duplicate attribute are each
|
||||
//! an `Err` (expat codes 4/4/4/7/7/11/9/8), while trailing whitespace after `</HTML>`, a leading
|
||||
//! newline, numeric refs, comments and CDATA are each an `Ok`. Its interleaved text/element child
|
||||
//! order also hands us the `<A>`/`<BR>` splice positions directly, so none of the note's
|
||||
//! plus numeric character references, and it was checked case by case against the reference's parse
|
||||
//! outcomes before this module was written — plain prose, a bare `&`, a bare `<`, an unclosed
|
||||
//! `<BR>`, a case-mismatched close tag, ` `, junk after the root and a duplicate attribute are
|
||||
//! each an `Err` (expat codes 4/4/4/7/7/11/9/8), while trailing whitespace after `</HTML>`, a
|
||||
//! leading newline, numeric refs, comments and CDATA are each an `Ok`. Its interleaved text/element
|
||||
//! child order also hands us the `<A>`/`<BR>` splice positions directly, so none of the reference's
|
||||
//! `offsetInParentText` arithmetic (`XMLTree` node `+0x18`) is needed.
|
||||
//!
|
||||
//! **The one behavioural difference found**, stated rather than hidden: roxmltree refuses a
|
||||
//! document carrying an internal DTD subset (`XML with DTD detected`) where expat 1.95.5 may
|
||||
//! accept one and honour its `<!ENTITY>` declarations. §9 of the note leaves expat's build options
|
||||
//! unread for exactly this case and observes that no `page_text` body carries a DOCTYPE; for us a
|
||||
//! DOCTYPE simply takes the plain-text fallback, which is the same place a rejected parse lands.
|
||||
//! accept one and honour its `<!ENTITY>` declarations. Expat's build options are unread for
|
||||
//! exactly this case, and no `page_text` body carries a DOCTYPE; for us a DOCTYPE simply takes
|
||||
//! the plain-text fallback, which is the same place a rejected parse lands.
|
||||
|
||||
use crate::justify;
|
||||
|
||||
@@ -54,7 +54,8 @@ pub(crate) const ALIGN_RIGHT: u32 = 0x04;
|
||||
|
||||
/// The ctor default of `hyperlinkFormat` (`+0x360`, string `0x87a838`, installed by
|
||||
/// `0x789ea7`→`0x78a540`): `<A href="X">Y</A>` becomes `|HX|hY|h`, which the font engine then
|
||||
/// parses as an ordinary hyperlink (§6.5 — nothing disables `|H` on a `CSimpleFontString`).
|
||||
/// parses as an ordinary hyperlink (`SetText 0x771d80` never touches `+0x120` — nothing disables
|
||||
/// `|H` on a `CSimpleFontString`).
|
||||
pub(crate) const DEFAULT_HYPERLINK_FORMAT: &str = "|H%s|h%s|h";
|
||||
|
||||
/// One block the walk produced — the arguments of `AddTextBlock 0x78adb0` / `AddImage 0x78ab40`,
|
||||
@@ -64,7 +65,7 @@ pub(crate) enum Block {
|
||||
/// A text block: one `CSimpleFontString`, `SetWidth(frame width)`, no height.
|
||||
Text {
|
||||
/// The already-spliced, already-collapsed string handed to `SetText` — except on the
|
||||
/// plain-text fallback path, which hands over the **raw** input (§3).
|
||||
/// plain-text fallback path, which hands over the **raw** input (`0x78a501`).
|
||||
text: String,
|
||||
/// The `elementFont[]` index this block draws with, before the empty-path fallback.
|
||||
elem: usize,
|
||||
@@ -76,14 +77,14 @@ pub(crate) enum Block {
|
||||
/// `src=`, used **verbatim** as a texture path — no prefix, no extension fix-up, no
|
||||
/// validation in this TU (`0x78ad02`).
|
||||
src: Option<String>,
|
||||
/// `width=`/`height=` in logical UI pixels (§7 step 2 — the same units as
|
||||
/// `width=`/`height=` in logical UI pixels (`0x78ab9e`/`0x78abd6` — the same units as
|
||||
/// `<AbsDimension>`); `0` when the attribute is absent.
|
||||
width: f32,
|
||||
height: f32,
|
||||
/// The `align` bits; selects which corner anchors to the previous block.
|
||||
align: u32,
|
||||
/// The **separate** float byte `[ebp-1]` (§7 step 1): a floated image reserves no height
|
||||
/// in the flow, so the following text overlaps it. Set only inside the
|
||||
/// The **separate** float byte `[ebp-1]` (zeroed at `0x78ab55`): a floated image reserves
|
||||
/// no height in the flow, so the following text overlaps it. Set only inside the
|
||||
/// attribute-present branch, which is why a bare `<IMG src=…/>` and
|
||||
/// `<IMG align="left" src=…/>` anchor identically and flow differently.
|
||||
floated: bool,
|
||||
@@ -104,7 +105,7 @@ pub(crate) struct Parse {
|
||||
pub(crate) errors: Vec<String>,
|
||||
}
|
||||
|
||||
/// `CSimpleHTML::SetText 0x78a3a0`'s parse half — §10's `SetText`/`WALK_BODY`/`ADD_PARAGRAPH`.
|
||||
/// `CSimpleHTML::SetText 0x78a3a0`'s parse half — its `WALK_BODY`/`ADD_PARAGRAPH` algorithm.
|
||||
///
|
||||
/// `frame` is the widget's name, the `%s` of the error strings. `hyperlink_format` is
|
||||
/// `+0x360`'s current value, used by the `<A>` splice.
|
||||
@@ -202,12 +203,12 @@ fn paragraph(
|
||||
errors: &mut Vec<String>,
|
||||
) -> Block {
|
||||
let align = align_of(node);
|
||||
// §6.1/§6.2 in one pass. The reference seeds the buffer with the node's whole accumulated
|
||||
// character data (expat concatenates the text either side of an inline child into the SAME
|
||||
// `+0x0c`) and then splices each child in at its recorded `+0x18` offset plus the running
|
||||
// `extra`. roxmltree hands us text and elements interleaved in document order instead, so
|
||||
// appending as we walk lands every splice exactly where the tag was, with no offset
|
||||
// arithmetic to get wrong.
|
||||
// `0x78a7b0` does this in one pass. The reference seeds the buffer with the node's whole
|
||||
// accumulated character data (expat concatenates the text either side of an inline child
|
||||
// into the SAME `+0x0c`) and then splices each child in at its recorded `+0x18` offset plus
|
||||
// the running `extra`. roxmltree hands us text and elements interleaved in document order
|
||||
// instead, so appending as we walk lands every splice exactly where the tag was, with no
|
||||
// offset arithmetic to get wrong.
|
||||
let mut buf = String::new();
|
||||
for child in node.children() {
|
||||
if child.is_text() {
|
||||
@@ -250,7 +251,7 @@ fn paragraph(
|
||||
fn image(node: roxmltree::Node) -> Block {
|
||||
let mut align = ALIGN_LEFT;
|
||||
let mut floated = false;
|
||||
// §7 step 1, the round's arbitration point. The float byte `[ebp-1]` is zeroed at `0x78ab55`
|
||||
// The float byte `[ebp-1]` is zeroed at `0x78ab55`
|
||||
// and set only INSIDE the attribute-present branch (`0x78ab67`/`0x78ab6c` both skip past it),
|
||||
// so it is a property of *the attribute being written*, not of the value:
|
||||
//
|
||||
@@ -287,7 +288,7 @@ fn align_of(node: roxmltree::Node) -> u32 {
|
||||
}
|
||||
|
||||
/// Attribute lookup, **case-insensitively** — every attribute-name compare in this engine goes
|
||||
/// through `0x64a4c0` → `SStrCmpI 0x414310` (§1.3), so `ALIGN`/`Align`/`align` are one attribute.
|
||||
/// through `0x64a4c0` → `SStrCmpI 0x414310`, so `ALIGN`/`Align`/`align` are one attribute.
|
||||
/// roxmltree's own `Node::attribute` is case-sensitive, which is why this scan exists.
|
||||
fn attr_ci<'a>(node: roxmltree::Node<'a, '_>, name: &str) -> Option<&'a str> {
|
||||
node.attributes()
|
||||
@@ -381,7 +382,7 @@ fn atof(s: &str) -> f32 {
|
||||
s[start..i].parse::<f32>().unwrap_or(0.0)
|
||||
}
|
||||
|
||||
/// The HTML-style whitespace collapse, §6.3 — `0x78a8af`–`0x78aa87`, a three-arm dispatch through
|
||||
/// The HTML-style whitespace collapse — `0x78a8af`–`0x78aa87`, a three-arm dispatch through
|
||||
/// the jump table at `.text 0x78aab8` with the byte remap at `.text 0x78aac4`.
|
||||
///
|
||||
/// | arm | characters | behaviour |
|
||||
@@ -400,7 +401,7 @@ fn atof(s: &str) -> f32 {
|
||||
/// verbatim. Operating on `&[u8]` here reproduces that exactly, and the result is valid UTF-8 by
|
||||
/// construction because only ASCII whitespace is ever dropped.
|
||||
///
|
||||
/// **This applies to markup blocks only.** The §3 plain-text path bypasses it entirely.
|
||||
/// **This applies to markup blocks only.** The plain-text path (`0x78a501`) bypasses it entirely.
|
||||
fn collapse(buf: &str) -> String {
|
||||
let src = buf.as_bytes();
|
||||
let mut out: Vec<u8> = Vec::with_capacity(src.len());
|
||||
@@ -670,8 +671,8 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
/// The `page_text` shape §8 quotes, end to end: an `<H1>` title, a `<BR/>`, and centred
|
||||
/// paragraphs, with the body's own inter-tag newlines contributing nothing.
|
||||
/// The stock `ItemTextFrame`'s `page_text` shape, end to end: an `<H1>` title, a `<BR/>`, and
|
||||
/// centred paragraphs, with the body's own inter-tag newlines contributing nothing.
|
||||
#[test]
|
||||
fn the_page_text_shape_walks_to_the_block_list_a_reader_would_draw() {
|
||||
let p = parse_markup(
|
||||
@@ -703,9 +704,10 @@ mod tests {
|
||||
assert!(p.errors.is_empty());
|
||||
}
|
||||
|
||||
/// The §8 signed-page consequence: `ItemTextFrame.lua` appends `From: <creator>` **after**
|
||||
/// `</HTML>`, which is expat error 9 — so a signed HTML page renders as raw markup on the
|
||||
/// reference client too. Our fallback must reach the same place rather than "fixing" it.
|
||||
/// The signed-page consequence: `ItemTextFrame.lua` appends `From: <creator>` **after**
|
||||
/// `</HTML>`, which is expat error 9 (`0x881fec`) — so a signed HTML page renders as raw
|
||||
/// markup on the reference client too. Our fallback must reach the same place rather than
|
||||
/// "fixing" it.
|
||||
#[test]
|
||||
fn a_signed_html_page_falls_back_exactly_as_the_reference_does() {
|
||||
let raw = "\n<HTML><BODY><P>The letter body.</P></BODY></HTML>\n\nFrom:\nMankrik\n\n";
|
||||
|
||||
@@ -9,9 +9,8 @@
|
||||
//! id doing double duty as both the row's identity and the group key (here the GROUP key is the
|
||||
//! category, the row's identity is the skill id).
|
||||
//!
|
||||
//! ## The engine grouping law (PINNED at the bytes — decision 1091; wow-re
|
||||
//! `system/tradeskill/scratch/skillframe-display-list.md`, the list build `0x4d2cb0` + its
|
||||
//! comparator `0x4d3070`. The 0530 follow-up this once carried is closed.)
|
||||
//! ## The engine grouping law (PINNED at the bytes — decision 1091; the list build `0x4d2cb0` +
|
||||
//! its comparator `0x4d3070`. The 0530 follow-up this once carried is closed.)
|
||||
//!
|
||||
//! Groups ordered by `category_order` ascending (`category_id` breaks a tie, for determinism);
|
||||
//! one header row per non-empty group (text = `category_name`); within a group, **untrained lines
|
||||
@@ -97,8 +96,7 @@ pub struct SkillEntry {
|
||||
pub max: u32,
|
||||
/// `SkillRaceClassInfo.flags & 0x400` (`SKILL_FLAG_MONO_VALUE`) for the player's race/class:
|
||||
/// a **single-rank** line. The real client's `GetSkillLineInfo` overrides `skillMaxRank` to
|
||||
/// `1` for these, whatever the descriptor says (wow-re
|
||||
/// `system/tradeskill/scratch/skillframe-seed-abandon.md`: `0x4d3610`, `4d38b1 test ah,0x4`) —
|
||||
/// `1` for these, whatever the descriptor says (`0x4d3610`, `4d38b1 test ah,0x4`) —
|
||||
/// so a hunter's `Beast Mastery`, which vmangos happily reports as `300/300`, reads as a
|
||||
/// proficiency and `SkillFrame.lua`'s `skillMaxRank == 1` branch draws it gray with no rank
|
||||
/// text. The raw [`Self::max`] stays untouched here; the override lives at the API return,
|
||||
@@ -308,11 +306,10 @@ fn set_collapsed(model: &mut Model, id: usize, collapse: bool) {
|
||||
/// module doc's by-id persistence); a header row or an out-of-range index clears the selection.
|
||||
/// `GetSkillLineInfo`'s **out-of-range tuple** — thirteen values, four of them numeric zeros.
|
||||
///
|
||||
/// Byte-verified in wow-re's `system/tradeskill/scratch/skillframe-selection-and-oob.md` (pushes
|
||||
/// `0x4d3a2c`…`0x4d3a98`, `mov eax,0xd` at `0x4d3a9f`); decision 1919. One shape serves five
|
||||
/// distinct conditions — index 0, a negative index, an index past the end, no active player, and an
|
||||
/// in-range row whose DBC record is missing — because the reference's bounds test (`0x4d3675`) is a
|
||||
/// single UNSIGNED compare that funnels them all to the same exit.
|
||||
/// Byte-verified (pushes `0x4d3a2c`…`0x4d3a98`, `mov eax,0xd` at `0x4d3a9f`); decision 1919. One
|
||||
/// shape serves five distinct conditions — index 0, a negative index, an index past the end, no
|
||||
/// active player, and an in-range row whose DBC record is missing — because the reference's
|
||||
/// bounds test (`0x4d3675`) is a single UNSIGNED compare that funnels them all to the same exit.
|
||||
///
|
||||
/// The numeric slots are 4 (`skillRank`), 5 (`numTempPoints`), 6 (`skillModifier`), 7
|
||||
/// (`skillMaxRank`), 11 (`minLevel`) and 12 (`skillCostType`). Slots 4 and 5 must be numbers or the
|
||||
@@ -407,8 +404,7 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
// a header (`0x4d3768`). `index` 1-based.
|
||||
//
|
||||
// **OUT OF RANGE IS THE 13-VALUE FALLBACK, NOT A SINGLE NIL.** This comment used to say the
|
||||
// opposite and it was wrong; the correction is decision 1919, from a wow-re trio cross-check
|
||||
// (`system/tradeskill/scratch/skillframe-selection-and-oob.md`). `0x4d3675`'s bounds test is
|
||||
// opposite and it was wrong; the correction is decision 1919. `0x4d3675`'s bounds test is
|
||||
// UNSIGNED against the total row count, so index 0, a negative index, an index past the end, no
|
||||
// active player, and an in-range row whose DBC record is missing all fall to `0x4d3a2a` and push
|
||||
// the same thirteen: `nil, nil, nil, 0, 0, 0, 0, nil, nil, nil, 0, 0, nil` (`0x4d3a2c`…
|
||||
@@ -894,7 +890,7 @@ mod tests {
|
||||
}
|
||||
|
||||
/// **Out of range answers the reference's THIRTEEN-value tuple, not a bare nil** — decision
|
||||
/// 1919, byte-verified in wow-re's `skillframe-selection-and-oob.md`.
|
||||
/// 1919, byte-verified (`0x4d3675`'s bounds test, `0x4d3a2c`…`0x4d3a98`).
|
||||
///
|
||||
/// The COUNT is the whole assertion. `GetSkillLineInfo(0) == nil` reads true either way,
|
||||
/// because Lua compares only the first returned value — which is precisely how the old
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
//! `0x6eee40`). A value in `[min, max]` with a step and orientation, driving a thumb texture along
|
||||
//! the track.
|
||||
//!
|
||||
//! Grounded in wow-re's byte-verified LoadXML table (RF-28, `rf28-typed-widget-loadxml.md`): the
|
||||
//! Grounded in the byte-verified LoadXML table (`Slider::LoadXML 0x789580`): the
|
||||
//! `<ThumbTexture>` sub-element (default layer OVERLAY=3), `minValue`/`maxValue`/`valueStep`/
|
||||
//! `defaultValue`, the `orientation` shared enum (HORIZONTAL=0/VERTICAL=1, `0x811b00`), and the
|
||||
//! widget's own `OnValueChanged` script slot (`+0x330`). The **thumb-position** mechanism — the
|
||||
@@ -11,13 +11,14 @@
|
||||
//! ScrollFrame's scroll; decisions 0112/0250).
|
||||
//!
|
||||
//! `SetValue` fires `OnValueChanged` on the **first-ever** value and after that **only on an
|
||||
//! actual change** ([`SliderState::store_value`] — the client's `+0x314` bit2, wow-re
|
||||
//! `slider-mouse-law.md` §6). The change-gate is load-bearing, not an optimization: the real
|
||||
//! scrollbar template wires `OnValueChanged → this:GetParent():SetVerticalScroll(arg1)` and the
|
||||
//! ScrollFrame's `OnVerticalScroll → scrollbar:SetValue(arg1)` back the other way, so a fire-always
|
||||
//! `SetValue` would recurse forever (`UIPanelTemplates.xml`); the gate breaks the loop after one
|
||||
//! hop. `SetMinMaxValues` re-clamps through the same gate only once a value exists, so a range set
|
||||
//! from `<OnLoad>` never runs a handler the addon has not armed yet.
|
||||
//! actual change** ([`SliderState::store_value`] — the client's `+0x314` bit2, `SetValue
|
||||
//! 0x789930`'s `0x789a06 test bl,4` gate). The change-gate is load-bearing, not an optimization:
|
||||
//! the real scrollbar template wires `OnValueChanged →
|
||||
//! this:GetParent():SetVerticalScroll(arg1)` and the ScrollFrame's `OnVerticalScroll →
|
||||
//! scrollbar:SetValue(arg1)` back the other way, so a fire-always `SetValue` would recurse
|
||||
//! forever (`UIPanelTemplates.xml`); the gate breaks the loop after one hop. `SetMinMaxValues`
|
||||
//! re-clamps through the same gate only once a value exists, so a range set from `<OnLoad>` never
|
||||
//! runs a handler the addon has not armed yet.
|
||||
//!
|
||||
//! The methods live in their own registry table, consulted by the frame `__index` dispatcher only
|
||||
//! for Slider frames — so duck-typing addons (`if frame:GetThumbTexture() then …`) see `nil` on
|
||||
@@ -60,7 +61,8 @@ fn with_slider<T>(
|
||||
|
||||
/// Get-or-create the thumb texture region (`SetThumbTexture`/`<ThumbTexture>`); `layer` re-layers an
|
||||
/// existing thumb. Returns the region's id (for wrapper lookup). The widget default layer is OVERLAY
|
||||
/// (RF-28 — the Slider's `drawLayer` attr defaults OVERLAY=3, unlike StatusBar's ARTWORK bar).
|
||||
/// (`Slider::LoadXML 0x789580` — the Slider's `drawLayer` attr defaults OVERLAY=3, unlike
|
||||
/// StatusBar's ARTWORK bar).
|
||||
fn ensure_thumb(lua: &Lua, this: &Table, layer: Option<DrawLayer>) -> mlua::Result<u32> {
|
||||
let h = frame_handle_of(lua, this)?;
|
||||
let mut model = lua.app_data_mut::<Model>().expect("model app_data");
|
||||
@@ -111,10 +113,11 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
m.set(
|
||||
"SetMinMaxValues",
|
||||
lua.create_function(|lua, (this, min, max): (Table, f32, f32)| {
|
||||
// Unlike StatusBar, a reversed pair is NOT swapped (the Slider LoadXML stores min +
|
||||
// (max−min) and does no swap, RF-28). A held value re-clamps into the new range and a
|
||||
// move fires OnValueChanged — but only once a value EXISTS (`SliderState::set_min_max`,
|
||||
// the client's bit2): on a fresh slider this sets the range and fires nothing.
|
||||
// Unlike StatusBar, a reversed pair is NOT swapped (the Slider LoadXML `0x789580`
|
||||
// stores min + (max−min) and does no swap). A held value re-clamps into the new
|
||||
// range and a move fires OnValueChanged — but only once a value EXISTS
|
||||
// (`SliderState::set_min_max`, the client's bit2): on a fresh slider this sets the
|
||||
// range and fires nothing.
|
||||
let changed = with_slider(lua, &this, |s| s.set_min_max(min, max))?;
|
||||
fire_value_changed(lua, &this, changed)
|
||||
})?,
|
||||
@@ -175,7 +178,7 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
// **A 1.12 Slider has no enabled state at all**: the three names are registered once each, in
|
||||
// the BUTTON table `0x879d00` (`0x77fef0`/`0x77ffd0`/`0x7800b0`), and the Slider's own LoadXML
|
||||
// `0x789580` takes `drawLayer`/`minValue`/`maxValue`/`valueStep`/`defaultValue`/`orientation`
|
||||
// and nothing else (wow-re `rf28-typed-widget-loadxml.md` §Slider) — so there was no way in
|
||||
// and nothing else — so there was no way in
|
||||
// from Lua *or* from XML, and the flag could only ever read `true`.
|
||||
//
|
||||
// They were a superset in PRESENCE, which is what 1189 records the cost of: a duck-typing addon
|
||||
@@ -250,8 +253,9 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Fire `OnValueChanged(self, value)` if `changed` carries the new value (RF-28: the Slider's own
|
||||
/// script slot `+0x330`). Fired outside any model borrow; errors go to [`Model::errors`].
|
||||
/// Fire `OnValueChanged(self, value)` if `changed` carries the new value (`Slider::LoadXML
|
||||
/// 0x789580`: the Slider's own script slot `+0x330`). Fired outside any model borrow; errors go
|
||||
/// to [`Model::errors`].
|
||||
fn fire_value_changed(lua: &Lua, this: &Table, changed: Option<f32>) -> mlua::Result<()> {
|
||||
let Some(value) = changed else { return Ok(()) };
|
||||
let id = {
|
||||
@@ -315,8 +319,8 @@ pub(super) fn thumb_rect(r: Rect, thumb_size: (f32, f32), vertical: bool, fracti
|
||||
/// (`0x789ba0`: `ff 50 1c call [eax+0x1c]` for the horizontal branch, `+0x20` for the vertical), and
|
||||
/// on a `CSimpleTexture` those slots are `0x770720`/`0x770790` — the **native-texel fallback**:
|
||||
/// authored span when it is non-zero on that axis, else the art's own texel span through the same
|
||||
/// `<AbsDimension>` converter, else `0.0` when there is no art at all (wow-re
|
||||
/// `region-size-fallback.md` §2, VERIFIED; ours is [`super::region::virtual_span`], decision 1349).
|
||||
/// `<AbsDimension>` converter, else `0.0` when there is no art at all (ours is
|
||||
/// [`super::region::virtual_span`], decision 1349).
|
||||
///
|
||||
/// Reading `RegionData::size` instead is what broke every Lua-built slider: `SetThumbTexture(path)`
|
||||
/// authors no size, all four stock `<ThumbTexture>`s declare one, and the old fallback — *the thumb
|
||||
|
||||
@@ -221,9 +221,10 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
})?,
|
||||
)?;
|
||||
|
||||
// The LFG pair, as the bytes define it (wow-re `lfg-set-get-law.md`, 1961 — which corrects
|
||||
// 1959's flag): the stock 1.12.1 FrameXML never calls either (FriendsFrame.xml's two call
|
||||
// sites sit inside its l.1212-1301 XML comment), so this is an addon surface.
|
||||
// The LFG pair, as the bytes define it (`GetLookingForGroup 0x4e95d0` / `SetLookingForGroup
|
||||
// 0x4e96b0`; decision 1961 — which corrects 1959's flag): the stock 1.12.1 FrameXML never
|
||||
// calls either (FriendsFrame.xml's two call sites sit inside its l.1212-1301 XML comment), so
|
||||
// this is an addon surface.
|
||||
//
|
||||
// `GetLookingForGroup()` → FOUR values: the three slot NAMES — each nil for a slot word whose
|
||||
// id maps to nothing, and the reference's own pack (below) leaves every slot word 0, so nil
|
||||
@@ -488,7 +489,7 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
)?;
|
||||
|
||||
// `SortWho(sortType) 0x5ad890` — the column-header and dropdown sorts, and three things at
|
||||
// once (wow-re `who-list-sort-law.md`; decision 2030):
|
||||
// once (decision 2030):
|
||||
//
|
||||
// 1. **promote** the key into the seven-slot chain, flipping its direction only if it was
|
||||
// already at the front — so a repeated click on the same header REVERSES;
|
||||
|
||||
@@ -37,9 +37,7 @@
|
||||
//! address-takes) is the whole of both verbs. What that shared head does before the branch, and
|
||||
//! what only the play arm does after it, is the app side's business (`sound::zone`'s
|
||||
//! `set_lua_music`) — the point here is that the two bindings are not independent, and a
|
||||
//! `MusicRequest` is the argument, not the verb (wow-re `sound/scratch/lua-music-bindings.md`,
|
||||
//! the §5 round dispatched for this report; it corrected wow-re's own table, which had recorded
|
||||
//! `StopMusic`'s callee as "—").
|
||||
//! `MusicRequest` is the argument, not the verb.
|
||||
|
||||
use mlua::{Lua, Value};
|
||||
|
||||
|
||||
@@ -88,9 +88,9 @@ pub struct SpellSlotView {
|
||||
/// nothing else: a passive can still be picked up/placed on a bar (the ref never blocks that).
|
||||
pub passive: bool,
|
||||
/// The `IsCurrentCast` verdict for this slot — the checked ring (`SpellButton_UpdateSelection`'s
|
||||
/// gold glow). The delegate `0x4b3600` has exactly two arms (wow-re
|
||||
/// `spellbook-checked-predicate.md`): a shapeshift spell whose form is the player's current
|
||||
/// form byte, or the open trade-skill window's own spell — never an ordinary in-flight cast.
|
||||
/// gold glow). The delegate `0x4b3600` has exactly two arms: a shapeshift spell whose form is
|
||||
/// the player's current form byte, or the open trade-skill window's own spell — never an
|
||||
/// ordinary in-flight cast.
|
||||
/// App-resolved (`benilla::ui_spellbook`), pushed with the book; the app fires
|
||||
/// `CURRENT_SPELL_CAST_CHANGED` on its edges.
|
||||
pub current: bool,
|
||||
@@ -194,7 +194,7 @@ impl super::UiScript {
|
||||
/// Push whether the app's cast lifecycle holds something `SpellStopCasting()` can stop — a
|
||||
/// running auto-repeat or an in-flight cast, but NOT a channel (the ref's `0x6e6e80` reads
|
||||
/// only the auto-repeat key `0xceac30` and the inflight id `0xceca88`, and the inflight id
|
||||
/// is already 0 during a channel — wow-re `esc-stopcasting.md`). Pushed each frame by the
|
||||
/// is already 0 during a channel). Pushed each frame by the
|
||||
/// app's cast feed (`benilla::ui_cast`), before the input pass runs the ESC chain.
|
||||
pub fn set_casting(&mut self, casting: bool) {
|
||||
self.model_mut().casting = casting;
|
||||
@@ -230,8 +230,8 @@ fn is_pet_book(book_type: &str) -> bool {
|
||||
book_type.eq_ignore_ascii_case(BOOKTYPE_PET)
|
||||
}
|
||||
|
||||
/// The shared argument marshaller of the reference's spell-slot bindings (`0x4b3ec0`, wow-re
|
||||
/// `getspellname-return-contract.md` §6 — `GetSpellName`, `GetSpellTexture`, `IsCurrentCast`,
|
||||
/// The shared argument marshaller of the reference's spell-slot bindings (`0x4b3ec0` —
|
||||
/// `GetSpellName`, `GetSpellTexture`, `IsCurrentCast`,
|
||||
/// `GetSpellCooldown`, `IsSpellPassive`, `CastSpell`, `PickupSpell` and the two autocast verbs all
|
||||
/// call it): arg 1 must be a number and arg 2 a number or string — the book type is **not**
|
||||
/// optional; the index is `trunc(arg1 - 1)` and must land in `[0, 0x400)`, else the binding
|
||||
@@ -427,8 +427,7 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
|
||||
// `UpdateSpells()` — twelve bytes in the reference (`[0x4b43e0,0x4b43ec)`), and its entire
|
||||
// content is a bare `SignalEvent(SPELLS_CHANGED)`: event 260, **no arguments**, and NO state
|
||||
// mutation whatsoever. Byte-carved by a wow-re cross-check (decision 1924, their
|
||||
// `system/ui/scratch/updatespells-verb.md`). Entered with both sort flags zero the worker
|
||||
// mutation whatsoever (decision 1924). Entered with both sort flags zero the worker
|
||||
// performs exactly one memory write — a `push esi` undone nine instructions later — and
|
||||
// `0x4b302f` is the sole fire site for event 260 image-wide.
|
||||
//
|
||||
@@ -475,8 +474,7 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
|
||||
// GetSpellTabInfo(i) -> name, texture, offset, numSpells. **FOUR values on every path that
|
||||
// returns** — `0x4b3ce0` has two live `ret`s and both `mov eax,4`; the `eax=0` one at `0x4b3d0d`
|
||||
// is dead code after `luaL_error` longjmps. Byte-carved by a wow-re cross-check (decision 1931,
|
||||
// `system/ui/scratch/spelltabinfo-return-contract.md`).
|
||||
// is dead code after `luaL_error` longjmps (decision 1931).
|
||||
//
|
||||
// **OUT OF RANGE IS `nil, nil, 0, 0` — LITERAL zeros, not a single nil.** This comment used to say
|
||||
// "out of range -> a single nil (GetMerchantItemInfo's own out-of-range shape)", i.e. one
|
||||
@@ -534,8 +532,7 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
)?;
|
||||
|
||||
// GetSpellName(id, bookType) -> name, rank. **Always TWO values, never one, and the rank of a
|
||||
// rankless spell is the EMPTY STRING, never nil** (wow-re
|
||||
// `scratch/getspellname-return-contract.md`, §5-derived from the bytes alone).
|
||||
// rankless spell is the EMPTY STRING, never nil** — derived from the bytes alone, below.
|
||||
//
|
||||
// Both returns go through the same push helper with **no rank-specific branch anywhere in the
|
||||
// binding** — a ranked and a rankless spell run byte-identical code: `0x4b4063 call 0x6f3890`
|
||||
@@ -748,7 +745,7 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
)?;
|
||||
|
||||
// CastSpellByName(name [, onSelf]) — the reference binding `0x4b4ab0`, whose only two callers
|
||||
// share the `0x4b3300` dispatcher with `CastSpell` above (wow-re ledger), so this queues onto
|
||||
// share the `0x4b3300` dispatcher with `CastSpell` above, so this queues onto
|
||||
// the same `spell_casts` list and the app's one cast tail handles both. `SlashCmdList["CAST"]`
|
||||
// is literally `CastSpellByName(msg)`, which is why `/cast` needs nothing else, and it is the
|
||||
// command the whole macro system is built to run.
|
||||
@@ -776,8 +773,8 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
})?,
|
||||
)?;
|
||||
|
||||
// SpellStopCasting() — the ref's Script::SpellStopCasting (`0x6e6e80`, §5-verified whole,
|
||||
// wow-re `esc-stopcasting.md`): stop the FIRST of {running auto-repeat (`0x6ea080`,
|
||||
// SpellStopCasting() — the ref's Script::SpellStopCasting (`0x6e6e80`): stop the FIRST of
|
||||
// {running auto-repeat (`0x6ea080`,
|
||||
// CMSG_CANCEL_AUTO_REPEAT_SPELL), in-flight cast (`AbortCast` → CMSG_CANCEL_CAST)} and
|
||||
// return 1; nil when neither runs. A CHANNEL is nil — the body's whole callee closure
|
||||
// never reaches the channel canceler `0x6e9b70`, and the inflight id `0xceca88` it gates
|
||||
@@ -801,8 +798,8 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
})?,
|
||||
)?;
|
||||
|
||||
// SpellIsTargeting() — the ref's Script::SpellIsTargeting (`0x6e6cd0`, wow-re
|
||||
// `wave-cast.md`): true while the targeting cursor is up (`flag_word != 0`), nil otherwise.
|
||||
// SpellIsTargeting() — the ref's Script::SpellIsTargeting (`0x6e6cd0`): true while the
|
||||
// targeting cursor is up (`flag_word != 0`), nil otherwise.
|
||||
// Read by FrameXML (PetFrame's right-click bind fork) and by the ESC chain's callers.
|
||||
g.set(
|
||||
"SpellIsTargeting",
|
||||
@@ -1557,7 +1554,7 @@ mod tests {
|
||||
}
|
||||
|
||||
/// **`UpdateSpells()` fires `SPELLS_CHANGED` and does nothing else** — decision 1924, from a
|
||||
/// wow-re byte read of `[0x4b43e0,0x4b43ec)`.
|
||||
/// byte read of `[0x4b43e0,0x4b43ec)`.
|
||||
///
|
||||
/// The assertion is that a registered handler RAN, not that any frame repainted: the reference
|
||||
/// verb mutates no state at all, and the repaint is FrameXML's, reached only through the event.
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user