- the combat log's last eight wire bodies decode — PARTYKILLLOG, SPELLINSTAKILLLOG, PROCRESIST, SPELLORDAMAGE_IMMUNE, SPELLDISPELLOG, DISPEL_FAILED, ENCHANTMENTLOG, SPELLLOGEXECUTE — each layout read off vmangos's own writer and pinned by a byte-exact golden (DISPEL_FAILED carries no count, the list runs to the end of the body; SPELLLOGEXECUTE's POWER_DRAIN row is (amount, power, multiplier), verified at the client's bytes as well as the server's); with the eleven chat types that were waiting on them, 44 chat types become 55 — 29 sentence families, the death pair and the aura rows riding descriptor diffs because 1.12 has no "unit died" opcode, plus the six trailers ((glancing), (30 blocked), (12 absorbed) and kin) that had been missing from every family; also corrected: the durability-death line was going to the red error frame with hard-coded English, and SMSG_SPELLNONMELEEDAMAGELOG lacked its split-damage branch and its blocked wording
- PlayerModel is its own frame kind: SetUnit, RefreshUnit and SetRotation were published on Model, but they belong to the <PlayerModel> method table (0x84f1fc), and the client's lookup chains derived → base only, so a plain <Model> can never reach them — they got there from a strings scan read as ownership, and a strings hit answers whether a name EXISTS, not who owns it (SetUnit's one pooled string is referenced by the PlayerModel and GameTooltip tables and by no Model entry); the new PlayerModel → Model → Frame → Region chain unblocks CreateFrame("PlayerModel"), which is how pfUI builds every unit-frame portrait — and through pfUI, pfQuest and ShaguDPS
- a nil where a number goes is four shapes, not one law: a census of all 408 widget-registrar entries found 62 numeric positions that validate-or-raise, 43 that gate then stage an alternate, 64 that silently coerce to 0.0 and 5 that gate then store nothing — so SetMaxLetters(nil) completes and stores 0, "no limit" (the argument COUNT is checked exactly, the type is not), and the backdrop colour setters are asymmetric on purpose: a nil r/g/b channel is 0.0 but a nil alpha is OPAQUE, and reading every nil as 0.0 would have turned those borders transparent; both setters now also quantize ×255 into the packed 0xAARRGGBB byte quad the client actually stores, so 0.09 reads back as 23/255
- two more addon-killers: LIGHTYELLOW_FONT_COLOR_CODE was the one missing line of Fonts.xml's seven-line colour block (aux-addon concatenates it at file scope and died on its own first line), and the guard now enumerates the whole block l.4-20 instead of the names an earlier round had noticed absent; and the unit frames publish the reference's names — 1.12 has no *PowerBar anywhere, every bar is ManaBar and the colour table is ManaBarColor (8 of the top-20 addons read it by name), so ManaBarColor is the published table our own bars index into, with 22 more regions renamed to the reference's own
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
238 lines
10 KiB
Rust
238 lines
10 KiB
Rust
//! Backdrop: Lua verbs + extract emission (backdrop-mechanism.md).
|
||
|
||
use super::common::script;
|
||
use crate::script::*;
|
||
|
||
// SetBackdrop installs the plate; SetBackdropColor tints the bg only, SetBackdropBorderColor all 8
|
||
// border pieces; extract emits bg-then-border at the frame's own slot with those colors.
|
||
#[test]
|
||
fn backdrop_installs_and_extracts_pieces_with_colors() {
|
||
let mut s = script();
|
||
s.set_screen_size(800.0, 600.0);
|
||
s.run(
|
||
r#"
|
||
local f = CreateFrame("Frame", "Plate")
|
||
f:SetPoint("TOPLEFT", nil, "TOPLEFT", 100, -100)
|
||
f:SetSize(200, 100)
|
||
f:SetBackdrop({
|
||
bgFile = "bg", edgeFile = "edge", tile = true,
|
||
tileSize = 16, edgeSize = 16,
|
||
insets = { left = 5, right = 5, top = 5, bottom = 5 },
|
||
})
|
||
f:SetBackdropColor(0.09, 0.09, 0.19)
|
||
f:SetBackdropBorderColor(1, 1, 1)
|
||
"#,
|
||
)
|
||
.unwrap();
|
||
s.resolve();
|
||
let pieces: Vec<(String, [f32; 4])> = s
|
||
.extract()
|
||
.into_iter()
|
||
.filter_map(|q| match q.content {
|
||
QuadContent::Backdrop { path, color, .. } => Some((path, color)),
|
||
_ => None,
|
||
})
|
||
.collect();
|
||
// bg (1) + 8 border pieces.
|
||
assert_eq!(pieces.len(), 9);
|
||
// First is the bg, tinted the tooltip background color — QUANTIZED. The reference's colour
|
||
// field is a packed `0xAARRGGBB` byte quad and the setter converts `×255 + 0.5` through
|
||
// `__ftol` (wow-re `numeric-arg-coercion-law.md` Q4), so `0.09` stores as 23 and reads back
|
||
// as `23/255`. This assertion used to hold `0.09` exactly, which was our lossless `[f32; 4]`
|
||
// showing through a store the client cannot make.
|
||
assert_eq!(pieces[0].0, "bg");
|
||
let q = |x: f32| f32::from((x * 255.0 + 0.5) as u8) / 255.0;
|
||
assert_eq!(pieces[0].1, [q(0.09), q(0.09), q(0.19), 1.0]);
|
||
// The remaining 8 are the border, white, from the edge file.
|
||
assert!(pieces[1..]
|
||
.iter()
|
||
.all(|(p, c)| p == "edge" && *c == [1.0, 1.0, 1.0, 1.0]));
|
||
}
|
||
|
||
// `GetBackdrop()` — the four traps of `0x777370` (wow-re `widget-api-batch-benilla.md` Q5), pinned
|
||
// where an addon can observe them. The corpus caller is `BuffCheck2.lua:448`, which reads the table
|
||
// back, edits `insets`, and feeds it straight to `SetBackdrop` — so a wrong `tile` type or a missing
|
||
// ctor default does not just read wrong, it round-trips wrong.
|
||
#[test]
|
||
fn get_backdrop_reconstructs_from_the_struct() {
|
||
let s = script();
|
||
s.run(
|
||
r#"
|
||
local f = CreateFrame("Frame", "GB")
|
||
|
||
-- TRAP 2: no backdrop => ZERO values, not nil. `select('#')` is the only way to see it.
|
||
assert(select('#', f:GetBackdrop()) == 0, "unset backdrop must return no values at all")
|
||
|
||
-- TRAP 1: the result is rebuilt from the struct, not the caller's table. The alien key is
|
||
-- the proof: SetBackdrop never accepted it, so it cannot come back out.
|
||
local passed = { bgFile = "bg", edgeFile = "edge", tile = true,
|
||
tileSize = 16, edgeSize = 24,
|
||
insets = { left = 1, right = 2, top = 3, bottom = 4 },
|
||
alien = "must not survive" }
|
||
f:SetBackdrop(passed)
|
||
local b = f:GetBackdrop()
|
||
assert(select('#', f:GetBackdrop()) == 1, "a set backdrop is exactly one value")
|
||
assert(b ~= passed, "must not hand back the caller's own table")
|
||
assert(b.alien == nil, "keys SetBackdrop never read cannot reappear")
|
||
assert(b.insets ~= passed.insets, "the insets subtable is rebuilt too")
|
||
assert(b.bgFile == "bg" and b.edgeFile == "edge", "files round-trip")
|
||
assert(b.tileSize == 16 and b.edgeSize == 24, "sizes round-trip")
|
||
assert(b.insets.left == 1 and b.insets.right == 2
|
||
and b.insets.top == 3 and b.insets.bottom == 4, "insets round-trip")
|
||
|
||
-- TRAP 4: `tile` is the NUMBER 1, never the boolean the caller passed.
|
||
assert(type(b.tile) == "number", "tile must be a number, got " .. type(b.tile))
|
||
assert(b.tile == 1, "tile must be 1")
|
||
|
||
-- Mutating the caller's table afterwards cannot reach the frame (no reference is kept).
|
||
passed.bgFile = "clobbered"
|
||
assert(f:GetBackdrop().bgFile == "bg", "no live reference to the caller's table")
|
||
|
||
-- TRAP 3: a partial SetBackdrop omits NOTHING — a fresh struct is allocated every call, so
|
||
-- the keys left out come back as CTOR DEFAULTS, and none of the old backdrop survives.
|
||
f:SetBackdrop({ edgeFile = "onlyedge" })
|
||
local p = f:GetBackdrop()
|
||
assert(p.bgFile == "", "an omitted bgFile is the empty string, not nil")
|
||
assert(p.edgeSize == 32, "an omitted edgeSize is the ctor's 32, not the previous 24")
|
||
assert(p.tileSize == 0, "an omitted tileSize is 0, not the previous 16")
|
||
assert(p.insets.left == 0 and p.insets.right == 0
|
||
and p.insets.top == 0 and p.insets.bottom == 0, "omitted insets are 0, not 1/2/3/4")
|
||
-- TRAP 4, the other half: tile false pushes nil, so the key is ABSENT — never `false`.
|
||
assert(p.tile == nil, "tile false means the key is absent, not `false`")
|
||
|
||
-- The undocumented in-place form: fills and returns YOUR table, recycling `insets`, and
|
||
-- erases a stale `tile` on the way (the nil push is a real `lua_settable`).
|
||
local ins = {}
|
||
local mine = { insets = ins, tile = true, stale = "kept" }
|
||
local got = f:GetBackdrop(mine)
|
||
assert(got == mine, "the in-place form returns the table it was given")
|
||
assert(mine.insets == ins, "an existing insets subtable is reused, not replaced")
|
||
assert(mine.tile == nil, "a stale tile key is erased from a recycled table")
|
||
assert(mine.stale == "kept", "keys it does not write are left alone")
|
||
assert(mine.edgeSize == 32 and ins.bottom == 0, "the recycled table is filled")
|
||
|
||
-- SetBackdrop(nil) is indistinguishable from never having set one: back to zero values.
|
||
f:SetBackdrop(nil)
|
||
assert(select('#', f:GetBackdrop()) == 0, "SetBackdrop(nil) returns to the zero-value shape")
|
||
"#,
|
||
)
|
||
.unwrap();
|
||
}
|
||
|
||
// BuffCheck2.lua:448's exact shape: read the plate back, edit `insets`, push it straight back in.
|
||
// It is the whole reason this method exists in our client, so it is the test that must not rot.
|
||
#[test]
|
||
fn get_backdrop_round_trips_through_set_backdrop() {
|
||
let mut s = script();
|
||
s.set_screen_size(800.0, 600.0);
|
||
s.run(
|
||
r#"
|
||
local f = CreateFrame("Frame", "BC2Plate")
|
||
f:SetPoint("CENTER")
|
||
f:SetSize(54, 54)
|
||
f:SetBackdrop({ bgFile = "bg", edgeFile = "edge", tile = true,
|
||
tileSize = 32, edgeSize = 32,
|
||
insets = { left = 11, right = 12, top = 12, bottom = 11 } })
|
||
local backdrop = f:GetBackdrop()
|
||
backdrop["insets"] = { top = 12, bottom = 11, right = 12, left = 11 }
|
||
backdrop["tile"] = true
|
||
backdrop["tileSize"] = 32
|
||
backdrop["edgeSize"] = 32
|
||
f:SetBackdrop(backdrop)
|
||
local b = f:GetBackdrop()
|
||
assert(b.bgFile == "bg" and b.edgeFile == "edge", "files survive the round trip")
|
||
assert(b.tile == 1 and b.tileSize == 32 and b.edgeSize == 32, "tiling survives")
|
||
assert(b.insets.left == 11 and b.insets.bottom == 11, "insets survive")
|
||
"#,
|
||
)
|
||
.unwrap();
|
||
s.resolve();
|
||
// The plate still draws: bg + 8 border pieces, unchanged by the round trip.
|
||
assert_eq!(
|
||
s.extract()
|
||
.iter()
|
||
.filter(|q| matches!(q.content, QuadContent::Backdrop { .. }))
|
||
.count(),
|
||
9
|
||
);
|
||
}
|
||
|
||
// SetBackdrop(nil) tears the plate down (no pieces after).
|
||
#[test]
|
||
fn set_backdrop_nil_tears_down() {
|
||
let mut s = script();
|
||
s.set_screen_size(800.0, 600.0);
|
||
s.run(
|
||
r#"
|
||
local f = CreateFrame("Frame", "Plate2")
|
||
f:SetPoint("CENTER")
|
||
f:SetSize(100, 100)
|
||
f:SetBackdrop({ bgFile = "bg", edgeFile = "edge" })
|
||
f:SetBackdrop(nil)
|
||
"#,
|
||
)
|
||
.unwrap();
|
||
s.resolve();
|
||
assert!(s
|
||
.extract()
|
||
.iter()
|
||
.all(|q| !matches!(q.content, QuadContent::Backdrop { .. })));
|
||
}
|
||
|
||
/// **The colour setters' argument gating is asymmetric, and getting it backwards is worse than
|
||
/// the bug it replaces** (wow-re `numeric-arg-coercion-law.md` Q4, VERIFIED at `0x777d30` /
|
||
/// `0x7780d0`).
|
||
///
|
||
/// r/g/b go through a bare `lua_tonumber` — a `nil` channel is `0.0` and the call COMPLETES.
|
||
/// benilla typed them `f32` and raised, which killed ShaguTweaks at `helpers.lua:248`, where
|
||
/// `color.r` comes off a table that does not always have one.
|
||
///
|
||
/// Alpha is different: `lua_isnumber`-gated at `0x778227` with `1.0f` staged at `0x778220`, so a
|
||
/// missing **or nil** alpha is OPAQUE. The tempting blanket fix — "treat every nil as 0.0, like
|
||
/// its neighbours" — would have turned every one of those borders transparent, which reads as a
|
||
/// rendering fault rather than an API bug and is the reason this test names both halves.
|
||
#[test]
|
||
fn backdrop_colors_coerce_rgb_but_default_alpha_opaque() {
|
||
let s = script();
|
||
s.run(
|
||
r#"
|
||
f = CreateFrame("Frame", "Plate2")
|
||
f:SetBackdrop({ bgFile = "bg", edgeFile = "edge", edgeSize = 16 })
|
||
"#,
|
||
)
|
||
.unwrap();
|
||
|
||
// A nil channel is 0.0 and the call completes — no raise.
|
||
s.run("f:SetBackdropBorderColor(nil, 1, 1, 1)").unwrap();
|
||
assert_eq!(
|
||
s.eval::<(f32, f32, f32, f32)>("return f:GetBackdropBorderColor()")
|
||
.unwrap(),
|
||
(0.0, 1.0, 1.0, 1.0)
|
||
);
|
||
|
||
// A missing alpha is 1.0, and so is an explicit nil one — NOT 0.0.
|
||
s.run("f:SetBackdropColor(0.2, 0.4, 0.6)").unwrap();
|
||
let (_, _, _, a) = s
|
||
.eval::<(f32, f32, f32, f32)>("return f:GetBackdropColor()")
|
||
.unwrap();
|
||
assert_eq!(a, 1.0, "a missing alpha is opaque");
|
||
s.run("f:SetBackdropColor(0.2, 0.4, 0.6, nil)").unwrap();
|
||
let (_, _, _, a) = s
|
||
.eval::<(f32, f32, f32, f32)>("return f:GetBackdropColor()")
|
||
.unwrap();
|
||
assert_eq!(
|
||
a, 1.0,
|
||
"an explicit nil alpha is opaque too — the isnumber gate"
|
||
);
|
||
|
||
// Out of range clamps; a table coerces to 0 like any non-number.
|
||
s.run("f:SetBackdropColor(5, -1, {}, 0.5)").unwrap();
|
||
let (r, g, b, a) = s
|
||
.eval::<(f32, f32, f32, f32)>("return f:GetBackdropColor()")
|
||
.unwrap();
|
||
assert_eq!((r, g, b), (1.0, 0.0, 0.0));
|
||
// Quantized `×255 + 0.5` through `__ftol`: 0.5 -> 128/255, not 0.5 exactly. The field is a
|
||
// packed 0xAARRGGBB byte quad and nothing finer survives the store.
|
||
assert_eq!(a, 128.0 / 255.0);
|
||
}
|