- the world map speaks the reference's names: WorldMapTooltip is declared (six corpus addons index it; pfUI dies on it at file scope), the blackout frame takes the reference's spelling BlackoutWorld, and the four hand-written party blips become the reference's own unit-blip templates — all 44 blips as instances, each with its published $parentIcon, the name pfUI recolours group blips by class through; the 40 raid ones are declared but not positioned yet, named in the OUT list rather than faked
- the templates addons build their own buttons from are published: ItemButtonTemplate, ContainerFrameItemButtonTemplate and the bag-slot chain — most of the handler scripts were live the whole time (this client executes the reference's own container and paper-doll code off the player's patch chain), so what was missing was mostly declarations, not behaviour — the bank arm stays out and is named as the omission it is; OptionsFrameSliders becomes real, with the three Graphics rows reading it and re-reading on show, so an addon's write to a slider's bounds actually moves the slider; and the template census that had ranked these at "zero demand" learns to see a local bound to a literal — its table went from 3 rows to 7
- the bindings answer in 1.12's own shapes: Button:IsEnabled() answers the NUMBER 1 or 0 (0 is truthy in Lua, so the reference's own == 0 tests got false forever, and three of our files had hand-worked around the divergence rather than fixed it); the constructors' string arguments are four shapes, not one rule — pfUI passes a Button where a template name goes and the reference constructs the FontString and ignores it; SetInventoryItem returns THREE values, the reference's own paper doll destructuring repairCost; UnitIsVisible is object presence and nothing else (57 bytes, one branch, no distance — an out-of-range party member is exists-yes, visible-nil); the tapped pair reads the two dynamic-flags bits, and its repaint wire fires UNIT_FACTION on the tap bit — the half a passing predicate test never sees; UnitIsPartyLeader is two ORed legs covering disjoint failures (a held stranger leading their own party, and an out-of-range member with no object to read a flag from); and the four nameplate verbs take no argument and return zero values
- the ghost's wisps compose onto its body instead of over it — the four emitters drew at full strength over a body at alpha 0.5, and now inherit the instance alpha down the attachment chain the way the reference recomposes it per frame; the residual we had parked ("the wisps should inherit the tint too") is REFUTED at the bytes: a particle batch commits hardcoded white RGB, and even that never reaches the fragment — particles are never tinted, only the alpha crosses, applied to the alpha byte alone — so the shipped shader split was already right and no code changes
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
181 lines
9.3 KiB
Rust
181 lines
9.3 KiB
Rust
use mlua::Table;
|
||
|
||
use crate::framexml::{self, Element};
|
||
|
||
use super::{abs_dim, children_named, Loader};
|
||
|
||
impl Loader<'_> {
|
||
/// LoadXML attribute handling (rf24 `0x769820`): the subset the v1 object model exposes, plus a
|
||
/// warn-once for the attributes whose methods don't exist yet (a next-phase gap, not a workaround).
|
||
pub(super) fn apply_attrs(&mut self, el: &Element, wrapper: &Table, dbg: &str) {
|
||
if el.attr_bool("hidden") {
|
||
self.call(wrapper, "Hide", (), dbg);
|
||
}
|
||
if let Some(strata) = el.attr("frameStrata") {
|
||
self.call(wrapper, "SetFrameStrata", strata.to_string(), dbg);
|
||
}
|
||
if let Some(level) = el.attr("frameLevel") {
|
||
if let Ok(n) = level.parse::<i64>() {
|
||
self.call(wrapper, "SetFrameLevel", n, dbg);
|
||
}
|
||
}
|
||
if let Some(alpha) = el.attr("alpha") {
|
||
if let Ok(a) = alpha.parse::<f32>() {
|
||
self.call(wrapper, "SetAlpha", a, dbg);
|
||
}
|
||
}
|
||
if let Some(id) = el.attr("id") {
|
||
if let Ok(n) = id.parse::<i64>() {
|
||
self.call(wrapper, "SetID", n, dbg);
|
||
}
|
||
}
|
||
// `clampedToScreen="true"` → SetClampedToScreen (rf24 `0x768cc0` — geometry flags bit4,
|
||
// the layout resolve's screen clamp; GameTooltip frames carry it by construction).
|
||
if el.attr_bool("clampedToScreen") {
|
||
self.call(wrapper, "SetClampedToScreen", true, dbg);
|
||
}
|
||
// `enableMouse` lands as the real EnableMouse call (the hit test gates on it —
|
||
// pointer.rs): a ref frame authored mouse-blocking (StaticPopup, BlackoutWorld, the
|
||
// minimap cluster) must actually capture. The loader ignored it for a while after the
|
||
// native landed — the stale "v1 gap" note here — which left those frames click-through
|
||
// and the minimap deaf to its ping click (0434 phase 6c's root cause).
|
||
if el.attr_bool("enableMouse") {
|
||
self.call(wrapper, "EnableMouse", true, dbg);
|
||
}
|
||
// `<HitRectInsets><AbsInset left= right= top= bottom=/></HitRectInsets>` (also accepted
|
||
// inline on the element) → SetHitRectInsets: the frame's MOUSE rect, inset from its
|
||
// resolved rect. Absent sides read 0, so a partial element insets only what it names.
|
||
if let Some(ins) = children_named(el, "HitRectInsets").next() {
|
||
let src = children_named(ins, "AbsInset").next().unwrap_or(ins);
|
||
let side = |k: &str| {
|
||
src.attr(k)
|
||
.and_then(|v| v.trim().parse::<f32>().ok())
|
||
.unwrap_or(0.0)
|
||
};
|
||
self.call(
|
||
wrapper,
|
||
"SetHitRectInsets",
|
||
(side("left"), side("right"), side("top"), side("bottom")),
|
||
dbg,
|
||
);
|
||
}
|
||
// `<ResizeBounds><minResize><AbsDimension x= y=/></minResize><maxResize>…` → the resize
|
||
// quad (rf24 `0x769820`'s child-loop arm at `0x769baa`; wow-re
|
||
// `system/ui/scratch/resize-bounds-and-button-fontstring.md` §4). Three clauses from that
|
||
// carve rather than from the shape of the element:
|
||
//
|
||
// · **Both pairs are written unconditionally once `<ResizeBounds>` matches**, each side
|
||
// defaulting to `0` before its lookup — so a block carrying only `<minResize>` RESETS
|
||
// the max pair to unbounded. That is why this writes both calls, never just the one it
|
||
// found.
|
||
// · `x` is the WIDTH and `y` the HEIGHT, through the same logical→internal transform
|
||
// `<Size>` uses — the same `abs_dim` reader, and the same inline-or-`AbsDimension` pair
|
||
// of forms.
|
||
// · `<Size>` and `<Anchors>` are consumed by the base-class `CLayoutFrame::LoadXML`
|
||
// *before* the child loop is entered at all, so they always precede this structurally
|
||
// whatever the document order — and neither path clamps, so an authored `<Size>`
|
||
// outside the authored bounds survives load unchanged.
|
||
//
|
||
// The shipped 1.12.1 FrameXML uses it exactly once (`FloatingChatFrame.xml:223`,
|
||
// min 296×75 / max 608×400) and never calls the Lua setters at all.
|
||
if let Some(rb) = children_named(el, "ResizeBounds").next() {
|
||
for (tag, verb) in [("minResize", "SetMinResize"), ("maxResize", "SetMaxResize")] {
|
||
let (x, y) = children_named(rb, tag).next().map_or((None, None), abs_dim);
|
||
self.call(wrapper, verb, (x.unwrap_or(0.0), y.unwrap_or(0.0)), dbg);
|
||
}
|
||
}
|
||
// `movable`/`resizable` → SetMovable/SetResizable — the same flag word the methods write
|
||
// (`0x76a3c0` with mask 0x100 / 0x200; wow-re `rf24-framexml-loader.md` records the loader
|
||
// calling that very setter). Both were in the gap list below until the movable family
|
||
// landed; leaving them there would have left every `movable="true"` reference window
|
||
// undraggable while `SetMovable` worked from Lua.
|
||
if el.attr_bool("movable") {
|
||
self.call(wrapper, "SetMovable", true, dbg);
|
||
}
|
||
if el.attr_bool("resizable") {
|
||
self.call(wrapper, "SetResizable", true, dbg);
|
||
}
|
||
// `toplevel` → SetToplevel — the third bit of that same flag word (`0x76a3c0` mask `0x1`,
|
||
// XML site `0x7698ec`; wow-re `ui/scratch/toplevel-raise.md`), and the third attribute to
|
||
// graduate out of the gap list below for the same reason: the raise law is built
|
||
// (`script::object::toplevel`), so accepting the attribute now means the behaviour, not
|
||
// silence. 82 corpus addons and thirteen of our own frames declare it.
|
||
if el.attr_bool("toplevel") {
|
||
self.call(wrapper, "SetToplevel", true, dbg);
|
||
}
|
||
// `enableKeyboard="true"` — the XML half of the flag, which enables BOTH key kinds
|
||
// (`scripts-auto-enable.md` §1-2). The flag is now real and round-trips; key delivery is
|
||
// still not gated on it, which the method's own doc states rather than this warning.
|
||
if el.attr_bool("enableKeyboard") {
|
||
self.call(wrapper, "EnableKeyboard", true, dbg);
|
||
}
|
||
}
|
||
|
||
/// `<Size>` → SetWidth/SetHeight (rf24 `0x767800`). Accepts either `<Size><AbsDimension x= y=/>`
|
||
/// or the inline `<Size x= y=/>` form; a dimension that's absent is left untouched (the client's
|
||
/// "0 = derive"). ALL `<Size>` children apply, in document order — template expansion appends the
|
||
/// instance's children after the template's ([`crate::framexml::expand`]), and the client simply
|
||
/// processes each child in turn, so an instance's own `<Size>` overwrites its template's. (Taking
|
||
/// only `.next()` here silently pinned every templated frame to the TEMPLATE's size; caught on
|
||
/// the quest log's Abandon button — 125×21 in the instance XML, 80×22 on screen.)
|
||
pub(super) fn apply_size(&mut self, el: &Element, wrapper: &Table, dbg: &str) {
|
||
for size in children_named(el, "Size") {
|
||
let (x, y) = abs_dim(size);
|
||
if let Some(w) = x {
|
||
self.call(wrapper, "SetWidth", w, dbg);
|
||
}
|
||
if let Some(h) = y {
|
||
self.call(wrapper, "SetHeight", h, dbg);
|
||
}
|
||
}
|
||
}
|
||
|
||
/// `<Anchors>` → SetPoint per `<Anchor point= relativeTo= relativePoint=><Offset .../></Anchor>`
|
||
/// (rf24 `0x767800`). `relativePoint` defaults to `point`; `relativeTo` is `$parent`-substituted
|
||
/// (rf27) and passed by name (the object model resolves it, falling back to the parent when
|
||
/// absent/unresolved); a missing `point` is skipped with a warning ("Invalid anchor point").
|
||
/// The `setAllPoints="true"` shorthand applies first, like the region path — a frame carrying
|
||
/// only the attribute (WorldMapFrame's chrome layers) pins TOPLEFT+BOTTOMRIGHT to its parent.
|
||
pub(super) fn apply_anchors(
|
||
&mut self,
|
||
el: &Element,
|
||
wrapper: &Table,
|
||
parent_name: &str,
|
||
dbg: &str,
|
||
) {
|
||
if el.attr_bool("setAllPoints") {
|
||
self.call(wrapper, "SetAllPoints", (), dbg);
|
||
}
|
||
for anchors in children_named(el, "Anchors") {
|
||
for anchor in children_named(anchors, "Anchor") {
|
||
let Some(point) = anchor.attr("point") else {
|
||
self.report
|
||
.warnings
|
||
.push(format!("{dbg}: <Anchor> without a point; skipped"));
|
||
continue;
|
||
};
|
||
let rel_point = anchor.attr("relativePoint").unwrap_or(point).to_string();
|
||
let rel_to: Option<String> = anchor
|
||
.attr("relativeTo")
|
||
.map(|r| framexml::resolve_name(r, parent_name));
|
||
let (x, y) = children_named(anchor, "Offset")
|
||
.next()
|
||
.map(abs_dim)
|
||
.unwrap_or((None, None));
|
||
self.call(
|
||
wrapper,
|
||
"SetPoint",
|
||
(
|
||
point.to_string(),
|
||
rel_to,
|
||
rel_point,
|
||
x.unwrap_or(0.0),
|
||
y.unwrap_or(0.0),
|
||
),
|
||
dbg,
|
||
);
|
||
}
|
||
}
|
||
}
|
||
}
|