coins ride the cursor, and the trade window and combat text go stock

- coins ride the cursor: `MoneyFrame.lua`/`.xml` load off the player's chain, and the stock kit reads the cursor's coins at load, so the money cursor — payload mode 2, the copper at `[0xb4e2f0]` — is the engine's: `PickupPlayerMoney` puts coins on the cursor and the purse is never debited, and `CoinPickupFrame.xml`, the shift-click coin dialog nothing of ours had ever answered, joins; wow-re's round then set the law: the pickup raises on a non-number and refuses silently on zero, lost control and more than the purse; `AddTradeMoney` folds the coins into the offer as an absolute `CMSG_SET_TRADE_GOLD` gated on the purse; the send-mail setters and `GetCoinIcon` answer as the bytes do; the coinage-change reflex trims an offer the purse no longer covers; and the four send-mail pickup/drop verbs the stock table names do not exist in 5875, so they are not built (1962, 1965)
- the trade window is the reference's own file: `TradeFrame.xml` + `TradeFrame.lua` off the chain, ours deleted; `GameTooltip:SetTradePlayerItem`/`SetTradeTargetItem` (`0x5341e0`/`0x534410`) render the slot's item the way the loot slot's is, and `TRADE_PLAYER_ITEM_CHANGED`/`TRADE_TARGET_ITEM_CHANGED` — which the stock file repaints one slot on, and nothing had fired — fire per changed slot beside `TRADE_UPDATE`; the stock file brings the chain's own `MoneyInputFrame`, a TARGET_TRADE `SmallMoneyFrame`, `ItemButtonTemplate` slots and the enchant slot's own wording — "Item not yet modified" where ours had read "Will Not Be Traded"; the wire's enchant id is not carried into the slot yet, so the tooltip shows the item without its enchant (1966)
- the dialogs' verbs, as the bytes define them: nine of the ten stock `StaticPopup.lua` bindings 1960 had left named are built off wow-re's round with their packets, events and UIParent arms — the bare trade pair, `ForceLogout`, the instance-boot timer (whole seconds), the area spirit healer's accept/cancel/time, `AcceptBattlefieldPort` out of a three-slot queue `SMSG_BATTLEFIELD_STATUS` fills, `CancelMeetingStoneRequest`, and the pet trainer's pair as the talent wipe's twin, latch for latch — `SMSG_PET_UNLEARN_CONFIRM` latches and asks, `ConfirmPetUnlearn` sends behind the money gate, `CheckPetUntrainerDist` reads the leash; not built, named: the healer's proximity scan (`0x4923b0`: 20 yd to acquire, 22 to retain), so no healer is cached yet, and `ReplaceTradeEnchant`, which rides trade-slot enchant targeting this engine does not build (1963)
- the scrolling combat text is the reference's own `Blizzard_CombatText`, LoadOnDemand off the chain: our transcription is deleted, and the addon loads through `UIParentLoadAddOn` at the reference's own sites; the family's fourteen defaults move to the options file where the reference keeps them, because a LoadOnDemand addon cannot define the options that decide whether it loads (1964); and the Looking-for-Group pair is what the bytes define, correcting 1959's flag: `SetLookingForGroup` takes three (type, entry) pairs and a comment, packs each slot as `(type << 24) & entry` — an AND (`0x4e9713`), so every admissible input stores zero — and sends `CMSG_SET_LOOKING_FOR_GROUP` only when the commit changed something; `GetLookingForGroup` answers three nilable slot names then the comment, never 1|nil; and the stock 1.12.1 UI never calls either — both call sites sit in an XML comment (1961)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Sam
2026-09-04 20:58:15 +07:00
co-authored by Claude Fable 5.1
parent 058ca4b6c8
commit f4951470a3
129 changed files with 2792 additions and 2352 deletions
-723
View File
@@ -1,723 +0,0 @@
<Ui>
<!--
The center-screen scrolling combat text (decision 0578) — the 1.11+ "Floating Combat Text"
option, written here in our own terms. The shipped load-on-demand addon
`Blizzard_CombatText` (patch.MPQ `Interface\AddOns\Blizzard_CombatText\`, extracted to
WoW/_extracted_addons/Blizzard_CombatText/) is the SPECIFICATION, and what it fixes is
contract: the message-type keys, the option family, the scroll/fade/crit envelope and
every number in them are all addressed by name from addons, so they are reproduced exactly
while the code that carries them is ours. The picture: screen-space FontStrings scrolling
up from UIParent-bottom +384 (≈ over the character), driven by the engine events this
frame registers. Where our behaviour departs from the spec, it is named:
- The ref is load-on-demand (`UIParentLoadAddOn` when SHOW_COMBAT_TEXT == "1"); we load
always and let CombatText_UpdateDisplayedMessages gate registration, ref-identically.
- SHOW_COMBAT_TEXT defaulted "1" here against the ref's "0" from 0578 (the director's ask
was the enabled experience) until 1804 put the whole option family back on the
reference's own values. All fourteen now sit at the spec's defaults
(UIOptionsFrame.lua l.69-84), and the Combat page's checkbox is where it turns on.
- The ref's virtual FontString template (CombatTextTemplate) expands to 20 identical
strings; region templates are outside the v1 loader's object model (BuffFrame.xml has
the precedent note), so the 20 instances inherit the CombatTextFont Font object and
carry the template's anchor directly.
- UNIT_MANA, the spec's own event. This used to read UNIT_POWER_UPDATE + the Era Unit* API
(0068's surface); 1819 retired that pair — 1.12 names the resource in the event, and the
Era spelling was a deviation this file's own header already flagged. Same low-mana law,
and now the handler is not woken by rage or energy at all. PLAYER_COMBO_POINTS rides the combo wire (0869); its arm reads
`GetComboPoints()`, so it is rogue/druid-only and per-target for free — the binding
gates, not this file (0875).
- Lua 5.0 `for k,v in t` iteration becomes explicit index loops (5.1 VM), backwards
where the body removes.
- COMBAT_TEXT_TYPE_INFO is grown from a compact spec (colour · gate · flags · the types
that share them) walked by the loop under it, instead of one literal per row: the same
facts with the repetition taken out. The table it produces IS the contract — the same
keys with the same values, and one fresh entry table per key, because `show` is
rewritten per entry and an addon may edit any of them.
- The two low-resource warnings, the two flow directions in CombatText_AddMessage, and the
"park a string back at the seat" step all appear once here; the spec writes each of them
out twice.
The wire → messageType mapping feeding COMBAT_TEXT_UPDATE lives app-side
(net/apply/combat_log.rs + ui_unit.rs) and is PROVISIONAL pending the dispatched wow-re
emission pin (decision 0578).
-->
<Script><![CDATA[
-- ── The pool ────────────────────────────────────────────────────────────────────────────
-- One screen line per pool string, and the list of the ones currently in flight. The pool
-- is fixed: a message arriving with all twenty busy evicts the oldest.
NUM_COMBAT_TEXT_LINES = 20
COMBAT_TEXT_TO_ANIMATE = {}
-- ── Where a line sits ───────────────────────────────────────────────────────────────────
-- COMBAT_TEXT_LOCATIONS is the seat and the target the current float mode implies; it is
-- filled in by CombatText_UpdateDisplayedMessages, not here. A new line is pushed clear of
-- everything already in flight by one line plus COMBAT_TEXT_SPACING, but the stack never
-- runs further than COMBAT_TEXT_MAX_OFFSET from the seat — past that it starts over. The
-- staggered types jitter sideways by up to half COMBAT_TEXT_STAGGER_RANGE either way, so a
-- flurry of hits does not print as one column.
COMBAT_TEXT_LOCATIONS = {}
COMBAT_TEXT_SPACING = 10
COMBAT_TEXT_MAX_OFFSET = 130
COMBAT_TEXT_STAGGER_RANGE = 20
-- ── How long a line lives ───────────────────────────────────────────────────────────────
-- It travels for COMBAT_TEXT_SCROLLSPEED seconds; COMBAT_TEXT_FADEOUT_TIME is when the
-- fade STARTS, not how long it takes, so the fade is the tail of the same window.
COMBAT_TEXT_SCROLLSPEED = 1.9
COMBAT_TEXT_FADEOUT_TIME = 1.3
-- ── How big it is drawn ─────────────────────────────────────────────────────────────────
-- An ordinary line is COMBAT_TEXT_HEIGHT and stays there. A crit "pops": seeded at the
-- minimum, grown to the maximum across CRIT_SCALE_TIME, and back down to ordinary size by
-- CRIT_SHRINKTIME (CombatText_OnUpdate walks that envelope).
COMBAT_TEXT_HEIGHT = 25
COMBAT_TEXT_CRIT_MINHEIGHT = 30
COMBAT_TEXT_CRIT_MAXHEIGHT = 60
COMBAT_TEXT_CRIT_SCALE_TIME = 0.05
COMBAT_TEXT_CRIT_SHRINKTIME = 0.2
-- ── When the low-resource warnings latch ────────────────────────────────────────────────
-- Fractions of the bar, tested on the way down; see CombatText_LowResourceWarning.
COMBAT_TEXT_LOW_HEALTH_THRESHOLD = 0.2
COMBAT_TEXT_LOW_MANA_THRESHOLD = 0.2
-- The option family: every saved global this feature owns, next to the value it ships
-- with. **All fourteen are the spec's own defaults** (UIOptionsFrame.lua l.69-84), the
-- master SHOW_COMBAT_TEXT included — it shipped "1" against the spec's "0" from 0578 to
-- 1804 (see the header). Held to that by
-- `reference_ui::tests::our_saved_ui_globals_default_to_the_references_own_values`, which
-- reads the reference's file off the player's chain rather than trusting this comment.
--
-- Each name is assigned AT FILE SCOPE and registered for saving in the same pass, because
-- the load order is what makes a saved value stick (decision 1128): defaults first, the
-- saved chunk over the top, consumers third; reverse any two and a saved value can never
-- win. The Options window's Combat page drives all fourteen (decision 1134) and takes its
-- "Defaults" from these very globals, read at each row's OnLoad — which runs before the
-- saved chunk — so a default lives in exactly one place, this table, and the panel cannot
-- drift from it.
local combatTextOptions = {
{"SHOW_COMBAT_TEXT", "0"},
{"COMBAT_TEXT_SHOW_LOW_HEALTH_MANA", "1"},
{"COMBAT_TEXT_SHOW_AURAS", "1"},
{"COMBAT_TEXT_SHOW_AURA_FADE", "0"},
{"COMBAT_TEXT_SHOW_COMBAT_STATE", "1"},
{"COMBAT_TEXT_SHOW_DODGE_PARRY_MISS", "0"},
{"COMBAT_TEXT_SHOW_RESISTANCES", "0"},
{"COMBAT_TEXT_SHOW_REPUTATION", "0"},
{"COMBAT_TEXT_SHOW_REACTIVES", "0"},
{"COMBAT_TEXT_SHOW_FRIENDLY_NAMES", "0"},
{"COMBAT_TEXT_SHOW_COMBO_POINTS", "0"},
{"COMBAT_TEXT_SHOW_MANA", "0"},
{"COMBAT_TEXT_SHOW_HONOR_GAINED", "1"},
{"COMBAT_TEXT_FLOAT_MODE", "1"},
}
for i = 1, getn(combatTextOptions) do
local name, default = combatTextOptions[i][1], combatTextOptions[i][2]
setglobal(name, default)
RegisterForSave(name)
end
-- The message words (shipped enUS GlobalStrings values, hardcoded like the rest of our
-- enUS-only data; or-guarded so an earlier definition wins).
MANA = MANA or "Mana"
RAGE = RAGE or "Rage"
FOCUS = FOCUS or "Focus"
ENERGY = ENERGY or "Energy"
HEALTH_LOW = HEALTH_LOW or "Health Low"
MANA_LOW = MANA_LOW or "Mana Low"
ENTERING_COMBAT = ENTERING_COMBAT or "Entering Combat"
LEAVING_COMBAT = LEAVING_COMBAT or "Leaving Combat"
BLOCK = BLOCK or "Block"
ABSORB = ABSORB or "Absorb"
RESIST = RESIST or "Resist"
SPELL_MISSED = SPELL_MISSED or "Miss"
SPELL_DODGED = SPELL_DODGED or "Dodge"
SPELL_PARRIED = SPELL_PARRIED or "Parry"
SPELL_BLOCKED = SPELL_BLOCKED or "Block"
SPELL_EVADED = SPELL_EVADED or "Evade"
SPELL_IMMUNE = SPELL_IMMUNE or "Immune"
SPELL_DEFLECTED = SPELL_DEFLECTED or "Deflect"
SPELL_REFLECTED = SPELL_REFLECTED or "Reflect"
SPELL_RESISTED = SPELL_RESISTED or "Resist"
SPELL_ABSORBED = SPELL_ABSORBED or "Absorb"
AURA_END = AURA_END or "<%s> fades"
BLOCK_TRAILER = BLOCK_TRAILER or " (%d blocked)"
ABSORB_TRAILER = ABSORB_TRAILER or " (%d absorbed)"
RESIST_TRAILER = RESIST_TRAILER or " (%d resisted)"
COMBAT_TEXT_HONOR_GAINED = COMBAT_TEXT_HONOR_GAINED or "Honor %s"
--[[
COMBAT_TEXT_TYPE_INFO — one entry per message type, and the four things an entry says:
r, g, b the colour the line prints in, as 0..1 floats
show 1 while this type is currently being printed, nil while it is not.
CombatText_UpdateDisplayedMessages rewrites it for every gated type from
the option `var` names, so an entry carrying `var` and no `show` has
merely not been refreshed yet, while an entry carrying `show` and no
`var` is on for as long as the feature itself is
isStaggered 1 to jitter the line sideways instead of printing it on the centre line
var the name of the saved global that gates this type; nil = ungated
This table is API: addons index it by message-type key and read and write these fields, so
the keys and the values are exactly the specification's. The rows below are OURS — almost
every type shares its colour and its gate with several others, so the facts are written
once per group and the loop underneath expands them into one entry per type. Fresh table
per entry, never a shared one: `show` is rewritten per entry, and an addon may edit any
single entry without that leaking into its neighbours.
]]
-- The six colours the whole table draws from.
local CT_WHITE = {1, 1, 1}
local CT_RED = {1, 0.1, 0.1}
local CT_GREEN = {0.1, 1, 0.1}
local CT_BLUE = {0.1, 0.1, 1}
local CT_VIOLET = {0.79, 0.3, 0.85}
local CT_GOLD = {1, 0.82, 0}
-- Colour · gate · flags · the message types that share them. `always` is the `show` an
-- ungated type is born with; `jitter` is `isStaggered`; `gate` is `var`.
local combatTextSpec = {
-- Always on. Only the master toggle takes these away.
{colour = CT_RED, always = 1, jitter = 1, types = {"DAMAGE"}},
{colour = CT_RED, always = 1, types = {"DAMAGE_CRIT"}},
{colour = CT_VIOLET, always = 1, types = {"SPELL_DAMAGE"}},
{colour = CT_WHITE, always = 1, types = {"SPLIT_DAMAGE"}},
{colour = CT_GREEN, always = 1, types = {"HEAL", "HEAL_CRIT", "PERIODIC_HEAL", "SPELL_CAST"}},
-- Never on by themselves: no `always`, no gate, so `show` is nil and stays nil. They
-- carry a colour so that anything which does switch one on has one ready.
{colour = CT_WHITE, types = {"INTERRUPT", "ENCHANTMENT_ADDED", "ENCHANTMENT_REMOVED",
"DISPEL_FAILED", "SPELL_CAST_START", "SPELL_DISPELLED", "DAMAGE_SHIELD",
"EXTRA_ATTACKS"}},
-- "Show dodge/parry/miss": swings that went nowhere, melee in red and spells in white.
-- The melee whiffs jitter because several can land in the same instant.
{colour = CT_RED, gate = "COMBAT_TEXT_SHOW_DODGE_PARRY_MISS", jitter = 1,
types = {"MISS", "DODGE", "PARRY", "EVADE"}},
{colour = CT_RED, gate = "COMBAT_TEXT_SHOW_DODGE_PARRY_MISS",
types = {"IMMUNE", "DEFLECT"}},
{colour = CT_WHITE, gate = "COMBAT_TEXT_SHOW_DODGE_PARRY_MISS",
types = {"SPELL_MISSED", "SPELL_DODGED", "SPELL_PARRIED", "SPELL_EVADED",
"SPELL_IMMUNE", "SPELL_DEFLECTED", "SPELL_REFLECTED"}},
-- "Show resistances": a hit that landed but was partly eaten.
{colour = CT_RED, gate = "COMBAT_TEXT_SHOW_RESISTANCES",
types = {"BLOCK", "ABSORB", "RESIST", "PROC_RESISTED"}},
{colour = CT_WHITE, gate = "COMBAT_TEXT_SHOW_RESISTANCES", types = {"SPELL_BLOCKED"}},
{colour = CT_VIOLET, gate = "COMBAT_TEXT_SHOW_RESISTANCES",
types = {"SPELL_ABSORBED", "SPELL_RESISTED"}},
-- The aura pair — what landed, and what wore off — each split by whose side it helps.
{colour = CT_GREEN, gate = "COMBAT_TEXT_SHOW_AURAS", types = {"AURA_START"}},
{colour = CT_RED, gate = "COMBAT_TEXT_SHOW_AURAS", types = {"AURA_START_HARMFUL"}},
{colour = CT_GREEN, gate = "COMBAT_TEXT_SHOW_AURA_FADE", types = {"AURA_END"}},
{colour = CT_RED, gate = "COMBAT_TEXT_SHOW_AURA_FADE", types = {"AURA_END_HARMFUL"}},
-- Resources and standing: everything you gained rather than took.
{colour = CT_BLUE, gate = "COMBAT_TEXT_SHOW_MANA",
types = {"MANA", "RAGE", "FOCUS", "ENERGY"}},
{colour = CT_BLUE, gate = "COMBAT_TEXT_SHOW_COMBO_POINTS", types = {"COMBO_POINTS"}},
{colour = CT_BLUE, gate = "COMBAT_TEXT_SHOW_REPUTATION", types = {"FACTION"}},
{colour = CT_BLUE, gate = "COMBAT_TEXT_SHOW_HONOR_GAINED", types = {"HONOR_GAINED"}},
-- The state warnings, each on its own switch.
{colour = CT_RED, gate = "COMBAT_TEXT_SHOW_LOW_HEALTH_MANA",
types = {"HEALTH_LOW", "MANA_LOW"}},
{colour = CT_RED, gate = "COMBAT_TEXT_SHOW_COMBAT_STATE",
types = {"ENTERING_COMBAT", "LEAVING_COMBAT"}},
{colour = CT_GOLD, gate = "COMBAT_TEXT_SHOW_REACTIVES", types = {"SPELL_ACTIVE"}},
}
COMBAT_TEXT_TYPE_INFO = {}
for i = 1, getn(combatTextSpec) do
local row = combatTextSpec[i]
for j = 1, getn(row.types) do
COMBAT_TEXT_TYPE_INFO[row.types[j]] = {
r = row.colour[1],
g = row.colour[2],
b = row.colour[3],
show = row.always,
isStaggered = row.jitter,
var = row.gate,
}
end
end
-- A latched low-resource warning. The line prints on the crossing INTO the danger band and
-- not again until the bar has climbed back out of it — otherwise every tick of a fight
-- spent under 20% would print another one. `flag` names the field on the CombatText frame
-- that remembers we already warned; `low` is the caller's verdict, because "low" is not
-- the same question for the two bars (mana additionally requires the bar to BE mana: a
-- rage or energy user is never low on mana). Returns the message type to print, or nil.
local function CombatText_LowResourceWarning(flag, low, messageType)
if not low then
CombatText[flag] = nil
return nil
end
if CombatText[flag] then
return nil
end
CombatText[flag] = 1
return messageType
end
-- The engine events whose entire meaning is "print this message type". Everything else
-- either carries its own type in arg1 (COMBAT_TEXT_UPDATE) or IS its own type.
local combatTextStateMessage = {
PLAYER_REGEN_DISABLED = "ENTERING_COMBAT",
PLAYER_REGEN_ENABLED = "LEAVING_COMBAT",
}
function CombatText_OnLoad()
CombatText_UpdateDisplayedMessages()
CombatText.previousMana = {}
end
function CombatText_OnEvent(event)
-- What we are about to print: the type (which picks the colour and the gating), the
-- payload it formats from, the finished line, and the optional "crit" presentation.
local messageType, message
local data = arg2
local displayType
if event == "UNIT_HEALTH" then
if arg1 == "player" then
local low = UnitHealth("player") / UnitHealthMax("player") <= COMBAT_TEXT_LOW_HEALTH_THRESHOLD
messageType = CombatText_LowResourceWarning("lowHealth", low, "HEALTH_LOW")
end
if not messageType then
return
end
elseif event == "UNIT_MANA" then
-- The powerType == 0 guard still earns its place: `power_token` reports an UNKNOWN
-- power type as MANA (its descriptor default), so this event can arrive for a unit
-- whose track is not really mana. Rage, focus and energy now have their own event
-- names and never reach here at all (1819).
local mana = UnitMana("player")
local powerType = UnitPowerType("player")
if arg1 == "player" then
local low = mana / UnitManaMax("player") <= COMBAT_TEXT_LOW_MANA_THRESHOLD and powerType == 0
messageType = CombatText_LowResourceWarning("lowMana", low, "MANA_LOW")
end
if not messageType then
return
end
elseif event == "PLAYER_COMBO_POINTS" then
-- The count itself is the payload, and a full bar is worth a crit-sized line.
-- Spending them fires this too, with nothing left to announce.
data = GetComboPoints()
if data <= 0 then
return
end
messageType = "COMBO_POINTS"
if data == MAX_COMBO_POINTS then
displayType = "crit"
end
elseif event == "COMBAT_TEXT_UPDATE" then
messageType = arg1
else
messageType = combatTextStateMessage[event] or event
end
-- The type decides the colour and whether this is printed at all. An unknown type
-- is white and, having no `show`, prints nothing — which is how a message type we do
-- not know about stays silent instead of erroring.
local info = COMBAT_TEXT_TYPE_INFO[messageType]
if not info then
info = {r = 1, g = 1, b = 1}
end
if not info.show then
return
end
local isStaggered = info.isStaggered
if messageType == "" then
message = nil
elseif messageType == "DAMAGE_CRIT" then
displayType = "crit"
message = "-" .. data
elseif messageType == "DAMAGE" or messageType == "SPELL_DAMAGE" then
message = "-" .. data
elseif messageType == "SPELL_CAST" or messageType == "AURA_START" or messageType == "AURA_START_HARMFUL" then
message = "<" .. data .. ">"
elseif messageType == "AURA_END" or messageType == "AURA_END_HARMFUL" then
message = format(AURA_END, data)
elseif messageType == "HEAL" or messageType == "PERIODIC_HEAL" then
if COMBAT_TEXT_SHOW_FRIENDLY_NAMES == "1" and messageType == "HEAL" and UnitName("player") ~= data then
message = "+" .. arg3 .. " [" .. data .. "]"
else
message = "+" .. arg3
end
elseif messageType == "HEAL_CRIT" then
displayType = "crit"
if COMBAT_TEXT_SHOW_FRIENDLY_NAMES == "1" and UnitName("player") ~= data then
message = "+" .. arg3 .. " [" .. data .. "]"
else
message = "+" .. arg3
end
elseif messageType == "MANA" then
if tonumber(data) > 0 then
data = "+" .. data
end
message = data .. " " .. MANA
elseif messageType == "RAGE" then
message = "+" .. data .. " " .. RAGE
elseif messageType == "FOCUS" then
message = "+" .. data .. " " .. FOCUS
elseif messageType == "ENERGY" then
message = "+" .. data .. " " .. ENERGY
elseif messageType == "FACTION" then
if tonumber(arg3) > 0 then
arg3 = "+" .. arg3
end
message = "(" .. data .. " " .. arg3 .. ")"
elseif messageType == "BLOCK" then
if arg3 then
message = data .. " " .. format(BLOCK_TRAILER, arg3)
else
message = BLOCK
end
elseif messageType == "ABSORB" then
if arg3 then
message = arg2 .. " " .. format(ABSORB_TRAILER, arg3)
else
message = ABSORB
end
elseif messageType == "RESIST" or messageType == "SPELL_RESISTED" then
if arg3 then
message = data .. " " .. format(RESIST_TRAILER, arg3)
else
message = RESIST
end
elseif messageType == "HONOR_GAINED" then
if tonumber(data) > 0 then
data = "+" .. data
end
message = format(COMBAT_TEXT_HONOR_GAINED, data)
elseif messageType == "SPELL_ACTIVE" then
displayType = "crit"
message = "<" .. data .. ">"
elseif messageType == "COMBO_POINTS" then
message = data .. " Combo Points"
else
message = getglobal(messageType)
if not message then
message = getglobal("COMBAT_TEXT_" .. messageType)
end
end
-- A branch that produced no line (the empty type, or a global-string lookup that
-- came back nil) simply prints nothing.
if message then
CombatText_AddMessage(message, COMBAT_TEXT_SCROLL_FUNCTION, info.r, info.g, info.b, displayType, isStaggered)
end
end
function CombatText_OnUpdate(elapsed)
-- Backwards, so the removal inside the body can't skip a neighbor (the ref's 5.0
-- `for in` + tremove tolerated it; a 5.1 index loop must not).
for i = getn(COMBAT_TEXT_TO_ANIMATE), 1, -1 do
local value = COMBAT_TEXT_TO_ANIMATE[i]
if value.scrollTime >= COMBAT_TEXT_SCROLLSPEED then
CombatText_RemoveMessage(value)
else
value.scrollTime = value.scrollTime + elapsed
local xPos, yPos = value.scrollFunction(value)
value.yPos = yPos
value:SetPoint("TOP", UIParent, "BOTTOM", xPos, yPos)
if value.scrollTime >= COMBAT_TEXT_FADEOUT_TIME then
local alpha = 1 - ((value.scrollTime - COMBAT_TEXT_FADEOUT_TIME) / (COMBAT_TEXT_SCROLLSPEED - COMBAT_TEXT_FADEOUT_TIME))
value:SetAlpha(max(alpha, 0))
end
-- The crit pop: up to the maximum inside the scale window, back down by the
-- shrink time, then it stops being a crit and holds its size.
if value.isCrit then
if value.scrollTime <= COMBAT_TEXT_CRIT_SCALE_TIME then
value:SetTextHeight(floor(COMBAT_TEXT_CRIT_MINHEIGHT + ((COMBAT_TEXT_CRIT_MAXHEIGHT - COMBAT_TEXT_CRIT_MINHEIGHT) * value.scrollTime / COMBAT_TEXT_CRIT_SCALE_TIME)))
elseif value.scrollTime <= COMBAT_TEXT_CRIT_SHRINKTIME then
value:SetTextHeight(floor(COMBAT_TEXT_CRIT_MAXHEIGHT - ((COMBAT_TEXT_CRIT_MAXHEIGHT - COMBAT_TEXT_CRIT_MINHEIGHT) * (value.scrollTime - COMBAT_TEXT_CRIT_SCALE_TIME) / (COMBAT_TEXT_CRIT_SHRINKTIME - COMBAT_TEXT_CRIT_SCALE_TIME))))
else
value.isCrit = nil
end
end
end
end
end
-- A string that is not in flight: invisible, hidden, and back on the seat so the next
-- caller measures it from a known place. Both ways out of the animation list end here.
local function CombatText_ParkString(string)
string:SetAlpha(0)
string:Hide()
string:SetPoint("TOP", UIParent, "BOTTOM", COMBAT_TEXT_LOCATIONS.startX, COMBAT_TEXT_LOCATIONS.startY)
end
function CombatText_AddMessage(message, scrollFunction, r, g, b, displayType, isStaggered)
local string, noStringsAvailable = CombatText_GetAvailableString()
if noStringsAvailable then
return
end
string:SetText(message)
string:SetTextColor(r, g, b)
string.scrollTime = 0
-- Where the line is seated. `flow` is the direction the STACK grows — back down the
-- way the run came, so -1 when the mode floats upward and +1 when it floats down —
-- and having it as a number is what lets one walk serve both modes. Start at the
-- string's own parked edge, then push clear of every line already in flight by one
-- 16-unit line plus COMBAT_TEXT_SPACING; if that has carried the stack further than
-- COMBAT_TEXT_MAX_OFFSET from the seat, start the column over at the parked edge
-- instead of marching off the screen. (`GetBottom`/`GetTop` are nil before the first
-- layout resolve — the parked seat stands in; identical after frame 1.)
local flow, parkedY
if COMBAT_TEXT_LOCATIONS.startY < COMBAT_TEXT_LOCATIONS.endY then
flow = -1
parkedY = string:GetBottom()
else
flow = 1
parkedY = string:GetTop()
end
parkedY = parkedY or COMBAT_TEXT_LOCATIONS.startY
local seatY = parkedY
for i = 1, getn(COMBAT_TEXT_TO_ANIMATE) do
local clearOf = COMBAT_TEXT_TO_ANIMATE[i].yPos + flow * (16 + COMBAT_TEXT_SPACING)
if (clearOf - seatY) * flow >= 0 then
seatY = clearOf
end
end
if (seatY - COMBAT_TEXT_LOCATIONS.startY) * flow > COMBAT_TEXT_MAX_OFFSET then
seatY = parkedY
end
-- Everything `displayType` decides, in one place. A crit pops through its own scale
-- envelope and always takes the straight scroll (an arc would fight the pop); a crit or
-- a sticky ends where it began, so it holds the seat instead of travelling; anything
-- else takes the caller's scroll and runs the whole way.
if displayType == "crit" then
string.scrollFunction = CombatText_StandardScroll
string.isCrit = 1
string.endY = COMBAT_TEXT_LOCATIONS.startY
string:SetTextHeight(COMBAT_TEXT_CRIT_MINHEIGHT)
else
string.scrollFunction = scrollFunction
string:SetTextHeight(COMBAT_TEXT_HEIGHT)
if displayType == "sticky" then
string.endY = COMBAT_TEXT_LOCATIONS.startY
else
string.endY = COMBAT_TEXT_LOCATIONS.endY
end
end
-- The fountain arcs alternate sides, so consecutive lines do not trace one path.
if CombatText.xDir == 1 then
CombatText.xDir = -1
else
CombatText.xDir = 1
end
string.xDir = CombatText.xDir
-- Staggered types jitter up to half COMBAT_TEXT_STAGGER_RANGE either side of centre.
string.startX = COMBAT_TEXT_LOCATIONS.startX
if isStaggered then
string.startX = string.startX + random(0, COMBAT_TEXT_STAGGER_RANGE) - COMBAT_TEXT_STAGGER_RANGE / 2
end
string.startY = seatY
string.yPos = seatY
string:SetPoint("TOP", UIParent, "BOTTOM", string.startX, seatY)
string:SetAlpha(1)
string:Show()
tinsert(COMBAT_TEXT_TO_ANIMATE, string)
end
function CombatText_RemoveMessage(string)
for i = 1, getn(COMBAT_TEXT_TO_ANIMATE) do
if COMBAT_TEXT_TO_ANIMATE[i] == string then
tremove(COMBAT_TEXT_TO_ANIMATE, i)
CombatText_ParkString(string)
break
end
end
end
function CombatText_GetAvailableString()
local string
for i = 1, NUM_COMBAT_TEXT_LINES do
string = getglobal("CombatText" .. i)
if not string:IsShown() then
return string
end
end
return CombatText_GetOldestString(), 1
end
function CombatText_GetOldestString()
local oldestString = COMBAT_TEXT_TO_ANIMATE[1]
CombatText_RemoveMessage(oldestString)
return oldestString
end
function CombatText_ClearAnimationList()
COMBAT_TEXT_TO_ANIMATE = {}
for i = 1, NUM_COMBAT_TEXT_LINES do
CombatText_ParkString(getglobal("CombatText" .. i))
end
end
function CombatText_UpdateDisplayedMessages()
-- The master toggle is enforced at the source: with the feature off we stop
-- listening entirely rather than filtering every message as it arrives.
if SHOW_COMBAT_TEXT == "0" then
CombatText:UnregisterEvent("COMBAT_TEXT_UPDATE")
CombatText:UnregisterEvent("UNIT_HEALTH")
CombatText:UnregisterEvent("UNIT_MANA")
CombatText:UnregisterEvent("PLAYER_COMBO_POINTS")
CombatText:UnregisterEvent("PLAYER_REGEN_DISABLED")
CombatText:UnregisterEvent("PLAYER_REGEN_ENABLED")
return
else
CombatText:RegisterEvent("COMBAT_TEXT_UPDATE")
CombatText:RegisterEvent("UNIT_HEALTH")
CombatText:RegisterEvent("UNIT_MANA")
CombatText:RegisterEvent("PLAYER_COMBO_POINTS")
CombatText:RegisterEvent("PLAYER_REGEN_DISABLED")
CombatText:RegisterEvent("PLAYER_REGEN_ENABLED")
end
-- Re-read every gated type's option into its `show`. This is the only writer of
-- `show` for a type that carries a `var`, so it runs on every option change.
for messageType, value in pairs(COMBAT_TEXT_TYPE_INFO) do
if value.var then
if getglobal(value.var) == "1" then
value.show = 1
else
value.show = nil
end
end
end
-- The engine's UI space is the 768-virtual coordinate system itself (decision
-- 0582), so the ref constants stand verbatim (0581's px shim retired). The ref's
-- uiScale-cvar division joins when a cvar system exists.
local uiScale = 1
-- The float mode picks the path and the run: modes 1 and 3 rise to 609 (3 on an
-- arc), mode 2 falls to 159. All three start at the same seat.
if COMBAT_TEXT_FLOAT_MODE == "1" then
COMBAT_TEXT_SCROLL_FUNCTION = CombatText_StandardScroll
COMBAT_TEXT_LOCATIONS = {
startX = 0,
startY = 384 / uiScale,
endX = 0,
endY = 609 / uiScale
}
elseif COMBAT_TEXT_FLOAT_MODE == "2" then
COMBAT_TEXT_SCROLL_FUNCTION = CombatText_StandardScroll
COMBAT_TEXT_LOCATIONS = {
startX = 0,
startY = 384 / uiScale,
endX = 0,
endY = 159 / uiScale
}
else
COMBAT_TEXT_SCROLL_FUNCTION = CombatText_FountainScroll
COMBAT_TEXT_LOCATIONS = {
startX = 0,
startY = 384 / uiScale,
endX = 0,
endY = 609 / uiScale
}
end
CombatText_ClearAnimationList()
end
function CombatText_StandardScroll(value)
local xPos = value.startX + ((COMBAT_TEXT_LOCATIONS.endX - COMBAT_TEXT_LOCATIONS.startX) * value.scrollTime / COMBAT_TEXT_SCROLLSPEED)
local yPos = value.startY + ((value.endY - COMBAT_TEXT_LOCATIONS.startY) * value.scrollTime / COMBAT_TEXT_SCROLLSPEED)
return xPos, yPos
end
function CombatText_FountainScroll(value)
local radius = 150
local xPos = value.xDir * (value.startX + radius * cos(90 * value.scrollTime / COMBAT_TEXT_SCROLLSPEED)) - value.xDir * radius
local yPos = value.startY + radius * sin(90 * value.scrollTime / COMBAT_TEXT_SCROLLSPEED)
return xPos, yPos
end
]]></Script>
<!-- The 20-string pool (ref CombatText1..20, each an expansion of CombatTextTemplate:
CombatTextFont, hidden, TOP anchored to UIParent BOTTOM (0,384) — re-seated per frame by
CombatText_OnUpdate). -->
<Frame name="CombatText" toplevel="true" parent="UIParent" hidden="false">
<Size><AbsDimension x="10" y="10"/></Size>
<Anchors>
<Anchor point="CENTER"><Offset><AbsDimension x="0" y="0"/></Offset></Anchor>
</Anchors>
<Layers>
<Layer level="BACKGROUND">
<FontString name="CombatText1" inherits="CombatTextFont" hidden="true">
<Anchors><Anchor point="TOP" relativeTo="UIParent" relativePoint="BOTTOM"><Offset><AbsDimension x="0" y="384"/></Offset></Anchor></Anchors>
</FontString>
<FontString name="CombatText2" inherits="CombatTextFont" hidden="true">
<Anchors><Anchor point="TOP" relativeTo="UIParent" relativePoint="BOTTOM"><Offset><AbsDimension x="0" y="384"/></Offset></Anchor></Anchors>
</FontString>
<FontString name="CombatText3" inherits="CombatTextFont" hidden="true">
<Anchors><Anchor point="TOP" relativeTo="UIParent" relativePoint="BOTTOM"><Offset><AbsDimension x="0" y="384"/></Offset></Anchor></Anchors>
</FontString>
<FontString name="CombatText4" inherits="CombatTextFont" hidden="true">
<Anchors><Anchor point="TOP" relativeTo="UIParent" relativePoint="BOTTOM"><Offset><AbsDimension x="0" y="384"/></Offset></Anchor></Anchors>
</FontString>
<FontString name="CombatText5" inherits="CombatTextFont" hidden="true">
<Anchors><Anchor point="TOP" relativeTo="UIParent" relativePoint="BOTTOM"><Offset><AbsDimension x="0" y="384"/></Offset></Anchor></Anchors>
</FontString>
<FontString name="CombatText6" inherits="CombatTextFont" hidden="true">
<Anchors><Anchor point="TOP" relativeTo="UIParent" relativePoint="BOTTOM"><Offset><AbsDimension x="0" y="384"/></Offset></Anchor></Anchors>
</FontString>
<FontString name="CombatText7" inherits="CombatTextFont" hidden="true">
<Anchors><Anchor point="TOP" relativeTo="UIParent" relativePoint="BOTTOM"><Offset><AbsDimension x="0" y="384"/></Offset></Anchor></Anchors>
</FontString>
<FontString name="CombatText8" inherits="CombatTextFont" hidden="true">
<Anchors><Anchor point="TOP" relativeTo="UIParent" relativePoint="BOTTOM"><Offset><AbsDimension x="0" y="384"/></Offset></Anchor></Anchors>
</FontString>
<FontString name="CombatText9" inherits="CombatTextFont" hidden="true">
<Anchors><Anchor point="TOP" relativeTo="UIParent" relativePoint="BOTTOM"><Offset><AbsDimension x="0" y="384"/></Offset></Anchor></Anchors>
</FontString>
<FontString name="CombatText10" inherits="CombatTextFont" hidden="true">
<Anchors><Anchor point="TOP" relativeTo="UIParent" relativePoint="BOTTOM"><Offset><AbsDimension x="0" y="384"/></Offset></Anchor></Anchors>
</FontString>
<FontString name="CombatText11" inherits="CombatTextFont" hidden="true">
<Anchors><Anchor point="TOP" relativeTo="UIParent" relativePoint="BOTTOM"><Offset><AbsDimension x="0" y="384"/></Offset></Anchor></Anchors>
</FontString>
<FontString name="CombatText12" inherits="CombatTextFont" hidden="true">
<Anchors><Anchor point="TOP" relativeTo="UIParent" relativePoint="BOTTOM"><Offset><AbsDimension x="0" y="384"/></Offset></Anchor></Anchors>
</FontString>
<FontString name="CombatText13" inherits="CombatTextFont" hidden="true">
<Anchors><Anchor point="TOP" relativeTo="UIParent" relativePoint="BOTTOM"><Offset><AbsDimension x="0" y="384"/></Offset></Anchor></Anchors>
</FontString>
<FontString name="CombatText14" inherits="CombatTextFont" hidden="true">
<Anchors><Anchor point="TOP" relativeTo="UIParent" relativePoint="BOTTOM"><Offset><AbsDimension x="0" y="384"/></Offset></Anchor></Anchors>
</FontString>
<FontString name="CombatText15" inherits="CombatTextFont" hidden="true">
<Anchors><Anchor point="TOP" relativeTo="UIParent" relativePoint="BOTTOM"><Offset><AbsDimension x="0" y="384"/></Offset></Anchor></Anchors>
</FontString>
<FontString name="CombatText16" inherits="CombatTextFont" hidden="true">
<Anchors><Anchor point="TOP" relativeTo="UIParent" relativePoint="BOTTOM"><Offset><AbsDimension x="0" y="384"/></Offset></Anchor></Anchors>
</FontString>
<FontString name="CombatText17" inherits="CombatTextFont" hidden="true">
<Anchors><Anchor point="TOP" relativeTo="UIParent" relativePoint="BOTTOM"><Offset><AbsDimension x="0" y="384"/></Offset></Anchor></Anchors>
</FontString>
<FontString name="CombatText18" inherits="CombatTextFont" hidden="true">
<Anchors><Anchor point="TOP" relativeTo="UIParent" relativePoint="BOTTOM"><Offset><AbsDimension x="0" y="384"/></Offset></Anchor></Anchors>
</FontString>
<FontString name="CombatText19" inherits="CombatTextFont" hidden="true">
<Anchors><Anchor point="TOP" relativeTo="UIParent" relativePoint="BOTTOM"><Offset><AbsDimension x="0" y="384"/></Offset></Anchor></Anchors>
</FontString>
<FontString name="CombatText20" inherits="CombatTextFont" hidden="true">
<Anchors><Anchor point="TOP" relativeTo="UIParent" relativePoint="BOTTOM"><Offset><AbsDimension x="0" y="384"/></Offset></Anchor></Anchors>
</FontString>
</Layer>
</Layers>
<Scripts>
<OnLoad>CombatText_OnLoad()</OnLoad>
<OnEvent>CombatText_OnEvent(event)</OnEvent>
<OnUpdate>CombatText_OnUpdate(arg1)</OnUpdate>
</Scripts>
</Frame>
</Ui>
-659
View File
@@ -1,659 +0,0 @@
<Ui>
<!--
THE MONEY FRAME KIT — `MoneyFrameTemplate` / `SmallMoneyFrameTemplate`, the `MoneyTypeInfo`
table and the `MoneyFrame_*` API that drives them.
This is **our implementation** of that kit. The specification it implements is the extracted
1.12 `Interface\FrameXML\MoneyFrame.lua` (356 lines) and `MoneyFrame.xml` (297), cited below
as `ref l.N`: the names, the published field names, the constants and every branch of the
collapse rule are the specification's, because addons call this by name and a compatible kit
has no freedom in any of that. How it is written — the structure, the helpers, the local
names, the comments — is ours.
WHY IT IS HERE: it is FrameXML's surface, not Rust's (decision 1190 — a name the shipped 1.12
UI defines belongs in a UI file, never hardcoded), and the addon corpus asks for it by name.
`MoneyFrame_SetType` is wanted by 11 addons, `MoneyFrame_Update` by 9, `SmallMoneyFrame_OnLoad`
by 9, and 13 declare a frame `inherits="SmallMoneyFrameTemplate"`. `MoneyTypeInfo` was one of
the top runtime walls the frame rename left standing.
── THE OTHER COIN KIT, and why there are still two ──────────────────────────────────────────
There WAS a second coin kit — `BenillaMoney_Set`/`_Clear`/`_SetColor`, a three-SLOT chain
(`<prefix>Coin1..3`) that filled only the non-zero denominations in display order and
re-flowed, born in MerchantFrame.xml and used by eight windows, with a layout the director had
signed off across them. The 2026-09-03 ruling — faithful wins, do not park on the director
what the reference already answers — superseded that sign-off: 1928 (quest reward money) and
1937 (quest required money, mail, trade, tooltip, quest log, trainer) moved every window onto
this file's templates, and the helpers were deleted with the last consumer. This file is the
only money kit now.
The auction house is the exception, and the reason the reconciliation started there rather
than anywhere else: the three-slot kit has no notion of a money TYPE, so it cannot express
`showSmallerCoins` or `fixedWidth` — and those two flags ARE the auction house's price column
(`MoneyTypeInfo["AUCTION"]`, declared below and, until AuctionFrame.xml moved, used by
nothing). On the three-slot kit "1g" showed as one lone gold coin where the reference shows
`1g 0s 0c`, and a column of prices packed tight instead of lining up. That is not a layout
preference to re-approve, it is the kit being the wrong kit. See decision 1522.
So the two still coexist, and the split is now by KIT CAPABILITY as much as by audience:
· `BenillaMoney_*` + `<prefix>Coin1..3` — the eight windows whose look is signed off and
whose money is all plain collapsing STATIC amounts, which that kit renders correctly.
· `MoneyFrame_*` + `MoneyTypeInfo` here — the names and structure an addon writes against,
what an addon's `inherits="SmallMoneyFrameTemplate"` resolves to, and what the auction
house is laid out on.
Finishing the migration — moving those eight onto `SmallMoneyFrameTemplate` and retiring
`BenillaMoney_*` — is still the right end state; it is a re-layout of hand-authored art in
eight files (an approved look), so it stays named here rather than done in passing.
── WHAT IS NOT CARRIED, and why (each is guarded at its own site below) ─────────────────────
· THE COIN PICKUP. `OpenCoinPickupFrame`/`UpdateCoinPickupFrame`/`CoinPickupFrame` belong to
CoinPickupFrame.lua/.xml, which we do not ship — and the engine verbs underneath them
(`PickupPlayerMoney`, `DropCursorMoney`, `PickupTradeMoney`, `AddTradeMoney`) are not built
either, so a picked-up coin would have nowhere to go. The coin buttons still take the mouse
for a `canPickup` type (that is the specified state) and the click is guarded, so it does
nothing.
· `GetCursorMoney` (PLAYER's amount, ref l.17) — the engine verb for coins held on the cursor.
With no coin pickup there is no cursor money, so its term is not approximated away: it is
structurally zero. Stated at the spot.
· `GetSendMailMoney` / `GetSendMailCOD` (ref l.73, l.90) — 1.12 engine verbs we have not
built. We ship their SETTERS (`SetSendMailMoney`/`SetSendMailCOD`, which MailFrame.xml
drives); nothing reads the amount back yet, so those two types read 0.
· `message()` (ref l.145, l.173, l.181) — the one-line StaticPopup shim, which belongs to
BasicControls.xml, a file family we have not built. Guarded, so a misuse of the API stays
silent instead of erroring on the diagnostic.
· `Button:GetTextWidth()` (ref l.202 etc.) — BUILT, and called here, exactly as the
specification calls it. It forwards to the label FontString's `GetStringWidth`, which the
engine now answers inside the tick that asked (`script::measure`).
· The four `Pickup*`/`Add*` mail verbs the pickup closures name are not in 1.12's `_G` either
(they are absent from reference/1.12-globals.tsv) — so those closures are as unreachable
here as they are there. They exist because the table's shape is the contract.
-->
<Script><![CDATA[
-- ══ THE THREE-SLOT COIN KIT (`BenillaMoney_*`), rehomed from MerchantFrame.xml ═══════════
--
-- The OTHER kit — see this file's header for why there are two and what the split is. It
-- lives beside `MoneyFrame_*` rather than inside a window because eight windows share it:
-- merchant, bank, trade, mail, quest, quest log, taxi and trainer. Nothing about the kit
-- changed in the move; the `SmallMoneyFrameTemplate` migration the header names is still
-- outstanding and still the director's call.
-- The coin display, quoted from the real SmallMoneyFrame (ref MoneyFrame.xml/lua): a copper
-- amount renders as a number + a 13px coin icon per NONZERO denomination, cut from the one
-- shared texture Interface\MoneyFrame\UI-MoneyIcons by TexCoord (gold 0..0.25, silver
-- 0.25..0.5, copper 0.5..0.75 — ref l.115/74/33), the real collapse that hides zero coins
-- (MoneyFrame.lua l.221-267). Each money display owns three position-"slots"; the XML chains
-- them either left-to-right (row prices + the quest reward: Coin1 leftmost, gold first) or
-- right-to-left (the purses: Coin1 rightmost, copper first). BenillaMoney_Set fills the
-- visible denominations into slots 1.. in DISPLAY order (highestFirst=true for the
-- left-aligned chains) and — the real shrink, MoneyFrame.lua l.202/269 — sizes each slot to
-- exactly its number + its 13px coin (`SetWidth(GetTextWidth() + iconWidth)` there; the
-- same read here, off the number FontString the slot already owns). The static ±4px
-- chain-anchor offsets are the real MONEY_BUTTON_SPACING gap, so a shrunk group packs tight,
-- left-flush in the ltr chains and right-flush in the rtl ones. BenillaMoney_Clear hides
-- all three slots (the real collapse hides, see the helper).
--
-- NOTE (for the loot window's fold): the reusable helpers are
-- BenillaMoney_Set(prefix, copper) and BenillaMoney_Clear(prefix)
-- where `prefix` names the slot frames `<prefix>Coin1..3` (each with a `<prefix>CoinNIcon`
-- texture + `<prefix>CoinNNum` fontstring child). They live in this file.
--
-- They lived in MerchantFrame.xml until 1751 came for that window: the stock
-- `MerchantFrame.xml` off the player's chain has no room for eight other windows'
-- shared helpers, and this file is where the reference keeps money code anyway. A pure
-- move — not the `SmallMoneyFrameTemplate` migration named at the top of this file,
-- which is a re-layout of an approved look and is the director's to call.
BENILLA_COIN_TC = {
{ 0.0, 0.25 }, -- [1] gold
{ 0.25, 0.5 }, -- [2] silver
{ 0.5, 0.75 }, -- [3] copper
}
-- ── What a coin is worth ─────────────────────────────────────────────────────────────────
-- Published globals: an addon that formats a price does the same split we do below, and
-- reads these rather than spelling 100 twice.
COPPER_PER_SILVER = 100
SILVER_PER_GOLD = 100
COPPER_PER_GOLD = COPPER_PER_SILVER * SILVER_PER_GOLD
-- ── How wide a coin draws ────────────────────────────────────────────────────────────────
-- One coin icon's width at each of the two sizes the templates below come in, and the gap
-- between two coin buttons — NEGATIVE, because the three deliberately bite into each other
-- so a group reads as one number rather than as three. `COIN_BUTTON_WIDTH` is the fixed slot
-- a `fixedWidth` type pads its smaller coins out to, so a column of prices lines up.
-- The values are the specification's (ref l.2-12): measurements of the shipped coin art, not
-- ours to re-derive.
MONEY_ICON_WIDTH = 19
MONEY_ICON_WIDTH_SMALL = 13
MONEY_BUTTON_SPACING = -4
MONEY_BUTTON_SPACING_SMALL = -4
COIN_BUTTON_WIDTH = 32
-- ── The money types (ref l.14-103) ───────────────────────────────────────────────────────
--
-- A money frame carries no amount of its own: it carries a TYPE, and the type says where the
-- number comes from and how the coins behave. An entry has three parts:
--
-- · `UpdateFunc()` returns the amount, in copper. It is called with the global `this` bound
-- to the frame being painted, so a type may read the frame as well as the engine.
-- · `PickupFunc(amount)` / `DropFunc()` are the cursor verbs a click-to-pick-up type drives.
-- · the display flags `MoneyFrame_Update` reads: `collapse` (drop leading zero
-- denominations), `showSmallerCoins` (keep the trailing ones on even at zero; the value
-- "Backpack" additionally keeps copper on by itself), `canPickup` (the coins take the
-- mouse), `fixedWidth` (pad the smaller coins to COIN_BUTTON_WIDTH), `truncateSmallCoins`
-- (drop copper once there is gold).
--
-- Nothing else is consulted. That is why those field names and the two function signatures
-- are the published contract and not an implementation detail: addons register types of their
-- own into this table and expect exactly these parts to be honoured.
MoneyTypeInfo = { }
-- Read an amount through an engine verb that may not be built here yet: absent, it reads 0
-- rather than calling into nil. Two of the seven types below need it — see the header.
local function AmountFrom(verbName)
local verb = getglobal(verbName)
if verb then
return verb()
end
return 0
end
-- The purse. The specification subtracts a third term here (ref l.17,
-- `GetMoney() - GetCursorMoney() - GetPlayerTradeMoney()`): coins lifted onto the cursor by
-- the coin-pickup frame. benilla ships neither that frame nor the `PickupPlayerMoney` verb
-- behind it, so the term is structurally 0 rather than approximated away — it comes back the
-- moment either lands.
MoneyTypeInfo["PLAYER"] = {
UpdateFunc = function() return GetMoney() - GetPlayerTradeMoney() end,
PickupFunc = function(amount) PickupPlayerMoney(amount) end,
DropFunc = function() DropCursorMoney() end,
collapse = 1,
canPickup = 1,
showSmallerCoins = "Backpack",
}
-- A number the owner parked on the frame itself — what `MoneyFrame_Update(name, amount)`
-- leaves behind, and how an addon shows a price it worked out for itself.
MoneyTypeInfo["STATIC"] = {
UpdateFunc = function() return this.staticMoney end,
collapse = 1,
}
-- The auction house's price column: the same source as STATIC, but the smaller coins pad out
-- to a fixed slot and stay on at zero, so a stack of prices reads as a column of digits.
MoneyTypeInfo["AUCTION"] = {
UpdateFunc = function() return this.staticMoney end,
collapse = 1,
showSmallerCoins = "Backpack",
fixedWidth = 1,
truncateSmallCoins = nil,
}
-- Our side of an open trade, and the other party's. Only ours is click-to-pick-up.
MoneyTypeInfo["PLAYER_TRADE"] = {
UpdateFunc = function() return GetPlayerTradeMoney() end,
PickupFunc = function(amount) PickupTradeMoney(amount) end,
DropFunc = function() AddTradeMoney() end,
collapse = 1,
canPickup = 1,
}
MoneyTypeInfo["TARGET_TRADE"] = {
UpdateFunc = function() return GetTargetTradeMoney() end,
collapse = 1,
}
-- Mail postage and cash-on-delivery. Postage explicitly does NOT collapse (ref l.84): the
-- send-mail frame wants all three coins standing whatever the amount, because you are typing
-- into them. Both read 0 until their engine getters exist — see the header.
MoneyTypeInfo["SEND_MAIL"] = {
UpdateFunc = function() return AmountFrom("GetSendMailMoney") end,
PickupFunc = function(amount) PickupSendMailMoney(amount) end,
DropFunc = function() AddSendMailMoney() end,
collapse = nil,
canPickup = 1,
showSmallerCoins = "Backpack",
}
MoneyTypeInfo["SEND_MAIL_COD"] = {
UpdateFunc = function() return AmountFrom("GetSendMailCOD") end,
PickupFunc = function(amount) PickupSendMailCOD(amount) end,
DropFunc = function() AddSendMailCOD() end,
collapse = 1,
canPickup = 1,
}
-- ── Hearing about a change (ref l.105-140) ───────────────────────────────────────────────
--
-- Every money frame registers all five money events; each then decides whether the one that
-- arrived is about the number IT is showing. That pairing is data, not control flow, so it is
-- written as data: one row per event, in the order the templates register them, naming the
-- money types that event can change.
local MONEY_EVENTS = {
{ "PLAYER_MONEY", { PLAYER = 1 } },
{ "PLAYER_TRADE_MONEY", { PLAYER = 1, PLAYER_TRADE = 1 } },
{ "TRADE_MONEY_CHANGED", { TARGET_TRADE = 1 } },
{ "SEND_MAIL_MONEY_CHANGED", { PLAYER = 1, SEND_MAIL = 1 } },
{ "SEND_MAIL_COD_CHANGED", { PLAYER = 1, SEND_MAIL_COD = 1 } },
}
-- The same rows keyed by event name, built once, so the handler below is a lookup.
local moneyEventTypes = { }
for _, row in ipairs(MONEY_EVENTS) do
moneyEventTypes[row[1]] = row[2]
end
local function MoneyFrameListen(frame)
for _, row in ipairs(MONEY_EVENTS) do
frame:RegisterEvent(row[1])
end
end
-- The two OnLoads differ in exactly one thing: `small`, which is what picks the 13px coin and
-- its tighter spacing over the 19px one in MoneyFrame_Update below. Both then take type
-- PLAYER, so a frame nobody tells otherwise shows the purse.
function MoneyFrame_OnLoad()
MoneyFrameListen(this)
MoneyFrame_SetType("PLAYER")
end
function SmallMoneyFrame_OnLoad()
MoneyFrameListen(this)
this.small = 1
MoneyFrame_SetType("PLAYER")
end
function MoneyFrame_OnEvent()
if not this.info or not this:IsVisible() then
return
end
local affected = moneyEventTypes[event]
if affected and affected[this.moneyType] then
MoneyFrame_UpdateMoney()
end
end
-- ── Painting ─────────────────────────────────────────────────────────────────────────────
-- A money frame's three coin buttons, biggest first. Every verb below addresses them as a
-- trio, so this is the one place their `$parent<Denomination>Button` names are spelled.
local function MoneyFrameCoins(frameName)
return getglobal(frameName .. "GoldButton"),
getglobal(frameName .. "SilverButton"),
getglobal(frameName .. "CopperButton")
end
-- Point a frame at a money type (ref l.142-162): remember it, wire the mouse to match, and
-- repaint. Reads `this` rather than taking a frame — that is the convention the eleven corpus
-- callers write against.
function MoneyFrame_SetType(type)
local info = MoneyTypeInfo[type]
if not info then
-- ref l.145 diagnoses this with `message()`, which we DO have: it comes off the
-- chain in `Interface\FrameXML\BasicControls.xml` (1881 migrated that file; before
-- it, our own copy carried the same function). The `if` is therefore belt-and-braces
-- for a harness that loads this file without BasicControls, not a missing verb.
if message then
message("Invalid money type: " .. type)
end
return
end
this.info = info
this.moneyType = type
-- Only a click-to-pick-up type wants the mouse; on the rest a click falls through to
-- whatever is behind the coins.
local gold, silver, copper = MoneyFrameCoins(this:GetName())
local pickable = info.canPickup and true or false
gold:EnableMouse(pickable)
silver:EnableMouse(pickable)
copper:EnableMouse(pickable)
MoneyFrame_UpdateMoney()
end
-- Re-read the amount from the type and repaint (ref l.164-175). `this` again.
function MoneyFrame_UpdateMoney()
if not this.info then
if message then
message("Error moneyType not set")
end
return
end
local money = this.info.UpdateFunc()
MoneyFrame_Update(this:GetName(), money)
-- ref l.169-171 also feeds the coin-pickup frame while one is open. `hasPickup` is set
-- only by the coin click, which is guarded off (see the header), so this is unreachable
-- today rather than removed.
if this.hasPickup == 1 and UpdateCoinPickupFrame then
UpdateCoinPickupFrame(money)
end
end
-- Lay one denomination out: its digits, and the width those digits plus the coin icon need.
--
-- `Button:GetTextWidth()` right after `SetText`, which is what the specification writes
-- (ref l.202 and kin) and what the real client answers synchronously off its inline font
-- engine, so the coin is the right width on the frame that set its text.
--
-- This read used to return 0 — our measure landed at extract, a frame after the tick — and
-- left all three coins at exactly one bare icon width with their digits overlapping: the
-- director's "gold numbers are all cramped up when I first open the bags; close and open
-- again and it looks good", the reopen being the first open's cached measure. It was served
-- for a while by a digits-only advance table, since a number's width is a pure sum of
-- per-glyph advances. The engine answers the general question now (`script::measure`), so
-- this asks it the way the specification does.
local function ShowCoin(coin, coinName, amount, iconWidth)
coin:SetText(amount)
coin:SetWidth(coin:GetTextWidth() + iconWidth)
coin:Show()
end
-- Chain the next-bigger coin onto `coin`, and report what `coin` contributes to the group's
-- width.
--
-- Showing: the bigger coin hangs off this one's LEFT edge by `spacing` (negative — they
-- overlap), and the group grows by this coin's width less that overlap, but only when the
-- bigger coin is actually on screen to overlap with. Not showing: this coin hides and the
-- bigger one collapses onto its RIGHT edge, contributing nothing. `padToSlot` is the
-- `fixedWidth` exception, which sizes the coin to the fixed column slot first.
local function ChainCoin(bigger, coin, coinName, showing, padToSlot, spacing)
if not showing then
coin:Hide()
bigger:SetPoint("RIGHT", coinName, "RIGHT", 0, 0)
return 0
end
if padToSlot then
coin:SetWidth(COIN_BUTTON_WIDTH)
end
local grown = coin:GetWidth()
bigger:SetPoint("RIGHT", coinName, "LEFT", spacing, 0)
if bigger:IsVisible() then
grown = grown - spacing
end
return grown
end
-- THE PAINTER (ref l.177-270). Split a copper amount into the three denominations, size each
-- coin to its own digits plus its icon, and then — if the type collapses — drop the leading
-- zeros, re-chain what is left so the group packs tight, and report the frame's true width.
--
-- This one takes a frame NAME, not `this`: it is the only entry point in the family an owner
-- can call about a frame other than the one currently dispatching, which is how an addon
-- paints a STATIC frame it has just filled.
function MoneyFrame_Update(frameName, money)
local frame = getglobal(frameName)
local info = frame.info
if not info then
-- ref l.181 diagnoses and, notably, does NOT return: the coins below are still laid
-- out, and the collapse read further down is what actually stops it. Kept as-is.
if message then
message("Error moneyType not set")
end
end
-- Copper is the low two digits, silver the next two, gold everything above. Silver comes
-- out by subtracting the gold back off rather than by a second `mod`: `mod` here is 1.12's
-- `math.mod`, which truncates towards zero, so `mod(-1, 100)` is -1 and not 99 — the two
-- forms part company the moment `money` goes negative (which `GetMoney()` minus a trade
-- offer can), and this one is the specified answer.
-- A STATIC/AUCTION frame's amount is whatever its owner last parked on it, and on the
-- very first paint — `MoneyFrame_SetType`'s own repaint, in OnLoad, before any owner has
-- filled it — that is nil. The specification divides straight into it and throws there;
-- we answer 0, because the throw would take the whole `<OnLoad>`/`<OnShow>` chain down
-- with it and an empty price column is the honest reading of "nothing set yet".
money = money or 0
local gold = floor(money / COPPER_PER_GOLD)
local silver = floor((money - gold * COPPER_PER_GOLD) / COPPER_PER_SILVER)
local copper = mod(money, COPPER_PER_SILVER)
local goldCoin, silverCoin, copperCoin = MoneyFrameCoins(frameName)
local iconWidth, spacing = MONEY_ICON_WIDTH, MONEY_BUTTON_SPACING
if frame.small then
iconWidth, spacing = MONEY_ICON_WIDTH_SMALL, MONEY_BUTTON_SPACING_SMALL
end
ShowCoin(goldCoin, frameName .. "GoldButton", gold, iconWidth)
ShowCoin(silverCoin, frameName .. "SilverButton", silver, iconWidth)
ShowCoin(copperCoin, frameName .. "CopperButton", copper, iconWidth)
-- Remember what the frame is displaying — STATIC's own reader reads it back.
frame.staticMoney = money
-- A type that does not collapse is finished: all three coins stand, at the width their
-- digits asked for, and the frame keeps whatever width it was given.
if not info.collapse then
return
end
-- Gold leads. Present, it stays and sets the terms for the two below it; absent, it goes.
-- The running width starts at one icon's worth of frame padding.
local width = iconWidth
local keepSmaller, dropCopper
if gold > 0 then
width = width + goldCoin:GetWidth()
keepSmaller = info.showSmallerCoins and 1 or nil
dropCopper = info.truncateSmallCoins and 1 or nil
else
goldCoin:Hide()
end
-- Silver, then copper: each either shows — padded, chained and counted — or hides and lets
-- the coin above it collapse onto its edge. Silver showing under a `showSmallerCoins` type
-- is what carries the exemption down to copper; copper also survives at zero for a
-- "Backpack" type, and dies whatever it is once gold truncated it.
local showSilver = silver > 0 or keepSmaller
width = width + ChainCoin(goldCoin, silverCoin, frameName .. "SilverButton",
showSilver, keepSmaller and info.fixedWidth, spacing)
if showSilver and info.showSmallerCoins then
keepSmaller = 1
end
local showCopper = (copper > 0 or keepSmaller or info.showSmallerCoins == "Backpack")
and not dropCopper
width = width + ChainCoin(silverCoin, copperCoin, frameName .. "CopperButton",
showCopper, keepSmaller and info.fixedWidth, spacing)
frame:SetWidth(width)
end
-- A NO-OP, deliberately. The specification's whole body for this one sits inside a `--[[ ]]`
-- block (ref l.272-346) — an earlier painter that MoneyFrame_Update replaced. The name is in
-- the shipped client's `_G`, and an addon that calls it there gets exactly this: nothing.
function RefreshMoneyFrame(frameName, money, small, collapse, showSmallerCoins)
end
-- Recolour the DIGITS; the coin icons keep their own texture (ref l.348-356). This is how a
-- price you cannot afford turns red.
function SetMoneyFrameColor(frameName, r, g, b)
local gold, silver, copper = MoneyFrameCoins(frameName)
gold:SetTextColor(r, g, b)
silver:SetTextColor(r, g, b)
copper:SetTextColor(r, g, b)
end
-- A click on a coin opens the pickup frame on that denomination, seeded with what the frame is
-- currently showing; `denomination` is one coin's worth in copper, which is the unit the
-- pickup frame counts in.
--
-- Guarded, because we ship no coin pickup (see the header) — so today the click does nothing,
-- and the mouse-enabled state a `canPickup` type gives the coins is all that is observable.
-- Note the amount is read by calling the type's own `UpdateFunc` while `this` is still the
-- BUTTON rather than the frame, which is what the specification's own coin `<OnClick>` does
-- (ref MoneyFrame.xml l.20-23) and what a frame-reading type would see through.
function BenillaMoneyFrameCoin_OnClick(coin, denomination)
if not OpenCoinPickupFrame then
return
end
local frame = coin:GetParent()
OpenCoinPickupFrame(denomination, MoneyTypeInfo[frame.moneyType].UpdateFunc(), frame)
frame.hasPickup = 1
end
-- Closing a money frame takes any pickup it opened down with it.
function BenillaMoneyFrame_OnHide()
if this.hasPickup == 1 and CoinPickupFrame then
CoinPickupFrame:Hide()
this.hasPickup = 0
end
end
]]></Script>
<!--
MoneyFrameTemplate (ref MoneyFrame.xml l.3-149) — the LARGE kit: 192x28, 19px coins, the
NumberFontNormalLarge digits. The three buttons chain RIGHT-to-LEFT from the copper end, which
is what lets MoneyFrame_Update above re-point them past a hidden denomination.
`<PushedTextOffset>` is carried for provenance; our loader does not model it and ignores it.
-->
<Frame name="MoneyFrameTemplate" virtual="true">
<Size><AbsDimension x="192" y="28"/></Size>
<Frames>
<Button name="$parentCopperButton">
<Size><AbsDimension x="47" y="19"/></Size>
<Anchors>
<Anchor point="RIGHT"><Offset><AbsDimension x="-13" y="0"/></Offset></Anchor>
</Anchors>
<Scripts>
<OnClick>BenillaMoneyFrameCoin_OnClick(this, 1)</OnClick>
</Scripts>
<NormalTexture file="Interface\MoneyFrame\UI-MoneyIcons">
<Size><AbsDimension x="19" y="19"/></Size>
<Anchors><Anchor point="RIGHT"/></Anchors>
<TexCoords left="0.5" right="0.75" top="0" bottom="1"/>
</NormalTexture>
<ButtonText name="$parentText">
<Anchors>
<Anchor point="RIGHT"><Offset><AbsDimension x="-19" y="0"/></Offset></Anchor>
</Anchors>
</ButtonText>
<NormalFont inherits="NumberFontNormalLarge" justifyH="RIGHT"/>
<PushedTextOffset><AbsDimension x="0" y="0"/></PushedTextOffset>
</Button>
<Button name="$parentSilverButton">
<Size><AbsDimension x="47" y="19"/></Size>
<Anchors>
<Anchor point="RIGHT" relativeTo="$parentCopperButton" relativePoint="LEFT"><Offset><AbsDimension x="-4" y="0"/></Offset></Anchor>
</Anchors>
<Scripts>
<OnClick>BenillaMoneyFrameCoin_OnClick(this, COPPER_PER_SILVER)</OnClick>
</Scripts>
<NormalTexture file="Interface\MoneyFrame\UI-MoneyIcons">
<Size><AbsDimension x="19" y="19"/></Size>
<Anchors><Anchor point="RIGHT"/></Anchors>
<TexCoords left="0.25" right="0.5" top="0" bottom="1"/>
</NormalTexture>
<ButtonText name="$parentText">
<Anchors>
<Anchor point="RIGHT"><Offset><AbsDimension x="-19" y="0"/></Offset></Anchor>
</Anchors>
</ButtonText>
<NormalFont inherits="NumberFontNormalLarge" justifyH="RIGHT"/>
<PushedTextOffset><AbsDimension x="0" y="0"/></PushedTextOffset>
</Button>
<Button name="$parentGoldButton">
<Size><AbsDimension x="47" y="19"/></Size>
<Anchors>
<Anchor point="RIGHT" relativeTo="$parentSilverButton" relativePoint="LEFT"><Offset><AbsDimension x="-4" y="0"/></Offset></Anchor>
</Anchors>
<Scripts>
<OnClick>BenillaMoneyFrameCoin_OnClick(this, COPPER_PER_GOLD)</OnClick>
</Scripts>
<NormalTexture file="Interface\MoneyFrame\UI-MoneyIcons">
<Size><AbsDimension x="19" y="19"/></Size>
<Anchors><Anchor point="RIGHT"/></Anchors>
<TexCoords left="0" right="0.25" top="0" bottom="1"/>
</NormalTexture>
<ButtonText name="$parentText">
<Anchors>
<Anchor point="RIGHT"><Offset><AbsDimension x="-19" y="0"/></Offset></Anchor>
</Anchors>
</ButtonText>
<NormalFont inherits="NumberFontNormalLarge" justifyH="RIGHT"/>
<PushedTextOffset><AbsDimension x="0" y="0"/></PushedTextOffset>
</Button>
</Frames>
<Scripts>
<OnLoad>MoneyFrame_OnLoad()</OnLoad>
<OnEvent>MoneyFrame_OnEvent()</OnEvent>
<OnShow>MoneyFrame_UpdateMoney()</OnShow>
<OnHide>BenillaMoneyFrame_OnHide()</OnHide>
</Scripts>
</Frame>
<!--
SmallMoneyFrameTemplate (ref MoneyFrame.xml l.150-296) — the same kit at 128x13 with 13px
coins and NumberFontNormal digits, and `SmallMoneyFrame_OnLoad` in place of `MoneyFrame_OnLoad`
so `this.small` is set. This is the one the shipped UI uses nearly everywhere (merchant prices,
the purse, mail postage, trade offers) and the one 13 corpus addons inherit.
-->
<Frame name="SmallMoneyFrameTemplate" virtual="true">
<Size><AbsDimension x="128" y="13"/></Size>
<Frames>
<Button name="$parentCopperButton">
<Size><AbsDimension x="32" y="13"/></Size>
<Anchors>
<Anchor point="RIGHT"><Offset><AbsDimension x="-13" y="0"/></Offset></Anchor>
</Anchors>
<Scripts>
<OnClick>BenillaMoneyFrameCoin_OnClick(this, 1)</OnClick>
</Scripts>
<NormalTexture file="Interface\MoneyFrame\UI-MoneyIcons">
<Size><AbsDimension x="13" y="13"/></Size>
<Anchors><Anchor point="RIGHT"/></Anchors>
<TexCoords left="0.5" right="0.75" top="0" bottom="1"/>
</NormalTexture>
<ButtonText name="$parentText">
<Anchors>
<Anchor point="RIGHT"><Offset><AbsDimension x="-13" y="0"/></Offset></Anchor>
</Anchors>
</ButtonText>
<NormalFont inherits="NumberFontNormal" justifyH="RIGHT"/>
<PushedTextOffset><AbsDimension x="0" y="0"/></PushedTextOffset>
</Button>
<Button name="$parentSilverButton">
<Size><AbsDimension x="32" y="13"/></Size>
<Anchors>
<Anchor point="RIGHT" relativeTo="$parentCopperButton" relativePoint="LEFT"><Offset><AbsDimension x="-4" y="0"/></Offset></Anchor>
</Anchors>
<Scripts>
<OnClick>BenillaMoneyFrameCoin_OnClick(this, COPPER_PER_SILVER)</OnClick>
</Scripts>
<NormalTexture file="Interface\MoneyFrame\UI-MoneyIcons">
<Size><AbsDimension x="13" y="13"/></Size>
<Anchors><Anchor point="RIGHT"/></Anchors>
<TexCoords left="0.25" right="0.5" top="0" bottom="1"/>
</NormalTexture>
<ButtonText name="$parentText">
<Anchors>
<Anchor point="RIGHT"><Offset><AbsDimension x="-13" y="0"/></Offset></Anchor>
</Anchors>
</ButtonText>
<NormalFont inherits="NumberFontNormal" justifyH="RIGHT"/>
<PushedTextOffset><AbsDimension x="0" y="0"/></PushedTextOffset>
</Button>
<Button name="$parentGoldButton">
<Size><AbsDimension x="32" y="13"/></Size>
<Anchors>
<Anchor point="RIGHT" relativeTo="$parentSilverButton" relativePoint="LEFT"><Offset><AbsDimension x="-4" y="0"/></Offset></Anchor>
</Anchors>
<Scripts>
<OnClick>BenillaMoneyFrameCoin_OnClick(this, COPPER_PER_GOLD)</OnClick>
</Scripts>
<NormalTexture file="Interface\MoneyFrame\UI-MoneyIcons">
<Size><AbsDimension x="13" y="13"/></Size>
<Anchors><Anchor point="RIGHT"/></Anchors>
<TexCoords left="0" right="0.25" top="0" bottom="1"/>
</NormalTexture>
<ButtonText name="$parentText">
<Anchors>
<Anchor point="RIGHT"><Offset><AbsDimension x="-13" y="0"/></Offset></Anchor>
</Anchors>
</ButtonText>
<NormalFont inherits="NumberFontNormal" justifyH="RIGHT"/>
<PushedTextOffset><AbsDimension x="0" y="0"/></PushedTextOffset>
</Button>
</Frames>
<Scripts>
<OnLoad>SmallMoneyFrame_OnLoad()</OnLoad>
<OnEvent>MoneyFrame_OnEvent()</OnEvent>
<OnShow>MoneyFrame_UpdateMoney()</OnShow>
<OnHide>BenillaMoneyFrame_OnHide()</OnHide>
</Scripts>
</Frame>
</Ui>
+63 -14
View File
@@ -220,6 +220,50 @@
RegisterForSave("QUEST_FADING_DISABLE")
AUTO_QUEST_WATCH = "1"
RegisterForSave("AUTO_QUEST_WATCH")
-- Combat text uvars (ref UIOptionsFrame.lua l.135-152, verbatim). They lived in our
-- CombatText.xml while that file stood in for the addon; the addon is the reference's
-- own `Blizzard_CombatText` off the chain now, LoadOnDemand as the reference ships it,
-- so its option family's defaults sit where the reference keeps them (1964).
SHOW_COMBAT_TEXT = "0"
RegisterForSave("SHOW_COMBAT_TEXT")
COMBAT_TEXT_SHOW_LOW_HEALTH_MANA = "1"
RegisterForSave("COMBAT_TEXT_SHOW_LOW_HEALTH_MANA")
COMBAT_TEXT_SHOW_AURAS = "1"
RegisterForSave("COMBAT_TEXT_SHOW_AURAS")
COMBAT_TEXT_SHOW_AURA_FADE = "0"
RegisterForSave("COMBAT_TEXT_SHOW_AURA_FADE")
COMBAT_TEXT_SHOW_COMBAT_STATE = "1"
RegisterForSave("COMBAT_TEXT_SHOW_COMBAT_STATE")
COMBAT_TEXT_SHOW_DODGE_PARRY_MISS = "0"
RegisterForSave("COMBAT_TEXT_SHOW_DODGE_PARRY_MISS")
COMBAT_TEXT_SHOW_RESISTANCES = "0"
RegisterForSave("COMBAT_TEXT_SHOW_RESISTANCES")
COMBAT_TEXT_SHOW_REPUTATION = "0"
RegisterForSave("COMBAT_TEXT_SHOW_REPUTATION")
COMBAT_TEXT_SHOW_REACTIVES = "0"
RegisterForSave("COMBAT_TEXT_SHOW_REACTIVES")
COMBAT_TEXT_SHOW_FRIENDLY_NAMES = "0"
RegisterForSave("COMBAT_TEXT_SHOW_FRIENDLY_NAMES")
COMBAT_TEXT_SHOW_COMBO_POINTS = "0"
RegisterForSave("COMBAT_TEXT_SHOW_COMBO_POINTS")
COMBAT_TEXT_SHOW_MANA = "0"
RegisterForSave("COMBAT_TEXT_SHOW_MANA")
COMBAT_TEXT_FLOAT_MODE = "1"
RegisterForSave("COMBAT_TEXT_FLOAT_MODE")
COMBAT_TEXT_SHOW_HONOR_GAINED = "1"
RegisterForSave("COMBAT_TEXT_SHOW_HONOR_GAINED")
-- The family's apply, ref UIOptionsFrame_Save l.371-376 verbatim: the addon is
-- LoadOnDemand, so turning the master on loads it, and the update runs only once it is
-- there. Every combat-text row's `applyFunc` is this (1964).
function CombatText_UpdateOrLoad()
if ( SHOW_COMBAT_TEXT == "1" ) then
UIParentLoadAddOn("Blizzard_CombatText")
end
if ( CombatText_UpdateDisplayedMessages ) then
CombatText_UpdateDisplayedMessages()
end
end
-- CHAT_LOCKED / REMOVE_CHAT_DELAY — the chat window's two uvars, the reference's own
-- UIOptionsFrame.lua l.96-99 defaults ("0" and "0"), read every frame by
-- FloatingChatFrame.lua's FCF_Get_ChatLocked and FCF_OnUpdate since the chat window went
@@ -1073,6 +1117,11 @@
end
function OptionsFrame_ApplySavedSettings()
-- ref UIOptionsFrame_OnEvent's VARIABLES_LOADED arm, l.224-226: the combat text
-- addon loads at login when the saved master is on (1964).
if ( SHOW_COMBAT_TEXT == "1" ) then
UIParentLoadAddOn("Blizzard_CombatText")
end
local ran = {}
for key, rows in pairs(OPTIONS_PAGE_ROWS) do
for _, rkey in ipairs(rows) do
@@ -2792,7 +2841,7 @@
<Scripts><OnLoad>
OptionsGlobalRow_OnLoad(self, "SHOW_COMBAT_TEXT",
"Enable Floating Combat Text", "OPTION_TOOLTIP_SHOW_COMBAT_TEXT")
self.applyFunc = "CombatText_UpdateDisplayedMessages" -- by NAME, resolved when applied: this file loads before its definer now (1938)
self.applyFunc = "CombatText_UpdateOrLoad" -- by NAME, resolved when applied: this file loads before its definer now (1938)
</OnLoad></Scripts>
</Frame>
<!-- The scroll mode. 1.12's own dropdown, values and all
@@ -2812,7 +2861,7 @@
{ text = "Arc", value = "3" },
})
self.parentRow = "RowCombatText"
self.applyFunc = "CombatText_UpdateDisplayedMessages" -- by NAME, resolved when applied: this file loads before its definer now (1938)
self.applyFunc = "CombatText_UpdateOrLoad" -- by NAME, resolved when applied: this file loads before its definer now (1938)
</OnLoad></Scripts>
</Frame>
<Frame name="$parentRowLowHealthMana" inherits="OptionsCheckboxRowTemplate">
@@ -2825,7 +2874,7 @@
"Show Low Mana and Health",
"OPTION_TOOLTIP_COMBAT_TEXT_SHOW_LOW_HEALTH_MANA", 1)
self.parentRow = "RowCombatText"
self.applyFunc = "CombatText_UpdateDisplayedMessages" -- by NAME, resolved when applied: this file loads before its definer now (1938)
self.applyFunc = "CombatText_UpdateOrLoad" -- by NAME, resolved when applied: this file loads before its definer now (1938)
</OnLoad></Scripts>
</Frame>
<Frame name="$parentRowAuras" inherits="OptionsCheckboxRowTemplate">
@@ -2837,7 +2886,7 @@
OptionsGlobalRow_OnLoad(self, "COMBAT_TEXT_SHOW_AURAS",
"Show Auras", "OPTION_TOOLTIP_COMBAT_TEXT_SHOW_AURAS", 1)
self.parentRow = "RowCombatText"
self.applyFunc = "CombatText_UpdateDisplayedMessages" -- by NAME, resolved when applied: this file loads before its definer now (1938)
self.applyFunc = "CombatText_UpdateOrLoad" -- by NAME, resolved when applied: this file loads before its definer now (1938)
</OnLoad></Scripts>
</Frame>
<Frame name="$parentRowAuraFade" inherits="OptionsCheckboxRowTemplate">
@@ -2849,7 +2898,7 @@
OptionsGlobalRow_OnLoad(self, "COMBAT_TEXT_SHOW_AURA_FADE",
"Show Fading Auras", "OPTION_TOOLTIP_COMBAT_TEXT_SHOW_AURA_FADE", 1)
self.parentRow = "RowCombatText"
self.applyFunc = "CombatText_UpdateDisplayedMessages" -- by NAME, resolved when applied: this file loads before its definer now (1938)
self.applyFunc = "CombatText_UpdateOrLoad" -- by NAME, resolved when applied: this file loads before its definer now (1938)
</OnLoad></Scripts>
</Frame>
<Frame name="$parentRowCombatState" inherits="OptionsCheckboxRowTemplate">
@@ -2862,7 +2911,7 @@
"Show Combat State",
"OPTION_TOOLTIP_COMBAT_TEXT_SHOW_COMBAT_STATE", 1)
self.parentRow = "RowCombatText"
self.applyFunc = "CombatText_UpdateDisplayedMessages" -- by NAME, resolved when applied: this file loads before its definer now (1938)
self.applyFunc = "CombatText_UpdateOrLoad" -- by NAME, resolved when applied: this file loads before its definer now (1938)
</OnLoad></Scripts>
</Frame>
<Frame name="$parentRowDodgeParryMiss" inherits="OptionsCheckboxRowTemplate">
@@ -2875,7 +2924,7 @@
"Show Dodge/Parries/Misses",
"OPTION_TOOLTIP_COMBAT_TEXT_SHOW_DODGE_PARRY_MISS", 1)
self.parentRow = "RowCombatText"
self.applyFunc = "CombatText_UpdateDisplayedMessages" -- by NAME, resolved when applied: this file loads before its definer now (1938)
self.applyFunc = "CombatText_UpdateOrLoad" -- by NAME, resolved when applied: this file loads before its definer now (1938)
</OnLoad></Scripts>
</Frame>
<Frame name="$parentRowResistances" inherits="OptionsCheckboxRowTemplate">
@@ -2888,7 +2937,7 @@
"Show Damage Reduction",
"OPTION_TOOLTIP_COMBAT_TEXT_SHOW_RESISTANCES", 1)
self.parentRow = "RowCombatText"
self.applyFunc = "CombatText_UpdateDisplayedMessages" -- by NAME, resolved when applied: this file loads before its definer now (1938)
self.applyFunc = "CombatText_UpdateOrLoad" -- by NAME, resolved when applied: this file loads before its definer now (1938)
</OnLoad></Scripts>
</Frame>
<Frame name="$parentRowReputation" inherits="OptionsCheckboxRowTemplate">
@@ -2901,7 +2950,7 @@
"Show Reputation",
"OPTION_TOOLTIP_COMBAT_TEXT_SHOW_REPUTATION", 1)
self.parentRow = "RowCombatText"
self.applyFunc = "CombatText_UpdateDisplayedMessages" -- by NAME, resolved when applied: this file loads before its definer now (1938)
self.applyFunc = "CombatText_UpdateOrLoad" -- by NAME, resolved when applied: this file loads before its definer now (1938)
</OnLoad></Scripts>
</Frame>
<Frame name="$parentRowReactives" inherits="OptionsCheckboxRowTemplate">
@@ -2914,7 +2963,7 @@
"Show Reactive Spells and Abilities",
"OPTION_TOOLTIP_COMBAT_TEXT_SHOW_REACTIVES", 1)
self.parentRow = "RowCombatText"
self.applyFunc = "CombatText_UpdateDisplayedMessages" -- by NAME, resolved when applied: this file loads before its definer now (1938)
self.applyFunc = "CombatText_UpdateOrLoad" -- by NAME, resolved when applied: this file loads before its definer now (1938)
</OnLoad></Scripts>
</Frame>
<Frame name="$parentRowFriendlyNames" inherits="OptionsCheckboxRowTemplate">
@@ -2927,7 +2976,7 @@
"Show Friendly Healer Names",
"OPTION_TOOLTIP_COMBAT_TEXT_SHOW_FRIENDLY_NAMES", 1)
self.parentRow = "RowCombatText"
self.applyFunc = "CombatText_UpdateDisplayedMessages" -- by NAME, resolved when applied: this file loads before its definer now (1938)
self.applyFunc = "CombatText_UpdateOrLoad" -- by NAME, resolved when applied: this file loads before its definer now (1938)
</OnLoad></Scripts>
</Frame>
<!-- Combo Points carries the reference's SECOND gate: greyed
@@ -2943,7 +2992,7 @@
"Show Combo Points",
"OPTION_TOOLTIP_COMBAT_TEXT_SHOW_COMBO_POINTS", 1)
self.parentRow = "RowCombatText"
self.applyFunc = "CombatText_UpdateDisplayedMessages" -- by NAME, resolved when applied: this file loads before its definer now (1938)
self.applyFunc = "CombatText_UpdateOrLoad" -- by NAME, resolved when applied: this file loads before its definer now (1938)
</OnLoad></Scripts>
</Frame>
<Frame name="$parentRowEnergyGains" inherits="OptionsCheckboxRowTemplate">
@@ -2956,7 +3005,7 @@
"Show Energy Gains",
"OPTION_TOOLTIP_COMBAT_TEXT_SHOW_MANA", 1)
self.parentRow = "RowCombatText"
self.applyFunc = "CombatText_UpdateDisplayedMessages" -- by NAME, resolved when applied: this file loads before its definer now (1938)
self.applyFunc = "CombatText_UpdateOrLoad" -- by NAME, resolved when applied: this file loads before its definer now (1938)
</OnLoad></Scripts>
</Frame>
<Frame name="$parentRowHonorGained" inherits="OptionsCheckboxRowTemplate">
@@ -2969,7 +3018,7 @@
"Show Honor Gained",
"OPTION_TOOLTIP_COMBAT_TEXT_SHOW_HONOR_GAINED", 1)
self.parentRow = "RowCombatText"
self.applyFunc = "CombatText_UpdateDisplayedMessages" -- by NAME, resolved when applied: this file loads before its definer now (1938)
self.applyFunc = "CombatText_UpdateOrLoad" -- by NAME, resolved when applied: this file loads before its definer now (1938)
</OnLoad></Scripts>
</Frame>
</Frames>
-512
View File
@@ -1,512 +0,0 @@
<Ui>
<!--
benilla's player-to-player trade window (decision 0592 P1). The geometry/art/texcoords are
transcribed from the reference `TradeFrame.xml` (WoW/_extracted_framexml/TradeFrame.xml) —
sizes, offsets, anchors, art files, texcoords — adapted only in (a) naming (every `Foo` →
`BenillaFoo`) and (b) feed/intent wiring onto the trade script API
(`benilla-ui/src/script/trade.rs`) + `crate::ui_trade`'s feed/drain. The house dressing
convention (decisions 0084/0112/0120/0544).
Where the reference inherits a shared template benilla does NOT ship (ItemButtonTemplate,
UIPanelButtonTemplate, UIPanelCloseButton, MoneyInputFrameTemplate, SmallMoneyFrameTemplate),
the body is inlined with the reference's own geometry/art (the MailFrame.xml precedent). The
two money displays reuse `MerchantFrame.xml`'s global `BenillaMoney_*` coin trio (read-only in
P1); the reference's editable `MoneyInputFrame` on the player's side is decision 0592 P2.
SCOPE (decision 0592 P1): the window opens on an incoming/initiated trade and mirrors both
sides' items + gold live, the partner's portrait (the shared "npc" booth) + name, and the
accept glow; the Trade/Cancel buttons drive the accept/cancel verbs. DEFERRED to P2: setting
items/gold onto the window (the drag-drop slots + the editable money widget), so the item
slots carry NO OnClick/OnEnter here, and the slot tooltips (GameTooltip:SetTradePlayerItem)
are P2. The enchant-slot spell name (slot 7's "enchantment") is decision 0592 P3.
-->
<Script><![CDATA[
MAX_TRADE_ITEMS = 7 -- MAX_TRADE_ITEMS (ref TradeFrame.lua l.1)
-- One side's accept glow. A column's highlight and the enchant slot's below it are one
-- indicator in two frames — they light and drop together, never apart — so both the repaint
-- and TradeFrame_SetAcceptState go through here rather than each spelling out four calls.
-- The four frames are TradeHighlight{Player,Recipient}[Enchant], declared at the bottom.
local function setTradeGlow(column, enchantSlot, lit)
if lit then
column:Show()
enchantSlot:Show()
else
column:Hide()
enchantSlot:Hide()
end
end
-- ── Load / event dispatch (ref TradeFrame_OnLoad l.4 / TradeFrame_OnEvent l.18) ───────────
function TradeFrame_OnLoad()
local frame = this
frame:RegisterEvent("TRADE_SHOW")
frame:RegisterEvent("TRADE_UPDATE")
frame:RegisterEvent("TRADE_CLOSED")
frame:RegisterEvent("TRADE_PLAYER_ITEM_CHANGED")
frame:RegisterEvent("TRADE_TARGET_ITEM_CHANGED")
frame:RegisterEvent("TRADE_ACCEPT_UPDATE")
-- Our accepted gold, echoed back so the input reflects the server's clamp (ref
-- TradePlayerInputMoneyFrame's own PLAYER_TRADE_MONEY registration, l.565). Kept off the
-- general TRADE_UPDATE repaint so it never overwrites what the player is mid-typing.
frame:RegisterEvent("PLAYER_TRADE_MONEY")
end
function TradeFrame_OnEvent()
local frame = this
if event == "TRADE_SHOW" or event == "TRADE_UPDATE" then
ShowUIPanel(frame)
if not frame:IsVisible() then
CloseTrade()
return
end
TradeFrameTradeButton:Enable()
TradeFrame_Update()
elseif event == "PLAYER_TRADE_MONEY" then
MoneyInputFrame_SetCopper(TradePlayerInputMoney, GetPlayerTradeMoney())
elseif event == "TRADE_CLOSED" then
HideUIPanel(frame)
elseif event == "TRADE_PLAYER_ITEM_CHANGED" then
TradeFrame_UpdatePlayerItem(arg1)
elseif event == "TRADE_TARGET_ITEM_CHANGED" then
TradeFrame_UpdateTargetItem(arg1)
elseif event == "TRADE_ACCEPT_UPDATE" then
TradeFrame_SetAcceptState(arg1, arg2)
end
end
function TradeFrame_OnShow()
TradeFrame.acceptState = 0
end
function TradeFrame_OnHide()
-- The client-side close verb (ref TradeFrame_OnHide l.144) → CMSG_CANCEL_TRADE + a local
-- clear (the app's drain); clear our money entry so a later trade opens fresh (ref l.146).
MoneyInputFrame_ResetMoney(TradePlayerInputMoney)
CloseTrade()
end
-- A keystroke in our money input (ref TradeFrame_UpdateMoney l.162): read the copper total,
-- clamp to what we actually carry, and offer it → SetTradeMoney → CMSG_SET_TRADE_GOLD.
function TradeFrame_UpdateMoney()
local copper = MoneyInputFrame_GetCopper(TradePlayerInputMoney)
if GetMoney and copper > GetMoney() then
copper = GetPlayerTradeMoney()
MoneyInputFrame_SetCopper(TradePlayerInputMoney, copper)
end
SetTradeMoney(copper)
end
-- ── Full repaint (ref TradeFrame_Update l.39): portraits, names, all 7 slots each side, the
-- two money displays, the accept highlights hidden. ────────────────────────────────────
function TradeFrame_Update()
SetPortraitTexture(TradeFramePlayerPortrait, "player")
-- The partner rides the shared "npc" portrait booth (the app points it at the partner
-- entity, decision 0592); the name comes from GetTradePartnerName() in place of the
-- reference's UnitName("NPC").
SetPortraitTexture(TradeFrameRecipientPortrait, "npc")
TradeFramePlayerNameText:SetText(UnitName("player") or "")
TradeFrameRecipientNameText:SetText(GetTradePartnerName() or "")
for i = 1, MAX_TRADE_ITEMS do
TradeFrame_UpdatePlayerItem(i)
TradeFrame_UpdateTargetItem(i)
end
-- Our gold is the editable input, reflected via PLAYER_TRADE_MONEY (not repainted here, so
-- the repaint never clobbers mid-typing); only the partner's read-only gold repaints.
-- The recipient's money frame is type TARGET_TRADE and repaints itself on
-- TRADE_MONEY_CHANGED (MoneyFrame_OnEvent); the reference issues no explicit update
-- here and neither do we (1937).
-- A repaint puts both glows down. TRADE_ACCEPT_UPDATE is the only thing that raises
-- them, so whatever is standing comes straight back on the next one.
setTradeGlow(TradeHighlightPlayer, TradeHighlightPlayerEnchant, false)
setTradeGlow(TradeHighlightRecipient, TradeHighlightRecipientEnchant, false)
end
-- One slot's paint, shared by both columns (ref TradeFrame_UpdatePlayerItem l.54 /
-- UpdateTargetItem l.83). `isTarget` gates the recipient-only not-usable red tint.
function BenillaTradeSlot_SetItem(prefix, id, name, texture, numItems, isUsable, enchantment, isTarget)
local nameText = getglobal(prefix.."Name")
local icon = getglobal(prefix.."ItemButtonIcon")
local count = getglobal(prefix.."ItemButtonCount")
-- The enchant slot (id 7): the enchant name (P3) or the "will not be traded" note.
if id == MAX_TRADE_ITEMS then
if name then
if enchantment then
nameText:SetText("|cff20ff20"..enchantment.."|r")
else
nameText:SetText("|cffffffffWill Not Be Traded|r")
end
else
nameText:SetText("")
end
else
nameText:SetText(name or "")
end
if texture then
icon:SetTexture(texture)
icon:Show()
else
icon:Hide()
end
if numItems and numItems > 1 then
count:SetText(numItems)
count:Show()
else
count:SetText("")
count:Hide()
end
-- The recipient not-usable red tint (ref l.105-113): red when the partner offers an item
-- our class/level/skill can't use; white otherwise (and always white on our own column).
local slotTex = getglobal(prefix.."SlotTexture")
local nameFrame = getglobal(prefix.."NameFrame")
if isTarget and name and not isUsable then
icon:SetVertexColor(0.9, 0, 0)
nameFrame:SetVertexColor(0.9, 0, 0)
slotTex:SetVertexColor(1.0, 0, 0)
else
icon:SetVertexColor(1, 1, 1)
nameFrame:SetVertexColor(1, 1, 1)
slotTex:SetVertexColor(1, 1, 1)
end
end
function TradeFrame_UpdatePlayerItem(id)
local name, texture, numItems, isUsable, enchantment = GetTradePlayerItemInfo(id)
BenillaTradeSlot_SetItem("TradePlayerItem"..id, id, name, texture, numItems, isUsable, enchantment, nil)
end
function TradeFrame_UpdateTargetItem(id)
local name, texture, numItems, quality, isUsable, enchantment = GetTradeTargetItemInfo(id)
BenillaTradeSlot_SetItem("TradeRecipientItem"..id, id, name, texture, numItems, isUsable, enchantment, 1)
end
-- A trade slot's item button as a cursor drop target (decision 0592 P2, ref ClickTradeButton /
-- ClickTargetTradeButton, TradeFrame.xml l.135/101). Both columns share the slot template, so
-- player-vs-recipient is read off the slot frame's name; the slot number is its frame id. Our
-- slot drops/clears a bag item (SET/CLEAR_TRADE_ITEM); the partner's column is read-only.
function BenillaTradeSlot_OnClick(button)
local slot = button:GetParent()
local id = slot:GetID()
if string.find(slot:GetName(), "Player", 1, true) then
ClickTradeButton(id)
else
ClickTargetTradeButton(id)
end
end
-- Who has pressed Trade (spec: ref TradeFrame_SetAcceptState l.116). The event carries BOTH
-- sides' states, so this is a straight repaint of two indicators rather than a toggle — and
-- our own half is kept on the frame, because the Cancel button below is the only reader of
-- whether there is an accept of ours to withdraw.
--
-- The extra thing our own accept does is lock the Trade button, so a standing accept cannot
-- be sent a second time; Cancel is what takes it back.
function TradeFrame_SetAcceptState(playerState, targetState)
TradeFrame.acceptState = playerState
local weAccepted = playerState == 1
setTradeGlow(TradeHighlightPlayer, TradeHighlightPlayerEnchant, weAccepted)
setTradeGlow(TradeHighlightRecipient, TradeHighlightRecipientEnchant, targetState == 1)
if weAccepted then
TradeFrameTradeButton:Disable()
else
TradeFrameTradeButton:Enable()
end
end
-- Cancel is two verbs on one button (spec: ref TradeFrameCancelButton_OnClick l.136). With
-- an accept of ours standing it withdraws only THAT and the trade stays open — the button
-- is the only way back out of the lock above. With no accept up there is nothing to
-- withdraw, so it means "leave", and closing the window is what sends that (→ OnHide →
-- CloseTrade).
function TradeFrameCancelButton_OnClick()
if TradeFrame.acceptState ~= 1 then
HideUIPanel(TradeFrame)
return
end
CancelTradeAccept()
end
]]></Script>
<!-- The accept-glow highlight (ref TradeHighlightTemplate l.4): a 3-slice UI-TradeFrame-Highlight
(top/bottom caps + a stretched middle), sized per-instance. -->
<Frame name="BenillaTradeHighlightTemplate" hidden="false" virtual="true">
<Layers>
<Layer level="BACKGROUND">
<Texture name="$parentTop" file="Interface\TradeFrame\UI-TradeFrame-Highlight">
<Size><AbsDimension x="161" y="16"/></Size>
<Anchors><Anchor point="TOPLEFT"/></Anchors>
<TexCoords left="0" right="0.62890625" top="0" bottom="0.0625"/>
</Texture>
<Texture name="$parentBottom" file="Interface\TradeFrame\UI-TradeFrame-Highlight">
<Size><AbsDimension x="161" y="16"/></Size>
<Anchors><Anchor point="BOTTOMLEFT"/></Anchors>
<TexCoords left="0" right="0.62890625" top="0.9375" bottom="1.0"/>
</Texture>
<Texture name="$parentMiddle" file="Interface\TradeFrame\UI-TradeFrame-Highlight">
<Size><AbsDimension x="161" y="10"/></Size>
<Anchors>
<Anchor point="TOP" relativeTo="$parentTop" relativePoint="BOTTOM"/>
<Anchor point="BOTTOM" relativeTo="$parentBottom" relativePoint="TOP"/>
</Anchors>
<TexCoords left="0" right="0.62890625" top="0.0625" bottom="0.9375"/>
</Texture>
</Layer>
</Layers>
</Frame>
<!-- One trade slot (ref TradeItemTemplate l.38 + PlayerTradeItemTemplate's inlined ItemButton
l.120): the empty-slot art, the name plate, the item name, and a 37px item button (icon +
stack count). Display-only in P1 — no OnClick/OnEnter (decision 0592 P2). -->
<Frame name="BenillaTradeItemTemplate" virtual="true">
<Size><AbsDimension x="153" y="37"/></Size>
<Layers>
<Layer level="BACKGROUND">
<Texture name="$parentSlotTexture" file="Interface\Buttons\UI-EmptySlot">
<Size><AbsDimension x="64" y="64"/></Size>
<Anchors><Anchor point="TOPLEFT"><Offset><AbsDimension x="-13" y="13"/></Offset></Anchor></Anchors>
</Texture>
<Texture name="$parentNameFrame" file="Interface\QuestFrame\UI-QuestItemNameFrame">
<Size><AbsDimension x="124" y="64"/></Size>
<Anchors>
<Anchor point="LEFT" relativeTo="$parentSlotTexture" relativePoint="RIGHT"><Offset><AbsDimension x="-20" y="0"/></Offset></Anchor>
</Anchors>
</Texture>
<FontString name="$parentName" inherits="GameFontNormalSmall" text="" justifyH="LEFT">
<Size><AbsDimension x="90" y="30"/></Size>
<Anchors>
<Anchor point="LEFT" relativeTo="$parentNameFrame"><Offset><AbsDimension x="15" y="0"/></Offset></Anchor>
</Anchors>
</FontString>
</Layer>
</Layers>
<Frames>
<Button name="$parentItemButton">
<Size><AbsDimension x="37" y="37"/></Size>
<Anchors><Anchor point="TOPLEFT" relativeTo="$parent"/></Anchors>
<Layers>
<Layer level="BACKGROUND">
<Texture name="$parentIcon" hidden="true" setAllPoints="true"/>
</Layer>
<Layer level="ARTWORK">
<FontString name="$parentCount" inherits="NumberFontNormal" hidden="true" justifyH="RIGHT">
<Anchors><Anchor point="BOTTOMRIGHT"><Offset><AbsDimension x="-2" y="2"/></Offset></Anchor></Anchors>
</FontString>
</Layer>
</Layers>
<HighlightTexture file="Interface\Buttons\ButtonHilight-Square" alphaMode="ADD"/>
<!-- A cursor drop target (decision 0592 P2): a click or a drag-release routes through
the shared slot handler, which places/clears on our side and is inert on the
partner's. Both gestures share one path, the reference's drag/click contract. -->
<Scripts>
<OnClick>BenillaTradeSlot_OnClick(self)</OnClick>
<OnReceiveDrag>BenillaTradeSlot_OnClick(self)</OnReceiveDrag>
</Scripts>
</Button>
</Frames>
</Frame>
<!-- The enchant / non-traded slot art (ref TradeRecipientItem7/TradePlayerItem7 l.393): the
EnchantIcon overlay sits over slot 7's item button. A virtual twin of the base slot with the
extra icon, instantiated only for id 7. -->
<Frame name="BenillaTradeEnchantItemTemplate" inherits="BenillaTradeItemTemplate" virtual="true">
<Frames>
<Frame name="$parentEnchant" setAllPoints="false">
<Size><AbsDimension x="62" y="62"/></Size>
<Anchors><Anchor point="TOPLEFT"/></Anchors>
<Layers>
<Layer level="OVERLAY">
<Texture file="Interface\TradeFrame\UI-TradeFrame-EnchantIcon" setAllPoints="true"/>
</Layer>
</Layers>
</Frame>
</Frames>
</Frame>
<Frame name="TradeFrame" parent="UIParent" toplevel="true" movable="true" enableMouse="true" hidden="true">
<!-- ref TradeFrame l.164: 384×512; ShowUIPanel slots it left. -->
<Size><AbsDimension x="384" y="512"/></Size>
<HitRectInsets><AbsInset left="0" right="35" top="0" bottom="72"/></HitRectInsets>
<Layers>
<Layer level="BACKGROUND">
<!-- ref l.180: the two 60×60 unit portraits (recipient right, player left). -->
<Texture name="TradeFrameRecipientPortrait">
<Size><AbsDimension x="60" y="60"/></Size>
<Anchors><Anchor point="TOPLEFT"><Offset><AbsDimension x="183" y="-6"/></Offset></Anchor></Anchors>
</Texture>
<Texture name="TradeFramePlayerPortrait">
<Size><AbsDimension x="60" y="60"/></Size>
<Anchors><Anchor point="TOPLEFT"><Offset><AbsDimension x="7" y="-6"/></Offset></Anchor></Anchors>
</Texture>
</Layer>
<Layer level="ARTWORK">
<!-- ref l.205: the four corner slabs. -->
<Texture file="Interface\TradeFrame\UI-TradeFrame-TopLeft">
<Size><AbsDimension x="256" y="256"/></Size>
<Anchors><Anchor point="TOPLEFT"/></Anchors>
</Texture>
<Texture file="Interface\TradeFrame\UI-TradeFrame-TopRight">
<Size><AbsDimension x="128" y="256"/></Size>
<Anchors><Anchor point="TOPRIGHT"/></Anchors>
</Texture>
<Texture file="Interface\TradeFrame\UI-TradeFrame-BotLeft">
<Size><AbsDimension x="256" y="256"/></Size>
<Anchors><Anchor point="BOTTOMLEFT"/></Anchors>
</Texture>
<Texture file="Interface\TradeFrame\UI-TradeFrame-BotRight">
<Size><AbsDimension x="128" y="256"/></Size>
<Anchors><Anchor point="BOTTOMRIGHT"/></Anchors>
</Texture>
<FontString name="TradeFrameRecipientNameText" inherits="GameFontNormal" text="">
<Size><AbsDimension x="80" y="12"/></Size>
<Anchors><Anchor point="TOPLEFT" relativeTo="TradeFrame"><Offset><AbsDimension x="245" y="-17"/></Offset></Anchor></Anchors>
</FontString>
<FontString name="TradeFramePlayerNameText" inherits="GameFontNormal" text="">
<Size><AbsDimension x="100" y="12"/></Size>
<Anchors><Anchor point="TOPLEFT" relativeTo="TradeFrame"><Offset><AbsDimension x="75" y="-17"/></Offset></Anchor></Anchors>
</FontString>
<!-- ref l.262: the "Will Be Enchanted" column labels above the enchant slots. -->
<FontString name="TradeFramePlayerEnchantText" inherits="GameFontHighlightSmall" text="Will Be Enchanted">
<Anchors><Anchor point="TOPLEFT" relativeTo="TradeFrame"><Offset><AbsDimension x="26" y="-374"/></Offset></Anchor></Anchors>
</FontString>
<FontString name="TradeFrameRecipientEnchantText" inherits="GameFontHighlightSmall" text="Will Be Enchanted">
<Anchors><Anchor point="LEFT" relativeTo="TradeFramePlayerEnchantText"><Offset><AbsDimension x="170" y="0"/></Offset></Anchor></Anchors>
</FontString>
</Layer>
</Layers>
<Frames>
<!-- The accept-glow highlights, one per column + its enchant slot (ref l.283). -->
<Frame name="TradeHighlightPlayer" inherits="BenillaTradeHighlightTemplate" hidden="true">
<Size><AbsDimension x="161" y="266"/></Size>
<Anchors><Anchor point="TOPLEFT" relativeTo="TradeFrame"><Offset><AbsDimension x="19" y="-100"/></Offset></Anchor></Anchors>
</Frame>
<Frame name="TradeHighlightRecipient" inherits="BenillaTradeHighlightTemplate" hidden="true">
<Size><AbsDimension x="161" y="266"/></Size>
<Anchors><Anchor point="TOPLEFT"><Offset><AbsDimension x="189" y="-100"/></Offset></Anchor></Anchors>
</Frame>
<Frame name="TradeHighlightPlayerEnchant" inherits="BenillaTradeHighlightTemplate" hidden="true">
<Size><AbsDimension x="161" y="61"/></Size>
<Anchors><Anchor point="TOPLEFT" relativeTo="TradeHighlightPlayer" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-4"/></Offset></Anchor></Anchors>
</Frame>
<Frame name="TradeHighlightRecipientEnchant" inherits="BenillaTradeHighlightTemplate" hidden="true">
<Size><AbsDimension x="161" y="61"/></Size>
<Anchors><Anchor point="TOPLEFT" relativeTo="TradeHighlightRecipient" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-4"/></Offset></Anchor></Anchors>
</Frame>
<!-- The partner's 6 trade slots + the 7th enchant slot (ref TradeRecipientItem1..7 l.331). -->
<Frame name="TradeRecipientItem1" inherits="BenillaTradeItemTemplate" id="1">
<Anchors><Anchor point="TOPLEFT"><Offset><AbsDimension x="195" y="-104"/></Offset></Anchor></Anchors>
</Frame>
<Frame name="TradeRecipientItem2" inherits="BenillaTradeItemTemplate" id="2">
<Anchors><Anchor point="TOPLEFT" relativeTo="TradeRecipientItem1" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-7"/></Offset></Anchor></Anchors>
</Frame>
<Frame name="TradeRecipientItem3" inherits="BenillaTradeItemTemplate" id="3">
<Anchors><Anchor point="TOPLEFT" relativeTo="TradeRecipientItem2" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-7"/></Offset></Anchor></Anchors>
</Frame>
<Frame name="TradeRecipientItem4" inherits="BenillaTradeItemTemplate" id="4">
<Anchors><Anchor point="TOPLEFT" relativeTo="TradeRecipientItem3" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-7"/></Offset></Anchor></Anchors>
</Frame>
<Frame name="TradeRecipientItem5" inherits="BenillaTradeItemTemplate" id="5">
<Anchors><Anchor point="TOPLEFT" relativeTo="TradeRecipientItem4" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-7"/></Offset></Anchor></Anchors>
</Frame>
<Frame name="TradeRecipientItem6" inherits="BenillaTradeItemTemplate" id="6">
<Anchors><Anchor point="TOPLEFT" relativeTo="TradeRecipientItem5" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-7"/></Offset></Anchor></Anchors>
</Frame>
<Frame name="TradeRecipientItem7" inherits="BenillaTradeEnchantItemTemplate" id="7">
<Anchors><Anchor point="TOPLEFT" relativeTo="TradeRecipientItem6" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-28"/></Offset></Anchor></Anchors>
</Frame>
<!-- Our 6 trade slots + the 7th enchant slot (ref TradePlayerItem1..7 l.410). -->
<Frame name="TradePlayerItem1" inherits="BenillaTradeItemTemplate" id="1">
<Anchors><Anchor point="TOPLEFT"><Offset><AbsDimension x="26" y="-104"/></Offset></Anchor></Anchors>
</Frame>
<Frame name="TradePlayerItem2" inherits="BenillaTradeItemTemplate" id="2">
<Anchors><Anchor point="TOPLEFT" relativeTo="TradePlayerItem1" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-7"/></Offset></Anchor></Anchors>
</Frame>
<Frame name="TradePlayerItem3" inherits="BenillaTradeItemTemplate" id="3">
<Anchors><Anchor point="TOPLEFT" relativeTo="TradePlayerItem2" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-7"/></Offset></Anchor></Anchors>
</Frame>
<Frame name="TradePlayerItem4" inherits="BenillaTradeItemTemplate" id="4">
<Anchors><Anchor point="TOPLEFT" relativeTo="TradePlayerItem3" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-7"/></Offset></Anchor></Anchors>
</Frame>
<Frame name="TradePlayerItem5" inherits="BenillaTradeItemTemplate" id="5">
<Anchors><Anchor point="TOPLEFT" relativeTo="TradePlayerItem4" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-7"/></Offset></Anchor></Anchors>
</Frame>
<Frame name="TradePlayerItem6" inherits="BenillaTradeItemTemplate" id="6">
<Anchors><Anchor point="TOPLEFT" relativeTo="TradePlayerItem5" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-7"/></Offset></Anchor></Anchors>
</Frame>
<Frame name="TradePlayerItem7" inherits="BenillaTradeEnchantItemTemplate" id="7">
<Anchors><Anchor point="TOPLEFT" relativeTo="TradePlayerItem6" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-28"/></Offset></Anchor></Anchors>
</Frame>
<!-- The Trade button (ref l.502, UIPanelButtonTemplate inlined): OnClick → AcceptTrade. -->
<Button name="TradeFrameTradeButton" text="Trade">
<Size><AbsDimension x="85" y="22"/></Size>
<Anchors><Anchor point="BOTTOMRIGHT" relativeTo="TradeFrame" relativePoint="BOTTOMRIGHT"><Offset><AbsDimension x="-113" y="55"/></Offset></Anchor></Anchors>
<NormalTexture file="Interface\Buttons\UI-Panel-Button-Up"><TexCoords left="0" right="0.625" top="0" bottom="0.6875"/></NormalTexture>
<PushedTexture file="Interface\Buttons\UI-Panel-Button-Down"><TexCoords left="0" right="0.625" top="0" bottom="0.6875"/></PushedTexture>
<DisabledTexture file="Interface\Buttons\UI-Panel-Button-Disabled"><TexCoords left="0" right="0.625" top="0" bottom="0.6875"/></DisabledTexture>
<HighlightTexture file="Interface\Buttons\UI-Panel-Button-Highlight" alphaMode="ADD"><TexCoords left="0" right="0.625" top="0" bottom="0.6875"/></HighlightTexture>
<ButtonText name="$parentText"/>
<NormalFont inherits="GameFontNormal"/><HighlightFont inherits="GameFontHighlight"/><DisabledFont inherits="GameFontDisable"/>
<Scripts><OnClick>AcceptTrade()</OnClick></Scripts>
</Button>
<Button name="TradeFrameCancelButton" text="Cancel">
<Size><AbsDimension x="77" y="22"/></Size>
<Anchors><Anchor point="TOPLEFT" relativeTo="TradeFrameTradeButton" relativePoint="TOPRIGHT"><Offset><AbsDimension x="3" y="0"/></Offset></Anchor></Anchors>
<NormalTexture file="Interface\Buttons\UI-Panel-Button-Up"><TexCoords left="0" right="0.625" top="0" bottom="0.6875"/></NormalTexture>
<PushedTexture file="Interface\Buttons\UI-Panel-Button-Down"><TexCoords left="0" right="0.625" top="0" bottom="0.6875"/></PushedTexture>
<HighlightTexture file="Interface\Buttons\UI-Panel-Button-Highlight" alphaMode="ADD"><TexCoords left="0" right="0.625" top="0" bottom="0.6875"/></HighlightTexture>
<ButtonText name="$parentText"/>
<NormalFont inherits="GameFontNormal"/><HighlightFont inherits="GameFontHighlight"/><DisabledFont inherits="GameFontDisable"/>
<Scripts><OnClick>TradeFrameCancelButton_OnClick()</OnClick></Scripts>
</Button>
<!-- The window close X (ref l.544, UIPanelCloseButton inlined). -->
<Button name="TradeFrameCloseButton">
<Size><AbsDimension x="32" y="32"/></Size>
<Anchors><Anchor point="TOPRIGHT" relativeTo="TradeFrame" relativePoint="TOPRIGHT"><Offset><AbsDimension x="-25" y="-8"/></Offset></Anchor></Anchors>
<NormalTexture file="Interface\Buttons\UI-Panel-MinimizeButton-Up"/>
<PushedTexture file="Interface\Buttons\UI-Panel-MinimizeButton-Down"/>
<HighlightTexture file="Interface\Buttons\UI-Panel-MinimizeButton-Highlight" alphaMode="ADD"/>
<Scripts><OnClick>HideUIPanel(TradeFrame)</OnClick></Scripts>
</Button>
<!-- Our gold — EDITABLE (ref TradeFrame.xml l.553 `TradePlayerInputMoneyFrame`). The three
boxes come off the chain now: `MoneyInputFrameTemplate` out of
`Interface\FrameXML\MoneyInputFrame.xml`, which benilla.toc has sourced since 1751
window 23. Our inline trio was a verbatim copy of it — same child names, same
textures and TexCoords, same tab chain, same 58/30/30 geometry — so this is the
rare swap that changes no pixel by itself (1882).
`SetMode("compact")` is the reference's own call on this frame and DOES move
things: Gold 58->56 wide, Silver's offset 26->22, Copper's 16->11. We did not have
it, so the trade money boxes tighten by a few pixels. That is the migration
restoring the reference's layout, not a new choice.
A keystroke drives TradeFrame_UpdateMoney -> SetTradeMoney -> CMSG_SET_TRADE_GOLD,
registered through the reference's own callback slot rather than a hand-written
OnTextChanged on each box; the server's echo comes back on PLAYER_TRADE_MONEY. -->
<Frame name="TradePlayerInputMoney" inherits="MoneyInputFrameTemplate">
<Size><AbsDimension x="176" y="18"/></Size>
<Anchors><Anchor point="TOPLEFT"><Offset><AbsDimension x="26" y="-73"/></Offset></Anchor></Anchors>
<Scripts>
<OnLoad>
MoneyInputFrame_SetMode(this, "compact")
MoneyInputFrame_SetOnvalueChangedFunc(this, TradeFrame_UpdateMoney)
</OnLoad>
</Scripts>
</Frame>
<!-- The partner's gold (read-only — GetTargetTradeMoney; ref TradeRecipientMoneyFrame l.574). -->
<!-- ref TradeFrame.xml:574-588: a SmallMoneyFrameTemplate of type TARGET_TRADE, which repaints
itself on TRADE_MONEY_CHANGED through MoneyFrame_OnEvent — no explicit update anywhere in
the reference's Lua, and none here now. Same TOPRIGHT (-40,-80) seat as before (1937). -->
<Frame name="TradeRecipientMoneyFrame" inherits="SmallMoneyFrameTemplate">
<Anchors><Anchor point="TOPRIGHT"><Offset><AbsDimension x="-40" y="-80"/></Offset></Anchor></Anchors>
<Scripts><OnLoad>SmallMoneyFrame_OnLoad(); MoneyFrame_SetType("TARGET_TRADE");</OnLoad></Scripts>
</Frame>
</Frames>
<Scripts>
<OnLoad>TradeFrame_OnLoad()</OnLoad>
<OnEvent>TradeFrame_OnEvent()</OnEvent>
<OnShow>TradeFrame_OnShow(); PlaySound("igCharacterInfoOpen")</OnShow>
<OnHide>TradeFrame_OnHide(); PlaySound("igCharacterInfoClose")</OnHide>
</Scripts>
</Frame>
</Ui>
+30
View File
@@ -75,6 +75,12 @@
this:RegisterEvent("CONFIRM_TALENT_WIPE")
this:RegisterEvent("CONFIRM_BINDER")
this:RegisterEvent("CONFIRM_SUMMON")
-- The dialog engine's verbs (1963): the feeds behind these exist now.
this:RegisterEvent("INSTANCE_BOOT_START")
this:RegisterEvent("INSTANCE_BOOT_STOP")
this:RegisterEvent("CONFIRM_PET_UNLEARN")
this:RegisterEvent("AREA_SPIRIT_HEALER_IN_RANGE")
this:RegisterEvent("AREA_SPIRIT_HEALER_OUT_OF_RANGE")
</OnLoad>
<OnEvent>UIParent_OnEvent(event)</OnEvent>
<!-- The reference's own UIParent OnUpdate line (its UIParent.xml l.19): the floating
@@ -685,6 +691,30 @@
StaticPopup_Show("CONFIRM_SUMMON")
return
end
-- ref UIParent.lua l.429-436 and l.525-546, verbatim (1963).
if ( event == "AREA_SPIRIT_HEALER_IN_RANGE" ) then
StaticPopup_Show("AREA_SPIRIT_HEAL")
return
end
if ( event == "AREA_SPIRIT_HEALER_OUT_OF_RANGE" ) then
StaticPopup_Hide("AREA_SPIRIT_HEAL")
return
end
if ( event == "INSTANCE_BOOT_START" ) then
StaticPopup_Show("INSTANCE_BOOT")
return
end
if ( event == "INSTANCE_BOOT_STOP" ) then
StaticPopup_Hide("INSTANCE_BOOT")
return
end
if ( event == "CONFIRM_PET_UNLEARN" ) then
local dialog = StaticPopup_Show("CONFIRM_PET_UNLEARN")
if ( dialog ) then
MoneyFrame_Update(dialog:GetName().."MoneyFrame", arg1)
end
return
end
-- The ready-check popup's opener (decision 1549), ref UIParent.lua l.570-572. The
-- event is the engine's own `0x218`, fired by the `MSG_RAID_READY_CHECK` open
-- handler; the frame and `ShowReadyCheck` live in RaidFrame.xml, so this is the one
+26 -20
View File
@@ -128,17 +128,15 @@ UIParent.xml
# `$parentNormalTexture`, 37x37) — which is what makes this the safest of the foundational swaps
# rather than the widest: everything that inherits it asks for the same four names.
Interface\FrameXML\ItemButtonTemplate.xml
# The reference's money-frame kit: MoneyFrameTemplate/SmallMoneyFrameTemplate + MoneyTypeInfo
# + the MoneyFrame_* API. Needs only Fonts (NumberFontNormal/NumberFontNormalLarge) — and it must
# come BEFORE UiPanels.xml, because the shared StaticPopup's coin row is a child frame declaring
# `inherits="SmallMoneyFrameTemplate"`, which is resolved at LOAD (decision 1580). That is the
# reference's own order too (FrameXML.toc: MoneyFrame.xml l.10, StaticPopup.xml l.23); this file
# used to sit 140 lines lower, beside the other shared widget kits, which was only ever tenable
# while nothing above it showed coins. AuctionFrame far below is laid out on it as well (its price
# columns need a money TYPE, which the other kit has no notion of); our other eight coin displays
# still run on the BenillaMoney_* slot kit (which now lives HERE), and this file's header is where
# the two are reconciled.
MoneyFrame.xml
# ══ THE MONEY KIT — the reference's own, off the player's chain (1751; decision 1962) ══
# MoneyFrame.lua + MoneyFrame.xml (two rows: the .xml sources no .lua): MoneyTypeInfo, the
# MoneyFrame_* API, MoneyFrameTemplate / SmallMoneyFrameTemplate. Needs only Fonts, and must come
# BEFORE the dialog engine (StaticPopup's coin row inherits SmallMoneyFrameTemplate at LOAD), which
# is the reference's own order too (FrameXML.toc l.9-10 against l.23). The money CURSOR family the
# kit's PickupFunc/DropFunc rows drive (PickupPlayerMoney, DropCursorMoney, GetCursorMoney, the
# trade and send-mail pairs) is this engine's now. 1190's transcription is gone.
Interface\FrameXML\MoneyFrame.lua
Interface\FrameXML\MoneyFrame.xml
# ══ THE MONEY INPUT KIT — the reference's own, off the player's chain (1751, window 23) ══
# `MoneyInputFrameTemplate` and the ten `MoneyInputFrame_*` verbs out of `MoneyInputFrame.lua` —
# the three-box gold/silver/copper editor a player types an amount into. Nothing of ours ever
@@ -312,10 +310,10 @@ Interface\FrameXML\PetFrame.xml
# below TargetFrame.xml. Its fade chain needs UiPanels.xml's `UIFrameFade` family, far above, at
# call time. `MAX_COMBO_POINTS` comes from TargetFrame.lua now, which is where 1.12 declares it.
Interface\FrameXML\ComboFrame.xml
# The center-screen scrolling combat text (decision 0578) — the Blizzard_CombatText
# transcription; consumes COMBAT_TEXT_UPDATE + the regen/health/power events, needs
# only Fonts.xml (CombatTextFont) + UIParent before it.
CombatText.xml
# The scrolling combat text is the reference's own `Blizzard_CombatText`, a LoadOnDemand addon
# off the chain (its toc says so), loaded by the options window when SHOW_COMBAT_TEXT is "1" —
# at login and on the switch, the reference's own two sites. No row here (decision 1964; 0578's
# transcription is gone).
# ── MEASUREMENT (1938): the options window at the reference's slot (FrameXML.toc l.21, before
# MainMenuBar l.34) with the four rows it inherits from. ──
# The faux-scroll kit (decisions 0247/0250/0251): BenillaScrollBarTemplate (the draggable
@@ -358,6 +356,10 @@ Interface\FrameXML\UIPanelTemplates.xml
# the 37 dialogs UiPanels.xml carried and the seven free-standing dialog files (Death, Duel,
# QuestShare, EnchantConfirm, BinderConfirm, ConfirmSummon, TalentWipeConfirm) are gone.
Interface\FrameXML\StaticPopup.xml
# The coin pickup dialog (the reference's toc l.24, right here): shift-click a purse coin and pick
# how many. Pure gain with the money kit — nothing of ours ever answered `OpenCoinPickupFrame`
# (decision 1962).
Interface\FrameXML\CoinPickupFrame.xml
# The Keybindings page MODULE (decision 1008, superseding 0997's standalone window):
# the templates + script of the Options window's Keybindings category, over the
@@ -859,11 +861,15 @@ Interface\FrameXML\MerchantFrame.xml
# beside MerchantFrame in the NPC-window cluster; its postage/enclosed/COD coin displays use
# the BenillaMoney_* kit, which is defined in MoneyFrame.xml far above.
MailFrame.xml
# The player-to-player trade window (decision 0592 P1): the two-sided offer over the trade
# API (`benilla-ui/src/script/trade.rs`) + `crate::ui_trade`'s feed/drain. Kept beside
# MerchantFrame in the NPC-window cluster; its two gold displays use the BenillaMoney_* kit,
# defined in MoneyFrame.xml far above.
TradeFrame.xml
# ══ THE TRADE WINDOW — the reference's own, off the player's chain (1751; decision 1966) ══
# TradeFrame.xml + TradeFrame.lua: both columns' seven slots over the trade API
# (`benilla-ui/src/script/trade.rs`) + `crate::ui_trade`'s feed/drain, the recipient's
# SmallMoneyFrame (TARGET_TRADE) and the player's MoneyInputFrame, the accept glows. Its slot
# tooltips are this engine's SetTradePlayerItem / SetTradeTargetItem; its per-slot events
# (TRADE_PLAYER_ITEM_CHANGED / TRADE_TARGET_ITEM_CHANGED) the feed's. Loaded here, after
# ItemButtonTemplate, MoneyInputFrame, the money kit and the dialog engine it inherits from.
# 0592's transcription is gone.
Interface\FrameXML\TradeFrame.xml
# ══ THE ITEM-TEXT READER — the reference's own, off the player's chain (1751, window 9) ══
# `ItemTextFrame` and its page chrome, over the `ItemTextGet*` seam
# (`benilla-ui/src/script/item_text.rs`) and `crate::ui_item_text`'s feed/drain. Letters (mail-made
+4
View File
@@ -45,6 +45,10 @@ fn payload_icon(script: &benilla_ui::script::UiScript) -> Option<String> {
// Mode 10 — the stabled pet's family icon (decision 1677). Always present: a non-empty
// icon path is the grab's own gate.
CursorPayload::StablePet(p) => Some(p.texture),
// Mode 2 (1965) — the coin bitmap by magnitude, `GetCoinIcon`'s own table.
CursorPayload::Money(m) => {
Some(benilla_ui::script::coin_icon(i64::from(m.copper)).to_string())
}
// Mode 5 — the vendor row's icon. The reference stores the row's `ItemDisplayInfo` id
// (`0xb4d8ec`) and resolves the art from it; we carry the resolved path, so this is the
// same picture one hop later.
+5
View File
@@ -126,6 +126,7 @@ mod ui_cast;
mod ui_char;
mod ui_chat;
mod ui_craft;
mod ui_dialog_verbs;
mod ui_dressup;
mod ui_duel;
mod ui_follow;
@@ -213,6 +214,7 @@ use ui_cast::UiCastPlugin;
use ui_char::UiCharPlugin;
use ui_chat::UiChatPlugin;
use ui_craft::UiCraftPlugin;
use ui_dialog_verbs::UiDialogVerbsPlugin;
use ui_duel::UiDuelPlugin;
use ui_follow::UiFollowPlugin;
use ui_gm_ticket::UiGmTicketPlugin;
@@ -636,6 +638,9 @@ pub fn run(build: BuildId) -> AppExit {
// CONFIRM_BINDER dialog it raises, and the CMSG_BINDER_ACTIVATE its Accept sends — the only
// packet in the flow that actually binds anything.
.add_plugins(UiBinderPlugin)
// The dialog engine's own verbs (decision 1963): the pet trainer's question, the instance
// boot clock, the area spirit healer, the battleground queue, the meeting stone.
.add_plugins(UiDialogVerbsPlugin)
// Being summoned (decision 1747): SMSG_SUMMON_REQUEST's latch, the CONFIRM_SUMMON dialog it
// raises, and the CMSG_SUMMON_RESPONSE its Accept sends. The binder's twin one line up — a
// server-asked question whose only wire answer is yes — and here for that reason.
+26
View File
@@ -1260,6 +1260,26 @@ pub(crate) enum ClientCommand {
TalentWipeConfirm {
trainer: u64,
},
// ── The dialog engine's verbs (decision 1963) ──
/// `ForceLogout()` — `CMSG_PLAYER_LOGOUT` (`0x4A`, empty), the forced flavour of the logout
/// dispatcher; sent only with a live world session.
ForceLogout,
/// `AcceptAreaSpiritHeal()` — `CMSG_AREA_SPIRIT_HEALER_QUEUE` with the cached healer.
AreaSpiritHealerQueue {
healer: u64,
},
/// `AcceptBattlefieldPort(index, accept)` — `CMSG_BATTLEFIELD_PORT`: the slot's map id and
/// the answer as one byte.
BattlefieldPort {
map_id: u32,
accept: bool,
},
/// `CancelMeetingStoneRequest()` — `CMSG 0x293`, empty.
MeetingStoneLeave,
/// `ConfirmPetUnlearn()` — `CMSG_PET_UNLEARN` with the latched trainer guid.
PetUnlearn {
trainer: u64,
},
/// Open the bank (`CMSG_BANKER_ACTIVATE`, decision 0604): the direct opener a right-click on
/// a pure banker (bit 8 the lowest service bit) uses — a gossip-flagged banker routes through
/// the gossip menu instead, whose bank option makes the server volunteer the same answer.
@@ -1998,6 +2018,12 @@ pub(crate) enum ClientCommand {
AddFriend {
name: String,
},
/// The client's LFG slots and comment (`CMSG_SET_LOOKING_FOR_GROUP`), sent by
/// `SetLookingForGroup` only when the commit changed something (1961).
SetLookingForGroup {
slots: [u32; 3],
comment: String,
},
/// Drop a friend by guid (`CMSG_DEL_FRIEND`) — the caller resolves the name first.
DelFriend {
guid: u64,
+47 -2
View File
@@ -246,7 +246,20 @@ pub(super) fn apply_net_updates(
// `MSG_TALENT_WIPE_CONFIRM` parks the trainer's guid + cost here and
// `crate::ui_talent_wipe` turns it into the CONFIRM_TALENT_WIPE dialog, whose
// Accept is the only thing that unlearns anything. The binder's twin above.
ResMut<crate::ui_talent_wipe::TalentWipeState>,
(
ResMut<crate::ui_talent_wipe::TalentWipeState>,
// The dialog engine's verbs (decision 1963): the pet trainer's latch, the
// instance-boot clock, the area spirit healer, the battleground queue and
// the meeting-stone queue — each a feed for a stock dialog. Nested beside
// the talent wipe because this tuple sits at Bevy's sixteen-entry limit.
(
ResMut<crate::ui_dialog_verbs::PetUnlearnState>,
ResMut<crate::ui_dialog_verbs::InstanceBoot>,
ResMut<crate::ui_dialog_verbs::AreaSpiritHealer>,
ResMut<crate::ui_dialog_verbs::BattlefieldQueue>,
ResMut<crate::ui_dialog_verbs::MeetingStone>,
),
),
// The guard's directions marker (`SMSG_GOSSIP_POI`) and the map id it has to be
// stamped with — the wire carries no map field, so "where you were standing when
// the guard told you" is the client's to remember (`crate::poi_marker`).
@@ -460,7 +473,16 @@ pub(super) fn apply_net_updates(
mut played_time_answer,
mut guild,
mut binder,
mut talent_wipe,
(
mut talent_wipe,
(
mut pet_unlearn,
mut instance_boot,
mut area_spirit,
mut battlefield_queue,
mut meeting_stone,
),
),
mut poi_marker,
current_map,
mut inspect_honor,
@@ -917,6 +939,29 @@ pub(super) fn apply_net_updates(
talent_wipe.ask(trainer, cost);
}
}
// The pet trainer's question (decision 1963) — the talent-wipe twin above; a zero
// guid is the reference's own `ERR_TALENT_WIPE_ERROR` leg, carried over as observed.
SessionEvent::PetUnlearnConfirm { trainer, cost } => {
if trainer == 0 {
debug!("net: pet unlearn refused (zero trainer) — no dialog");
ui_error_keys
.0
.push(crate::ui_action::UiError::key("ERR_TALENT_WIPE_ERROR"));
} else {
debug!("net: trainer {trainer:#x} asks to unlearn the pet for {cost} copper");
pet_unlearn.ask(trainer, cost);
}
}
SessionEvent::RaidGroupOnly { delay_ms, reason } => {
instance_boot.apply(delay_ms, reason, std::time::Instant::now());
}
SessionEvent::AreaSpiritHealerTime { healer, ms } => {
area_spirit.on_time(healer, ms, std::time::Instant::now());
}
SessionEvent::BattlefieldStatus(status) => battlefield_queue.apply(status),
SessionEvent::MeetingStoneSetQueue { area, status } => {
meeting_stone.apply(area, status);
}
SessionEvent::PlayerBound { binder: npc, area } => {
debug!("net: bound to area {area} by {npc:#x}");
crate::ui_binder::apply::bound(
+12
View File
@@ -1064,6 +1064,15 @@ fn writer_loop(
ClientCommand::BinderActivate { binder } => w.binder_activate(binder),
ClientCommand::SummonResponse { summoner } => w.summon_response(summoner),
ClientCommand::TalentWipeConfirm { trainer } => w.talent_wipe_confirm(trainer),
ClientCommand::ForceLogout => w.player_logout(),
ClientCommand::AreaSpiritHealerQueue { healer } => {
w.area_spirit_healer_queue(healer)
}
ClientCommand::BattlefieldPort { map_id, accept } => {
w.battlefield_port(map_id, accept)
}
ClientCommand::MeetingStoneLeave => w.meeting_stone_leave(),
ClientCommand::PetUnlearn { trainer } => w.pet_unlearn(trainer),
ClientCommand::BankerActivate { guid } => w.banker_activate(guid),
ClientCommand::BuyBankSlot { guid } => w.buy_bank_slot(guid),
ClientCommand::AutoBankItem { bag, slot } => w.autobank_item(bag, slot),
@@ -1326,6 +1335,9 @@ fn writer_loop(
ClientCommand::ToggleCloak => w.toggle_cloak(),
ClientCommand::FriendListRequest => w.friend_list(),
ClientCommand::AddFriend { name } => w.add_friend(&name),
ClientCommand::SetLookingForGroup { slots, comment } => {
w.set_looking_for_group(slots, &comment)
}
ClientCommand::DelFriend { guid } => w.del_friend(guid),
ClientCommand::AddIgnore { name } => w.add_ignore(&name),
ClientCommand::DelIgnore { guid } => w.del_ignore(guid),
+11 -4
View File
@@ -128,6 +128,8 @@ fn load_item_sounds(mut commands: Commands, assets: Option<Res<WorldAssets>>) {
/// payload transition (decision 0216) — an Item transition always plays its own per-item kit
/// instead, never this pair.
const INTERFACESOUND_CURSORGRABOBJECT: u32 = 902;
/// `LOOTWINDOWCOINSOUND` — SoundEntries kit 895, the coin clink the money pickup names (1962).
const LOOTWINDOWCOINSOUND: u32 = 895;
const INTERFACESOUND_CURSORDROPOBJECT: u32 = 903;
/// Which half of a gesture pair a transition plays: the payload landing on the cursor (`Gain`,
@@ -224,10 +226,15 @@ fn play_item_gesture_sounds(
| CursorPayload::PetAction(_)
// Mode 10 joins them: the stabled-pet grab `0x495010` calls the same generic path and
// names no per-item kit (decision 1677).
| CursorPayload::StablePet(_) => {
let kit_id = match gesture {
CursorGesture::Gain => INTERFACESOUND_CURSORGRABOBJECT,
CursorGesture::Loss => INTERFACESOUND_CURSORDROPOBJECT,
| CursorPayload::StablePet(_)
// Mode 2 (1962, 1965): the money pickup AND drop both play `LOOTWINDOWCOINSOUND` — the
// same kit 895 the purse plays on a change (`sound/money.rs`) — and never the generic drop
// kit (wow-re `money-cursor-law.md` §2).
| CursorPayload::Money(_) => {
let kit_id = match (&payload, gesture) {
(CursorPayload::Money(_), _) => LOOTWINDOWCOINSOUND,
(_, CursorGesture::Gain) => INTERFACESOUND_CURSORGRABOBJECT,
(_, CursorGesture::Loss) => INTERFACESOUND_CURSORDROPOBJECT,
};
if let Err(e) = kit::play_kit(
&mut kits,
+2 -1
View File
@@ -439,7 +439,8 @@ fn chat_vm() -> benilla_ui::script::UiScript {
// dependency — its MenuBackdrop reads `TOOLTIP_DEFAULT_COLOR`.)
for file in [
"Interface\\FrameXML\\Fonts.xml",
"MoneyFrame.xml",
r"Interface\FrameXML\MoneyFrame.lua",
r"Interface\FrameXML\MoneyFrame.xml",
"GameTooltip.xml",
"Interface\\FrameXML\\UIDropDownMenu.xml",
// The UIMenu kit is the reference's own file since 1751 window 21, so this reads both
+443
View File
@@ -0,0 +1,443 @@
//! The dialog engine's verbs, app half (decision 1963): the feeds behind the stock
//! `StaticPopup.lua` dialogs benilla never raised and the drains behind their buttons, each to
//! wow-re's `staticpopup-dialog-bindings.md` (VERIFIED at the bytes unless a line says INFERRED).
//!
//! * **Pet trainer** — `SMSG_PET_UNLEARN_CONFIRM {guid, cost}` latches both and owes
//! `CONFIRM_PET_UNLEARN(cost)`; `ConfirmPetUnlearn()` answers with `CMSG_PET_UNLEARN {guid}` unless
//! the cost outruns the purse (`ERR_NOT_ENOUGH_MONEY`, nothing sent). The talent-wipe twin
//! (`crate::ui_talent_wipe`), latch for latch, leash for leash: the trainer walking out of
//! `INTERACT_DISTANCE` closes the question, which is what `CheckPetUntrainerDist()` polls.
//! * **Instance boot** — every `SMSG_RAID_GROUP_ONLY {delayMs, reason}` fires an event: a positive
//! delay arms the deadline and `INSTANCE_BOOT_START`, zero clears it and `INSTANCE_BOOT_STOP`,
//! and only the zero leg names reason 1/2 on screen. `GetInstanceBootTimeRemaining()` reads
//! whole seconds off the deadline; nothing clears it but a zero packet.
//! * **Area spirit healer** — `SMSG_AREA_SPIRIT_HEALER_TIME {guid, ms}` arms the wave clock and
//! fires `AREA_SPIRIT_HEALER_IN_RANGE` when the guid is the cached healer's; Accept sends
//! `0x2E3` with that guid, Cancel is the cancel-aura of spell 2584 plus `_OUT_OF_RANGE`. **The
//! cache has no writer yet**: the reference's per-frame proximity scan (20 yd to acquire, 22 to
//! retain, `0x4923b0`) is not carved for its unit filter, so no healer is ever cached here and
//! Accept stays silent — named in the record, not guessed at.
//! * **Battlefield queue** — `SMSG_BATTLEFIELD_STATUS` fills one of three slots and fires
//! `UPDATE_BATTLEFIELD_STATUS`; `AcceptBattlefieldPort(index, accept)` sends the slot's map id
//! with the answer as one byte.
//! * **Meeting stone** — `SMSG 0x295 {areaId, status}` stores the queued area and fires
//! `MEETINGSTONE_CHANGED`; `CancelMeetingStoneRequest()` sends `0x293` unless in a party led by
//! someone else (`ERR_MEETING_STONE_NOT_LEADER`). The five status messages the reference prints
//! are not built: the carve lists the five strings but not which status byte picks which.
use std::time::Instant;
use benilla_protocol::messages::BattlefieldStatus;
use benilla_ui::script::{ScriptValue, UiScript};
use bevy::prelude::*;
use crate::net::{ClientCommand, NetCommands, ObjectStore, SelfGuid, SelfPlayer};
use crate::ui_party::GroupState;
use crate::ui_script::UiInput;
use crate::ui_session::{close_npc_session_out_of_range, NpcSession};
/// The pet trainer's pending question: the latch (`0xc4d7b0/b4`) and the cost (`0xc4d7b8`).
#[derive(Resource, Default)]
pub(crate) struct PetUnlearnState {
npc: Option<u64>,
cost: u32,
ask: bool,
}
impl PetUnlearnState {
/// The inbound `SMSG_PET_UNLEARN_CONFIRM`: latch and owe the dialog.
pub(crate) fn ask(&mut self, npc: u64, cost: u32) {
self.npc = Some(npc);
self.cost = cost;
self.ask = true;
}
fn pending(&self) -> Option<u64> {
self.npc
}
}
impl NpcSession for PetUnlearnState {
fn npc(&self) -> Option<u64> {
self.npc
}
fn close(&mut self) {
self.npc = None;
self.cost = 0;
self.ask = false;
}
}
/// The instance-boot clock (`[0xb4e34c]`), and what the last packet owes the UI.
#[derive(Resource, Default)]
pub(crate) struct InstanceBoot {
deadline: Option<Instant>,
/// Events owed, in arrival order (`INSTANCE_BOOT_START` / `_STOP`).
events: Vec<&'static str>,
/// Error lines owed (`ERR_RAID_GROUP_ONLY` / `_FULL`), the zero-delay leg's.
errors: Vec<&'static str>,
}
impl InstanceBoot {
/// `SMSG_RAID_GROUP_ONLY`: `delay > 0` arms, else clears — and the event fires either way.
pub(crate) fn apply(&mut self, delay_ms: u32, reason: u32, now: Instant) {
if delay_ms > 0 {
self.deadline = Some(now + std::time::Duration::from_millis(u64::from(delay_ms)));
self.events.push("INSTANCE_BOOT_START");
} else {
self.deadline = None;
self.events.push("INSTANCE_BOOT_STOP");
match reason {
1 => self.errors.push("ERR_RAID_GROUP_ONLY"),
2 => self.errors.push("ERR_RAID_GROUP_FULL"),
_ => {}
}
}
}
/// Whole seconds left, 0 when idle or past — the reference's unsigned divide of a clamped
/// millisecond remainder.
pub(crate) fn secs(&self, now: Instant) -> u32 {
self.deadline
.map(|d| d.saturating_duration_since(now).as_secs())
.map_or(0, |s| u32::try_from(s).unwrap_or(u32::MAX))
}
}
/// The current-area spirit healer (`[0xb4e330/334]`) and its wave clock (`[0xb4e338]`).
#[derive(Resource, Default)]
pub(crate) struct AreaSpiritHealer {
/// The cached healer. No writer yet — see the module doc.
healer: Option<u64>,
deadline: Option<Instant>,
in_range: bool,
}
impl AreaSpiritHealer {
/// `SMSG_AREA_SPIRIT_HEALER_TIME`: for the cached healer with a positive time, arm the clock
/// (a zero-landing deadline reads as 1 ms in the reference) and owe `_IN_RANGE`.
pub(crate) fn on_time(&mut self, healer: u64, ms: u32, now: Instant) {
if self.healer == Some(healer) && ms > 0 {
self.deadline = Some(now + std::time::Duration::from_millis(u64::from(ms)));
self.in_range = true;
}
}
fn secs(&self, now: Instant) -> u32 {
self.deadline
.map(|d| d.saturating_duration_since(now).as_secs())
.map_or(0, |s| u32::try_from(s).unwrap_or(u32::MAX))
}
}
/// The three battleground queue slots (`0xb6e9d0`, stride `0x20`).
#[derive(Resource, Default)]
pub(crate) struct BattlefieldQueue {
slots: [Option<BattlefieldStatus>; 3],
changed: bool,
}
impl BattlefieldQueue {
/// `SMSG_BATTLEFIELD_STATUS`: an out-of-range slot aborts the handler; a zero map clears.
pub(crate) fn apply(&mut self, status: BattlefieldStatus) {
let Some(slot) = self.slots.get_mut(status.slot as usize) else {
return;
};
*slot = (status.map_id != 0).then_some(status);
self.changed = true;
}
/// The map id `AcceptBattlefieldPort` sends for a 1-based slot, if the slot holds a queue.
fn map_id(&self, index: u8) -> Option<u32> {
self.slots
.get(usize::from(index).checked_sub(1)?)
.and_then(|s| s.as_ref())
.map(|s| s.map_id)
}
}
/// The meeting-stone queue (`[0xb72038]`).
#[derive(Resource, Default)]
pub(crate) struct MeetingStone {
pub(crate) area: u32,
pub(crate) status: Option<u8>,
changed: bool,
}
impl MeetingStone {
/// `SMSG 0x295`: the area is stored unconditionally; the status picks the (unbuilt) message.
pub(crate) fn apply(&mut self, area: u32, status: u8) {
self.area = area;
self.status = Some(status);
self.changed = true;
}
}
fn feed_dialog_verbs(
script: Option<NonSendMut<UiScript>>,
mut pet: ResMut<PetUnlearnState>,
mut boot: ResMut<InstanceBoot>,
mut spirit: ResMut<AreaSpiritHealer>,
mut queue: ResMut<BattlefieldQueue>,
mut stone: ResMut<MeetingStone>,
mut sink: crate::ui_action::MessageSink,
) {
let Some(mut script) = script else {
return;
};
let now = Instant::now();
script.set_pet_untrainer_pending(pet.pending().is_some());
if pet.ask {
pet.ask = false;
script.fire_event(
"CONFIRM_PET_UNLEARN",
vec![ScriptValue::Int(i64::from(pet.cost))],
);
}
script.set_instance_boot_secs(boot.secs(now));
for event in std::mem::take(&mut boot.events) {
script.fire_event(event, vec![]);
}
let lines: Vec<_> = std::mem::take(&mut boot.errors)
.into_iter()
.filter_map(|key| crate::ui_action::keyed_line(&script, key))
.collect();
if !lines.is_empty() {
crate::ui_action::show_messages(&mut script, &mut sink, "ui_dialog_verbs", lines);
}
let secs = spirit.secs(now);
script.set_area_spirit_healer(spirit.healer.is_some(), secs);
if std::mem::take(&mut spirit.in_range) {
script.fire_event("AREA_SPIRIT_HEALER_IN_RANGE", vec![]);
}
if std::mem::take(&mut queue.changed) {
script.fire_event("UPDATE_BATTLEFIELD_STATUS", vec![]);
}
if std::mem::take(&mut stone.changed) {
script.fire_event("MEETINGSTONE_CHANGED", vec![]);
}
}
/// The pet trainer's confirm and the spirit healer's accept — the two drains over a latch.
fn drain_latch_verbs(
script: Option<NonSendMut<UiScript>>,
pet: Res<PetUnlearnState>,
spirit: Res<AreaSpiritHealer>,
self_q: Query<&ObjectStore, With<SelfPlayer>>,
commands: Res<NetCommands>,
mut sink: crate::ui_action::MessageSink,
) {
let Some(mut script) = script else {
return;
};
// ConfirmPetUnlearn: the latch, then the money gate — `cost > coinage` shows
// ERR_NOT_ENOUGH_MONEY and sends nothing; otherwise `0x2F0` with the latched guid.
let confirms = script.take_pet_unlearn_confirms();
if confirms > 0 {
if let Some(npc) = pet.pending() {
let money = self_q
.single()
.ok()
.and_then(|store| store.0.player_money())
.unwrap_or(0);
if pet.cost > money {
if let Some(line) = crate::ui_action::keyed_line(&script, "ERR_NOT_ENOUGH_MONEY") {
crate::ui_action::show_messages(
&mut script,
&mut sink,
"ui_dialog_verbs",
[line],
);
}
} else {
for _ in 0..confirms {
let _ = commands.0.send(ClientCommand::PetUnlearn { trainer: npc });
}
}
}
}
// AcceptAreaSpiritHeal: the cached healer's guid (the binding was silent without one).
let accepts = script.take_area_spirit_accepts();
if let Some(healer) = spirit.healer {
for _ in 0..accepts {
let _ = commands
.0
.send(ClientCommand::AreaSpiritHealerQueue { healer });
}
}
}
/// The battleground port and the meeting-stone leave — the two drains over a queue.
fn drain_queue_verbs(
script: Option<NonSendMut<UiScript>>,
queue: Res<BattlefieldQueue>,
group: Option<Res<GroupState>>,
self_guid: Res<SelfGuid>,
commands: Res<NetCommands>,
mut sink: crate::ui_action::MessageSink,
) {
let Some(mut script) = script else {
return;
};
// AcceptBattlefieldPort: the slot's map id and the one-byte answer.
for (index, accept) in script.take_battlefield_port_requests() {
if let Some(map_id) = queue.map_id(index) {
let _ = commands
.0
.send(ClientCommand::BattlefieldPort { map_id, accept });
}
}
// CancelMeetingStoneRequest: in a party and not its leader → ERR_MEETING_STONE_NOT_LEADER;
// otherwise `0x293`, whatever is or is not queued.
let cancels = script.take_meeting_stone_cancels();
if cancels > 0 {
let not_leader = group
.as_deref()
.is_some_and(|g| g.in_group && Some(g.leader) != self_guid.0);
if not_leader {
if let Some(line) =
crate::ui_action::keyed_line(&script, "ERR_MEETING_STONE_NOT_LEADER")
{
crate::ui_action::show_messages(&mut script, &mut sink, "ui_dialog_verbs", [line]);
}
} else {
for _ in 0..cancels {
let _ = commands.0.send(ClientCommand::MeetingStoneLeave);
}
}
}
}
pub(crate) struct UiDialogVerbsPlugin;
impl Plugin for UiDialogVerbsPlugin {
fn build(&self, app: &mut App) {
app.init_resource::<PetUnlearnState>()
.init_resource::<InstanceBoot>()
.init_resource::<AreaSpiritHealer>()
.init_resource::<BattlefieldQueue>()
.init_resource::<MeetingStone>()
.add_systems(
Update,
(
close_npc_session_out_of_range::<PetUnlearnState>.before(feed_dialog_verbs),
feed_dialog_verbs.before(UiInput),
drain_latch_verbs.after(UiInput),
drain_queue_verbs.after(UiInput),
),
);
}
}
/// The area spirit healer's aura, `0xA18` — `CancelAreaSpiritHeal`'s one spell (the engine's
/// `dialog_verbs::AREA_SPIRIT_HEALER_SPELL`), read here only to check its flags against the data.
#[cfg(test)]
const AREA_SPIRIT_HEALER_SPELL: u32 = 2584;
/// The generic cancel-aura routine's refusal (`0x6e7040`, wow-re `staticpopup-dialog-bindings.md`
/// §6): it returns without sending when the spell's `AttributesEx` has bit 13 set and bit 2
/// clear **and** `0x5ee290(player)` holds. Whether the third leg ever matters for spell 2584 is
/// decided by the first two, read off the shipped Spell.dbc in [`tests::spell_2584_never_trips_the_cancel_gate`].
#[cfg(test)]
fn cancel_gate_could_apply(attributes_ex: u32) -> bool {
attributes_ex & 0x2000 != 0 && attributes_ex & 0x4 == 0
}
#[cfg(test)]
mod tests {
use super::*;
/// Spell 2584's flags off the shipped Spell.dbc: the cancel-aura gate's first two legs.
#[test]
fn spell_2584_never_trips_the_cancel_gate() {
let data = benilla_formats::wow_data_or_skip!();
let mut chain = benilla_formats::open_chain(&data).expect("open chain");
let catalog = benilla_formats::load_spell_catalog(&mut chain).expect("Spell.dbc");
let ex = catalog
.get(AREA_SPIRIT_HEALER_SPELL)
.map(|d| d.attributes_ex)
.expect("spell 2584 in Spell.dbc");
assert!(
!cancel_gate_could_apply(ex),
"spell 2584 AttributesEx = {ex:#x}: the gate's third leg would decide, and it is uncarved"
);
}
#[test]
fn the_boot_clock_arms_on_a_delay_and_clears_on_zero_with_the_reason() {
let mut boot = InstanceBoot::default();
let now = Instant::now();
boot.apply(60_500, 0, now);
assert_eq!(boot.events, vec!["INSTANCE_BOOT_START"]);
assert_eq!(boot.secs(now), 60, "whole seconds, truncated");
boot.apply(0, 1, now);
assert_eq!(
boot.events,
vec!["INSTANCE_BOOT_START", "INSTANCE_BOOT_STOP"]
);
assert_eq!(boot.errors, vec!["ERR_RAID_GROUP_ONLY"]);
assert_eq!(boot.secs(now), 0);
boot.apply(0, 7, now);
assert_eq!(boot.errors.len(), 1, "a reason outside 1/2 names nothing");
}
#[test]
fn the_spirit_healer_clock_only_arms_for_the_cached_healer() {
let mut spirit = AreaSpiritHealer::default();
let now = Instant::now();
spirit.on_time(0x77, 30_000, now);
assert!(!spirit.in_range, "no healer cached: the packet is ignored");
spirit.healer = Some(0x77);
spirit.on_time(0x78, 30_000, now);
assert!(!spirit.in_range, "another guid: ignored");
spirit.on_time(0x77, 0, now);
assert!(!spirit.in_range, "a zero time: ignored");
spirit.on_time(0x77, 30_000, now);
assert!(spirit.in_range);
assert_eq!(spirit.secs(now), 30);
}
#[test]
fn the_queue_keeps_three_slots_and_answers_a_port_by_map() {
let mut q = BattlefieldQueue::default();
let status = |slot, map_id| BattlefieldStatus {
slot,
map_id,
bracket: 0,
unknown: 0,
status: 2,
time_ms: Some(0),
in_progress: None,
};
q.apply(status(1, 489));
q.apply(status(5, 30));
assert_eq!(
q.map_id(2),
Some(489),
"1-based from Lua, 0-based on the wire"
);
assert_eq!(q.map_id(1), None);
assert_eq!(q.map_id(4), None);
q.apply(status(1, 0));
assert_eq!(q.map_id(2), None, "a zero map clears the slot");
}
#[test]
fn the_pet_question_latches_and_closes_like_the_talent_wipe() {
let mut pet = PetUnlearnState::default();
pet.ask(0x2b, 10_000);
assert_eq!(pet.pending(), Some(0x2b));
assert!(pet.ask);
pet.close();
assert_eq!(pet.pending(), None);
assert_eq!(pet.cost, 0);
}
}
+2 -1
View File
@@ -1174,7 +1174,8 @@ mod bind_confirm_tests {
// reader that joins `assets/ui` cannot resolve one (1838, 1887, 1888).
for file in [
"Interface\\FrameXML\\Fonts.xml",
"MoneyFrame.xml",
r"Interface\FrameXML\MoneyFrame.lua",
r"Interface\FrameXML\MoneyFrame.xml",
"UiPanels.xml",
"Interface\\FrameXML\\GlobalStrings.lua",
"Interface\\FrameXML\\BasicControls.xml",
+8
View File
@@ -189,6 +189,14 @@ fn drain_logout(
info!("logout: force quit");
exit.write(AppExit::Success);
}
// `CMSG_PLAYER_LOGOUT`, the forced flavour: the dispatcher's own gate is a live
// in-world session, and nothing happens without one (decision 1963).
SessionRequest::ForceLogout => {
if self_guid.0.is_some() {
info!("logout: forced");
let _ = commands.0.send(ClientCommand::ForceLogout);
}
}
// Not this module's business beyond routing: the rebuild itself is
// [`crate::ui_script::run_pending_reload`]'s, at the top of the next frame.
SessionRequest::ReloadUi => reload.0 = true,
+3 -15
View File
@@ -49,14 +49,6 @@ use crate::ui_script::UiInput;
/// The icon *order* is the client's own and it is not the numeric one — `_05, _06, _03, _04, _01,
/// _02` as the amount climbs. What each of the six pieces of art depicts is not claimed here; the
/// ladder is byte-derived and the art is whatever the reference picks at that step.
const COIN_ICONS: [(u32, &str); 6] = [
(10, "Interface\\Icons\\INV_Misc_Coin_05"),
(100, "Interface\\Icons\\INV_Misc_Coin_06"),
(1_000, "Interface\\Icons\\INV_Misc_Coin_03"),
(10_000, "Interface\\Icons\\INV_Misc_Coin_04"),
(100_000, "Interface\\Icons\\INV_Misc_Coin_01"),
(u32::MAX, "Interface\\Icons\\INV_Misc_Coin_02"),
];
/// `item_template.bonding == BIND_WHEN_PICKED_UP` — the first of the two conjuncts that defer a
/// loot take behind the LOOT_BIND confirm (VERIFIED vmangos `ItemPrototype.h`'s `ItemBondingType`:
/// `NO_BIND` 0, `BIND_WHEN_PICKED_UP` 1, `BIND_WHEN_EQUIPPED` 2, `BIND_WHEN_USE` 3, `QUEST_ITEM` 4;
@@ -677,14 +669,10 @@ fn format_money(copper: u32) -> String {
parts.join(" ")
}
/// The coin-pile icon for a copper amount — the reference's six-step ladder, see [`COIN_ICONS`].
/// `u32::MAX` is the last step's bound, so the `map_or` fallback is unreachable and is there only
/// because the table is data rather than a match.
/// The coin-pile icon for a copper amount — the reference's six-step ladder, one table for the
/// loot slot, `GetCoinIcon` and the money cursor's bitmap (`benilla_ui::script::coin_icon`, 1965).
fn coin_icon(copper: u32) -> &'static str {
COIN_ICONS
.iter()
.find(|(below, _)| copper < *below)
.map_or(COIN_ICONS[5].1, |(_, icon)| icon)
benilla_ui::script::coin_icon(i64::from(copper))
}
/// Resolve one wire [`LootItem`] into the Lua-facing [`LootRow`]: the icon comes straight from the
@@ -22,7 +22,8 @@ fn shipped_action_bar_drives_end_to_end() {
super::test_ui::load_ui(&s, "Interface\\FrameXML\\TextStatusBar.xml");
super::test_ui::load_ui(&s, "Interface\\FrameXML\\Fonts.xml");
super::test_ui::load_ui(&s, "UIParent.xml");
super::test_ui::load_ui(&s, "MoneyFrame.xml");
super::test_ui::load_ui(&s, r"Interface\FrameXML\MoneyFrame.lua");
super::test_ui::load_ui(&s, r"Interface\FrameXML\MoneyFrame.xml");
super::test_ui::load_ui(&s, "GameTooltip.xml");
super::test_ui::load_ui(&s, "Interface\\FrameXML\\GlobalStrings.lua");
let frames = super::test_ui::load_ui(&s, "Interface\\FrameXML\\MainMenuBar.xml")
@@ -187,7 +188,8 @@ fn load_action_bar(s: &UiScript) {
super::test_ui::load_ui(s, "UIParent.xml");
super::test_ui::load_ui(s, "Interface\\FrameXML\\GlobalStrings.lua");
super::test_ui::load_ui(s, "Interface\\FrameXML\\MainMenuBar.xml");
super::test_ui::load_ui(s, "MoneyFrame.xml");
super::test_ui::load_ui(s, r"Interface\FrameXML\MoneyFrame.lua");
super::test_ui::load_ui(s, r"Interface\FrameXML\MoneyFrame.xml");
super::test_ui::load_ui(s, "GameTooltip.xml");
super::test_ui::load_ui(s, "Interface\\FrameXML\\ActionBarFrame.xml");
super::test_ui::load_ui(s, "Interface\\FrameXML\\BonusActionBarFrame.xml");
@@ -836,8 +838,6 @@ fn shipped_bag_frame_drives_end_to_end() {
load_ui(&s, "UIParent.xml");
load_ui(&s, "Interface\\FrameXML\\GlobalStrings.lua");
load_ui(&s, "Interface\\FrameXML\\MainMenuBar.xml");
load_ui(&s, "MoneyFrame.xml");
load_ui(&s, "GameTooltip.xml");
load_ui(&s, "Interface\\FrameXML\\ActionBarFrame.xml");
load_ui(&s, "Interface\\FrameXML\\BonusActionBarFrame.xml");
}
@@ -1357,7 +1357,8 @@ fn the_main_bar_pages_and_a_bonus_page_still_outranks_it() {
"Interface\\FrameXML\\TextStatusBar.xml",
"Interface\\FrameXML\\GlobalStrings.lua",
"Interface\\FrameXML\\MainMenuBar.xml",
"MoneyFrame.xml",
r"Interface\FrameXML\MoneyFrame.lua",
r"Interface\FrameXML\MoneyFrame.xml",
"GameTooltip.xml",
"Interface\\FrameXML\\ActionBarFrame.xml",
"Interface\\FrameXML\\BonusActionBarFrame.xml",
@@ -1525,7 +1526,8 @@ fn bonus_bar_slides_up_with_sound_and_down_without() {
super::test_ui::load_ui(&s, "UIParent.xml");
super::test_ui::load_ui(&s, "Interface\\FrameXML\\GlobalStrings.lua");
super::test_ui::load_ui(&s, "Interface\\FrameXML\\MainMenuBar.xml");
super::test_ui::load_ui(&s, "MoneyFrame.xml");
super::test_ui::load_ui(&s, r"Interface\FrameXML\MoneyFrame.lua");
super::test_ui::load_ui(&s, r"Interface\FrameXML\MoneyFrame.xml");
super::test_ui::load_ui(&s, "GameTooltip.xml");
super::test_ui::load_ui(&s, "Interface\\FrameXML\\ActionBarFrame.xml");
super::test_ui::load_ui(&s, "Interface\\FrameXML\\BonusActionBarFrame.xml");
@@ -1746,7 +1748,8 @@ fn bonus_bar_turnaround_continues_from_position() {
super::test_ui::load_ui(&s, "UIParent.xml");
super::test_ui::load_ui(&s, "Interface\\FrameXML\\GlobalStrings.lua");
super::test_ui::load_ui(&s, "Interface\\FrameXML\\MainMenuBar.xml");
super::test_ui::load_ui(&s, "MoneyFrame.xml");
super::test_ui::load_ui(&s, r"Interface\FrameXML\MoneyFrame.lua");
super::test_ui::load_ui(&s, r"Interface\FrameXML\MoneyFrame.xml");
super::test_ui::load_ui(&s, "GameTooltip.xml");
super::test_ui::load_ui(&s, "Interface\\FrameXML\\ActionBarFrame.xml");
super::test_ui::load_ui(&s, "Interface\\FrameXML\\BonusActionBarFrame.xml");
@@ -1827,7 +1830,8 @@ fn the_page_arrows_do_not_steal_each_other_s_clicks() {
super::test_ui::load_ui(&s, "UIParent.xml");
super::test_ui::load_ui(&s, "Interface\\FrameXML\\GlobalStrings.lua");
super::test_ui::load_ui(&s, "Interface\\FrameXML\\MainMenuBar.xml");
super::test_ui::load_ui(&s, "MoneyFrame.xml");
super::test_ui::load_ui(&s, r"Interface\FrameXML\MoneyFrame.lua");
super::test_ui::load_ui(&s, r"Interface\FrameXML\MoneyFrame.xml");
super::test_ui::load_ui(&s, "GameTooltip.xml");
super::test_ui::load_ui(&s, "Interface\\FrameXML\\ActionBarFrame.xml");
super::test_ui::load_ui(&s, "Interface\\FrameXML\\BonusActionBarFrame.xml");
@@ -12,7 +12,8 @@ fn setup() -> UiScript {
let mut s = UiScript::new().unwrap();
s.set_screen_size(1024.0, 768.0);
load_xml(&s, "Interface\\FrameXML\\Fonts.xml");
load_xml(&s, "MoneyFrame.xml");
load_xml(&s, r"Interface\FrameXML\MoneyFrame.lua");
load_xml(&s, r"Interface\FrameXML\MoneyFrame.xml");
load_xml(&s, "UiPanels.xml");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
@@ -30,10 +30,12 @@ fn harness() -> UiScript {
// `GameTooltip`, which the reference's BuffButton_Update indexes on EVERY repaint to ask
// `IsOwned(this)` (BuffFrame.lua l.104) — not just on hover. Ours guarded it; the reference
// does not, so a session without the tooltip loses the whole repaint.
load_xml(&s, "MoneyFrame.xml");
load_xml(&s, r"Interface\FrameXML\MoneyFrame.lua");
load_xml(&s, r"Interface\FrameXML\MoneyFrame.xml");
load_xml(&s, "GameTooltip.xml");
// `SecondsToTimeAbbrev`, which 1.12 keeps in UIParent.lua and so do we since window 18.
load_xml(&s, "MoneyFrame.xml");
load_xml(&s, r"Interface\FrameXML\MoneyFrame.lua");
load_xml(&s, r"Interface\FrameXML\MoneyFrame.xml");
load_xml(&s, "UiPanels.xml");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
@@ -392,7 +392,8 @@ fn managed_positions_track_the_bottom_bar_stack() {
load_xml(&s, "Interface\\FrameXML\\GlobalStrings.lua");
load_xml(&s, "Interface\\FrameXML\\BasicControls.xml");
load_xml(&s, "Interface\\FrameXML\\ChatFrame.xml");
load_xml(&s, "MoneyFrame.xml");
load_xml(&s, r"Interface\FrameXML\MoneyFrame.lua");
load_xml(&s, r"Interface\FrameXML\MoneyFrame.xml");
load_xml(&s, "GameTooltip.xml"); // the dropdown kit's MenuBackdrop reads TOOLTIP_DEFAULT_COLOR
load_xml(&s, "Interface\\FrameXML\\UIDropDownMenu.xml");
load_xml(&s, "Interface\\FrameXML\\UIPanelTemplates.lua");
@@ -1201,7 +1201,8 @@ fn an_addons_tab_click_selects_through_the_generic_entry_point() {
let mut s = UiScript::new().unwrap();
s.set_screen_size(1024.0, 768.0);
load_xml(&s, "Interface\\FrameXML\\Fonts.xml");
load_xml(&s, "MoneyFrame.xml");
load_xml(&s, r"Interface\FrameXML\MoneyFrame.lua");
load_xml(&s, r"Interface\FrameXML\MoneyFrame.xml");
load_xml(&s, "UiPanels.xml");
// The reference's `PanelTemplates_SelectTab` ends with `if GameTooltip:IsOwned(tab)` —
// an arm our deleted copy omitted ("our tabs set no tooltip"), so selecting a tab now needs
@@ -40,7 +40,8 @@ fn chat_with_menu() -> UiScript {
let mut s = UiScript::new().unwrap();
for file in [
"Interface\\FrameXML\\Fonts.xml",
"MoneyFrame.xml",
r"Interface\FrameXML\MoneyFrame.lua",
r"Interface\FrameXML\MoneyFrame.xml",
"UiPanels.xml",
"UIParent.xml",
"GameTooltip.xml",
@@ -18,7 +18,8 @@ fn chat_ui() -> UiScript {
let mut s = UiScript::new().unwrap();
for file in [
"Interface\\FrameXML\\Fonts.xml",
"MoneyFrame.xml",
r"Interface\FrameXML\MoneyFrame.lua",
r"Interface\FrameXML\MoneyFrame.xml",
"UiPanels.xml",
"UIParent.xml",
"GameTooltip.xml",
@@ -19,7 +19,8 @@ fn chat_frame() -> UiScript {
// `benilla.toc` already orders both ahead of ChatFrame.xml (l.60/64 vs l.399); the harness
// says so too, rather than a guard that would hide a real ordering fault. (The tooltip file is
// the dropdown kit's own dependency — its MenuBackdrop reads `TOOLTIP_DEFAULT_COLOR`.)
load_xml(&s, "MoneyFrame.xml");
load_xml(&s, r"Interface\FrameXML\MoneyFrame.lua");
load_xml(&s, r"Interface\FrameXML\MoneyFrame.xml");
load_xml(&s, "GameTooltip.xml");
load_xml(&s, "Interface\\FrameXML\\UIDropDownMenu.xml");
load_xml(&s, "Interface\\FrameXML\\UIMenu.xml"); // the kit the chat menus build from
@@ -992,7 +993,8 @@ fn the_chat_menu_builds_its_rows_on_the_references_kit() {
s.set_screen_size(1024.0, 768.0);
for f in [
"Interface\\FrameXML\\Fonts.xml",
"MoneyFrame.xml",
r"Interface\FrameXML\MoneyFrame.lua",
r"Interface\FrameXML\MoneyFrame.xml",
"UiPanels.xml",
r"Interface\FrameXML\UIPanelTemplates.lua",
r"Interface\FrameXML\UIPanelTemplates.xml",
@@ -24,7 +24,8 @@ fn ui_with_the_cinematic_frame() -> UiScript {
s.set_screen_size(1024.0, 768.0);
load_xml(&s, "Interface\\FrameXML\\Fonts.xml");
load_xml(&s, "UIParent.xml");
load_xml(&s, "MoneyFrame.xml"); // StaticPopup's money row, or UiPanels errors at load
load_xml(&s, r"Interface\FrameXML\MoneyFrame.lua"); // StaticPopup's money row, or UiPanels errors at load
load_xml(&s, r"Interface\FrameXML\MoneyFrame.xml");
load_xml(&s, "UiPanels.xml");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
@@ -18,7 +18,8 @@ fn picker() -> UiScript {
let mut s = UiScript::new().unwrap();
for file in [
"Interface\\FrameXML\\Fonts.xml",
"MoneyFrame.xml",
r"Interface\FrameXML\MoneyFrame.lua",
r"Interface\FrameXML\MoneyFrame.xml",
"UiPanels.xml",
"UIParent.xml",
"GameTooltip.xml",
@@ -18,11 +18,35 @@ use super::test_ui::load_ui as load_xml;
fn load_combat_text() -> UiScript {
let mut s = UiScript::new().unwrap();
s.set_screen_size(1024.0, 768.0);
load_xml(&s, "Interface\\FrameXML\\GlobalStrings.lua"); // ENTERING_COMBAT & co.
load_xml(&s, "Interface\\FrameXML\\BasicControls.xml"); // TEXT()
load_xml(&s, "Interface\\FrameXML\\Fonts.xml");
load_xml(&s, "UIParent.xml");
load_xml(&s, "CombatText.xml");
s.run("SHOW_COMBAT_TEXT = \"1\"; CombatText_UpdateDisplayedMessages()")
.unwrap();
// The reference's own addon, LoadOnDemand off the chain, loaded the way the options window
// loads it (1964).
super::test_ui::seat_chain_addon(&mut s, "Blizzard_CombatText");
// The family's saved-variable defaults are the options window's file-scope block (the
// reference's UIOptionsFrame.lua l.135-152, carried by our OptionsFrame.xml since 1964); a
// harness without that window seats the same block by hand, the master switched on.
s.run(
r#"SHOW_COMBAT_TEXT = "1"
COMBAT_TEXT_SHOW_LOW_HEALTH_MANA = "1"
COMBAT_TEXT_SHOW_AURAS = "1"
COMBAT_TEXT_SHOW_AURA_FADE = "0"
COMBAT_TEXT_SHOW_COMBAT_STATE = "1"
COMBAT_TEXT_SHOW_DODGE_PARRY_MISS = "0"
COMBAT_TEXT_SHOW_RESISTANCES = "0"
COMBAT_TEXT_SHOW_REPUTATION = "0"
COMBAT_TEXT_SHOW_REACTIVES = "0"
COMBAT_TEXT_SHOW_FRIENDLY_NAMES = "0"
COMBAT_TEXT_SHOW_COMBO_POINTS = "0"
COMBAT_TEXT_SHOW_MANA = "0"
COMBAT_TEXT_FLOAT_MODE = "1"
COMBAT_TEXT_SHOW_HONOR_GAINED = "1"
UIParentLoadAddOn("Blizzard_CombatText")
CombatText_UpdateDisplayedMessages()"#,
)
.unwrap();
s.resolve();
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
s
@@ -33,6 +57,7 @@ fn load_combat_text() -> UiScript {
/// shows nothing.
#[test]
fn combat_text_damage_scrolls_and_expires() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = load_combat_text();
// All 20 pool strings start hidden.
@@ -113,6 +138,7 @@ fn combat_text_damage_scrolls_and_expires() {
/// scrolls away from the seat).
#[test]
fn combat_text_crit_pops_and_parks() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = load_combat_text();
s.fire_event(
"COMBAT_TEXT_UPDATE",
@@ -163,6 +189,7 @@ fn extracted_text_height(s: &mut UiScript, text: &str) -> Option<f32> {
/// paints "Health Low" once and re-arms only after recovering above the threshold.
#[test]
fn combat_text_state_and_low_health_triggers() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = load_combat_text();
s.fire_event("PLAYER_REGEN_DISABLED", vec![]);
let ok: bool = s
@@ -231,6 +258,7 @@ fn combat_text_state_and_low_health_triggers() {
/// so the ref constants stand verbatim.
#[test]
fn combat_text_crit_peak_is_uncapped() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = load_combat_text();
let ok: bool = s
.eval("return COMBAT_TEXT_LOCATIONS.startY == 384 and COMBAT_TEXT_LOCATIONS.endY == 609")
@@ -263,6 +291,7 @@ fn combat_text_crit_peak_is_uncapped() {
/// something would otherwise have.
#[test]
fn combat_text_master_toggle_unregisters() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = load_combat_text();
s.fire_event(
"COMBAT_TEXT_UPDATE",
@@ -281,6 +310,10 @@ fn combat_text_master_toggle_unregisters() {
s.run("SHOW_COMBAT_TEXT = \"0\"; CombatText_UpdateDisplayedMessages()")
.unwrap();
s.run("CombatText_ClearAnimationList()").unwrap();
// The stock `CombatText_ClearAnimationList` hides the strings and leaves the list to the
// ticker (our transcription had emptied it): "nothing paints" is the list not growing and no
// string on screen.
let before: i64 = s.eval("return getn(COMBAT_TEXT_TO_ANIMATE)").unwrap();
s.fire_event(
"COMBAT_TEXT_UPDATE",
vec![
@@ -289,7 +322,9 @@ fn combat_text_master_toggle_unregisters() {
],
);
let none: bool = s
.eval("return getn(COMBAT_TEXT_TO_ANIMATE) == 0 and CombatText1:IsShown() == nil")
.eval(&format!(
"return getn(COMBAT_TEXT_TO_ANIMATE) == {before} and CombatText1:IsShown() == nil"
))
.unwrap();
assert!(none, "disabled: nothing paints ({:?})", s.errors());
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
@@ -20,12 +20,14 @@ fn load_combo_frame() -> UiScript {
s.set_screen_size(1024.0, 768.0);
load_xml(&s, "Interface\\FrameXML\\Fonts.xml");
load_xml(&s, "UIParent.xml");
load_xml(&s, "MoneyFrame.xml");
load_xml(&s, r"Interface\FrameXML\MoneyFrame.lua");
load_xml(&s, r"Interface\FrameXML\MoneyFrame.xml");
load_xml(&s, "GameTooltip.xml");
load_xml(&s, "Interface\\FrameXML\\UIDropDownMenu.xml");
load_xml(&s, "Interface\\FrameXML\\BasicControls.xml");
load_xml(&s, "Interface\\FrameXML\\UnitPopup.xml");
load_xml(&s, "MoneyFrame.xml");
load_xml(&s, r"Interface\FrameXML\MoneyFrame.lua");
load_xml(&s, r"Interface\FrameXML\MoneyFrame.xml");
load_xml(&s, "UiPanels.xml");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
@@ -11,7 +11,8 @@ fn setup() -> UiScript {
let mut s = UiScript::new().unwrap();
s.set_screen_size(1024.0, 768.0);
load_xml(&s, "Interface\\FrameXML\\Fonts.xml");
load_xml(&s, "MoneyFrame.xml");
load_xml(&s, r"Interface\FrameXML\MoneyFrame.lua");
load_xml(&s, r"Interface\FrameXML\MoneyFrame.xml");
load_xml(&s, "UiPanels.xml");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
@@ -57,7 +57,8 @@ fn setup() -> UiScript {
let mut s = UiScript::new().unwrap();
s.set_screen_size(1024.0, 768.0);
load_xml(&s, "Interface\\FrameXML\\Fonts.xml");
load_xml(&s, "MoneyFrame.xml");
load_xml(&s, r"Interface\FrameXML\MoneyFrame.lua");
load_xml(&s, r"Interface\FrameXML\MoneyFrame.xml");
load_xml(&s, "UiPanels.xml");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
@@ -49,7 +49,6 @@ const ROOM_UI: &[&str] = &[
"Interface\\FrameXML\\UIPanelTemplates.xml",
"UiPanels.xml",
"Interface\\FrameXML\\LocaleProperties.lua",
"Interface\\FrameXML\\StaticPopup.xml",
"Interface\\FrameXML\\FloatingChatFrame.xml",
];
@@ -60,7 +59,8 @@ const ROOM_UI: &[&str] = &[
fn load_room(s: &UiScript) {
for file in [
"Interface\\FrameXML\\Fonts.xml",
"MoneyFrame.xml",
r"Interface\FrameXML\MoneyFrame.lua",
r"Interface\FrameXML\MoneyFrame.xml",
"UiPanels.xml",
r"Interface\FrameXML\UIPanelTemplates.lua",
r"Interface\FrameXML\UIPanelTemplates.xml",
@@ -124,7 +124,6 @@ fn shown_paper_doll() -> UiScript {
"Interface\\FrameXML\\UIPanelTemplates.xml",
"UiPanels.xml",
"Interface\\FrameXML\\LocaleProperties.lua",
"Interface\\FrameXML\\StaticPopup.xml",
"Interface\\FrameXML\\FloatingChatFrame.xml",
] {
super::test_ui::load_ui_strict(&s, file);
@@ -22,7 +22,8 @@ fn load_dropdown_kit(s: &UiScript) {
for file in [
"Interface\\FrameXML\\Fonts.xml",
"UIParent.xml",
"MoneyFrame.xml",
r"Interface\FrameXML\MoneyFrame.lua",
r"Interface\FrameXML\MoneyFrame.xml",
"GameTooltip.xml",
"Interface\\FrameXML\\UIDropDownMenu.xml",
] {
@@ -11,7 +11,8 @@ fn setup() -> UiScript {
let mut s = UiScript::new().unwrap();
s.set_screen_size(1024.0, 768.0);
load_xml(&s, "Interface\\FrameXML\\Fonts.xml");
load_xml(&s, "MoneyFrame.xml");
load_xml(&s, r"Interface\FrameXML\MoneyFrame.lua");
load_xml(&s, r"Interface\FrameXML\MoneyFrame.xml");
load_xml(&s, "UiPanels.xml");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
@@ -16,7 +16,8 @@ fn harness() -> UiScript {
s.set_screen_size(1024.0, 768.0);
for f in [
"Interface\\FrameXML\\Fonts.xml",
"MoneyFrame.xml",
r"Interface\FrameXML\MoneyFrame.lua",
r"Interface\FrameXML\MoneyFrame.xml",
"UiPanels.xml",
r"Interface\FrameXML\UIPanelTemplates.lua",
r"Interface\FrameXML\UIPanelTemplates.xml",
@@ -392,7 +393,8 @@ fn the_quest_tracker_stacks_below_the_durability_guy() {
s.set_screen_size(1024.0, 768.0);
for f in [
"Interface\\FrameXML\\Fonts.xml",
"MoneyFrame.xml",
r"Interface\FrameXML\MoneyFrame.lua",
r"Interface\FrameXML\MoneyFrame.xml",
"Interface\\FrameXML\\GlobalStrings.lua",
"UiPanels.xml",
r"Interface\FrameXML\UIPanelTemplates.lua",
@@ -16,7 +16,8 @@ fn setup() -> UiScript {
let mut s = UiScript::new().unwrap();
s.set_screen_size(1024.0, 768.0);
load_xml(&s, "Interface\\FrameXML\\Fonts.xml");
load_xml(&s, "MoneyFrame.xml");
load_xml(&s, r"Interface\FrameXML\MoneyFrame.lua");
load_xml(&s, r"Interface\FrameXML\MoneyFrame.xml");
load_xml(&s, "UiPanels.xml");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
@@ -110,7 +110,8 @@ fn an_error_toast_draws_over_an_open_panel_window() {
let mut s = UiScript::new().unwrap();
s.set_screen_size(1024.0, 768.0);
load_xml(&s, "Interface\\FrameXML\\Fonts.xml");
load_xml(&s, "MoneyFrame.xml");
load_xml(&s, r"Interface\FrameXML\MoneyFrame.lua");
load_xml(&s, r"Interface\FrameXML\MoneyFrame.xml");
load_xml(&s, "Interface\\FrameXML\\GlobalStrings.lua");
load_xml(&s, "UiPanels.xml");
load_xml(&s, "GameTooltip.xml");
@@ -71,6 +71,9 @@ fn escape_closes_bag_and_panel_releases_loot_and_clears_cursor() {
// `PartyFrame.xml` is needed at LOAD (MAX_PARTY_MEMBERS) and skipping it fails loudly, but
// `ItemButtonTemplate` is only a warning and skipping it fails silently.
for file in super::test_ui::LOOT_UI {
if BAG_UI.contains(file) {
continue; // a file loads once — the bag chain above carried it
}
load_xml(&s, file);
}
load_xml(&s, "Interface\\FrameXML\\LootFrame.xml");
@@ -156,7 +159,6 @@ fn escape_is_consumed_by_a_focused_editbox_and_leaves_windows_open() {
load_xml(&s, "Interface\\FrameXML\\UIPanelTemplates.xml");
load_xml(&s, "UiPanels.xml");
load_xml(&s, r"Interface\FrameXML\LocaleProperties.lua");
load_xml(&s, r"Interface\FrameXML\StaticPopup.xml");
load_xml(&s, "Interface\\FrameXML\\FloatingChatFrame.xml");
s.set_money(0);
s.set_container(0, Some(one_item_backpack()));
@@ -189,7 +191,8 @@ fn escape_closes_the_options_window_before_opening_the_menu() {
let mut s = UiScript::new().unwrap();
s.set_screen_size(1024.0, 768.0);
load_xml(&s, "Interface\\FrameXML\\Fonts.xml");
load_xml(&s, "MoneyFrame.xml");
load_xml(&s, r"Interface\FrameXML\MoneyFrame.lua");
load_xml(&s, r"Interface\FrameXML\MoneyFrame.xml");
load_xml(&s, "UiPanels.xml");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
@@ -258,7 +261,6 @@ fn escape_closes_an_open_stack_split_frame() {
load_xml(&s, "Interface\\FrameXML\\UIPanelTemplates.xml");
load_xml(&s, "UiPanels.xml");
load_xml(&s, r"Interface\FrameXML\LocaleProperties.lua");
load_xml(&s, r"Interface\FrameXML\StaticPopup.xml");
load_xml(&s, "Interface\\FrameXML\\FloatingChatFrame.xml");
load_xml(&s, "Interface\\FrameXML\\StackSplitFrame.xml");
s.set_money(0);
@@ -503,7 +505,8 @@ fn an_addon_frame_registered_in_uispecialframes_closes_on_escape() {
let mut s = UiScript::new().unwrap();
s.set_screen_size(1024.0, 768.0);
load_xml(&s, "Interface\\FrameXML\\Fonts.xml");
load_xml(&s, "MoneyFrame.xml");
load_xml(&s, r"Interface\FrameXML\MoneyFrame.lua");
load_xml(&s, r"Interface\FrameXML\MoneyFrame.xml");
load_xml(&s, "UiPanels.xml");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
@@ -17,7 +17,8 @@ fn exp_bar_harness() -> UiScript {
for file in [
"Interface\\FrameXML\\Fonts.xml",
"UIParent.xml",
"MoneyFrame.xml",
r"Interface\FrameXML\MoneyFrame.lua",
r"Interface\FrameXML\MoneyFrame.xml",
"GameTooltip.xml",
"Interface\\FrameXML\\TextStatusBar.lua",
"Interface\\FrameXML\\TextStatusBar.xml",
@@ -1163,6 +1163,9 @@ pub(super) fn drive_script(
CursorPayload::PetAction(p) => p.texture,
CursorPayload::StablePet(p) => Some(p.texture),
CursorPayload::Merchant(m) => m.texture,
CursorPayload::Money(m) => {
Some(benilla_ui::script::coin_icon(i64::from(m.copper)).to_string())
}
});
if let (Some(texture), Some(pos)) = (texture, window.cursor_position()) {
if let Some(handle) = assets
@@ -16,7 +16,8 @@ use super::test_ui::load_ui as load_xml;
fn an_idle_fade_driver_parks_itself_off_the_tick() {
let mut s = UiScript::new().unwrap();
s.set_screen_size(1024.0, 768.0);
load_xml(&s, "MoneyFrame.xml");
load_xml(&s, r"Interface\FrameXML\MoneyFrame.lua");
load_xml(&s, r"Interface\FrameXML\MoneyFrame.xml");
load_xml(&s, "UiPanels.xml");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
@@ -59,7 +60,8 @@ fn an_idle_fade_driver_parks_itself_off_the_tick() {
fn a_started_fade_still_ramps_and_the_driver_reparks_after() {
let mut s = UiScript::new().unwrap();
s.set_screen_size(1024.0, 768.0);
load_xml(&s, "MoneyFrame.xml");
load_xml(&s, r"Interface\FrameXML\MoneyFrame.lua");
load_xml(&s, r"Interface\FrameXML\MoneyFrame.xml");
load_xml(&s, "UiPanels.xml");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
@@ -124,7 +126,8 @@ fn a_started_fade_still_ramps_and_the_driver_reparks_after() {
fn a_flash_alternates_then_stops_and_the_driver_reparks() {
let mut s = UiScript::new().unwrap();
s.set_screen_size(1024.0, 768.0);
load_xml(&s, "MoneyFrame.xml");
load_xml(&s, r"Interface\FrameXML\MoneyFrame.lua");
load_xml(&s, r"Interface\FrameXML\MoneyFrame.xml");
load_xml(&s, "UiPanels.xml");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
@@ -185,13 +185,6 @@ const KNOWN: &[Known] = &[
// the reference inherits `TextStatusBar` — true of our transcription, and no longer true of
// anything. `TargetFrame.xml` is the reference's own now and its ToT bars inherit
// `TextStatusBar` like every other unit bar. The gate found them itself, which is its job.
Known {
frame: "TradePlayerItem7",
flag: Flag::Mouse,
why:
"the enchant slot: ours inherits BenillaTradeEnchantItemTemplate, the reference's the \
ordinary PlayerTradeItemTemplate. A structural difference in our own trade window.",
},
Known {
frame: "WorldStateAlwaysUpFrame",
flag: Flag::Mouse,
@@ -41,7 +41,8 @@ fn harness_with(extra: &[&str]) -> UiScript {
// `GameMenuFrame` and the panels it opens declare `parent="UIParent"`, resolved at LOAD
// (decision 1734) — UIParent must already be there, as it is in the manifest.
"UIParent.xml",
"MoneyFrame.xml",
r"Interface\FrameXML\MoneyFrame.lua",
r"Interface\FrameXML\MoneyFrame.xml",
"UiPanels.xml",
r"Interface\FrameXML\UIPanelTemplates.lua",
r"Interface\FrameXML\UIPanelTemplates.xml",
@@ -562,7 +563,6 @@ fn escape_during_a_countdown_cancels_it_and_does_not_open_the_menu() {
fn the_world_map_cannot_open_behind_the_menu_and_gives_its_slot_back() {
let _data = benilla_formats::wow_data_or_skip!();
let s = harness_with(&[
"MoneyFrame.xml",
"GameTooltip.xml",
"Interface\\FrameXML\\UIDropDownMenu.xml", // the map's continent/zone pickers initialize into it at OnLoad
"ScrollTemplates.xml",
@@ -614,7 +614,6 @@ fn the_world_map_cannot_open_behind_the_menu_and_gives_its_slot_back() {
fn nothing_opens_behind_the_world_map_and_escape_closes_it_first() {
let _data = benilla_formats::wow_data_or_skip!();
let s = harness_with(&[
"MoneyFrame.xml",
"GameTooltip.xml",
"Interface\\FrameXML\\UIDropDownMenu.xml",
"ScrollTemplates.xml",
@@ -681,7 +680,6 @@ fn the_bag_row_greys_under_the_menu_without_any_of_it_disappearing() {
"UIParent.xml",
"Interface\\FrameXML\\GlobalStrings.lua",
"Interface\\FrameXML\\MainMenuBar.xml",
"MoneyFrame.xml",
"GameTooltip.xml",
"Interface\\FrameXML\\ActionBarFrame.xml",
"Interface\\FrameXML\\BonusActionBarFrame.xml",
@@ -811,7 +809,6 @@ fn backpack() -> ContainerState {
fn the_menu_rides_the_shared_era_window_scale() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = harness_with(&[
"MoneyFrame.xml",
"GameTooltip.xml",
"Interface\\FrameXML\\UIDropDownMenu.xml",
"ScrollTemplates.xml",
@@ -76,7 +76,8 @@ fn setup() -> UiScript {
// `UIPanelCloseButton`, which the loot window's four close buttons inherit.
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
load_xml(&s, "MoneyFrame.xml");
load_xml(&s, r"Interface\FrameXML\MoneyFrame.lua");
load_xml(&s, r"Interface\FrameXML\MoneyFrame.xml");
load_xml(&s, "UiPanels.xml");
load_xml(&s, r"Interface\FrameXML\BasicControls.xml");
load_xml(&s, r"Interface\FrameXML\LocaleProperties.lua");
@@ -547,20 +548,21 @@ fn managed_positions_engage_for_the_bare_frame_name() {
s.set_screen_size(1024.0, 768.0);
load_xml(&s, "Interface\\FrameXML\\Fonts.xml");
load_xml(&s, "UIParent.xml");
load_xml(&s, r"Interface\FrameXML\MoneyFrame.lua");
load_xml(&s, r"Interface\FrameXML\MoneyFrame.xml");
// UiPanels.xml before GroupLootFrame.xml, mirroring the shipped manifest order
// (`ui_script::load_default_ui`): the roll file's CONFIRM_LOOT_ROLL entry indexes
// `StaticPopupDialogs`, and indexing a nil there aborts the WHOLE inline <Script> chunk —
// taking every BenillaGroupLootFrame_* function down with it, not just the popup.
load_xml(&s, "MoneyFrame.xml");
load_xml(&s, "UiPanels.xml");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
load_xml(&s, r"Interface\FrameXML\GlobalStrings.lua");
load_xml(&s, r"Interface\FrameXML\BasicControls.xml");
load_xml(&s, r"Interface\FrameXML\LocaleProperties.lua");
load_xml(&s, r"Interface\FrameXML\StaticPopup.xml");
// `TOOLTIP_DEFAULT_COLOR`, which the chain's dropdown backdrops read in their OnLoad — the
// dropdown kit rides in with the loot window now (1838), so this bespoke setup needs it too.
load_xml(&s, r"Interface\FrameXML\StaticPopup.xml");
load_xml(&s, "GameTooltip.xml");
load_group_loot(&s);
@@ -626,7 +628,6 @@ fn ctrl_and_shift_on_the_roll_icon_preview_and_post_its_link() {
load_xml(&s, "Interface\\FrameXML\\UIPanelTemplates.xml");
load_xml(&s, "UiPanels.xml");
load_xml(&s, r"Interface\FrameXML\LocaleProperties.lua");
load_xml(&s, r"Interface\FrameXML\StaticPopup.xml");
load_xml(&s, "Interface\\FrameXML\\FloatingChatFrame.xml");
s.set_loot_rolls(rolls());
@@ -24,12 +24,14 @@ fn setup() -> UiScript {
r"Interface\FrameXML\UIPanelTemplates.lua",
r"Interface\FrameXML\UIPanelTemplates.xml",
"ScrollTemplates.xml",
"MoneyFrame.xml",
r"Interface\FrameXML\MoneyFrame.lua",
r"Interface\FrameXML\MoneyFrame.xml",
"GameTooltip.xml",
// Before UiPanels.xml: the shared StaticPopup carries a `SmallMoneyFrameTemplate` coin
// row, whose OnLoad calls `SmallMoneyFrame_OnLoad` — the TOC's own order (1580's
// talent-wipe fixture hit this first).
"MoneyFrame.xml",
r"Interface\FrameXML\MoneyFrame.lua",
r"Interface\FrameXML\MoneyFrame.xml",
"Interface\\FrameXML\\GlobalStrings.lua",
"UiPanels.xml",
"Interface\\FrameXML\\LocaleProperties.lua",
@@ -369,7 +369,8 @@ fn shown_inspect_honor_page() -> UiScript {
"Interface\\FrameXML\\Fonts.xml",
// `TEXT`, the stock level line's formatter.
"Interface\\FrameXML\\BasicControls.xml",
"MoneyFrame.xml",
r"Interface\FrameXML\MoneyFrame.lua",
r"Interface\FrameXML\MoneyFrame.xml",
"UiPanels.xml",
r"Interface\FrameXML\UIPanelTemplates.lua",
r"Interface\FrameXML\UIPanelTemplates.xml",
@@ -117,7 +117,8 @@ fn armed() -> UiScript {
load_xml(&s, "Interface\\FrameXML\\Fonts.xml");
// `TEXT`, which the stock `InspectPaperDollFrame_SetLevel` formats its level line through.
load_xml(&s, "Interface\\FrameXML\\BasicControls.xml");
load_xml(&s, "MoneyFrame.xml");
load_xml(&s, r"Interface\FrameXML\MoneyFrame.lua");
load_xml(&s, r"Interface\FrameXML\MoneyFrame.xml");
load_xml(&s, "UiPanels.xml");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
@@ -166,7 +167,8 @@ fn shipped_inspect_frame_loads_clean() {
load_xml(&s, "Interface\\FrameXML\\Fonts.xml");
// `TEXT`, which the stock `InspectPaperDollFrame_SetLevel` formats its level line through.
load_xml(&s, "Interface\\FrameXML\\BasicControls.xml");
load_xml(&s, "MoneyFrame.xml");
load_xml(&s, r"Interface\FrameXML\MoneyFrame.lua");
load_xml(&s, r"Interface\FrameXML\MoneyFrame.xml");
load_xml(&s, "UiPanels.xml");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
@@ -89,7 +89,8 @@ fn the_self_menu_row_gates_on_the_binding_and_confirms_before_sending() {
for file in [
"Interface\\FrameXML\\Fonts.xml",
"UIParent.xml",
"MoneyFrame.xml",
r"Interface\FrameXML\MoneyFrame.lua",
r"Interface\FrameXML\MoneyFrame.xml",
"UiPanels.xml",
r"Interface\FrameXML\UIPanelTemplates.lua",
r"Interface\FrameXML\UIPanelTemplates.xml",
@@ -45,7 +45,8 @@ pub(crate) fn harness() -> UiScript {
s.set_screen_size(1024.0, 768.0);
for file in [
"Interface\\FrameXML\\Fonts.xml",
"MoneyFrame.xml",
r"Interface\FrameXML\MoneyFrame.lua",
r"Interface\FrameXML\MoneyFrame.xml",
"UiPanels.xml",
r"Interface\FrameXML\UIPanelTemplates.lua",
r"Interface\FrameXML\UIPanelTemplates.xml",
@@ -478,8 +478,6 @@ fn shipped_loot_pushed_to_center_by_merchant() {
load_xml(&s, f);
}
load_xml(&s, "Interface\\FrameXML\\LootFrame.xml");
load_xml(&s, "MoneyFrame.xml");
load_xml(&s, "GameTooltip.xml"); // app load order: tooltip before merchant
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
// Loot opens onto the empty left slot.
@@ -621,7 +619,6 @@ fn ctrl_and_shift_on_a_loot_row_preview_and_post_without_looting() {
"Interface\\FrameXML\\UIPanelTemplates.xml",
"UiPanels.xml",
"Interface\\FrameXML\\LocaleProperties.lua",
"Interface\\FrameXML\\StaticPopup.xml",
"Interface\\FrameXML\\FloatingChatFrame.xml",
] {
load_xml(&s, file);
@@ -91,7 +91,8 @@ fn harness_with(player: &str) -> UiScript {
r"Interface\FrameXML\GlobalStrings.lua",
"Interface\\FrameXML\\Fonts.xml",
"Interface\\FrameXML\\BasicControls.xml", // `TEXT`
"MoneyFrame.xml",
r"Interface\FrameXML\MoneyFrame.lua",
r"Interface\FrameXML\MoneyFrame.xml",
"UiPanels.xml",
"Interface\\FrameXML\\LocaleProperties.lua",
"Interface\\FrameXML\\StaticPopup.xml",
@@ -1081,7 +1081,7 @@ fn trade_recipient_money_renders_the_digit_not_ellipsis() {
// TradeFrame's money entry is the chain's own since 1882 — its OnLoad calls MoneyInputFrame_*.
load_xml(&s, "Interface\\FrameXML\\MoneyInputFrame.lua");
load_xml(&s, "Interface\\FrameXML\\MoneyInputFrame.xml");
load_xml(&s, "TradeFrame.xml");
load_xml(&s, "Interface\\FrameXML\\TradeFrame.xml");
s.set_text_measurer(Box::new(super::FixedWidthFont(6.0)));
let target = benilla_ui::script::TradeSideState {
@@ -1144,11 +1144,8 @@ fn ctrl_and_shift_on_a_vendor_row_preview_and_post_without_buying() {
"Interface\\FrameXML\\BasicControls.xml",
"Interface\\FrameXML\\ChatFrame.xml",
"Interface\\FrameXML\\UIDropDownMenu.xml",
"Interface\\FrameXML\\UIPanelTemplates.lua",
"Interface\\FrameXML\\UIPanelTemplates.xml",
"UiPanels.xml",
"Interface\\FrameXML\\LocaleProperties.lua",
"Interface\\FrameXML\\StaticPopup.xml",
// (the panel templates, UiPanels and the dialog engine are MERCHANT_UI's already — a
// second load of the stock money kit's frames trips its own global-named update)
"Interface\\FrameXML\\FloatingChatFrame.xml",
] {
load_xml(&s, file);
@@ -30,7 +30,8 @@ fn harness_with(extra: &[&str]) -> UiScript {
"UIParent.xml",
"Interface\\FrameXML\\GlobalStrings.lua",
"Interface\\FrameXML\\MainMenuBar.xml",
"MoneyFrame.xml",
r"Interface\FrameXML\MoneyFrame.lua",
r"Interface\FrameXML\MoneyFrame.xml",
"GameTooltip.xml",
"Interface\\FrameXML\\ActionBarFrame.xml",
"Interface\\FrameXML\\BonusActionBarFrame.xml",
@@ -255,7 +256,7 @@ fn the_talent_button_appears_at_level_ten_and_the_row_closes_up_below_it() {
/// single missing argument is invisible until someone hovers that one button.
#[test]
fn every_micro_button_hovers_with_its_reference_explanation() {
let mut s = harness_with(&["MoneyFrame.xml", "UIParent.xml", "GameTooltip.xml"]);
let mut s = harness_with(&["UIParent.xml"]);
set_player_level(&mut s, 10);
s.resolve();
@@ -24,7 +24,8 @@ fn minimap_zoom_buttons_resync_when_switching_inside_and_outside() {
load_xml(&s, "Interface\\FrameXML\\Fonts.xml");
// The shipped load order provides GameTooltip before the cluster; Minimap_Update's tooltip
// half (the PVP tint slice, decision 0287) touches it from OnLoad on.
load_xml(&s, "MoneyFrame.xml");
load_xml(&s, r"Interface\FrameXML\MoneyFrame.lua");
load_xml(&s, r"Interface\FrameXML\MoneyFrame.xml");
load_xml(&s, "GameTooltip.xml");
load_xml(&s, "MinimapCluster.xml");
@@ -88,7 +89,8 @@ fn tracking_frame_follows_get_tracking_texture_across_player_auras_changed() {
.unwrap();
s.run("function PlaySound() end").unwrap();
load_xml(&s, "Interface\\FrameXML\\Fonts.xml");
load_xml(&s, "MoneyFrame.xml");
load_xml(&s, r"Interface\FrameXML\MoneyFrame.lua");
load_xml(&s, r"Interface\FrameXML\MoneyFrame.xml");
load_xml(&s, "GameTooltip.xml");
load_xml(&s, "MinimapCluster.xml");
@@ -137,7 +139,8 @@ fn game_time_session(hour: u32, minute: u32) -> UiScript {
load_xml(&s, "Interface\\FrameXML\\Fonts.xml");
// `TEXT()`, which the reference's own tooltip and label formatting passes every string through.
load_xml(&s, "Interface\\FrameXML\\BasicControls.xml");
load_xml(&s, "MoneyFrame.xml");
load_xml(&s, r"Interface\FrameXML\MoneyFrame.lua");
load_xml(&s, r"Interface\FrameXML\MoneyFrame.xml");
load_xml(&s, "GameTooltip.xml");
load_xml(&s, "MinimapCluster.xml");
load_xml(&s, "Interface\\FrameXML\\GameTime.xml");
@@ -249,7 +252,8 @@ fn a_click_on_the_minimap_parks_a_centre_relative_ping_request() {
.unwrap();
s.run("function PlaySound() end").unwrap();
load_xml(&s, "Interface\\FrameXML\\Fonts.xml");
load_xml(&s, "MoneyFrame.xml");
load_xml(&s, r"Interface\FrameXML\MoneyFrame.lua");
load_xml(&s, r"Interface\FrameXML\MoneyFrame.xml");
load_xml(&s, "GameTooltip.xml");
load_xml(&s, "MinimapCluster.xml");
s.resolve();
@@ -294,7 +298,8 @@ fn get_ping_position_is_nil_until_there_is_a_ping() {
.unwrap();
s.run("function PlaySound() end").unwrap();
load_xml(&s, "Interface\\FrameXML\\Fonts.xml");
load_xml(&s, "MoneyFrame.xml");
load_xml(&s, r"Interface\FrameXML\MoneyFrame.lua");
load_xml(&s, r"Interface\FrameXML\MoneyFrame.xml");
load_xml(&s, "GameTooltip.xml");
load_xml(&s, "MinimapCluster.xml");
@@ -23,7 +23,8 @@ fn harness() -> UiScript {
// `STATICPOPUP_NUMDIALOGS`, which the reference's own `MirrorTimer_Show` bounds its free-bar
// search by (MirrorTimer.lua l.32 — a copy-paste from StaticPopup.lua, and its own bug). A
// session without it searches `1, nil` and finds no free bar at all, so this is not scenery.
load_xml(&s, "MoneyFrame.xml");
load_xml(&s, r"Interface\FrameXML\MoneyFrame.lua");
load_xml(&s, r"Interface\FrameXML\MoneyFrame.xml");
load_xml(&s, "UiPanels.xml");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
@@ -27,7 +27,9 @@ fn harness(money: u64) -> UiScript {
// every expected number readable as `digits x 8 + icon`.
s.set_text_measurer(Box::new(super::FixedWidthFont(8.0)));
load_xml(&s, "Interface\\FrameXML\\Fonts.xml");
load_xml(&s, "MoneyFrame.xml");
load_xml(&s, "Interface\\FrameXML\\BasicControls.xml"); // `message`, the kit's own error path
load_xml(&s, r"Interface\FrameXML\MoneyFrame.lua");
load_xml(&s, r"Interface\FrameXML\MoneyFrame.xml");
let doc = benilla_ui::framexml::parse(
r#"<Ui>
@@ -375,8 +377,9 @@ fn the_chains_money_input_frame_splits_an_amount_across_its_three_boxes() {
let _data = benilla_formats::wow_data_or_skip!();
let s = UiScript::new().unwrap();
load_xml(&s, "Interface\\FrameXML\\Fonts.xml");
load_xml(&s, "MoneyFrame.xml"); // COPPER_PER_GOLD / COPPER_PER_SILVER live here
// The manifest's own order: the `.lua` brings the ten verbs, the `.xml` the template.
load_xml(&s, r"Interface\FrameXML\MoneyFrame.lua"); // COPPER_PER_GOLD / COPPER_PER_SILVER live here
load_xml(&s, r"Interface\FrameXML\MoneyFrame.xml");
// The manifest's own order: the `.lua` brings the ten verbs, the `.xml` the template.
load_xml(&s, r"Interface\FrameXML\MoneyInputFrame.lua");
load_xml(&s, r"Interface\FrameXML\MoneyInputFrame.xml");
@@ -20,7 +20,8 @@ fn load_action_bar(s: &UiScript) {
for file in [
"Interface\\FrameXML\\Fonts.xml",
"UIParent.xml",
"MoneyFrame.xml",
r"Interface\FrameXML\MoneyFrame.lua",
r"Interface\FrameXML\MoneyFrame.xml",
"GameTooltip.xml",
"Cooldown.xml",
"Interface\\FrameXML\\ActionButtonTemplate.xml",
@@ -278,8 +279,6 @@ fn shipped_stance_bar_drives_end_to_end() {
// other state.
load_xml(&s, "Cooldown.xml");
load_xml(&s, "Interface\\FrameXML\\ActionButtonTemplate.xml");
load_xml(&s, "MoneyFrame.xml");
load_xml(&s, "GameTooltip.xml");
load_xml(&s, "Interface\\FrameXML\\Fonts.xml");
load_xml(&s, "UIParent.xml");
load_xml(&s, "UiPanels.xml");
@@ -290,7 +289,6 @@ fn shipped_stance_bar_drives_end_to_end() {
load_xml(&s, r"Interface\FrameXML\GlobalStrings.lua");
load_xml(&s, r"Interface\FrameXML\BasicControls.xml");
load_xml(&s, r"Interface\FrameXML\LocaleProperties.lua");
load_xml(&s, r"Interface\FrameXML\StaticPopup.xml");
load_xml(&s, "KeyBindingsPage.xml");
load_xml(&s, "OptionsFrame.xml");
load_xml(&s, "Interface\\FrameXML\\MultiActionBars.xml");
@@ -466,7 +464,8 @@ fn multibar_hover_renders_the_buttons_own_action() {
for file in [
"Interface\\FrameXML\\Fonts.xml",
"UIParent.xml",
"MoneyFrame.xml",
r"Interface\FrameXML\MoneyFrame.lua",
r"Interface\FrameXML\MoneyFrame.xml",
"GameTooltip.xml",
"Cooldown.xml",
"Interface\\FrameXML\\ActionButtonTemplate.xml",
@@ -585,8 +584,6 @@ fn the_vertical_multibars_exist_hidden_on_the_reference_pages() {
load_action_bar(&s);
load_xml(&s, "Cooldown.xml");
load_xml(&s, "Interface\\FrameXML\\ActionButtonTemplate.xml");
load_xml(&s, "MoneyFrame.xml");
load_xml(&s, "GameTooltip.xml");
load_xml(&s, "Interface\\FrameXML\\Fonts.xml");
load_xml(&s, "UIParent.xml");
load_xml(&s, "UiPanels.xml");
@@ -597,7 +594,6 @@ fn the_vertical_multibars_exist_hidden_on_the_reference_pages() {
load_xml(&s, r"Interface\FrameXML\GlobalStrings.lua");
load_xml(&s, r"Interface\FrameXML\BasicControls.xml");
load_xml(&s, r"Interface\FrameXML\LocaleProperties.lua");
load_xml(&s, r"Interface\FrameXML\StaticPopup.xml");
load_xml(&s, "KeyBindingsPage.xml");
load_xml(&s, "OptionsFrame.xml");
load_xml(&s, "Interface\\FrameXML\\MultiActionBars.xml");
@@ -651,8 +647,6 @@ fn every_extra_bar_stays_down_until_its_own_toggle_is_set() {
load_action_bar(&s);
load_xml(&s, "Cooldown.xml");
load_xml(&s, "Interface\\FrameXML\\ActionButtonTemplate.xml");
load_xml(&s, "MoneyFrame.xml");
load_xml(&s, "GameTooltip.xml");
load_xml(&s, "Interface\\FrameXML\\Fonts.xml");
load_xml(&s, "UIParent.xml");
load_xml(&s, "UiPanels.xml");
@@ -663,7 +657,6 @@ fn every_extra_bar_stays_down_until_its_own_toggle_is_set() {
load_xml(&s, r"Interface\FrameXML\GlobalStrings.lua");
load_xml(&s, r"Interface\FrameXML\BasicControls.xml");
load_xml(&s, r"Interface\FrameXML\LocaleProperties.lua");
load_xml(&s, r"Interface\FrameXML\StaticPopup.xml");
load_xml(&s, "KeyBindingsPage.xml");
load_xml(&s, "OptionsFrame.xml");
load_xml(&s, "Interface\\FrameXML\\MultiActionBars.xml");
@@ -742,8 +735,6 @@ fn raising_a_bottom_bar_moves_the_managed_bottom_stack() {
load_action_bar(&s);
load_xml(&s, "Cooldown.xml");
load_xml(&s, "Interface\\FrameXML\\ActionButtonTemplate.xml");
load_xml(&s, "MoneyFrame.xml");
load_xml(&s, "GameTooltip.xml");
load_xml(&s, "Interface\\FrameXML\\Fonts.xml");
load_xml(&s, "UIParent.xml");
load_xml(&s, "UiPanels.xml");
@@ -754,7 +745,6 @@ fn raising_a_bottom_bar_moves_the_managed_bottom_stack() {
load_xml(&s, r"Interface\FrameXML\GlobalStrings.lua");
load_xml(&s, r"Interface\FrameXML\BasicControls.xml");
load_xml(&s, r"Interface\FrameXML\LocaleProperties.lua");
load_xml(&s, r"Interface\FrameXML\StaticPopup.xml");
load_xml(&s, "KeyBindingsPage.xml");
load_xml(&s, "OptionsFrame.xml");
load_xml(&s, "Interface\\FrameXML\\MultiActionBars.xml");
@@ -810,8 +800,6 @@ fn viewable_action_bar_pages_follow_the_bar_toggles() {
load_action_bar(&s);
load_xml(&s, "Cooldown.xml");
load_xml(&s, "Interface\\FrameXML\\ActionButtonTemplate.xml");
load_xml(&s, "MoneyFrame.xml");
load_xml(&s, "GameTooltip.xml");
load_xml(&s, "Interface\\FrameXML\\Fonts.xml");
load_xml(&s, "UIParent.xml");
load_xml(&s, "UiPanels.xml");
@@ -822,7 +810,6 @@ fn viewable_action_bar_pages_follow_the_bar_toggles() {
load_xml(&s, r"Interface\FrameXML\GlobalStrings.lua");
load_xml(&s, r"Interface\FrameXML\BasicControls.xml");
load_xml(&s, r"Interface\FrameXML\LocaleProperties.lua");
load_xml(&s, r"Interface\FrameXML\StaticPopup.xml");
load_xml(&s, "KeyBindingsPage.xml");
load_xml(&s, "OptionsFrame.xml");
load_xml(&s, "Interface\\FrameXML\\MultiActionBars.xml");
@@ -880,8 +867,6 @@ fn the_grid_option_holds_the_extra_bars_empty_wells_open() {
load_action_bar(&s);
load_xml(&s, "Cooldown.xml");
load_xml(&s, "Interface\\FrameXML\\ActionButtonTemplate.xml");
load_xml(&s, "MoneyFrame.xml");
load_xml(&s, "GameTooltip.xml");
load_xml(&s, "Interface\\FrameXML\\Fonts.xml");
load_xml(&s, "UIParent.xml");
load_xml(&s, "UiPanels.xml");
@@ -892,7 +877,6 @@ fn the_grid_option_holds_the_extra_bars_empty_wells_open() {
load_xml(&s, r"Interface\FrameXML\GlobalStrings.lua");
load_xml(&s, r"Interface\FrameXML\BasicControls.xml");
load_xml(&s, r"Interface\FrameXML\LocaleProperties.lua");
load_xml(&s, r"Interface\FrameXML\StaticPopup.xml");
load_xml(&s, "KeyBindingsPage.xml");
load_xml(&s, "OptionsFrame.xml");
load_xml(&s, "Interface\\FrameXML\\MultiActionBars.xml");
@@ -987,8 +971,6 @@ fn a_held_payload_ghosts_the_empty_wells_it_opens() {
load_action_bar(&s);
load_xml(&s, "Cooldown.xml");
load_xml(&s, "Interface\\FrameXML\\ActionButtonTemplate.xml");
load_xml(&s, "MoneyFrame.xml");
load_xml(&s, "GameTooltip.xml");
load_xml(&s, "Interface\\FrameXML\\Fonts.xml");
load_xml(&s, "UIParent.xml");
load_xml(&s, "UiPanels.xml");
@@ -999,7 +981,6 @@ fn a_held_payload_ghosts_the_empty_wells_it_opens() {
load_xml(&s, r"Interface\FrameXML\GlobalStrings.lua");
load_xml(&s, r"Interface\FrameXML\BasicControls.xml");
load_xml(&s, r"Interface\FrameXML\LocaleProperties.lua");
load_xml(&s, r"Interface\FrameXML\StaticPopup.xml");
load_xml(&s, "KeyBindingsPage.xml");
load_xml(&s, "OptionsFrame.xml");
load_xml(&s, "Interface\\FrameXML\\MultiActionBars.xml");
@@ -1134,8 +1115,6 @@ fn a_bar_toggle_sends_the_byte_its_globals_pack_to() {
load_action_bar(&s);
load_xml(&s, "Cooldown.xml");
load_xml(&s, "Interface\\FrameXML\\ActionButtonTemplate.xml");
load_xml(&s, "MoneyFrame.xml");
load_xml(&s, "GameTooltip.xml");
load_xml(&s, "Interface\\FrameXML\\Fonts.xml");
load_xml(&s, "UIParent.xml");
load_xml(&s, "UiPanels.xml");
@@ -1146,7 +1125,6 @@ fn a_bar_toggle_sends_the_byte_its_globals_pack_to() {
load_xml(&s, r"Interface\FrameXML\GlobalStrings.lua");
load_xml(&s, r"Interface\FrameXML\BasicControls.xml");
load_xml(&s, r"Interface\FrameXML\LocaleProperties.lua");
load_xml(&s, r"Interface\FrameXML\StaticPopup.xml");
load_xml(&s, "KeyBindingsPage.xml");
load_xml(&s, "OptionsFrame.xml");
load_xml(&s, "Interface\\FrameXML\\MultiActionBars.xml");
@@ -1225,8 +1203,6 @@ fn the_shipped_setter_passes_exactly_four_arguments() {
load_action_bar(&s);
load_xml(&s, "Cooldown.xml");
load_xml(&s, "Interface\\FrameXML\\ActionButtonTemplate.xml");
load_xml(&s, "MoneyFrame.xml");
load_xml(&s, "GameTooltip.xml");
load_xml(&s, "Interface\\FrameXML\\Fonts.xml");
load_xml(&s, "UIParent.xml");
load_xml(&s, "UiPanels.xml");
@@ -1237,7 +1213,6 @@ fn the_shipped_setter_passes_exactly_four_arguments() {
load_xml(&s, r"Interface\FrameXML\GlobalStrings.lua");
load_xml(&s, r"Interface\FrameXML\BasicControls.xml");
load_xml(&s, r"Interface\FrameXML\LocaleProperties.lua");
load_xml(&s, r"Interface\FrameXML\StaticPopup.xml");
load_xml(&s, "KeyBindingsPage.xml");
load_xml(&s, "OptionsFrame.xml");
load_xml(&s, "Interface\\FrameXML\\MultiActionBars.xml");
@@ -1281,8 +1256,6 @@ fn the_stance_bar_sits_where_the_pass_puts_it() {
load_action_bar(&s);
load_xml(&s, "Cooldown.xml");
load_xml(&s, "Interface\\FrameXML\\ActionButtonTemplate.xml");
load_xml(&s, "MoneyFrame.xml");
load_xml(&s, "GameTooltip.xml");
load_xml(&s, "Interface\\FrameXML\\Fonts.xml");
load_xml(&s, "UIParent.xml");
load_xml(&s, "UiPanels.xml");
@@ -1293,7 +1266,6 @@ fn the_stance_bar_sits_where_the_pass_puts_it() {
load_xml(&s, r"Interface\FrameXML\GlobalStrings.lua");
load_xml(&s, r"Interface\FrameXML\BasicControls.xml");
load_xml(&s, r"Interface\FrameXML\LocaleProperties.lua");
load_xml(&s, r"Interface\FrameXML\StaticPopup.xml");
load_xml(&s, "KeyBindingsPage.xml");
load_xml(&s, "OptionsFrame.xml");
load_xml(&s, "Interface\\FrameXML\\MultiActionBars.xml");
@@ -1357,8 +1329,6 @@ fn the_stance_shelf_follows_the_bottom_left_bar() {
load_action_bar(&s);
load_xml(&s, "Cooldown.xml");
load_xml(&s, "Interface\\FrameXML\\ActionButtonTemplate.xml");
load_xml(&s, "MoneyFrame.xml");
load_xml(&s, "GameTooltip.xml");
load_xml(&s, "Interface\\FrameXML\\Fonts.xml");
load_xml(&s, "UIParent.xml");
load_xml(&s, "UiPanels.xml");
@@ -1369,7 +1339,6 @@ fn the_stance_shelf_follows_the_bottom_left_bar() {
load_xml(&s, r"Interface\FrameXML\GlobalStrings.lua");
load_xml(&s, r"Interface\FrameXML\BasicControls.xml");
load_xml(&s, r"Interface\FrameXML\LocaleProperties.lua");
load_xml(&s, r"Interface\FrameXML\StaticPopup.xml");
load_xml(&s, "KeyBindingsPage.xml");
load_xml(&s, "OptionsFrame.xml");
load_xml(&s, "Interface\\FrameXML\\MultiActionBars.xml");
@@ -1482,8 +1451,6 @@ fn the_stance_shelf_is_as_long_as_the_form_count() {
load_action_bar(&s);
load_xml(&s, "Cooldown.xml");
load_xml(&s, "Interface\\FrameXML\\ActionButtonTemplate.xml");
load_xml(&s, "MoneyFrame.xml");
load_xml(&s, "GameTooltip.xml");
load_xml(&s, "Interface\\FrameXML\\Fonts.xml");
load_xml(&s, "UIParent.xml");
load_xml(&s, "UiPanels.xml");
@@ -1494,7 +1461,6 @@ fn the_stance_shelf_is_as_long_as_the_form_count() {
load_xml(&s, r"Interface\FrameXML\GlobalStrings.lua");
load_xml(&s, r"Interface\FrameXML\BasicControls.xml");
load_xml(&s, r"Interface\FrameXML\LocaleProperties.lua");
load_xml(&s, r"Interface\FrameXML\StaticPopup.xml");
load_xml(&s, "KeyBindingsPage.xml");
load_xml(&s, "OptionsFrame.xml");
load_xml(&s, "Interface\\FrameXML\\MultiActionBars.xml");
@@ -1603,8 +1569,6 @@ fn an_extra_bars_empty_well_keeps_its_bound_hotkey_label() {
load_action_bar(&s);
load_xml(&s, "Cooldown.xml");
load_xml(&s, "Interface\\FrameXML\\ActionButtonTemplate.xml");
load_xml(&s, "MoneyFrame.xml");
load_xml(&s, "GameTooltip.xml");
load_xml(&s, "Interface\\FrameXML\\Fonts.xml");
load_xml(&s, "UIParent.xml");
load_xml(&s, "UiPanels.xml");
@@ -1615,7 +1579,6 @@ fn an_extra_bars_empty_well_keeps_its_bound_hotkey_label() {
load_xml(&s, r"Interface\FrameXML\GlobalStrings.lua");
load_xml(&s, r"Interface\FrameXML\BasicControls.xml");
load_xml(&s, r"Interface\FrameXML\LocaleProperties.lua");
load_xml(&s, r"Interface\FrameXML\StaticPopup.xml");
load_xml(&s, "KeyBindingsPage.xml");
load_xml(&s, "OptionsFrame.xml");
load_xml(&s, "Interface\\FrameXML\\MultiActionBars.xml");
@@ -36,12 +36,23 @@ fn harness() -> UiScript {
/// before it for the kit's TOOLTIP_DEFAULT_COLOR — the app's own order.
fn harness_on(mut s: UiScript) -> UiScript {
s.set_screen_size(1024.0, 768.0);
// A file loads ONCE, as in the client: the definer harnesses below load some of these ahead
// of this list to capture file-scope values, and a second load of the stock money kit's
// frames trips its own global-named update (`MoneyFrame_Update` writes the amount onto
// `getglobal(name)` — the first instance — while the second instance reads its own).
let loaded = |s: &UiScript, file: &str| -> bool {
s.eval::<bool>(&format!(
"return (BENILLA_TEST_LOADED or {{}})[{file:?}] == true"
))
.unwrap_or(false)
};
for file in [
"Interface\\FrameXML\\Fonts.xml",
// Every panel window declares `parent="UIParent"`, resolved at LOAD — so UIParent has to
// exist by the time they are read, exactly as it does in the manifest (decision 1734).
"UIParent.xml",
"MoneyFrame.xml",
r"Interface\FrameXML\MoneyFrame.lua",
r"Interface\FrameXML\MoneyFrame.xml",
"UiPanels.xml",
r"Interface\FrameXML\UIPanelTemplates.lua",
r"Interface\FrameXML\UIPanelTemplates.xml",
@@ -64,6 +75,9 @@ fn harness_on(mut s: UiScript) -> UiScript {
// here is what made this kit break the moment a file it loads migrated (1751).
// The dialect announces DROPPED subtrees as warnings, not errors — for the new file,
// a warning is a silently-missing piece of chrome, so it fails there.
if loaded(&s, file) {
continue;
}
if file == "OptionsFrame.xml" {
super::test_ui::load_ui_strict(&s, file);
} else {
@@ -680,15 +694,17 @@ fn audio_harness() -> UiScript {
s
}
/// The Combat page's harness (decision 1134): the **real** `CombatText.xml` loaded ahead of the
/// window, in the manifest's own order (it is file 59, OptionsFrame.xml file 281), so the rows
/// capture the same file-scope defaults and the same `applyFunc` they capture in the client —
/// nothing about the family is restated here. `UIParent.xml` comes first because CombatText's
/// strings anchor to it.
/// The Combat page's harness (decision 1134, 1964): the family's defaults are the window's own
/// file-scope block (the reference's UIOptionsFrame.lua l.135-152), and the addon they drive is
/// `Blizzard_CombatText` off the chain, loaded on demand by the master row's apply.
fn combat_harness() -> UiScript {
let mut s = audio_harness();
s.set_screen_size(1024.0, 768.0);
load_definers(&s, &["UIParent.xml", "CombatText.xml"]);
load_definers(&s, &["UIParent.xml"]);
// The family's definers are the options window's own since 1964 (the reference's
// UIOptionsFrame.lua block); the addon itself is LoadOnDemand off the chain, seated so the
// master row's apply can load it the way the client does.
super::test_ui::seat_chain_addon(&mut s, "Blizzard_CombatText");
harness_on(s)
}
@@ -698,9 +714,12 @@ fn combat_harness() -> UiScript {
/// exactly the ordering the real manifest guarantees.
fn load_definers(s: &UiScript, files: &[&str]) {
for file in files {
// Both stores: a definer can be the reference's own file now (BuffFrame.xml since 1751
// window 18), and `test_ui::load_ui` is the reader that speaks either.
super::test_ui::load_ui(s, file);
// Remembered so `harness_on` loads each file once (see there).
s.run(&format!(
"BENILLA_TEST_LOADED = BENILLA_TEST_LOADED or {{}} BENILLA_TEST_LOADED[{file:?}] = true"
))
.unwrap();
}
}
@@ -721,7 +740,8 @@ fn interface_harness() -> UiScript {
&s,
&[
"Interface\\FrameXML\\Fonts.xml",
"MoneyFrame.xml",
r"Interface\FrameXML\MoneyFrame.lua",
r"Interface\FrameXML\MoneyFrame.xml",
"Interface\\FrameXML\\GlobalStrings.lua",
"UiPanels.xml",
r"Interface\FrameXML\UIPanelTemplates.lua",
@@ -796,7 +816,8 @@ fn chat_harness() -> UiScript {
&s,
&[
"Interface\\FrameXML\\Fonts.xml",
"MoneyFrame.xml",
r"Interface\FrameXML\MoneyFrame.lua",
r"Interface\FrameXML\MoneyFrame.xml",
"UiPanels.xml",
r"Interface\FrameXML\UIPanelTemplates.lua",
r"Interface\FrameXML\UIPanelTemplates.xml",
@@ -838,7 +859,8 @@ fn actionbars_harness() -> UiScript {
"Interface\\FrameXML\\TextStatusBar.xml",
"Interface\\FrameXML\\GlobalStrings.lua",
"Interface\\FrameXML\\MainMenuBar.xml",
"MoneyFrame.xml",
r"Interface\FrameXML\MoneyFrame.lua",
r"Interface\FrameXML\MoneyFrame.xml",
"GameTooltip.xml",
"Interface\\FrameXML\\ActionBarFrame.xml",
"Interface\\FrameXML\\BonusActionBarFrame.xml",
@@ -3150,6 +3172,9 @@ fn a_saved_switch_with_a_side_effect_is_applied_when_the_variables_land() {
// What the saved chunk does, verbatim: assign over the file-scope default, then the event.
s.run("SHOW_COMBAT_TEXT = \"1\"").unwrap();
s.fire_event("VARIABLES_LOADED", vec![]);
// The walk loaded the addon (1964); a frame passes before any message can arrive, and the
// stock message placement reads the strings' screen positions, which that frame lays out.
s.resolve();
s.fire_event(
"COMBAT_TEXT_UPDATE",
@@ -45,7 +45,8 @@ fn harness() -> UiScript {
for file in [
"Interface\\FrameXML\\Fonts.xml",
"UIParent.xml",
"MoneyFrame.xml",
r"Interface\FrameXML\MoneyFrame.lua",
r"Interface\FrameXML\MoneyFrame.xml",
"UiPanels.xml",
"GameTooltip.xml",
"ScrollTemplates.xml",
+26 -25
View File
@@ -48,7 +48,8 @@ fn shipped_gossip_frame_drives_end_to_end() {
// `shipped_gossip_rows_grow_to_their_wrapped_labels` for why this is a harness gap and not an
// engine one.
s.set_text_measurer(Box::new(super::FixedWidthFont(6.0)));
load_xml(&s, "MoneyFrame.xml");
load_xml(&s, r"Interface\FrameXML\MoneyFrame.lua");
load_xml(&s, r"Interface\FrameXML\MoneyFrame.xml");
load_xml(&s, "UiPanels.xml");
load_xml(&s, "ScrollTemplates.xml"); // the shared scroll kit the window rides
// UIPanelScrollFrameTemplate lives here, and the gossip scroll frame inherits it. NOT
@@ -253,7 +254,8 @@ fn shipped_gossip_frame_renders_quest_rows_above_options() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = UiScript::new().unwrap();
s.set_screen_size(1024.0, 768.0);
load_xml(&s, "MoneyFrame.xml");
load_xml(&s, r"Interface\FrameXML\MoneyFrame.lua");
load_xml(&s, r"Interface\FrameXML\MoneyFrame.xml");
load_xml(&s, "UiPanels.xml");
load_xml(&s, "ScrollTemplates.xml"); // the shared scroll kit the window rides
// UIPanelScrollFrameTemplate lives here, and the gossip scroll frame inherits it. NOT
@@ -416,7 +418,8 @@ fn shipped_gossip_rows_grow_to_their_wrapped_labels() {
// (`ui_script::extract`'s `AtlasMeasurer`); only a bare test VM does not. The gap was in the
// harness, and it hid behind a window we had written to measure a frame later.
s.set_text_measurer(Box::new(super::FixedWidthFont(6.0)));
load_xml(&s, "MoneyFrame.xml");
load_xml(&s, r"Interface\FrameXML\MoneyFrame.lua");
load_xml(&s, r"Interface\FrameXML\MoneyFrame.xml");
load_xml(&s, "UiPanels.xml");
load_xml(&s, "ScrollTemplates.xml"); // the shared scroll kit the window rides
// UIPanelScrollFrameTemplate lives here, and the gossip scroll frame inherits it. NOT
@@ -534,7 +537,8 @@ fn gossip_show_hide_plays_open_and_close_kits() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = UiScript::new().unwrap();
s.set_screen_size(1024.0, 768.0);
load_xml(&s, "MoneyFrame.xml");
load_xml(&s, r"Interface\FrameXML\MoneyFrame.lua");
load_xml(&s, r"Interface\FrameXML\MoneyFrame.xml");
load_xml(&s, "UiPanels.xml");
load_xml(&s, "ScrollTemplates.xml"); // the shared scroll kit the window rides
// UIPanelScrollFrameTemplate lives here, and the gossip scroll frame inherits it. NOT
@@ -582,7 +586,6 @@ fn shipped_panel_slot_replaces_gossip_with_merchant() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = UiScript::new().unwrap();
s.set_screen_size(1024.0, 768.0);
load_xml(&s, "MoneyFrame.xml");
load_xml(&s, "UiPanels.xml");
load_xml(&s, "ScrollTemplates.xml"); // the shared scroll kit the window rides
// UIPanelScrollFrameTemplate lives here, and the gossip scroll frame inherits it. NOT
@@ -593,12 +596,10 @@ fn shipped_panel_slot_replaces_gossip_with_merchant() {
load_xml(&s, r"Interface\FrameXML\GlobalStrings.lua");
load_xml(&s, r"Interface\FrameXML\BasicControls.xml");
load_xml(&s, r"Interface\FrameXML\LocaleProperties.lua");
load_xml(&s, r"Interface\FrameXML\StaticPopup.xml");
load_xml(&s, "UIParent.xml");
load_xml(&s, "Interface\\FrameXML\\GossipFrame.xml");
load_xml(&s, "GameTooltip.xml"); // app load order: tooltip before merchant
// The vendor window is the reference's own since 1751, and its `MerchantFrame_UpdateMerchantInfo`
// calls `TEXT()` while building every row — see `test_ui::MERCHANT_UI` for the rest.
// The vendor window is the reference's own since 1751, and its `MerchantFrame_UpdateMerchantInfo`
// calls `TEXT()` while building every row — see `test_ui::MERCHANT_UI` for the rest.
for f in super::test_ui::MERCHANT_UI {
load_xml(&s, f);
}
@@ -681,7 +682,6 @@ fn displacing_an_npc_window_ends_the_displaced_session() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = UiScript::new().unwrap();
s.set_screen_size(1024.0, 768.0);
load_xml(&s, "MoneyFrame.xml");
load_xml(&s, "UiPanels.xml");
load_xml(&s, "ScrollTemplates.xml"); // the shared scroll kit the window rides
// UIPanelScrollFrameTemplate lives here, and the gossip scroll frame inherits it. NOT
@@ -692,12 +692,10 @@ fn displacing_an_npc_window_ends_the_displaced_session() {
load_xml(&s, r"Interface\FrameXML\GlobalStrings.lua");
load_xml(&s, r"Interface\FrameXML\BasicControls.xml");
load_xml(&s, r"Interface\FrameXML\LocaleProperties.lua");
load_xml(&s, r"Interface\FrameXML\StaticPopup.xml");
load_xml(&s, "UIParent.xml");
load_xml(&s, "Interface\\FrameXML\\GossipFrame.xml");
load_xml(&s, "GameTooltip.xml"); // app load order: tooltip before merchant
// The vendor window is the reference's own since 1751, and its `MerchantFrame_UpdateMerchantInfo`
// calls `TEXT()` while building every row — see `test_ui::MERCHANT_UI` for the rest.
// The vendor window is the reference's own since 1751, and its `MerchantFrame_UpdateMerchantInfo`
// calls `TEXT()` while building every row — see `test_ui::MERCHANT_UI` for the rest.
for f in super::test_ui::MERCHANT_UI {
load_xml(&s, f);
}
@@ -744,18 +742,15 @@ fn shipped_panel_slot_pushable_promotes_to_center() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = UiScript::new().unwrap();
s.set_screen_size(1024.0, 768.0);
load_xml(&s, "MoneyFrame.xml");
load_xml(&s, "UiPanels.xml");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
load_xml(&s, r"Interface\FrameXML\GlobalStrings.lua");
load_xml(&s, r"Interface\FrameXML\BasicControls.xml");
load_xml(&s, r"Interface\FrameXML\LocaleProperties.lua");
load_xml(&s, r"Interface\FrameXML\StaticPopup.xml");
load_xml(&s, "UIParent.xml");
load_xml(&s, "GameTooltip.xml"); // app load order: tooltip before merchant
// The vendor window is the reference's own since 1751, and its `MerchantFrame_UpdateMerchantInfo`
// calls `TEXT()` while building every row — see `test_ui::MERCHANT_UI` for the rest.
// The vendor window is the reference's own since 1751, and its `MerchantFrame_UpdateMerchantInfo`
// calls `TEXT()` while building every row — see `test_ui::MERCHANT_UI` for the rest.
for f in super::test_ui::MERCHANT_UI {
load_xml(&s, f);
}
@@ -854,7 +849,8 @@ fn gossip_bank_option_hands_the_left_slot_to_the_bank() {
s.set_screen_size(1024.0, 768.0);
load_xml(&s, "Interface\\FrameXML\\Fonts.xml");
load_xml(&s, "Interface\\FrameXML\\ItemButtonTemplate.xml"); // the reference bank's slot buttons inherit it
load_xml(&s, "MoneyFrame.xml");
load_xml(&s, r"Interface\FrameXML\MoneyFrame.lua");
load_xml(&s, r"Interface\FrameXML\MoneyFrame.xml");
load_xml(&s, "UiPanels.xml");
load_xml(&s, r"Interface\FrameXML\GlobalStrings.lua");
load_xml(&s, r"Interface\FrameXML\BasicControls.xml");
@@ -942,7 +938,8 @@ fn an_overflowing_gossip_menu_scrolls_instead_of_spilling() {
// `shipped_gossip_rows_grow_to_their_wrapped_labels` for why this is a harness gap and not an
// engine one.
s.set_text_measurer(Box::new(super::FixedWidthFont(6.0)));
load_xml(&s, "MoneyFrame.xml");
load_xml(&s, r"Interface\FrameXML\MoneyFrame.lua");
load_xml(&s, r"Interface\FrameXML\MoneyFrame.xml");
load_xml(&s, "UiPanels.xml");
load_xml(&s, "ScrollTemplates.xml");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
@@ -1103,7 +1100,8 @@ fn an_addons_own_frame_registered_in_uipanelwindows_takes_the_left_slot() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = UiScript::new().unwrap();
s.set_screen_size(1024.0, 768.0);
load_xml(&s, "MoneyFrame.xml");
load_xml(&s, r"Interface\FrameXML\MoneyFrame.lua");
load_xml(&s, r"Interface\FrameXML\MoneyFrame.xml");
load_xml(&s, "UiPanels.xml");
load_xml(&s, "ScrollTemplates.xml");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
@@ -1184,7 +1182,8 @@ fn an_addons_own_frame_registered_in_uipanelwindows_takes_the_left_slot() {
fn the_1507_registry_rows_match_the_reference_bytes() {
let _data = benilla_formats::wow_data_or_skip!();
let s = UiScript::new().unwrap();
load_xml(&s, "MoneyFrame.xml");
load_xml(&s, r"Interface\FrameXML\MoneyFrame.lua");
load_xml(&s, r"Interface\FrameXML\MoneyFrame.xml");
load_xml(&s, "UiPanels.xml");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
@@ -1228,7 +1227,8 @@ fn a_dead_player_opens_only_whiledead_windows() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = UiScript::new().unwrap();
s.set_screen_size(1024.0, 768.0);
load_xml(&s, "MoneyFrame.xml");
load_xml(&s, r"Interface\FrameXML\MoneyFrame.lua");
load_xml(&s, r"Interface\FrameXML\MoneyFrame.xml");
load_xml(&s, "UiPanels.xml");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
@@ -1301,7 +1301,8 @@ fn a_frame_arriving_at_center_puts_the_child_windows_away() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = UiScript::new().unwrap();
s.set_screen_size(1024.0, 768.0);
load_xml(&s, "MoneyFrame.xml");
load_xml(&s, r"Interface\FrameXML\MoneyFrame.lua");
load_xml(&s, r"Interface\FrameXML\MoneyFrame.xml");
load_xml(&s, "UiPanels.xml");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
@@ -22,7 +22,8 @@ fn harness(extra: &[&str]) -> UiScript {
load_xml(&s, "UIParent.xml");
load_xml(&s, "Interface\\FrameXML\\GlobalStrings.lua");
load_xml(&s, "Interface\\FrameXML\\MainMenuBar.xml");
load_xml(&s, "MoneyFrame.xml");
load_xml(&s, r"Interface\FrameXML\MoneyFrame.lua");
load_xml(&s, r"Interface\FrameXML\MoneyFrame.xml");
load_xml(&s, "GameTooltip.xml");
load_xml(&s, "Interface\\FrameXML\\ActionBarFrame.xml");
load_xml(&s, "Interface\\FrameXML\\BonusActionBarFrame.xml");
@@ -228,7 +229,7 @@ fn get_net_stats_reports_the_pushed_latency() {
/// tooltip that is quietly one line short.
#[test]
fn hovering_the_meter_shows_the_live_latency() {
let mut s = harness(&["MoneyFrame.xml", "UIParent.xml", "GameTooltip.xml"]);
let mut s = harness(&["UIParent.xml"]);
s.set_latency_ms(Some(42));
s.resolve();
@@ -26,7 +26,8 @@ pub(super) fn load_pet_bar(s: &UiScript) {
"Interface\\FrameXML\\GlobalStrings.lua",
"Interface\\FrameXML\\BasicControls.xml",
"Interface\\FrameXML\\MainMenuBar.xml",
"MoneyFrame.xml",
r"Interface\FrameXML\MoneyFrame.lua",
r"Interface\FrameXML\MoneyFrame.xml",
"GameTooltip.xml",
"Interface\\FrameXML\\ActionBarFrame.xml",
"Interface\\FrameXML\\BonusActionBarFrame.xml",
@@ -28,7 +28,8 @@ fn load_pet_frame() -> UiScript {
s.set_screen_size(1024.0, 768.0);
load_xml(&s, "Interface\\FrameXML\\Fonts.xml");
load_xml(&s, "UIParent.xml");
load_xml(&s, "MoneyFrame.xml");
load_xml(&s, r"Interface\FrameXML\MoneyFrame.lua");
load_xml(&s, r"Interface\FrameXML\MoneyFrame.xml");
load_xml(&s, "GameTooltip.xml");
load_xml(&s, "Interface\\FrameXML\\UIDropDownMenu.xml");
load_xml(&s, "Interface\\FrameXML\\BasicControls.xml");
@@ -92,7 +92,8 @@ fn setup() -> UiScript {
load_xml(&s, "Interface\\FrameXML\\GlobalStrings.lua");
load_xml(&s, "Interface\\FrameXML\\Fonts.xml");
load_xml(&s, "Interface\\FrameXML\\BasicControls.xml");
load_xml(&s, "MoneyFrame.xml");
load_xml(&s, r"Interface\FrameXML\MoneyFrame.lua");
load_xml(&s, r"Interface\FrameXML\MoneyFrame.xml");
load_xml(&s, "UiPanels.xml");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
@@ -14,7 +14,8 @@ fn setup() -> UiScript {
let mut s = UiScript::new().unwrap();
s.set_screen_size(1024.0, 768.0);
load_xml(&s, "Interface\\FrameXML\\Fonts.xml");
load_xml(&s, "MoneyFrame.xml");
load_xml(&s, r"Interface\FrameXML\MoneyFrame.lua");
load_xml(&s, r"Interface\FrameXML\MoneyFrame.xml");
load_xml(&s, "UiPanels.xml");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
+20 -10
View File
@@ -18,7 +18,8 @@ fn questgiver_show_hide_plays_open_and_close_kits() {
let mut s = UiScript::new().unwrap();
s.set_screen_size(1024.0, 768.0);
load_xml(&s, "Interface\\FrameXML\\Fonts.xml");
load_xml(&s, "MoneyFrame.xml");
load_xml(&s, r"Interface\FrameXML\MoneyFrame.lua");
load_xml(&s, r"Interface\FrameXML\MoneyFrame.xml");
load_xml(&s, "Interface\\FrameXML\\GlobalStrings.lua");
load_xml(&s, "UiPanels.xml");
load_xml(&s, "GameTooltip.xml");
@@ -88,7 +89,8 @@ fn panel_events_show_exactly_one_child_panel_and_hide_the_others() {
let mut s = UiScript::new().unwrap();
s.set_screen_size(1024.0, 768.0);
load_xml(&s, "Interface\\FrameXML\\Fonts.xml");
load_xml(&s, "MoneyFrame.xml");
load_xml(&s, r"Interface\FrameXML\MoneyFrame.lua");
load_xml(&s, r"Interface\FrameXML\MoneyFrame.xml");
load_xml(&s, "Interface\\FrameXML\\GlobalStrings.lua");
load_xml(&s, "UiPanels.xml");
load_xml(&s, "GameTooltip.xml");
@@ -186,7 +188,8 @@ fn detail_panel_reward_grid_follows_the_refs_two_per_row_layout() {
let mut s = UiScript::new().unwrap();
s.set_screen_size(1024.0, 768.0);
load_xml(&s, "Interface\\FrameXML\\Fonts.xml");
load_xml(&s, "MoneyFrame.xml");
load_xml(&s, r"Interface\FrameXML\MoneyFrame.lua");
load_xml(&s, r"Interface\FrameXML\MoneyFrame.xml");
load_xml(&s, "Interface\\FrameXML\\GlobalStrings.lua");
load_xml(&s, "UiPanels.xml");
load_xml(&s, "GameTooltip.xml");
@@ -288,7 +291,8 @@ fn reward_panel_choice_click_selects_and_completes_with_zero_based_index() {
let mut s = UiScript::new().unwrap();
s.set_screen_size(1024.0, 768.0);
load_xml(&s, "Interface\\FrameXML\\Fonts.xml");
load_xml(&s, "MoneyFrame.xml");
load_xml(&s, r"Interface\FrameXML\MoneyFrame.lua");
load_xml(&s, r"Interface\FrameXML\MoneyFrame.xml");
load_xml(&s, "Interface\\FrameXML\\GlobalStrings.lua");
load_xml(&s, "UiPanels.xml");
load_xml(&s, "GameTooltip.xml");
@@ -367,7 +371,8 @@ fn greeting_goodbye_button_closes_the_window() {
let mut s = UiScript::new().unwrap();
s.set_screen_size(1024.0, 768.0);
load_xml(&s, "Interface\\FrameXML\\Fonts.xml");
load_xml(&s, "MoneyFrame.xml");
load_xml(&s, r"Interface\FrameXML\MoneyFrame.lua");
load_xml(&s, r"Interface\FrameXML\MoneyFrame.xml");
load_xml(&s, "Interface\\FrameXML\\GlobalStrings.lua");
load_xml(&s, "UiPanels.xml");
load_xml(&s, "GameTooltip.xml");
@@ -424,7 +429,8 @@ fn detail_panel_action_buttons_resolve_to_real_onscreen_rects() {
let mut s = UiScript::new().unwrap();
s.set_screen_size(1024.0, 768.0);
load_xml(&s, "Interface\\FrameXML\\Fonts.xml");
load_xml(&s, "MoneyFrame.xml");
load_xml(&s, r"Interface\FrameXML\MoneyFrame.lua");
load_xml(&s, r"Interface\FrameXML\MoneyFrame.xml");
load_xml(&s, "Interface\\FrameXML\\GlobalStrings.lua");
load_xml(&s, "UiPanels.xml");
load_xml(&s, "GameTooltip.xml");
@@ -552,7 +558,8 @@ fn write_on_still_fades_when_instant_text_is_off() {
let mut s = UiScript::new().unwrap();
s.set_screen_size(1024.0, 768.0);
load_xml(&s, "Interface\\FrameXML\\Fonts.xml");
load_xml(&s, "MoneyFrame.xml");
load_xml(&s, r"Interface\FrameXML\MoneyFrame.lua");
load_xml(&s, r"Interface\FrameXML\MoneyFrame.xml");
load_xml(&s, "Interface\\FrameXML\\GlobalStrings.lua");
load_xml(&s, "UiPanels.xml");
load_xml(&s, "GameTooltip.xml");
@@ -629,7 +636,8 @@ fn npc_name_reaches_the_title_bar_on_open_and_on_refresh() {
let mut s = UiScript::new().unwrap();
s.set_screen_size(1024.0, 768.0);
load_xml(&s, "Interface\\FrameXML\\Fonts.xml");
load_xml(&s, "MoneyFrame.xml");
load_xml(&s, r"Interface\FrameXML\MoneyFrame.lua");
load_xml(&s, r"Interface\FrameXML\MoneyFrame.xml");
load_xml(&s, "Interface\\FrameXML\\GlobalStrings.lua");
load_xml(&s, "UiPanels.xml");
load_xml(&s, "GameTooltip.xml");
@@ -712,7 +720,8 @@ fn greeting_panel_title_rows_grow_to_their_wrapped_titles() {
// So is the app's (measure.rs); the harness installs the same seam's stand-in.
s.set_text_measurer(Box::new(super::FixedWidthFont(7.0)));
load_xml(&s, "Interface\\FrameXML\\Fonts.xml");
load_xml(&s, "MoneyFrame.xml");
load_xml(&s, r"Interface\FrameXML\MoneyFrame.lua");
load_xml(&s, r"Interface\FrameXML\MoneyFrame.xml");
load_xml(&s, "Interface\\FrameXML\\GlobalStrings.lua");
load_xml(&s, "UiPanels.xml");
load_xml(&s, "GameTooltip.xml");
@@ -812,7 +821,8 @@ fn reward_rows_preview_and_post_without_selecting_the_choice() {
let mut s = UiScript::new().unwrap();
s.set_screen_size(1024.0, 768.0);
load_xml(&s, "Interface\\FrameXML\\Fonts.xml");
load_xml(&s, "MoneyFrame.xml");
load_xml(&s, r"Interface\FrameXML\MoneyFrame.lua");
load_xml(&s, r"Interface\FrameXML\MoneyFrame.xml");
load_xml(&s, "Interface\\FrameXML\\GlobalStrings.lua");
load_xml(&s, "UiPanels.xml");
load_xml(&s, "GameTooltip.xml");
@@ -32,7 +32,8 @@ fn harness() -> UiScript {
// `MAX_QUESTS`, the loop bound the reference's repaint hides its spare rows with. 1.12
// declares it on QuestLogFrame.lua:2 and so do we (1751 window 16) — a nil there is
// `'for' limit must be a number` on the first repaint, not a missing row.
load_xml(&s, "MoneyFrame.xml");
load_xml(&s, r"Interface\FrameXML\MoneyFrame.lua");
load_xml(&s, r"Interface\FrameXML\MoneyFrame.xml");
load_xml(&s, "UiPanels.xml");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
@@ -85,7 +85,8 @@ fn shipped_questlog_frame_loads_clean() {
let _data = benilla_formats::wow_data_or_skip!();
let s = UiScript::new().unwrap();
load_xml(&s, "Interface\\FrameXML\\Fonts.xml");
load_xml(&s, "MoneyFrame.xml");
load_xml(&s, r"Interface\FrameXML\MoneyFrame.lua");
load_xml(&s, r"Interface\FrameXML\MoneyFrame.xml");
load_xml(&s, "Interface\\FrameXML\\GlobalStrings.lua");
load_xml(&s, "UiPanels.xml");
load_xml(&s, "GameTooltip.xml");
@@ -118,7 +119,8 @@ fn shipped_questlog_frame_drives_end_to_end() {
let mut s = UiScript::new().unwrap();
s.set_screen_size(1024.0, 768.0);
load_xml(&s, "Interface\\FrameXML\\Fonts.xml");
load_xml(&s, "MoneyFrame.xml");
load_xml(&s, r"Interface\FrameXML\MoneyFrame.lua");
load_xml(&s, r"Interface\FrameXML\MoneyFrame.xml");
load_xml(&s, "Interface\\FrameXML\\GlobalStrings.lua");
load_xml(&s, "UiPanels.xml");
load_xml(&s, "GameTooltip.xml");
@@ -327,7 +329,8 @@ fn shift_click_toggles_the_watch_checkbox_and_the_tracker_hud() {
// synchronously — the app's measure seam is synchronous too (measure.rs); its stand-in here.
s.set_text_measurer(Box::new(super::FixedWidthFont(6.0)));
load_xml(&s, "Interface\\FrameXML\\Fonts.xml");
load_xml(&s, "MoneyFrame.xml");
load_xml(&s, r"Interface\FrameXML\MoneyFrame.lua");
load_xml(&s, r"Interface\FrameXML\MoneyFrame.xml");
load_xml(&s, "Interface\\FrameXML\\GlobalStrings.lua");
load_xml(&s, "UiPanels.xml");
load_xml(&s, "GameTooltip.xml");
@@ -474,7 +477,8 @@ fn watch_guards_no_op_without_erroring() {
let mut s = UiScript::new().unwrap();
s.set_screen_size(1024.0, 768.0);
load_xml(&s, "Interface\\FrameXML\\Fonts.xml");
load_xml(&s, "MoneyFrame.xml");
load_xml(&s, r"Interface\FrameXML\MoneyFrame.lua");
load_xml(&s, r"Interface\FrameXML\MoneyFrame.xml");
load_xml(&s, "Interface\\FrameXML\\GlobalStrings.lua");
load_xml(&s, "UiPanels.xml");
load_xml(&s, "GameTooltip.xml");
@@ -590,7 +594,8 @@ fn progress_auto_watches_for_five_minutes() {
let mut s = UiScript::new().unwrap();
s.set_screen_size(1024.0, 768.0);
load_xml(&s, "Interface\\FrameXML\\Fonts.xml");
load_xml(&s, "MoneyFrame.xml");
load_xml(&s, r"Interface\FrameXML\MoneyFrame.lua");
load_xml(&s, r"Interface\FrameXML\MoneyFrame.xml");
load_xml(&s, "Interface\\FrameXML\\GlobalStrings.lua");
load_xml(&s, "UiPanels.xml");
load_xml(&s, "GameTooltip.xml");
@@ -674,7 +679,8 @@ fn the_auto_watch_flag_is_the_references_uvar_and_gates_the_watch() {
let mut s = UiScript::new().unwrap();
s.set_screen_size(1024.0, 768.0);
load_xml(&s, "Interface\\FrameXML\\Fonts.xml");
load_xml(&s, "MoneyFrame.xml");
load_xml(&s, r"Interface\FrameXML\MoneyFrame.lua");
load_xml(&s, r"Interface\FrameXML\MoneyFrame.xml");
load_xml(&s, "Interface\\FrameXML\\GlobalStrings.lua");
load_xml(&s, "UiPanels.xml");
load_xml(&s, "GameTooltip.xml");
@@ -740,7 +746,8 @@ fn the_row_tag_is_its_own_right_flush_string_and_the_state_word_wins() {
// ref-verbatim tag handling — the app loads GlobalStrings.lua as `benilla.toc`'s second entry,
// so this harness must too or the override silently reads nil.
load_xml(&s, "Interface\\FrameXML\\GlobalStrings.lua");
load_xml(&s, "MoneyFrame.xml");
load_xml(&s, r"Interface\FrameXML\MoneyFrame.lua");
load_xml(&s, r"Interface\FrameXML\MoneyFrame.xml");
load_xml(&s, "UiPanels.xml");
load_xml(&s, "GameTooltip.xml");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
@@ -817,7 +824,8 @@ fn empty_quest_log_hides_rows_and_disables_abandon() {
let mut s = UiScript::new().unwrap();
s.set_screen_size(1024.0, 768.0);
load_xml(&s, "Interface\\FrameXML\\Fonts.xml");
load_xml(&s, "MoneyFrame.xml");
load_xml(&s, r"Interface\FrameXML\MoneyFrame.lua");
load_xml(&s, r"Interface\FrameXML\MoneyFrame.xml");
load_xml(&s, "Interface\\FrameXML\\GlobalStrings.lua");
load_xml(&s, "UiPanels.xml");
load_xml(&s, "GameTooltip.xml");
@@ -881,7 +889,8 @@ fn reward_rows_follow_the_refs_two_per_row_layout() {
let mut s = UiScript::new().unwrap();
s.set_screen_size(1024.0, 768.0);
load_xml(&s, "Interface\\FrameXML\\Fonts.xml");
load_xml(&s, "MoneyFrame.xml");
load_xml(&s, r"Interface\FrameXML\MoneyFrame.lua");
load_xml(&s, r"Interface\FrameXML\MoneyFrame.xml");
load_xml(&s, "Interface\\FrameXML\\GlobalStrings.lua");
load_xml(&s, "UiPanels.xml");
load_xml(&s, "GameTooltip.xml");
@@ -1069,7 +1078,8 @@ fn overflowing_detail_content_clips_to_the_scrollframe_rect() {
let mut s = UiScript::new().unwrap();
s.set_screen_size(1024.0, 768.0);
load_xml(&s, "Interface\\FrameXML\\Fonts.xml");
load_xml(&s, "MoneyFrame.xml");
load_xml(&s, r"Interface\FrameXML\MoneyFrame.lua");
load_xml(&s, r"Interface\FrameXML\MoneyFrame.xml");
load_xml(&s, "Interface\\FrameXML\\GlobalStrings.lua");
load_xml(&s, "UiPanels.xml");
load_xml(&s, "GameTooltip.xml");
@@ -1143,7 +1153,8 @@ fn wheel_over_the_detail_pane_changes_vertical_scroll() {
s.set_screen_size(1024.0, 768.0);
s.set_text_measurer(Box::new(super::FixedWidthFont(6.0)));
load_xml(&s, "Interface\\FrameXML\\Fonts.xml");
load_xml(&s, "MoneyFrame.xml");
load_xml(&s, r"Interface\FrameXML\MoneyFrame.lua");
load_xml(&s, r"Interface\FrameXML\MoneyFrame.xml");
load_xml(&s, "Interface\\FrameXML\\GlobalStrings.lua");
load_xml(&s, "UiPanels.xml");
load_xml(&s, "GameTooltip.xml");
@@ -1207,7 +1218,8 @@ fn selection_change_resets_detail_scroll_but_a_quest_log_update_refresh_does_not
s.set_screen_size(1024.0, 768.0);
s.set_text_measurer(Box::new(super::FixedWidthFont(6.0)));
load_xml(&s, "Interface\\FrameXML\\Fonts.xml");
load_xml(&s, "MoneyFrame.xml");
load_xml(&s, r"Interface\FrameXML\MoneyFrame.lua");
load_xml(&s, r"Interface\FrameXML\MoneyFrame.xml");
load_xml(&s, "Interface\\FrameXML\\GlobalStrings.lua");
load_xml(&s, "UiPanels.xml");
load_xml(&s, "GameTooltip.xml");
@@ -1283,7 +1295,8 @@ fn reward_row_hover_serves_the_shared_item_tooltip() {
let mut s = UiScript::new().unwrap();
s.set_screen_size(1024.0, 768.0);
load_xml(&s, "Interface\\FrameXML\\Fonts.xml");
load_xml(&s, "MoneyFrame.xml");
load_xml(&s, r"Interface\FrameXML\MoneyFrame.lua");
load_xml(&s, r"Interface\FrameXML\MoneyFrame.xml");
load_xml(&s, "Interface\\FrameXML\\GlobalStrings.lua");
load_xml(&s, "UiPanels.xml");
load_xml(&s, "GameTooltip.xml");
@@ -1386,7 +1399,8 @@ fn popup_children_inherit_the_dialog_stratum() {
let mut s = UiScript::new().unwrap();
s.set_screen_size(1024.0, 768.0);
load_xml(&s, "Interface\\FrameXML\\Fonts.xml");
load_xml(&s, "MoneyFrame.xml");
load_xml(&s, r"Interface\FrameXML\MoneyFrame.lua");
load_xml(&s, r"Interface\FrameXML\MoneyFrame.xml");
load_xml(&s, "UiPanels.xml");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
@@ -1441,7 +1455,8 @@ fn reward_rows_preview_and_post_and_a_plain_click_stays_inert() {
let mut s = UiScript::new().unwrap();
s.set_screen_size(1024.0, 768.0);
load_xml(&s, "Interface\\FrameXML\\Fonts.xml");
load_xml(&s, "MoneyFrame.xml");
load_xml(&s, r"Interface\FrameXML\MoneyFrame.lua");
load_xml(&s, r"Interface\FrameXML\MoneyFrame.xml");
load_xml(&s, "Interface\\FrameXML\\GlobalStrings.lua");
load_xml(&s, "UiPanels.xml");
load_xml(&s, "GameTooltip.xml");
@@ -1539,7 +1554,8 @@ fn shift_click_on_a_title_posts_the_quest_name_with_chat_open_and_watches_with_i
let mut s = UiScript::new().unwrap();
s.set_screen_size(1024.0, 768.0);
load_xml(&s, "Interface\\FrameXML\\Fonts.xml");
load_xml(&s, "MoneyFrame.xml");
load_xml(&s, r"Interface\FrameXML\MoneyFrame.lua");
load_xml(&s, r"Interface\FrameXML\MoneyFrame.xml");
load_xml(&s, "Interface\\FrameXML\\GlobalStrings.lua");
load_xml(&s, "UiPanels.xml");
load_xml(&s, "GameTooltip.xml");
@@ -1648,7 +1664,8 @@ fn share_quest_needs_both_a_sharable_selection_and_a_party() {
let mut s = UiScript::new().unwrap();
s.set_screen_size(1024.0, 768.0);
load_xml(&s, "Interface\\FrameXML\\Fonts.xml");
load_xml(&s, "MoneyFrame.xml");
load_xml(&s, r"Interface\FrameXML\MoneyFrame.lua");
load_xml(&s, r"Interface\FrameXML\MoneyFrame.xml");
load_xml(&s, "Interface\\FrameXML\\GlobalStrings.lua");
load_xml(&s, "UiPanels.xml");
load_xml(&s, "GameTooltip.xml");
@@ -1734,7 +1751,8 @@ fn share_quest_is_dark_on_an_empty_log_even_in_a_party() {
let mut s = UiScript::new().unwrap();
s.set_screen_size(1024.0, 768.0);
load_xml(&s, "Interface\\FrameXML\\Fonts.xml");
load_xml(&s, "MoneyFrame.xml");
load_xml(&s, r"Interface\FrameXML\MoneyFrame.lua");
load_xml(&s, r"Interface\FrameXML\MoneyFrame.xml");
load_xml(&s, "Interface\\FrameXML\\GlobalStrings.lua");
load_xml(&s, "UiPanels.xml");
load_xml(&s, "GameTooltip.xml");
@@ -1774,7 +1792,8 @@ fn share_quest_click_queues_the_selected_quests_id() {
let mut s = UiScript::new().unwrap();
s.set_screen_size(1024.0, 768.0);
load_xml(&s, "Interface\\FrameXML\\Fonts.xml");
load_xml(&s, "MoneyFrame.xml");
load_xml(&s, r"Interface\FrameXML\MoneyFrame.lua");
load_xml(&s, r"Interface\FrameXML\MoneyFrame.xml");
load_xml(&s, "Interface\\FrameXML\\GlobalStrings.lua");
load_xml(&s, "UiPanels.xml");
load_xml(&s, "GameTooltip.xml");
@@ -1523,72 +1523,12 @@ mod tests {
update; our `inventory_alerts` snapshot is recomputed on every inventory push \
instead, so the recompute exists and only the Lua verb that forces one does not.",
),
(
"StaticPopup.xml",
"AcceptAreaSpiritHeal",
"a registered 1.12 binding whose body is uncarved (wow-re `bindings.md`, structural row only); a \
wow-re orchestrator is out on it and it is built when the carve lands (1960). Reached by AREA_SPIRIT_HEAL's Accept, an event this engine does not fire yet.",
),
(
"StaticPopup.xml",
"CancelAreaSpiritHeal",
"a registered 1.12 binding whose body is uncarved (wow-re `bindings.md`, structural row only); a \
wow-re orchestrator is out on it and it is built when the carve lands (1960). Reached by AREA_SPIRIT_HEAL's Cancel, an event this engine does not fire yet.",
),
(
"StaticPopup.xml",
"GetAreaSpiritHealerTime",
"a registered 1.12 binding whose body is uncarved (wow-re `bindings.md`, structural row only); a \
wow-re orchestrator is out on it and it is built when the carve lands (1960). Reached by AREA_SPIRIT_HEAL's OnShow, an event this engine does not fire yet.",
),
(
"StaticPopup.xml",
"AcceptBattlefieldPort",
"a registered 1.12 binding whose body is uncarved (wow-re `bindings.md`, structural row only); a \
wow-re orchestrator is out on it and it is built when the carve lands (1960). Reached by CONFIRM_BATTLEFIELD_ENTRY / CONFIRM_LEAVE_QUEUE, raised only by BattlefieldFrame.lua, which is not on the chain yet.",
),
(
"StaticPopup.xml",
"BeginTrade",
"a registered 1.12 binding whose body is uncarved (wow-re `bindings.md`, structural row only); a \
wow-re orchestrator is out on it and it is built when the carve lands (1960). Reached by the TRADE dialog, which 1.12.1 can never show (wow-re `incoming-trade-request-law.md`: TRADE_REQUEST is signalled by nothing).",
),
(
"StaticPopup.xml",
"CancelTrade",
"a registered 1.12 binding whose body is uncarved (wow-re `bindings.md`, structural row only); a \
wow-re orchestrator is out on it and it is built when the carve lands (1960). Reached by the TRADE dialog, which 1.12.1 can never show (wow-re `incoming-trade-request-law.md`: TRADE_REQUEST is signalled by nothing).",
),
(
"StaticPopup.xml",
"ReplaceTradeEnchant",
"a registered 1.12 binding whose body is uncarved (wow-re `bindings.md`, structural row only); a \
wow-re orchestrator is out on it and it is built when the carve lands (1960). Reached by TRADE_REPLACE_ENCHANT's Accept, an event this engine does not fire yet.",
),
(
"StaticPopup.xml",
"CancelMeetingStoneRequest",
"a registered 1.12 binding whose body is uncarved (wow-re `bindings.md`, structural row only); a \
wow-re orchestrator is out on it and it is built when the carve lands (1960). Reached by CONFIRM_LEAVE_QUEUE's meeting-stone arm, raised only by BattlefieldFrame.lua / the meeting-stone gossip, neither on the chain yet.",
),
(
"StaticPopup.xml",
"CheckPetUntrainerDist",
"a registered 1.12 binding whose body is uncarved (wow-re `bindings.md`, structural row only); a \
wow-re orchestrator is out on it and it is built when the carve lands (1960). Reached by CONFIRM_PET_UNLEARN's OnUpdate, an event this engine does not fire yet.",
),
(
"StaticPopup.xml",
"ConfirmPetUnlearn",
"a registered 1.12 binding whose body is uncarved (wow-re `bindings.md`, structural row only); a \
wow-re orchestrator is out on it and it is built when the carve lands (1960). Reached by CONFIRM_PET_UNLEARN's Accept, an event this engine does not fire yet.",
),
(
"StaticPopup.xml",
"GetInstanceBootTimeRemaining",
"a registered 1.12 binding whose body is uncarved (wow-re `bindings.md`, structural row only); a \
wow-re orchestrator is out on it and it is built when the carve lands (1960). Reached by INSTANCE_BOOT's OnShow, an event this engine does not fire yet.",
),
];
let toc = &super::super::addons::Addon::builtin().toc.files;
@@ -2147,6 +2087,25 @@ mod tests {
stale.join("\n ")
);
}
/// `text` with every whitespace run that precedes a `.` removed — a method chain rustfmt
/// broke across lines reads as one call again.
fn glue_chains(text: &str) -> String {
let mut out = String::with_capacity(text.len());
let mut pending = String::new();
for c in text.chars() {
if c.is_whitespace() {
pending.push(c);
} else {
if c != '.' {
out.push_str(&pending);
}
pending.clear();
out.push(c);
}
}
out.push_str(&pending);
out
}
/// **A stock file listening for an event nothing produces is silent on both sides.**
///
@@ -2291,6 +2250,9 @@ mod tests {
stack.push(p);
} else if p.extension().is_some_and(|x| x == "rs") {
let text = std::fs::read_to_string(&p).unwrap_or_default();
// rustfmt splits a long chain at its dots (`model\n.pending_events\n.push((`), so
// the three shapes are matched with the whitespace before each `.` removed.
let text = glue_chains(&text);
// The engine's deferred lane (`pending_events.push((name, args))`,
// `cursor.rs`) is a fire too — the pet grid pair rides it (1953).
const CALLS: [&str; 3] = [
@@ -15,7 +15,8 @@ fn harness() -> UiScript {
s.set_screen_size(1024.0, 768.0);
for f in [
"Interface\\FrameXML\\Fonts.xml",
"MoneyFrame.xml",
r"Interface\FrameXML\MoneyFrame.lua",
r"Interface\FrameXML\MoneyFrame.xml",
"UiPanels.xml",
r"Interface\FrameXML\UIPanelTemplates.lua",
r"Interface\FrameXML\UIPanelTemplates.xml",
@@ -97,7 +97,8 @@ pub(super) fn spellbook_ui(w: f32, h: f32) -> UiScript {
"Interface\\FrameXML\\GlobalStrings.lua",
"Interface\\FrameXML\\BasicControls.xml",
"Interface\\FrameXML\\MainMenuBar.xml",
"MoneyFrame.xml",
r"Interface\FrameXML\MoneyFrame.lua",
r"Interface\FrameXML\MoneyFrame.xml",
"GameTooltip.xml",
"Interface\\FrameXML\\ActionBarFrame.xml",
"Interface\\FrameXML\\BonusActionBarFrame.xml",
@@ -129,7 +130,8 @@ pub(super) fn spellbook_ui(w: f32, h: f32) -> UiScript {
fn shipped_spellbook_loads_clean() {
let s = UiScript::new().unwrap();
load_xml(&s, "Interface\\FrameXML\\Fonts.xml");
load_xml(&s, "MoneyFrame.xml");
load_xml(&s, r"Interface\FrameXML\MoneyFrame.lua");
load_xml(&s, r"Interface\FrameXML\MoneyFrame.xml");
load_xml(&s, "UiPanels.xml");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
@@ -683,7 +685,8 @@ fn the_macro_editor_takes_a_shift_click_and_only_a_shift_click() {
r"Interface\FrameXML\GlobalStrings.lua",
"Interface\\FrameXML\\Fonts.xml",
"Interface\\FrameXML\\BasicControls.xml", // `TEXT`
"MoneyFrame.xml",
r"Interface\FrameXML\MoneyFrame.lua",
r"Interface\FrameXML\MoneyFrame.xml",
"UiPanels.xml",
"Interface\\FrameXML\\LocaleProperties.lua",
"Interface\\FrameXML\\StaticPopup.xml",
@@ -11,7 +11,8 @@ fn setup() -> UiScript {
let mut s = UiScript::new().unwrap();
s.set_screen_size(1024.0, 768.0);
load_xml(&s, "Interface\\FrameXML\\Fonts.xml");
load_xml(&s, "MoneyFrame.xml");
load_xml(&s, r"Interface\FrameXML\MoneyFrame.lua");
load_xml(&s, r"Interface\FrameXML\MoneyFrame.xml");
load_xml(&s, "UiPanels.xml");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
@@ -262,3 +263,56 @@ fn hide_and_find_address_one_instance_by_data_only_for_a_multiple_dialog() {
.unwrap());
assert!(s.errors().is_empty(), "{:?}", s.errors());
}
/// The dialog engine's verbs (decision 1963): the five UIParent arms the feeds behind them now
/// reach — each verbatim from UIParent.lua, each raising a stock dialog whose Accept calls a
/// binding this engine answers.
#[test]
fn the_verb_dialogs_open_from_their_events_and_call_their_verbs() {
use benilla_ui::script::ScriptValue;
let mut s = setup();
load_xml(&s, "UIParent.xml"); // the arms
s.set_money(50_000);
// The pet trainer's question: the dialog, its money frame at the cost, Accept → the confirm.
s.fire_event("CONFIRM_PET_UNLEARN", vec![ScriptValue::Int(12_345)]);
s.tick(0.0);
assert_eq!(
s.eval::<String>("return StaticPopup1.which").unwrap(),
"CONFIRM_PET_UNLEARN"
);
assert_eq!(
s.eval::<f64>("return StaticPopup1MoneyFrame.staticMoney")
.unwrap(),
12_345.0
);
s.run("StaticPopup1Button1:Click()").unwrap();
assert_eq!(s.take_pet_unlearn_confirms(), 1);
// The instance boot: START shows, STOP hides.
s.set_instance_boot_secs(30);
s.fire_event("INSTANCE_BOOT_START", vec![]);
s.tick(0.0);
assert!(s
.eval::<bool>("return StaticPopup_Visible(\"INSTANCE_BOOT\") ~= nil")
.unwrap());
s.fire_event("INSTANCE_BOOT_STOP", vec![]);
s.tick(0.0);
assert!(!s
.eval::<bool>("return StaticPopup_Visible(\"INSTANCE_BOOT\") ~= nil")
.unwrap());
// The area spirit healer: in range shows, out of range hides; Accept queues the heal.
s.set_area_spirit_healer(true, 20);
s.fire_event("AREA_SPIRIT_HEALER_IN_RANGE", vec![]);
s.tick(0.0);
assert!(s
.eval::<bool>("return StaticPopup_Visible(\"AREA_SPIRIT_HEAL\") ~= nil")
.unwrap());
s.run("StaticPopup_OnClick(StaticPopup_FindVisible(\"AREA_SPIRIT_HEAL\"), 1)")
.unwrap();
assert_eq!(s.take_area_spirit_accepts(), 1);
s.fire_event("AREA_SPIRIT_HEALER_OUT_OF_RANGE", vec![]);
s.tick(0.0);
assert!(!s
.eval::<bool>("return StaticPopup_Visible(\"AREA_SPIRIT_HEAL\") ~= nil")
.unwrap());
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
}
@@ -27,7 +27,8 @@ fn setup() -> UiScript {
let mut s = UiScript::new().unwrap();
s.set_screen_size(1024.0, 768.0);
load_xml(&s, "Interface\\FrameXML\\Fonts.xml");
load_xml(&s, "MoneyFrame.xml");
load_xml(&s, r"Interface\FrameXML\MoneyFrame.lua");
load_xml(&s, r"Interface\FrameXML\MoneyFrame.xml");
load_xml(&s, "UiPanels.xml");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
@@ -13,7 +13,8 @@ fn setup() -> UiScript {
let mut s = UiScript::new().unwrap();
s.set_screen_size(1024.0, 768.0);
load_xml(&s, "Interface\\FrameXML\\Fonts.xml");
load_xml(&s, "MoneyFrame.xml");
load_xml(&s, r"Interface\FrameXML\MoneyFrame.lua");
load_xml(&s, r"Interface\FrameXML\MoneyFrame.xml");
load_xml(&s, "UiPanels.xml");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
@@ -20,7 +20,8 @@ fn harness() -> UiScript {
let mut s = UiScript::new().unwrap();
s.set_screen_size(1024.0, 768.0);
load_xml(&s, "Interface\\FrameXML\\Fonts.xml");
load_xml(&s, "MoneyFrame.xml");
load_xml(&s, r"Interface\FrameXML\MoneyFrame.lua");
load_xml(&s, r"Interface\FrameXML\MoneyFrame.xml");
load_xml(&s, "GameTooltip.xml"); // TOOLTIP_DEFAULT_* (the dropdown kit's MenuBackdrop)
load_xml(&s, "Interface\\FrameXML\\UIDropDownMenu.xml"); // the unit popups' kit (TargetFrameDropDown's template)
load_xml(&s, "Cooldown.xml");
@@ -15,7 +15,8 @@ fn taxi_script() -> UiScript {
let mut s = UiScript::new().unwrap();
s.set_screen_size(1024.0, 768.0);
load_xml(&s, "Interface\\FrameXML\\Fonts.xml");
load_xml(&s, "MoneyFrame.xml");
load_xml(&s, r"Interface\FrameXML\MoneyFrame.lua");
load_xml(&s, r"Interface\FrameXML\MoneyFrame.xml");
load_xml(&s, "Interface\\FrameXML\\GlobalStrings.lua");
load_xml(&s, "UiPanels.xml");
load_xml(&s, r"Interface\FrameXML\BasicControls.xml");
+23 -5
View File
@@ -145,7 +145,8 @@ pub(super) const MERCHANT_UI: &[&str] = &[
"Interface\\FrameXML\\Fonts.xml",
"Interface\\FrameXML\\BasicControls.xml", // TEXT()
"Interface\\FrameXML\\ItemButtonTemplate.xml",
"MoneyFrame.xml",
r"Interface\FrameXML\MoneyFrame.lua",
r"Interface\FrameXML\MoneyFrame.xml",
"UiPanels.xml",
r"Interface\FrameXML\UIPanelTemplates.lua",
r"Interface\FrameXML\UIPanelTemplates.xml",
@@ -158,7 +159,8 @@ pub(super) const LOOT_UI: &[&str] = &[
"Interface\\FrameXML\\GlobalStrings.lua",
"Interface\\FrameXML\\Fonts.xml",
"Interface\\FrameXML\\ItemButtonTemplate.xml",
"MoneyFrame.xml",
r"Interface\FrameXML\MoneyFrame.lua",
r"Interface\FrameXML\MoneyFrame.xml",
"UiPanels.xml", // StaticPopup, and the LOOT_BIND / CONFIRM_LOOT_DISTRIBUTION dialogs
r"Interface\FrameXML\UIPanelTemplates.lua",
r"Interface\FrameXML\UIPanelTemplates.xml",
@@ -237,7 +239,8 @@ pub(super) const CHARACTER_UI: &[&str] = &[
r"Interface\FrameXML\LocaleProperties.lua",
"Interface\\FrameXML\\BasicControls.xml", // TEXT(), which every one of those label sets goes through
"Interface\\FrameXML\\ItemButtonTemplate.xml", // PaperDollItemSlotButtonTemplate's base
"MoneyFrame.xml",
r"Interface\FrameXML\MoneyFrame.lua",
r"Interface\FrameXML\MoneyFrame.xml",
"UIParent.xml", // Model_OnLoad/_Rotate*/_OnUpdate — the model panes' turntable
"UiPanels.xml", // CharacterFrameTabButtonTemplate, UIPanelWindows, Show/HideUIPanel
"GameTooltip.xml",
@@ -312,7 +315,8 @@ pub(super) const SOCIAL_UI: &[&str] = &[
"Interface\\FrameXML\\TextStatusBar.lua",
"Interface\\FrameXML\\TextStatusBar.xml",
"Interface\\FrameXML\\MainMenuBar.xml",
"MoneyFrame.xml",
r"Interface\FrameXML\MoneyFrame.lua",
r"Interface\FrameXML\MoneyFrame.xml",
"GameTooltip.xml",
"Interface\\FrameXML\\ActionBarFrame.xml",
"Interface\\FrameXML\\BonusActionBarFrame.xml",
@@ -362,7 +366,8 @@ pub(super) const BAG_UI: &[&str] = &[
// windows fall out of the cascade and the reference's own layout pass has nothing to measure.
"UIParent.xml",
"Interface\\FrameXML\\ItemButtonTemplate.xml",
"MoneyFrame.xml",
r"Interface\FrameXML\MoneyFrame.lua",
r"Interface\FrameXML\MoneyFrame.xml",
"UiPanels.xml",
"Interface\\FrameXML\\LocaleProperties.lua",
"GameTooltip.xml",
@@ -500,3 +505,16 @@ pub(super) fn click(s: &mut UiScript, name: &str, button: &str) {
s.mouse_button(x, y, button, true);
s.mouse_button(x, y, button, false);
}
/// Seat one of the reference's LoadOnDemand Blizzard addons off the chain, so a harness's
/// `UIParentLoadAddOn(name)` can load it the way the app does (1957; the combat text since
/// 1964). Needs client data — the caller has already checked with `wow_data_or_skip!`.
pub(super) fn seat_chain_addon(s: &mut UiScript, name: &str) {
let toc = super::reference_ui::read(&format!("Interface/AddOns/{name}/{name}.toc"))
.map(|b| benilla_ui::toc::Toc::parse(&benilla_ui::source::decode(&b)))
.unwrap_or_else(|| panic!("{name}: no toc off the chain"));
let mut info = super::addons::info_from_toc(name, &toc);
info.chain = true;
s.set_addon_chain_reader(Box::new(super::reference_ui::read));
s.register_addons(vec![info], None, None, None);
}
@@ -22,7 +22,8 @@ fn harness(extra: &[&str]) -> UiScript {
s.set_screen_size(1024.0, 768.0);
load_xml(&s, "Interface\\FrameXML\\Fonts.xml");
load_xml(&s, "UIParent.xml");
load_xml(&s, "MoneyFrame.xml");
load_xml(&s, r"Interface\FrameXML\MoneyFrame.lua");
load_xml(&s, r"Interface\FrameXML\MoneyFrame.xml");
load_xml(&s, "GameTooltip.xml");
for f in extra {
load_xml(&s, f);
@@ -239,8 +240,6 @@ fn action_button_hover_takes_the_default_corner() {
"UIParent.xml",
"Interface\\FrameXML\\GlobalStrings.lua",
"Interface\\FrameXML\\MainMenuBar.xml",
"MoneyFrame.xml",
"GameTooltip.xml",
"Interface\\FrameXML\\ActionBarFrame.xml",
"Interface\\FrameXML\\BonusActionBarFrame.xml",
]);
@@ -282,8 +281,6 @@ fn ubertooltips_off_seats_action_bar_plates_beside_the_button() {
"UIParent.xml",
"Interface\\FrameXML\\GlobalStrings.lua",
"Interface\\FrameXML\\MainMenuBar.xml",
"MoneyFrame.xml",
"GameTooltip.xml",
"Interface\\FrameXML\\ActionBarFrame.xml",
"Interface\\FrameXML\\BonusActionBarFrame.xml",
"Interface\\FrameXML\\ActionBarFrame.xml",
@@ -365,8 +362,6 @@ fn ubertooltips_off_seats_stance_plates_beside_the_button() {
"UIParent.xml",
"Interface\\FrameXML\\GlobalStrings.lua",
"Interface\\FrameXML\\MainMenuBar.xml",
"MoneyFrame.xml",
"GameTooltip.xml",
"Interface\\FrameXML\\ActionBarFrame.xml",
"Interface\\FrameXML\\BonusActionBarFrame.xml",
"Interface\\FrameXML\\ActionBarFrame.xml",
@@ -420,8 +415,6 @@ fn buff_hover_hangs_below_left_of_the_button() {
"UIParent.xml",
"Interface\\FrameXML\\GlobalStrings.lua",
"Interface\\FrameXML\\MainMenuBar.xml",
"MoneyFrame.xml",
"GameTooltip.xml",
"Interface\\FrameXML\\ActionBarFrame.xml",
"Interface\\FrameXML\\BonusActionBarFrame.xml",
"Interface\\FrameXML\\TextStatusBar.lua",
@@ -44,7 +44,8 @@ fn harness() -> UiScript {
for f in [
"Interface\\FrameXML\\Fonts.xml",
"UIParent.xml",
"MoneyFrame.xml",
r"Interface\FrameXML\MoneyFrame.lua",
r"Interface\FrameXML\MoneyFrame.xml",
"GameTooltip.xml",
] {
load_xml(&s, f);
@@ -62,7 +62,8 @@ fn load_tot() -> UiScript {
s.set_screen_size(1024.0, 768.0);
load_xml(&s, "Interface\\FrameXML\\Fonts.xml");
load_xml(&s, "UIParent.xml");
load_xml(&s, "MoneyFrame.xml");
load_xml(&s, r"Interface\FrameXML\MoneyFrame.lua");
load_xml(&s, r"Interface\FrameXML\MoneyFrame.xml");
load_xml(&s, "GameTooltip.xml");
load_xml(&s, "Interface\\FrameXML\\UIDropDownMenu.xml");
load_xml(&s, "Interface\\FrameXML\\BasicControls.xml");
@@ -54,7 +54,8 @@ fn trainer_script_base() -> UiScript {
"Interface\\FrameXML\\TextStatusBar.lua",
"Interface\\FrameXML\\TextStatusBar.xml",
"Interface\\FrameXML\\MainMenuBar.xml",
"MoneyFrame.xml",
r"Interface\FrameXML\MoneyFrame.lua",
r"Interface\FrameXML\MoneyFrame.xml",
"GameTooltip.xml",
"Interface\\FrameXML\\ActionBarFrame.xml",
"Interface\\FrameXML\\BonusActionBarFrame.xml",
@@ -25,7 +25,8 @@ fn load_unit_frames(s: &UiScript) {
// UnitFrames.xml and which every bar's OnLoad wires into since 1143.
load_xml(s, "Interface\\FrameXML\\TextStatusBar.lua");
load_xml(s, "Interface\\FrameXML\\TextStatusBar.xml");
load_xml(s, "MoneyFrame.xml");
load_xml(s, r"Interface\FrameXML\MoneyFrame.lua");
load_xml(s, r"Interface\FrameXML\MoneyFrame.xml");
load_xml(s, "GameTooltip.xml");
load_xml(s, "Interface\\FrameXML\\UIDropDownMenu.xml");
load_xml(s, "Interface\\FrameXML\\BasicControls.xml"); // `TEXT`, which UnitPopup.lua reads at file scope
@@ -646,7 +647,6 @@ fn shipped_target_frame_runs_the_level_law() {
s.set_screen_size(1024.0, 768.0);
load_unit_frames(&s);
// GetDifficultyColor's own load chain (the quest log window, its ref home).
load_xml(&s, "MoneyFrame.xml");
load_xml(&s, "Interface\\FrameXML\\GlobalStrings.lua");
load_xml(&s, "UiPanels.xml");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
@@ -1212,7 +1212,8 @@ fn the_party_art_paints_over_the_bars() {
// files resolve it at LOAD (`CombatFeedback.lua` l.7-17, `UnitFrame.lua` l.1-6).
load_xml(&s, "Interface\\FrameXML\\GlobalStrings.lua");
load_xml(&s, "Interface\\FrameXML\\Fonts.xml");
load_xml(&s, "MoneyFrame.xml");
load_xml(&s, r"Interface\FrameXML\MoneyFrame.lua");
load_xml(&s, r"Interface\FrameXML\MoneyFrame.xml");
load_xml(&s, "UiPanels.xml");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
@@ -18,7 +18,8 @@ fn load_popup_frames(s: &UiScript) {
"Interface\\FrameXML\\Fonts.xml",
"UIParent.xml",
// `SmallMoneyFrame_OnLoad`, which UiPanels' own StaticPopup money rows call at load.
"MoneyFrame.xml",
r"Interface\FrameXML\MoneyFrame.lua",
r"Interface\FrameXML\MoneyFrame.xml",
// `StaticPopupDialogs` and the `PanelTemplates_*` family, both of which FriendsFrame.xml
// reaches at LOAD (its tab row and its confirm dialogs).
"UiPanels.xml",
@@ -484,7 +485,8 @@ fn load_pet_menu_frames(s: &UiScript) {
for file in [
"Interface\\FrameXML\\Fonts.xml",
"UIParent.xml",
"MoneyFrame.xml",
r"Interface\FrameXML\MoneyFrame.lua",
r"Interface\FrameXML\MoneyFrame.xml",
"Interface\\FrameXML\\GlobalStrings.lua",
"UiPanels.xml",
r"Interface\FrameXML\UIPanelTemplates.lua",
@@ -14,7 +14,8 @@ fn harness() -> UiScript {
s.set_screen_size(1024.0, 768.0);
for file in [
"Interface\\FrameXML\\Fonts.xml",
"MoneyFrame.xml",
r"Interface\FrameXML\MoneyFrame.lua",
r"Interface\FrameXML\MoneyFrame.xml",
"UiPanels.xml",
r"Interface\FrameXML\UIPanelTemplates.lua",
r"Interface\FrameXML\UIPanelTemplates.xml",
@@ -16,7 +16,8 @@ pub(crate) fn harness() -> UiScript {
s.set_screen_size(1024.0, 768.0);
for f in [
"Interface\\FrameXML\\Fonts.xml",
"MoneyFrame.xml",
r"Interface\FrameXML\MoneyFrame.lua",
r"Interface\FrameXML\MoneyFrame.xml",
"UiPanels.xml",
r"Interface\FrameXML\UIPanelTemplates.lua",
r"Interface\FrameXML\UIPanelTemplates.xml",
@@ -43,7 +43,8 @@ fn harness() -> UiScript {
load_xml(&s, "Interface\\FrameXML\\Fonts.xml");
// `TEXT()`, which AutoFollowStatus_OnEvent puts its message through.
load_xml(&s, "Interface\\FrameXML\\BasicControls.xml");
load_xml(&s, "MoneyFrame.xml");
load_xml(&s, r"Interface\FrameXML\MoneyFrame.lua");
load_xml(&s, r"Interface\FrameXML\MoneyFrame.xml");
load_xml(&s, "UiPanels.xml");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
+2
View File
@@ -488,6 +488,8 @@ mod tests {
// the registrar's case, the server does not close the menu first.
("TalentWipeState", "rides the still-open gossip session"),
("BinderState", "rides the still-open gossip session"),
// The pet trainer's question (1963): the talent wipe's twin, reached the same way.
("PetUnlearnState", "rides the still-open gossip session"),
];
// The chain's own source is the authority on what is wired — not a hand-copied list here,
+5
View File
@@ -325,6 +325,11 @@ pub(super) fn drain_social(
SocialRequest::AddFriend(name) => {
let _ = commands.0.send(ClientCommand::AddFriend { name });
}
SocialRequest::SetLookingForGroup { slots, comment } => {
let _ = commands
.0
.send(ClientCommand::SetLookingForGroup { slots, comment });
}
SocialRequest::RemoveFriendIndex(index) => {
if let Some(guid) = row_guid(&social.display_order, index) {
let _ = commands.0.send(ClientCommand::DelFriend { guid });
+97 -2
View File
@@ -299,6 +299,10 @@ impl Plugin for UiTradePlugin {
// engine state — so it sits ahead of the feed, and an accepted request's
// `partner` is on screen the same frame the window opens.
answer_trade_request.before(feed_trade),
// The coinage-change reflex's trade half (1965): after the world's fields land.
trim_offer_to_purse
.after(crate::ui_unit::UnitFeed)
.before(feed_trade),
drain_trade.after(UiInput),
),
);
@@ -608,7 +612,7 @@ fn snapshot(
#[allow(clippy::too_many_arguments)]
fn feed_trade(
script: Option<NonSendMut<UiScript>>,
trade: Res<TradeSession>,
mut trade: ResMut<TradeSession>,
mut items: ResMut<Items>,
icons: Option<Res<ItemDisplays>>,
mut names: ResMut<NameCache>,
@@ -617,6 +621,7 @@ fn feed_trade(
mut last_open: Local<crate::ui_script::VmMemo<bool>>,
mut last_accept: Local<crate::ui_script::VmMemo<(bool, bool)>>,
mut last_player_gold: Local<crate::ui_script::VmMemo<u32>>,
mut last_their_gold: Local<crate::ui_script::VmMemo<u32>>,
) {
let Some(mut script) = script else {
return;
@@ -625,6 +630,7 @@ fn feed_trade(
let last_open = last_open.get(&script);
let last_accept = last_accept.get(&script);
let last_player_gold = last_player_gold.get(&script);
let last_their_gold = last_their_gold.get(&script);
let fresh = snapshot(&trade, &mut items, icons.as_deref(), &mut names, &commands);
let opened = !*last_open && trade.is_open();
@@ -633,7 +639,40 @@ fn feed_trade(
if changed {
script.set_trade(fresh.clone());
}
// The per-slot events the stock TradeFrame.lua repaints one slot on (`0x4bf414`/`0x4bf452`
// TRADE_TARGET_ITEM_CHANGED, `0x4bf487`/`0x4bfaef` TRADE_PLAYER_ITEM_CHANGED, arg1 the 1-based
// slot — wow-re `event-firesites.tsv`; decision 1966). The full TRADE_UPDATE below is kept:
// the reference fires it too, from `0x4c034f`.
if changed && trade.is_open() && !opened {
let empty = TradeState::default();
let old = last.as_ref().unwrap_or(&empty);
if let Some(new) = fresh.as_ref() {
let changed_slots = |mine: &[Option<TradeSlotItem>],
theirs: &[Option<TradeSlotItem>]| {
mine.iter()
.zip(theirs.iter())
.enumerate()
.filter(|(_, (a, b))| a != b)
.map(|(i, _)| ScriptValue::Int(i as i64 + 1))
.collect::<Vec<_>>()
};
for slot in changed_slots(&new.player.slots, &old.player.slots) {
script.fire_event("TRADE_PLAYER_ITEM_CHANGED", vec![slot]);
}
for slot in changed_slots(&new.target.slots, &old.target.slots) {
script.fire_event("TRADE_TARGET_ITEM_CHANGED", vec![slot]);
}
}
}
if opened {
// `SetTradePartner 0x4bf4e0`'s open leg (decision 1965): coins held on the cursor fold
// into the offer before anything else — the one leg that fires the two money events
// locally, the send following through the money drain below.
if let Some(offer) = script.fold_cursor_money_into_trade() {
trade.set_own_gold(offer);
script.fire_event("PLAYER_TRADE_MONEY", vec![]);
script.fire_event("PLAYER_MONEY", vec![]);
}
script.fire_event("TRADE_SHOW", vec![]);
} else if closed {
script.fire_event("TRADE_CLOSED", vec![]);
@@ -666,6 +705,16 @@ fn feed_trade(
script.fire_event("PLAYER_TRADE_MONEY", vec![]);
}
*last_player_gold = if trade.is_open() { player_gold } else { 0 };
// The partner's gold: TRADE_MONEY_CHANGED, the event the stock money kit's TARGET_TRADE
// frame repaints on (MoneyFrame.lua's OnEvent pairs the two exactly this way — ours on
// PLAYER_TRADE_MONEY, theirs on TRADE_MONEY_CHANGED; the reference's fire site `0x4bf4d6`
// sits beside PLAYER_TRADE_MONEY's `0x4bf4ab` in the status handler). Which side each site
// reads is INFERRED from the consumer; flagged (1962).
let their_gold = trade.their.gold;
if trade.is_open() && their_gold != *last_their_gold {
script.fire_event("TRADE_MONEY_CHANGED", vec![]);
}
*last_their_gold = if trade.is_open() { their_gold } else { 0 };
*last = fresh;
*last_open = trade.is_open();
@@ -768,6 +817,50 @@ fn drain_trade(
}
trade.close();
}
// The TRADE dialog's pair (decision 1963): `BeginTrade` is the empty `0x117`, `CancelTrade`
// the bare `0x11C` — no teardown of ours, the server's status reply drives the window.
if script.take_trade_begin() {
let _ = commands.0.send(ClientCommand::BeginTrade);
}
if script.take_trade_cancel() {
let _ = commands.0.send(ClientCommand::CancelTrade);
}
}
/// The offer the purse can no longer cover, if any — the coinage-change reflex (`0x5ddf30`,
/// decision 1965): when the purse drops below the standing offer, the client trims the offer to
/// the purse and sends it.
fn trimmed_offer(offer: u32, purse: u32) -> Option<u32> {
(offer > purse).then_some(purse)
}
/// The coinage-change reflex's trade half: on every change of `PLAYER_FIELD_COINAGE` with a trade
/// open, an offer past the purse is trimmed to it and re-sent as an absolute `CMSG_SET_TRADE_GOLD`
/// (the coin sound and `PLAYER_MONEY` are `sound/money.rs`'s and the aura feed's).
fn trim_offer_to_purse(
self_q: Query<&ObjectStore, With<SelfPlayer>>,
mut trade: ResMut<TradeSession>,
commands: Res<NetCommands>,
mut prev: Local<Option<u32>>,
) {
let Some(money) = self_q
.iter()
.next()
.and_then(|store| store.0.player_money())
else {
*prev = None;
return;
};
let old = prev.replace(money);
if !matches!(old, Some(p) if p != money) || !trade.is_open() {
return;
}
if let Some(trimmed) = trimmed_offer(trade.our.gold, money) {
trade.set_own_gold(trimmed);
let _ = commands
.0
.send(ClientCommand::SetTradeGold { copper: trimmed });
}
}
/// Resolve a UnitPopup unit token to the player guid to trade with (decision 0592 P1). The
@@ -1270,7 +1363,9 @@ mod tests {
.init_resource::<Items>()
.insert_resource(NetCommands(tx))
.insert_resource(Selection::default());
app.insert_non_send_resource(UiScript::new().unwrap());
let mut vm = UiScript::new().unwrap();
vm.set_money(20_000); // the engine's purse gate on SetTradeMoney (1965)
app.insert_non_send_resource(vm);
// No window yet → the offer is dropped.
app.world_mut()
+2 -1
View File
@@ -22,7 +22,8 @@ mod common;
/// on its way to the chain, so the dependency is gone.
const FILES: &[&str] = &[
"Interface\\FrameXML\\Fonts.xml",
"MoneyFrame.xml",
r"Interface\FrameXML\MoneyFrame.lua",
r"Interface\FrameXML\MoneyFrame.xml",
// The four money-entry frames come off the chain since 1882 — `MoneyInputFrameTemplate` and
// the `MoneyInputFrame_*` verbs, replacing our own verbatim copy of both.
r"Interface\FrameXML\MoneyInputFrame.lua",
+2 -1
View File
@@ -39,7 +39,8 @@ const FILES: &[&str] = &[
"Interface\\FrameXML\\BasicControls.xml",
// The reference's own since 1751 window 24 — `common::load_ui` speaks both stores.
"Interface\\FrameXML\\ItemButtonTemplate.xml",
"MoneyFrame.xml",
r"Interface\FrameXML\MoneyFrame.lua",
r"Interface\FrameXML\MoneyFrame.xml",
// `Model_OnLoad` — the model pane's `<OnLoad>` calls it, so this is a LOAD-time dependency of
// the paper doll, not of the window. The reference declares it in `UIParent.lua`; ours lives
// in our counterpart of that file.
@@ -41,7 +41,8 @@ const FILES: &[&str] = &[
// `UnitFrame.lua` fails the same way (`TEXT(MANA)`). Structural dependencies are not the only
// kind.
"Interface\\FrameXML\\FadingFrame.xml",
"MoneyFrame.xml",
r"Interface\FrameXML\MoneyFrame.lua",
r"Interface\FrameXML\MoneyFrame.xml",
"UiPanels.xml",
r"Interface\FrameXML\UIPanelTemplates.lua",
r"Interface\FrameXML\UIPanelTemplates.xml",
+2 -1
View File
@@ -24,7 +24,8 @@ const FILES: &[&str] = &[
// so the window simply never opens. Our deleted copy carried the string as a local fallback.
"Interface\\FrameXML\\GlobalStrings.lua",
"Interface\\FrameXML\\Fonts.xml",
"MoneyFrame.xml",
r"Interface\FrameXML\MoneyFrame.lua",
r"Interface\FrameXML\MoneyFrame.xml",
"UiPanels.xml",
// `GetMaterialTextColors`, which the reference's own `ItemTextFrame_OnEvent` calls to pick the
// page and title ink. 1.12 keeps it in UIParent.lua and ours does the same (1751 window 9).
+2 -1
View File
@@ -14,7 +14,8 @@ const UI_DIR: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/assets/ui");
/// helpers (postage display), so a load error in either fails here.
const FILES: &[&str] = &[
"Interface\\FrameXML\\Fonts.xml",
"MoneyFrame.xml",
r"Interface\FrameXML\MoneyFrame.lua",
r"Interface\FrameXML\MoneyFrame.xml",
// The send tab's money entry comes off the chain since 1882 — `MoneyInputFrameTemplate` and
// the `MoneyInputFrame_*` verbs. Seated straight after MoneyFrame.xml, benilla.toc's order.
r"Interface\FrameXML\MoneyInputFrame.lua",
+2 -1
View File
@@ -27,7 +27,8 @@ const FILES: &[&str] = &[
// `TEXT`.
"Interface\\FrameXML\\BasicControls.xml",
"Interface\\FrameXML\\ItemButtonTemplate.xml",
"MoneyFrame.xml",
r"Interface\FrameXML\MoneyFrame.lua",
r"Interface\FrameXML\MoneyFrame.xml",
"UiPanels.xml",
// `ToggleTalentFrame` lives here now, not in the window's own file (decision 1833).
"UIParent.xml",
+33 -17
View File
@@ -15,7 +15,8 @@ const UI_DIR: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/assets/ui");
/// helpers (the two gold displays), so a load error in either fails here.
const FILES: &[&str] = &[
"Interface\\FrameXML\\Fonts.xml",
"MoneyFrame.xml",
r"Interface\FrameXML\MoneyFrame.lua",
r"Interface\FrameXML\MoneyFrame.xml",
// The money entry comes off the chain since 1882 — `MoneyInputFrameTemplate` and the
// `MoneyInputFrame_*` verbs the window's OnLoad calls. Seated straight after MoneyFrame.xml,
// which is benilla.toc's own order.
@@ -29,7 +30,9 @@ const FILES: &[&str] = &[
"Interface\\FrameXML\\LocaleProperties.lua",
"Interface\\FrameXML\\StaticPopup.xml", // the dialog engine (1960)
"GameTooltip.xml",
"TradeFrame.xml",
// The stock slot updates go through ItemButtonTemplate.lua's SetItemButton* helpers (1966).
r"Interface\FrameXML\ItemButtonTemplate.xml",
"Interface\\FrameXML\\TradeFrame.xml",
];
fn load_ui(script: &UiScript) {
@@ -123,7 +126,7 @@ fn trade_frame_loads_and_key_regions_exist() {
"TradeRecipientItem7",
"TradeFrameTradeButton",
"TradeFrameCancelButton",
"TradePlayerInputMoneyGold", // our gold is now the editable input (P2)
"TradePlayerInputMoneyFrameGold", // our gold is now the editable input (P2)
"TradeRecipientMoneyFrameCopperButton",
"TradeHighlightPlayer",
"TradeHighlightRecipientEnchant",
@@ -144,6 +147,16 @@ fn trade_show_opens_and_both_columns_populate() {
let mut s = UiScript::new().unwrap();
load_ui(&s);
s.set_trade(Some(state()));
// The stock header reads `UnitName("NPC")` — the partner rides the shared "npc" booth the
// app's session feed points at them (1966).
s.set_unit(
"npc",
Some(benilla_ui::script::UnitState {
exists: true,
name: Some("Thrall".into()),
..Default::default()
}),
);
s.fire_event("TRADE_SHOW", vec![]);
assert!(
@@ -174,18 +187,18 @@ fn trade_show_opens_and_both_columns_populate() {
"Silk Cloth"
);
assert!(
s.eval::<bool>("return TradePlayerItem1ItemButtonIcon:IsShown()")
s.eval::<bool>("return TradePlayerItem1ItemButtonIconTexture:IsShown()")
.unwrap(),
"a filled slot shows its icon"
);
// An empty slot clears its name + hides its icon.
assert_eq!(
s.eval::<String>("return TradePlayerItem2Name:GetText()")
.unwrap(),
""
);
// An empty slot clears its name (the stock's `SetText(nil)`) + hides its icon.
assert!(s
.eval::<Option<String>>("return TradePlayerItem2Name:GetText()")
.unwrap()
.unwrap_or_default()
.is_empty());
assert!(!s
.eval::<bool>("return TradePlayerItem2ItemButtonIcon:IsShown()")
.eval::<bool>("return TradePlayerItem2ItemButtonIconTexture:IsShown()")
.unwrap());
// The partner's name paints from GetTradePartnerName().
@@ -224,7 +237,8 @@ fn enchant_slot_shows_the_not_traded_note() {
assert_eq!(
s.eval::<String>("return TradePlayerItem7Name:GetText()")
.unwrap(),
"|cffffffffWill Not Be Traded|r"
// The stock wording, TRADEFRAME_NOT_MODIFIED_TEXT (ours had read "Will Not Be Traded").
"|cffffffffItem not yet modified|r"
);
assert!(s.take_errors().is_empty());
}
@@ -313,6 +327,7 @@ fn trade_button_click_queues_accept() {
fn player_money_input_reflects_then_offers() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = UiScript::new().unwrap();
s.set_money(1_000_000); // SetTradeMoney is purse-gated (1965)
load_ui(&s);
s.set_trade(Some(state())); // state().player.gold == 12345 (1g 23s 45c)
s.fire_event("TRADE_SHOW", vec![]);
@@ -321,7 +336,7 @@ fn player_money_input_reflects_then_offers() {
for box_ in ["Gold", "Silver", "Copper"] {
assert!(
s.eval::<bool>(&format!(
"return getglobal('TradePlayerInputMoney{box_}') ~= nil"
"return getglobal('TradePlayerInputMoneyFrame{box_}') ~= nil"
))
.unwrap(),
"money box {box_} exists"
@@ -334,9 +349,9 @@ fn player_money_input_reflects_then_offers() {
s.fire_event("PLAYER_TRADE_MONEY", vec![]);
assert_eq!(
s.eval::<(String, String, String)>(
"return TradePlayerInputMoneyGold:GetText(), \
TradePlayerInputMoneySilver:GetText(), \
TradePlayerInputMoneyCopper:GetText()"
"return TradePlayerInputMoneyFrameGold:GetText(), \
TradePlayerInputMoneyFrameSilver:GetText(), \
TradePlayerInputMoneyFrameCopper:GetText()"
)
.unwrap(),
("1".into(), "23".into(), "45".into()),
@@ -351,7 +366,8 @@ fn player_money_input_reflects_then_offers() {
// A genuine keystroke offers the running total (bypass the affordability clamp — a bare harness
// has no purse).
s.run("GetMoney = function() return 100000000 end").unwrap();
s.run("TradePlayerInputMoneyGold:SetText('2')").unwrap();
s.run("TradePlayerInputMoneyFrameGold:SetText('2')")
.unwrap();
s.tick(0.0); // the deferred OnTextChanged drains here (decision 1831)
assert_eq!(
s.take_trade_money(),
+2 -1
View File
@@ -22,7 +22,8 @@ mod common;
/// its guarded-global utilities) so a load error in EITHER window fails here.
const FILES: &[&str] = &[
"Interface\\FrameXML\\Fonts.xml",
"MoneyFrame.xml",
r"Interface\FrameXML\MoneyFrame.lua",
r"Interface\FrameXML\MoneyFrame.xml",
"UiPanels.xml",
r"Interface\FrameXML\UIPanelTemplates.lua",
r"Interface\FrameXML\UIPanelTemplates.xml",
+18
View File
@@ -465,6 +465,24 @@ pub enum SessionEvent {
/// `trainer` to answer with and the `cost` in copper its money frame shows. Answering means
/// sending the SAME opcode back with the guid; declining sends nothing (decision 1580).
TalentWipeConfirm { trainer: u64, cost: u32 },
/// A pet trainer is asking whether to unlearn the pet's skills (`SMSG_PET_UNLEARN_CONFIRM`) —
/// the `CONFIRM_PET_UNLEARN(cost)` dialog's question, the talent-wipe twin for a pet
/// (decision 1963). Answering sends `CMSG_PET_UNLEARN` with the guid; declining sends nothing.
PetUnlearnConfirm { trainer: u64, cost: u32 },
/// The instance-boot clock (`SMSG_RAID_GROUP_ONLY`): a positive delay arms it
/// (`INSTANCE_BOOT_START`), zero clears it (`INSTANCE_BOOT_STOP`) and names `reason` 1/2 on
/// screen (decision 1963).
RaidGroupOnly { delay_ms: u32, reason: u32 },
/// A battleground spirit healer's next resurrection wave (`SMSG_AREA_SPIRIT_HEALER_TIME`):
/// arms `GetAreaSpiritHealerTime` and fires `AREA_SPIRIT_HEALER_IN_RANGE` for the cached
/// healer (decision 1963).
AreaSpiritHealerTime { healer: u64, ms: u32 },
/// One battleground queue slot's state (`SMSG_BATTLEFIELD_STATUS`), the client's three-slot
/// queue that `AcceptBattlefieldPort` answers out of (decision 1963).
BattlefieldStatus(crate::messages::BattlefieldStatus),
/// The meeting-stone queue state (`SMSG 0x295`): the area queued for and a status byte
/// (decision 1963).
MeetingStoneSetQueue { area: u32, status: u8 },
/// The bind took (`SMSG_PLAYERBOUND`): `area` is the AreaTable id we are now bound in, the
/// same one [`Self::BindPoint`] carries in the packet beside it.
PlayerBound { binder: u64, area: u32 },
@@ -774,6 +774,19 @@ pub fn decode(packet: ServerPacket) -> Vec<SessionEvent> {
ServerPacket::TalentWipeConfirm { trainer, cost } => {
vec![SessionEvent::TalentWipeConfirm { trainer, cost }]
}
ServerPacket::PetUnlearnConfirm { trainer, cost } => {
vec![SessionEvent::PetUnlearnConfirm { trainer, cost }]
}
ServerPacket::RaidGroupOnly { delay_ms, reason } => {
vec![SessionEvent::RaidGroupOnly { delay_ms, reason }]
}
ServerPacket::AreaSpiritHealerTime { healer, ms } => {
vec![SessionEvent::AreaSpiritHealerTime { healer, ms }]
}
ServerPacket::BattlefieldStatus(status) => vec![SessionEvent::BattlefieldStatus(status)],
ServerPacket::MeetingStoneSetQueue { area, status } => {
vec![SessionEvent::MeetingStoneSetQueue { area, status }]
}
ServerPacket::PlayerBound { binder, area } => {
vec![SessionEvent::PlayerBound { binder, area }]
}
@@ -0,0 +1,102 @@
//! The battleground queue's wire (decision 1963; wow-re `staticpopup-dialog-bindings.md` §7):
//! the server's per-slot status and the port answer `AcceptBattlefieldPort` sends. Three queue
//! slots exist in the client (`0xb6e9d0`, stride `0x20`), addressed 1-based from Lua.
use std::io::{self, Read};
use crate::wire::{read_u32_le, read_u8};
/// `SMSG_BATTLEFIELD_STATUS`, one slot's update (VERIFIED at the bytes, handler `0x4aa850`).
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct BattlefieldStatus {
/// Which of the three slots, 0-based on the wire.
pub slot: u32,
/// The battleground's Map.dbc row id; zero clears the slot.
pub map_id: u32,
pub bracket: u8,
/// Read into `+0x10`; no Lua reader was found for it (parked by the carve).
pub unknown: u32,
pub status: u32,
/// Status 2: the queue wait so far, in ms (`+0x14 = now + v`).
pub time_ms: Option<u32>,
/// Status 3: the two dwords the client parks beside the selected battlefield.
pub in_progress: Option<(u32, u32)>,
}
/// Parse `SMSG_BATTLEFIELD_STATUS`: `u32 slot`, `u32 mapId`, and — only for a non-zero map —
/// `u8 bracket`, `u32`, `u32 status`, then the status-conditional tail.
pub(super) fn read_battlefield_status(r: &mut impl Read) -> io::Result<BattlefieldStatus> {
let slot = read_u32_le(r)?;
let map_id = read_u32_le(r)?;
if map_id == 0 {
return Ok(BattlefieldStatus {
slot,
map_id,
bracket: 0,
unknown: 0,
status: 0,
time_ms: None,
in_progress: None,
});
}
let bracket = read_u8(r)?;
let unknown = read_u32_le(r)?;
let status = read_u32_le(r)?;
let time_ms = if status == 2 {
Some(read_u32_le(r)?)
} else {
None
};
let in_progress = if status == 3 {
Some((read_u32_le(r)?, read_u32_le(r)?))
} else {
None
};
Ok(BattlefieldStatus {
slot,
map_id,
bracket,
unknown,
status,
time_ms,
in_progress,
})
}
/// Body of `CMSG_BATTLEFIELD_PORT` (VERIFIED, `0x4ab3b0`): `u32 mapId` then a genuinely
/// one-byte `accept`, normalised to 0/1 before it reaches the wire.
pub fn battlefield_port(map_id: u32, accept: bool) -> Vec<u8> {
let mut body = map_id.to_le_bytes().to_vec();
body.push(u8::from(accept));
body
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn status_reads_the_conditional_tails() {
let mut body = vec![
1u8, 0, 0, 0, 30, 0, 0, 0, 5, 7, 0, 0, 0, 2, 0, 0, 0, 100, 0, 0, 0,
];
let s = read_battlefield_status(&mut body.as_slice()).unwrap();
assert_eq!(
(s.slot, s.map_id, s.bracket, s.unknown, s.status),
(1, 30, 5, 7, 2)
);
assert_eq!(s.time_ms, Some(100));
body = vec![0u8, 0, 0, 0, 0, 0, 0, 0];
let s = read_battlefield_status(&mut body.as_slice()).unwrap();
assert_eq!(
s.map_id, 0,
"a zero map clears the slot and ends the packet"
);
}
#[test]
fn port_is_a_map_id_and_one_byte() {
assert_eq!(battlefield_port(489, true), vec![0xE9, 1, 0, 0, 1]);
assert_eq!(battlefield_port(489, false), vec![0xE9, 1, 0, 0, 0]);
}
}

Some files were not shown because too many files have changed in this diff Show More