the numerals kit goes stock, and a chat link is its own mouse target

- the status-bar numerals kit (`TextStatusBar.xml` + `.lua`) is the reference's own, and the attempt found a class of silent failure: `TextStatusBar_Initialize()` takes no argument in 1.12 and acts on `this`; ours took an optional bar, our unit frames passed one, and the reference's version ignores it — so the numerals would simply never appear, no error, nothing missing; seventeen of our signatures differ from the reference's that way (Lua drops a surplus argument in silence), written down before their files migrate, and the three "ours narrower" ones — `StaticPopup_Hide(which, data)`, `StaticPopup_FindVisible`, `WorldMapButton_OnClick(button, frame)` — closed now, because that direction bites when a STOCK file arrives and calls our version
- a chat link needs no mouse on its frame: wow-re read the client's ctors as mouse-enabling Button, CheckButton, Slider, ColorSelect and EditBox and NOT the scroll frames; the objection ("something makes a 1.12 chat link clickable") resolved as the engine synthesising one mouse-enabled `CSimpleHyperlinkButton` per link span (`0x7a3240`) — so a link span is a hit-test candidate on its own, the chat window is inert everywhere else, and the wheel is its own predicate rather than a borrowed mouse bit; seven `*ScrollFrame` known divergences retire
- the relic fit rule: `IsValidForSlot`'s slot-17 leg is an EQUALITY — `(InventoryType == RELIC) == hasRelicSlot` — so a relic lights the ranged slot for a druid and a bow does not, on the bag bar and the paper doll alike; closing it half-right would have lit the highlight for a druid dragging a bow
- a chain `.xml` whose `.lua` never loaded: `MoneyInputFrame.xml` had been on the chain without its `.lua`, so the template loaded and all ten `MoneyInputFrame_*` verbs read nil, silently — a gate now fails any chain `.xml` whose `.lua` sibling is neither sourced nor listed; the gap report's LOAD column carries the probe's collision caveat, its fx column stops crying wolf (it follows `<Script file=>`, stops reading `.Field(` as a global, subtracts a file's own locals), and a window is its xml PLUS the lua it sources

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Sam
2026-09-02 17:01:18 +07:00
co-authored by Claude Fable 5.1
parent 112e82d276
commit feca23de3f
30 changed files with 981 additions and 404 deletions
+1 -1
View File
@@ -1115,7 +1115,7 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
// a property of the SLOT, not of the player. It pairs with `UnitHasRelicSlot("player")`,
// which is the class half, and stock reads the two as a conjunction
// (`PaperDollFrame.lua:680`/`:744`). This comment used to say `UnitHasRelicSlot` is a
// later-era concept, always false in vanilla; it is not — decision 1785.
// later-era concept, always false in vanilla; it is not — decision 1796.
g.set(
"GetInventorySlotInfo",
lua.create_function(|lua, name: String| {
+1
View File
@@ -101,6 +101,7 @@ mod model;
mod modelframe;
mod net_stats;
mod object;
pub use object::frame_kind_from_tag;
mod party;
mod pet;
mod petition;
+9 -1
View File
@@ -192,12 +192,20 @@ pub(super) fn draw_layer_name(l: DrawLayer) -> &'static str {
}
}
/// A widget tag or `CreateFrame` type string → its [`FrameKind`], case- and separator-insensitive
/// ([`enum_token`]). Public because it is the ONE mapping: `benilla-app`'s `frame_flag_gate` sweep
/// needs it to ask the engine whether a tag is mouse-enabled by construction, and a second copy
/// there is exactly what drifted.
pub fn frame_kind_from_tag(s: &str) -> Option<FrameKind> {
frame_kind_from_str(s)
}
fn frame_kind_from_str(s: &str) -> Option<FrameKind> {
Some(match enum_token(s).as_str() {
"FRAME" => FrameKind::Frame,
"BUTTON" => FrameKind::Button,
"CHECKBUTTON" => FrameKind::CheckButton,
// A real registered type (`0x4959a6`), not an alias for Button — decision 1788. Registering
// A real registered type (`0x4959a6`), not an alias for Button — decision 1799. Registering
// it AS a Button would load `LootFrame.xml` and leave every row dead, because 1.12 has no
// Lua verb for "take slot N": `LootSlot` is the bind-confirm continuation, hard-wired to
// `flag = 1`.
+53 -3
View File
@@ -50,7 +50,7 @@ use crate::order;
use crate::widget::FrameHandle;
use super::clip::{effective_clip, scroll_clip_sources};
use super::{button, cursor, editbox, event, UiScript};
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
@@ -127,6 +127,15 @@ pub(super) fn install(lua: &mlua::Lua) -> mlua::Result<()> {
Ok(())
}
/// Whether `(x, y)` falls inside one of this frame's hyperlink spans — the engine half of the
/// reference's per-span `CSimpleHyperlinkButton` children.
fn link_span_hit(model: &Model, fh: FrameHandle, x: f32, y: f32) -> bool {
model
.link_spans
.get(&fh)
.is_some_and(|spans| spans.iter().any(|(r, _, _)| point_in_rect(*r, x, y)))
}
impl UiScript {
/// Hit-test the cursor at `(x, y)` and return the **handle** of the captured frame (the
/// topmost-drawn mouse-enabled, effective-visible frame whose rect contains the point **and**
@@ -146,7 +155,22 @@ impl UiScript {
let sorted = order::traversal(&model.arena);
let scroll_sources = scroll_clip_sources(&model);
order::hit_test(&sorted, |fh| {
model.arena.is_mouse_enabled(fh)
// Mouse-enabled, **or inside one of this frame's hyperlink spans**.
//
// That second half is how the reference makes a chat link clickable on a frame that
// takes no mouse at all. `CSimpleMessageScrollFrame`'s ctor leaves `[+0xcc] = 0`, so
// `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`).
//
// 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
// the split: the chat window is a hit candidate only over a LINK, so `GetMouseFocus()`
// answers what is behind it elsewhere, its `movable="true"` +
// `<TitleRegion setAllPoints="true"/>` stay inert so it drags by its tab and not its
// body, and a hover over plain text reassigns to whatever is underneath.
(model.arena.is_mouse_enabled(fh) || link_span_hit(&model, fh, x, y))
&& model.resolved.get(&fh).is_some_and(|r| {
point_in_rect(inset_rect(*r, model.arena.hit_rect_insets(fh)), x, y)
})
@@ -155,6 +179,26 @@ impl UiScript {
})
}
/// [`Self::hit_test`] over the frames that take the WHEEL — either bit, since a mouse-enabled
/// frame with an `<OnMouseWheel>` is a candidate for both. Same rect and clip rules; only the
/// enable predicate differs.
fn hit_test_wheel(&self, x: f32, y: f32) -> Option<u32> {
let model = self.model_ref();
let sorted = order::traversal(&model.arena);
let scroll_sources = scroll_clip_sources(&model);
let fh = order::hit_test(&sorted, |fh| {
(model.arena.is_mouse_wheel_enabled(fh)
|| model.arena.is_mouse_enabled(fh)
|| link_span_hit(&model, fh, x, y))
&& model.resolved.get(&fh).is_some_and(|r| {
point_in_rect(inset_rect(*r, model.arena.hit_rect_insets(fh)), x, y)
})
&& effective_clip(&model, &scroll_sources, fh)
.is_none_or(|c| point_in_rect(c, x, y))
})?;
model.frame_to_id.get(&fh).copied()
}
/// Hit-test the cursor at `(x, y)` and return the **id** of the captured frame, or `None` —
/// [`Self::hit_test_frame`] with the id lookup every caller in the input path wants. The id is
/// the same one used across this host (e.g. the layout [`crate::layout::Handle`]); the app can
@@ -749,7 +793,13 @@ impl UiScript {
/// [`UiScript::errors`].
pub fn mouse_wheel(&mut self, x: f32, y: f32, delta: f32) {
let target: Option<u32> = {
let hit = self.hit_test(x, y);
// **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
// 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);
let model = self.model_ref();
hit.and_then(|id| {
let mut h = *model.id_to_frame.get(&id)?;
@@ -599,7 +599,7 @@ pub(in crate::script) fn install(lua: &Lua) -> mlua::Result<()> {
// **This shipped absent for a long time, on a claim that was simply false** — that the relic
// slot post-dates 1.12 and `UnitHasRelicSlot` is a later-era verb. The base `dbc.MPQ` copy of
// `ChrClasses.dbc` really does have no field 16; the `patch.MPQ` copy the client actually
// reads has it, set for Paladin, Shaman and Druid. Decision 1785. Stock `PaperDollFrame.lua`
// reads has it, set for Paladin, Shaman and Druid. Decision 1796. Stock `PaperDollFrame.lua`
// calls this **unconditionally** at l.429 and l.580 — so while it was missing, the character
// sheet raised `attempt to call global 'UnitHasRelicSlot' (a nil value)` for *every* class,
// not just the three.
+1 -1
View File
@@ -173,7 +173,7 @@ pub struct UnitState {
/// which is the nil leg.
///
/// True for exactly Paladin, Shaman and Druid in 1.12 (Libram, Totem, Idol). It was believed
/// here for a long time that no 1.12 class had one; see decision 1785 for how that survived.
/// here for a long time that no 1.12 class had one; see decision 1796 for how that survived.
pub has_relic_slot: bool,
/// The unit's sex on the `UnitSex` scale: `2` male, `3` female (`1` = neuter/unknown). `0`
/// (the unfilled default) reports as nil — the API's "can't tell", like [`Self::reaction`].
+1 -1
View File
@@ -1295,7 +1295,7 @@ fn unit_is_party_leader_ors_two_legs_and_answers_one_when_solo() {
/// `UnitHasRelicSlot` — the number 1 or nil, per token, never a boolean.
///
/// This shipped **absent** for months on the belief that the relic slot post-dates 1.12, which is
/// false (decision 1785). Stock `PaperDollFrame.lua` calls it unconditionally at l.429 and l.580,
/// false (decision 1796). Stock `PaperDollFrame.lua` calls it unconditionally at l.429 and l.580,
/// so while it was missing the character sheet raised `attempt to call global` for every class —
/// which is why the nil-global case is asserted here too, not just the answer.
#[test]