the buff bar goes stock, and gossip quests show their level
- the buff bar is the reference's own `BuffFrame.xml`, and the swap found an engine defect: an empty `<OnLoad>` in 1.12's XML is a BLANK that takes an inherited handler away again (seven such sites across four stock files), which our loader read as "none given" — so the two enchant slots would have painted the player's first buff in the corner and cancelled it on right-click; the first fix then had to be corrected at the bytes (`SetScript 0x7025c0` tests only NULL and empty, and nothing trims, so `<OnLoad>↵</OnLoad>` compiles to a valid empty function rather than storing nil, which any addon calling `GetScript` can observe); `SHOW_BUFF_DURATIONS` is applied once post-load because 1.12 leaves that to `UIOptionsFrame.lua`, which benilla does not build — without it the bar ships a 10px pitch error on every buff - gossip quests come through the reference's four verbs — `GetGossipAvailableQuests`/`GetGossipActiveQuests` returning (title, level) PAIRS, with the two `Select*` verbs indexing into that list — replacing three invented names that sat on the beyond-1.12 exception list; the quest LEVEL was on the wire, parsed and dropped because the invented verb had nowhere to put it, and the reference's order (available before active, whatever the packet interleaved) comes with them - eight more files off the chain as groundwork: `LocaleProperties.lua` (the gendered/plural `GetText` selector that eleven stock call sites read their words through — fifty lines of FrameXML that looked like an engine binding), `CinematicFrame.xml`, the UIMenu kit, the `UnitFrame.lua`/`CombatFeedback.lua` kits (the only two names blocking the stock player, party, target and pet frames), the money-input kit, and `ItemButtonTemplate.xml` — the base every bag, bank, merchant, loot, trade and mail slot inherits; `GetMerchantItemMaxStack` (on the wire the whole time, dropped) and the two trade item-link verbs join the engine - two instruments for the migration: a gap report that reads a stock window's call sites against a LOADED VM and says which engine verbs it still needs (28 windows need none, the rest name 1–5), and the readiness probe's documented false positive — loading a stock window on top of our same-named one leaves `getglobal(name)` pointing at ours while the stock handlers run against theirs, so a failure inside a colliding name is a question, not an answer Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
@@ -1,829 +0,0 @@
|
||||
<Ui>
|
||||
<!--
|
||||
The player buff/debuff bar and the temporary-weapon-enchant row (decisions 0255/0257 — the
|
||||
player aura arc). benilla's own code against the real 1.12 BuffFrame's contract: the
|
||||
extracted Interface\FrameXML\BuffFrame.xml/.lua is the SPECIFICATION, and geometry,
|
||||
textures, colours, every timing constant AND the data path are read off it.
|
||||
|
||||
THE INDEX SPACE — the thing to understand before touching a handler. There are TWO numbers
|
||||
here and they are not the same number:
|
||||
|
||||
* a button's `id` is a 0-BASED ORDINAL WITHIN ITS OWN FILTER. BuffButton0..15 are
|
||||
id="0".."15" under HELPFUL; BuffButton16..23 are id="0".."7" under HARMFUL. It is a
|
||||
loop counter and nothing else.
|
||||
* `GetPlayerBuff(id, filter)` turns that ordinal into a CACHE POSITION — the aura's
|
||||
physical index in the player's display cache — plus `untilCancelled`. **Every** sibling
|
||||
verb (GetPlayerBuffTexture/…DispelType/…Applications/…TimeLeft, CancelPlayerBuff,
|
||||
GameTooltip:SetPlayerBuff) consumes that position, never the button's id.
|
||||
|
||||
So a button is a two-step: enumerate to a position on the event, then read the position. The
|
||||
position is ABSOLUTE — the number a "HARMFUL" walk hands back is the number an unfiltered
|
||||
walk would — which is why a debuff button's handle can never resolve against the buff list,
|
||||
structurally, with no filter argument threaded through the read path. A miss is `-1`, never
|
||||
nil, and the accessors all tolerate `-1` directly (byte-verified; the family's signature
|
||||
table and the traps are `benilla_ui::script::aura`'s module header).
|
||||
|
||||
This file used to read the Era `UnitBuff`/`UnitDebuff` pair because the 1.12 verbs did not
|
||||
exist. They landed in b2ede294, so the adaptation is gone: the handlers below run on the
|
||||
reference's own verbs and the reference's own numbers.
|
||||
|
||||
SCOPE. The LOCAL PLAYER's auras, plus the reference's two temporary-weapon-enchant slots.
|
||||
0255 left TemporaryEnchantFrame out ("there is no weapon-enchant wire yet"); there is —
|
||||
`ITEM_FIELD_ENCHANTMENT`'s TEMP slot for the charges and `SMSG_ITEM_ENCHANT_TIME_UPDATE`
|
||||
for the countdown, both already feeding the item tooltip (decisions 0915/0920) — so
|
||||
`GetWeaponEnchantInfo` reads them and the row is here. The bar's resting place is
|
||||
unchanged: right-aligned under the minimap at UIParent TOPRIGHT (-175,-13), which is where
|
||||
BuffFrame_Enchant_OnUpdate's no-enchant branch parks it. With an enchant equipped the TOP
|
||||
buff row alone slides left to clear the enchant icons (BuffFrame re-anchors to
|
||||
TemporaryEnchantFrame's TOPLEFT); rows 2 and 3 hang off TempEnchant1/TemporaryEnchantFrame
|
||||
and do not move. That is the reference's behaviour, not a choice made here.
|
||||
|
||||
DURATIONS ARE A SETTING (decision 1139). 0255 shipped the durations-shown geometry
|
||||
unconditionally — the director's call, and there was no panel to hang a switch on — and said
|
||||
in as many words that "if a CVar system later arrives, the geometry is already the branch it
|
||||
would select". It arrived, as the saved-variable store rather than a CVar (1134): the store
|
||||
below is SHOW_BUFF_DURATIONS, the Interface page's Buff Durations row drives it, and BOTH of
|
||||
the reference's geometries are live again — rows at y = 0/−45/−90 with timers, 0/−35/−70
|
||||
without (BuffButtons_UpdatePositions). Our default is "1", the reference's "0": the
|
||||
director's look is what the bar comes up wearing, and what Defaults walks it back to.
|
||||
|
||||
ORDER IS STATE. The player's list the app pushes (crate::ui_aura, decision 0257) IS the
|
||||
reference's insertion-ordered display cache: a new aura appends at the end, a dropped one
|
||||
repacks — NOT ascending slot. That is precisely why a "cache position" needs no structure of
|
||||
its own on our side; position N of that list is position N of the reference's cache.
|
||||
|
||||
DURATION / PERMANENCE is `untilCancelled`, GetPlayerBuff's second return — NOT
|
||||
"expirationTime is 0", which is what this file tested before the verbs landed. They are
|
||||
different questions and the difference is visible: untilCancelled is derived from the
|
||||
SPELL (SpellDuration.dbc's infinite row, plus the area-aura override —
|
||||
`benilla::ui_aura::until_cancelled`, byte-verified at 0x4e452e-0x4e45c5), so it is right on
|
||||
the frame an aura appears, BEFORE any duration packet has landed. An aura that really is
|
||||
timed but whose duration has not arrived yet therefore behaves as the reference does — it
|
||||
counts from "0 s" and pulses for the frame or two before the stamp joins — instead of
|
||||
silently showing no timer at all, which is what an expirationTime test cannot tell apart
|
||||
from a permanent aura.
|
||||
|
||||
THE COUNTDOWN IS A POLL, NOT A CACHE. The aura EVENT is the discrete-change edge (icon,
|
||||
stacks, dispel class); the remaining time is re-read every frame by every visible button.
|
||||
That is the reference's own division of labour and this file keeps it exactly —
|
||||
BuffButton_Update caches the buff POSITION on PLAYER_AURAS_CHANGED, BuffButton_OnUpdate
|
||||
calls GetPlayerBuffTimeLeft with it each frame (ref BuffFrame.lua l.128-130) — and caching
|
||||
the expiry on the event instead is what made refreshed buffs freeze at "0 s" and freshly
|
||||
cast ones show no timer (decision 0846).
|
||||
|
||||
RIGHT-CLICK CANCEL. RegisterForClicks("RightButtonUp") + CancelPlayerBuff(this.buffIndex).
|
||||
The server cancels by spell id and refuses anything the wire's cancelable bit didn't allow,
|
||||
so a debuff right-click is a silent no-op, as in 1.12. `CancelUnitBuff` (the Era name) is
|
||||
the same verb over the same cache behind the same gate and the same CMSG_CANCEL_AURA queue;
|
||||
this file simply no longer needs the Era spelling.
|
||||
|
||||
BEHAVIOURAL DEPARTURES from the specification (noted, not bugs — everything else observable
|
||||
is the reference's, however differently this file spells it):
|
||||
- The count FontString is BLANKED ("") rather than hidden — an empty string draws nothing,
|
||||
same look. A site that predates region Show/Hide; equivalent either way. (The DURATION
|
||||
strings used the same shortcut until 1139 and no longer do: a hidden setting has to hide
|
||||
a region, not write an empty string into a visible one.)
|
||||
- The dispel-class lookup sits INSIDE the `if (debuffSlot)` guard. The reference runs
|
||||
`GetPlayerBuffDispelType(GetPlayerBuff(this:GetID(), "HARMFUL"))` for all 24 buttons and
|
||||
then throws the answer away for the 16 helpful ones, which have no $parentBorder to tint.
|
||||
Provably invisible (the value's only consumer is that SetVertexColor), and it drops 16
|
||||
cache walks per aura event.
|
||||
- The colour is `DebuffTypeColor[debuffType] or DebuffTypeColor["none"]` where the
|
||||
reference branches on `if (debuffType)`. Same answer for the five known classes; ours also
|
||||
survives a dispel name the table has no row for, where the reference indexes nil.
|
||||
- An icon path we cannot resolve falls back to BENILLA_FALLBACK_ICON rather than
|
||||
SetTexture(nil) — carried forward from 0255, and the same fallback the action bar uses.
|
||||
The reference has no such art; ours would otherwise draw an invisible button.
|
||||
- BuffButtons_UpdatePositions is CALLED ONCE AT LOAD (from TemporaryEnchantFrame's OnLoad,
|
||||
the first moment every frame it names exists). The reference never calls it at load
|
||||
because its default is "0" and its static XML anchors already ARE the "0" geometry; ours
|
||||
defaults to "1", so the load-time call is what makes the default honest.
|
||||
- TempEnchant1/2 ship `hidden="true"`. The reference leaves them shown and lets the first
|
||||
BuffFrame_Enchant_OnUpdate hide them — one frame of two empty enchant borders at the top
|
||||
right, which is a flicker worth not having.
|
||||
- BuffButtonTempEnchant is STANDALONE rather than inheriting BuffButtonTemplate and blanking
|
||||
its handlers with empty elements — our loader keeps the inherited handler where the
|
||||
reference clears it. Measured, not assumed; see the template's own note.
|
||||
- `hasEnchant = 1` (ref BuffFrame.lua l.185/208) is dropped: a leaked local that becomes a
|
||||
global, is never cleared, and is read by nothing.
|
||||
- GameTooltip is nil-guarded. Engine tests load file subsets without GameTooltip.xml.
|
||||
-->
|
||||
<Script><![CDATA[
|
||||
-- The timer switch (ref uvar, UIOptionsFrame.lua l.21/104 — its `default = "0"`). This
|
||||
-- assignment is the ONLY statement of our default: the saved chunk executes over it at load
|
||||
-- (1128) and the Interface page's row captures it as its Defaults value at OnLoad, which
|
||||
-- runs first. "1" because the director asked for visible timers (0255) — the divergence is
|
||||
-- written down once, here, and Defaults honours it rather than the reference's table.
|
||||
SHOW_BUFF_DURATIONS = "1"
|
||||
RegisterForSave("SHOW_BUFF_DURATIONS")
|
||||
|
||||
-- The reference's flash cadence + thresholds (BuffFrame.lua l.1-14). An aura in its last 31s
|
||||
-- pulses between full and BUFF_MIN_ALPHA; its duration text turns highlight-white under 60s.
|
||||
BUFF_FLASH_TIME_ON = 0.75
|
||||
BUFF_FLASH_TIME_OFF = 0.75
|
||||
BUFF_MIN_ALPHA = 0.3
|
||||
BUFF_WARNING_TIME = 31
|
||||
BUFF_DURATION_WARNING_TIME = 60
|
||||
|
||||
-- The dispel-class border tints (BuffFrame.lua l.9-14); "none" is the undispellable red the
|
||||
-- debuff overlay wears by default. GetPlayerBuffDispelType's return keys this table — it
|
||||
-- hands back the SpellDispelType.dbc name, or nil, never "" and never "none".
|
||||
DebuffTypeColor = {
|
||||
["none"] = { r = 0.80, g = 0.00, b = 0.00 },
|
||||
["Magic"] = { r = 0.20, g = 0.60, b = 1.00 },
|
||||
["Curse"] = { r = 0.60, g = 0.00, b = 1.00 },
|
||||
["Disease"] = { r = 0.60, g = 0.40, b = 0.00 },
|
||||
["Poison"] = { r = 0.00, g = 0.60, b = 0.00 },
|
||||
}
|
||||
|
||||
-- SecondsToTimeAbbrev + its GlobalStrings (UIParent.lua l.1034 / GlobalStrings.lua). We load
|
||||
-- no UIParent/GlobalStrings, so define them here (guarded, in case a later file ships them).
|
||||
DAY_ONELETTER_ABBR = DAY_ONELETTER_ABBR or "%d d"
|
||||
HOUR_ONELETTER_ABBR = HOUR_ONELETTER_ABBR or "%d h"
|
||||
MINUTE_ONELETTER_ABBR = MINUTE_ONELETTER_ABBR or "%d m"
|
||||
SECOND_ONELETTER_ABBR = SECOND_ONELETTER_ABBR or "%d s"
|
||||
|
||||
if not SecondsToTimeAbbrev then
|
||||
function SecondsToTimeAbbrev(seconds)
|
||||
if seconds > 86400 then
|
||||
return format(DAY_ONELETTER_ABBR, ceil(seconds / 86400))
|
||||
elseif seconds > 3600 then
|
||||
return format(HOUR_ONELETTER_ABBR, ceil(seconds / 3600))
|
||||
elseif seconds > 60 then
|
||||
return format(MINUTE_ONELETTER_ABBR, ceil(seconds / 60))
|
||||
end
|
||||
return format(SECOND_ONELETTER_ABBR, seconds)
|
||||
end
|
||||
end
|
||||
|
||||
-- The frame-wide flash pulse (BuffFrame_OnLoad / _OnUpdate, BuffFrame.lua l.17-56). One
|
||||
-- BUFF_ALPHA_VALUE is computed per frame and every low-on-time button reads it, so they blink
|
||||
-- in unison. The bar also seats each duration string under its button here.
|
||||
function BuffFrame_OnLoad()
|
||||
BuffFrameUpdateTime = 0
|
||||
BuffFrameFlashTime = 0
|
||||
BuffFrameFlashState = 1
|
||||
BUFF_ALPHA_VALUE = 1
|
||||
for i = 0, 23 do
|
||||
getglobal("BuffButton" .. i .. "Duration"):SetPoint("TOP", "BuffButton" .. i, "BOTTOM", 0, 0)
|
||||
end
|
||||
end
|
||||
|
||||
-- Half of the square wave, run down by one frame. BuffFrameFlashTime is the time LEFT in
|
||||
-- the current half; when it goes under, the state flips and the counter reloads with the
|
||||
-- new half's length. The overshoot is carried into that half — unless it is longer than
|
||||
-- the half itself, in which case the whole period is kept rather than skipped, so a
|
||||
-- stalled frame costs at most one beat of drift instead of racing the pulse forward.
|
||||
local function buffFlashAdvance(elapsed)
|
||||
BuffFrameFlashTime = BuffFrameFlashTime - elapsed
|
||||
if BuffFrameFlashTime >= 0 then
|
||||
return
|
||||
end
|
||||
local overtime = -BuffFrameFlashTime
|
||||
if BuffFrameFlashState == 0 then
|
||||
BuffFrameFlashState = 1
|
||||
BuffFrameFlashTime = BUFF_FLASH_TIME_ON
|
||||
else
|
||||
BuffFrameFlashState = 0
|
||||
BuffFrameFlashTime = BUFF_FLASH_TIME_OFF
|
||||
end
|
||||
if overtime < BuffFrameFlashTime then
|
||||
BuffFrameFlashTime = BuffFrameFlashTime - overtime
|
||||
end
|
||||
end
|
||||
|
||||
function BuffFrame_OnUpdate(elapsed)
|
||||
-- The tooltip re-poll's throttle (ref l.29-33): oscillates around zero with a
|
||||
-- TOOLTIP_UPDATE_TIME period, and BuffButton_OnUpdate refreshes a hovered plate only on
|
||||
-- the frames it is non-positive. ActionBar.xml declares TOOLTIP_UPDATE_TIME and loads
|
||||
-- first (benilla.toc), so it is always a number by the time this runs.
|
||||
if BuffFrameUpdateTime > 0 then
|
||||
BuffFrameUpdateTime = BuffFrameUpdateTime - elapsed
|
||||
else
|
||||
BuffFrameUpdateTime = BuffFrameUpdateTime + TOOLTIP_UPDATE_TIME
|
||||
end
|
||||
|
||||
-- The pulse's level for this frame: a 0..1 ramp up across the ON half and back down
|
||||
-- across the OFF half (both scaled by BUFF_FLASH_TIME_ON — the reference's own choice,
|
||||
-- and invisible while the two halves are the same length), squashed into
|
||||
-- [BUFF_MIN_ALPHA, 1] so a warning icon dims rather than disappears.
|
||||
--
|
||||
-- Computed into a LOCAL and published once. BUFF_ALPHA_VALUE is read by every visible
|
||||
-- button, so it is not scratch space: the reference assigns the raw ramp to it and then
|
||||
-- squashes it in a second statement, which leaves the global holding an un-squashed
|
||||
-- value for the length of one statement.
|
||||
buffFlashAdvance(elapsed)
|
||||
local level
|
||||
if BuffFrameFlashState == 1 then
|
||||
level = (BUFF_FLASH_TIME_ON - BuffFrameFlashTime) / BUFF_FLASH_TIME_ON
|
||||
else
|
||||
level = BuffFrameFlashTime / BUFF_FLASH_TIME_ON
|
||||
end
|
||||
BUFF_ALPHA_VALUE = (level * (1 - BUFF_MIN_ALPHA)) + BUFF_MIN_ALPHA
|
||||
end
|
||||
|
||||
-- BuffButtons_UpdatePositions (BuffFrame.lua l.152-160): the ROW PITCH depends on whether a
|
||||
-- timer sits under each icon. Shown, the three rows land at y = 0/−45/−90 (a 30px icon + a
|
||||
-- 15px gutter); hidden, at 0/−35/−70 — the icons close up to the same 5px the columns use.
|
||||
-- The anchors are the reference's own, through the TempEnchant chain: row 2 hangs off
|
||||
-- TempEnchant1 and row 3 off TemporaryEnchantFrame, which is what keeps them still while the
|
||||
-- top row slides left to clear an equipped enchant. SetPoint on a point already held
|
||||
-- overwrites it (layout.rs — the client's own SetPoint law).
|
||||
--
|
||||
-- This is the Buff Durations row's `applyFunc`, and the ONLY thing the setting needs applied:
|
||||
-- the timer text itself is re-decided every frame by the poll below, so a click changes what
|
||||
-- the next frame draws without anyone telling it. Geometry is the part that is stated once.
|
||||
function BuffButtons_UpdatePositions()
|
||||
if SHOW_BUFF_DURATIONS == "1" then
|
||||
BuffButton8:SetPoint("TOP", "TempEnchant1", "BOTTOM", 0, -15)
|
||||
BuffButton16:SetPoint("TOPRIGHT", "TemporaryEnchantFrame", "TOPRIGHT", 0, -90)
|
||||
else
|
||||
BuffButton8:SetPoint("TOP", "TempEnchant1", "BOTTOM", 0, -5)
|
||||
BuffButton16:SetPoint("TOPRIGHT", "TemporaryEnchantFrame", "TOPRIGHT", 0, -70)
|
||||
end
|
||||
end
|
||||
|
||||
-- Void a button's memory of what its duration region last had WRITTEN to it (the
|
||||
-- benillaDur* fields BuffFrame_UpdateDuration gates on). Called by everything ELSE that
|
||||
-- touches the region — the aura event's repaint here, the enchant row's clear — so the
|
||||
-- per-frame gate can never skip a write against a region somebody moved under it; a
|
||||
-- forgotten state just means the next poll writes once.
|
||||
local function buffDurationForget(button)
|
||||
button.benillaDurShown = nil
|
||||
button.benillaDurText = nil
|
||||
button.benillaDurWarn = nil
|
||||
button.benillaDurSetting = nil
|
||||
end
|
||||
|
||||
-- The button's aura, resolved on the discrete-change edge and CACHED AS A POSITION
|
||||
-- (ref BuffButton_Update, l.58-107). `this:GetID()` is the 0-based ordinal within
|
||||
-- this.buffFilter; buffIndex is the cache position every read below uses.
|
||||
function BuffButton_Update()
|
||||
local buffIndex, untilCancelled = GetPlayerBuff(this:GetID(), this.buffFilter)
|
||||
this.buffIndex = buffIndex
|
||||
this.untilCancelled = untilCancelled
|
||||
local buffDuration = this.durationFS or getglobal(this:GetName() .. "Duration")
|
||||
this.durationFS = buffDuration
|
||||
|
||||
-- -1, not nil: the miss sentinel the whole family agrees on.
|
||||
if buffIndex < 0 then
|
||||
this:Hide()
|
||||
buffDuration:Hide()
|
||||
buffDurationForget(this)
|
||||
return
|
||||
end
|
||||
this:SetAlpha(1.0)
|
||||
this.benillaLastAlpha = 1.0
|
||||
this:Show()
|
||||
if SHOW_BUFF_DURATIONS == "1" then
|
||||
buffDuration:Show()
|
||||
else
|
||||
buffDuration:Hide()
|
||||
end
|
||||
buffDurationForget(this)
|
||||
|
||||
getglobal(this:GetName() .. "Icon"):SetTexture(GetPlayerBuffTexture(buffIndex) or BENILLA_FALLBACK_ICON)
|
||||
|
||||
-- Debuff border, tinted by dispel class (harmful buttons only — helpful ones have no
|
||||
-- $parentBorder, so getglobal is nil and the whole lookup is skipped; see the header).
|
||||
local debuffSlot = getglobal(this:GetName() .. "Border")
|
||||
if debuffSlot then
|
||||
local debuffType = GetPlayerBuffDispelType(GetPlayerBuff(this:GetID(), "HARMFUL"))
|
||||
local color = DebuffTypeColor[debuffType] or DebuffTypeColor["none"]
|
||||
debuffSlot:SetVertexColor(color.r, color.g, color.b)
|
||||
end
|
||||
|
||||
-- Stack count, shown only above 1 (ref l.94-102). GetPlayerBuffApplications is a NUMBER
|
||||
-- on every path — 1 for an absent position — so `> 1` needs no nil guard.
|
||||
local buffCount = getglobal(this:GetName() .. "Count")
|
||||
local count = GetPlayerBuffApplications(buffIndex)
|
||||
if count > 1 then
|
||||
buffCount:SetText(count)
|
||||
else
|
||||
buffCount:SetText("")
|
||||
end
|
||||
|
||||
if GameTooltip and GameTooltip:IsOwned(this) then
|
||||
GameTooltip:SetPlayerBuff(buffIndex)
|
||||
end
|
||||
end
|
||||
|
||||
-- A button takes right-clicks and listens for the aura rebuild. The first paint hides it
|
||||
-- until the feed reports an aura. PLAYER_AURAS_CHANGED is the reference's event and the app
|
||||
-- fires it beside the Era-shaped UNIT_AURA on the same rebuild (`ui_aura`).
|
||||
function BuffButton_OnLoad()
|
||||
-- Valid tokens for "buffFilter": HELPFUL, HARMFUL, CANCELABLE, NOT_CANCELABLE. Note the
|
||||
-- mask starts at ZERO once a filter string is given, so a bare "CANCELABLE" would match
|
||||
-- nothing — the sign must always be named.
|
||||
BuffButton_Update()
|
||||
this:RegisterForClicks("RightButtonUp")
|
||||
this:RegisterEvent("PLAYER_AURAS_CHANGED")
|
||||
end
|
||||
|
||||
function BuffButton_OnEvent(event)
|
||||
if event == "PLAYER_AURAS_CHANGED" then
|
||||
BuffButton_Update()
|
||||
end
|
||||
end
|
||||
|
||||
-- Per-frame: RE-READ the remaining time from the cached POSITION (never a cached duration),
|
||||
-- flash in the last 31s, write the abbreviated timer, and tick a hovered tooltip.
|
||||
--
|
||||
-- The re-read every frame is the reference's own shape, and it is load-bearing: a duration
|
||||
-- that changes without any discrete change — a recast, a DoT reapplied, cast pushback, the
|
||||
-- server's replay of every duration at world entry — fires no aura event, so a button that
|
||||
-- cached its expiry on the event would keep counting the OLD one down, sit at "0 s" while
|
||||
-- the buff is still live, and never show a timer for an aura whose duration landed after
|
||||
-- its icon did. Both were live defects; the poll is what makes them unrepresentable
|
||||
-- (decision 0846).
|
||||
--
|
||||
-- The untilCancelled early-out (ref l.124-127) is what keeps a permanent aura from flashing
|
||||
-- a "0 s" countdown while waiting for a duration packet that is never coming: it is answered
|
||||
-- from the SPELL, so it is right before any packet arrives. This frame runs only on visible
|
||||
-- frames (`UiScript::tick`), exactly as the client's does, so a hidden button's stale
|
||||
-- buffIndex is never read.
|
||||
function BuffButton_OnUpdate()
|
||||
local button = this
|
||||
-- A permanent aura has nothing to count down and nothing to flash toward, so it is
|
||||
-- done in one line: UpdateDuration with no time left is exactly "hide the timer",
|
||||
-- which is the whole of the reference's early-out here. (The reference reaches for the
|
||||
-- $parentDuration region itself to do it, on every frame of every visible button, and
|
||||
-- then throws that region away and lets UpdateDuration look it up again.)
|
||||
if button.untilCancelled == 1 then
|
||||
BuffFrame_UpdateDuration(button, nil)
|
||||
return
|
||||
end
|
||||
|
||||
local buffIndex = button.buffIndex
|
||||
local timeLeft = GetPlayerBuffTimeLeft(buffIndex)
|
||||
-- The flash is NOT gated on the timer setting: the reference pulses a button in its last
|
||||
-- 31s whether or not a number is drawn under it, and with the timers off it is the only
|
||||
-- thing left saying "about to drop".
|
||||
--
|
||||
-- Written only when it moves (decision 1396's class — a settled bar must not rewrite
|
||||
-- per frame what changes only on an edge). Inside the warning window BUFF_ALPHA_VALUE
|
||||
-- is a fresh ramp value every frame, so the pulse flows through the compare untouched;
|
||||
-- outside it the value is the 1.0 that is already there, forever.
|
||||
local alpha
|
||||
if timeLeft < BUFF_WARNING_TIME then
|
||||
alpha = BUFF_ALPHA_VALUE
|
||||
else
|
||||
alpha = 1.0
|
||||
end
|
||||
if alpha ~= button.benillaLastAlpha then
|
||||
button.benillaLastAlpha = alpha
|
||||
button:SetAlpha(alpha)
|
||||
end
|
||||
|
||||
BuffFrame_UpdateDuration(button, timeLeft)
|
||||
|
||||
-- A tooltip we own is re-read on the throttle's beat, not every frame.
|
||||
if BuffFrameUpdateTime <= 0 and GameTooltip and GameTooltip:IsOwned(button) then
|
||||
GameTooltip:SetPlayerBuff(buffIndex)
|
||||
end
|
||||
end
|
||||
|
||||
-- Right-click cancels by CACHE POSITION (ref l.148-150). The app sends CMSG_CANCEL_AURA for
|
||||
-- a cancelable aura and drops the rest, so a debuff right-click is a silent no-op.
|
||||
function BuffButton_OnClick()
|
||||
CancelPlayerBuff(this.buffIndex)
|
||||
end
|
||||
|
||||
-- Clear enchant slots `first`..2 — the tail of the row nothing is using this frame. The
|
||||
-- Hide moves the Duration region under the poll's write gate, so the gate's memory is
|
||||
-- voided with it.
|
||||
local function buffEnchantClear(first)
|
||||
for slot = first, 2 do
|
||||
local button = getglobal("TempEnchant" .. slot)
|
||||
button:Hide()
|
||||
getglobal("TempEnchant" .. slot .. "Duration"):Hide()
|
||||
buffDurationForget(button)
|
||||
end
|
||||
end
|
||||
|
||||
-- One hand's six steps (the same block the reference spells out twice, ref l.180-207):
|
||||
-- an enchanted hand packs into the next slot rightwards and keeps its countdown and
|
||||
-- warning pulse live; a bare hand packs nothing. Returns the new fill count. A plain
|
||||
-- function of its arguments so the per-frame path allocates nothing — this used to be a
|
||||
-- two-row table built fresh every frame.
|
||||
--
|
||||
-- `slot` is the live API's inventory id, which is also what the button's hover reads
|
||||
-- back off itself to ask for the ITEM's tooltip.
|
||||
local function buffEnchantHand(filled, enchanted, expiration, slot)
|
||||
if not enchanted then
|
||||
return filled
|
||||
end
|
||||
filled = filled + 1
|
||||
local button = getglobal("TempEnchant" .. filled)
|
||||
button:SetID(slot)
|
||||
getglobal(button:GetName() .. "Icon"):SetTexture(
|
||||
GetInventoryItemTexture("player", slot) or BENILLA_FALLBACK_ICON)
|
||||
button:Show()
|
||||
|
||||
-- The wire counts an enchant down in MILLISECONDS; everything the row draws is
|
||||
-- in seconds. A nil expiration is "no countdown known", which the timer and the
|
||||
-- warning flash both read as "nothing to warn about".
|
||||
local timeLeft = expiration and (expiration / 1000)
|
||||
BuffFrame_UpdateDuration(button, timeLeft)
|
||||
if timeLeft and timeLeft < BUFF_WARNING_TIME then
|
||||
button:SetAlpha(BUFF_ALPHA_VALUE)
|
||||
else
|
||||
button:SetAlpha(1.0)
|
||||
end
|
||||
return filled
|
||||
end
|
||||
|
||||
-- The temporary-weapon-enchant row (ref l.162-233). GetWeaponEnchantInfo is the whole data
|
||||
-- path: two flags, two remaining-MILLISECOND counts, two charge counts (the charges are
|
||||
-- read by nobody here, and are named only to spell the API's shape out).
|
||||
--
|
||||
-- OFF HAND FIRST is the part that matters: the row packs from slot 1 rightwards, so with
|
||||
-- both weapons enchanted the main hand lands in slot 2 and therefore sits to the LEFT of
|
||||
-- the off hand.
|
||||
function BuffFrame_Enchant_OnUpdate(elapsed)
|
||||
local hasMainHandEnchant, mainHandExpiration, mainHandCharges,
|
||||
hasOffHandEnchant, offHandExpiration, offHandCharges = GetWeaponEnchantInfo()
|
||||
|
||||
-- Nothing enchanted: clear the whole row and park BuffFrame back on its resting
|
||||
-- point — ONCE per transition, latched on benillaCleared (decision 1396's class:
|
||||
-- re-deriving per frame what changes only on an edge). The latch starts unset, so
|
||||
-- the FIRST pass still writes — the load-time hide the header's TempEnchant note
|
||||
-- leans on — and any enchant appearing clears it below.
|
||||
if not hasMainHandEnchant and not hasOffHandEnchant then
|
||||
if not TemporaryEnchantFrame.benillaCleared then
|
||||
TemporaryEnchantFrame.benillaCleared = true
|
||||
-- Forget the last row width too: after a park, the next enchanted pass must
|
||||
-- re-write the width AND the bar's TOPLEFT anchor whatever its fill count is.
|
||||
TemporaryEnchantFrame.benillaFilled = nil
|
||||
buffEnchantClear(1)
|
||||
BuffFrame:SetPoint("TOPRIGHT", "TemporaryEnchantFrame", "TOPRIGHT", 0, 0)
|
||||
end
|
||||
return
|
||||
end
|
||||
TemporaryEnchantFrame.benillaCleared = nil
|
||||
|
||||
local filled = buffEnchantHand(0, hasOffHandEnchant, offHandExpiration, 17)
|
||||
filled = buffEnchantHand(filled, hasMainHandEnchant, mainHandExpiration, 16)
|
||||
buffEnchantClear(filled + 1)
|
||||
|
||||
-- Position buff frame: the row is `filled` icons wide, and the bar starts one 5px
|
||||
-- gutter to its left. The geometry depends on nothing but `filled`, so it is written
|
||||
-- only when that changes.
|
||||
if TemporaryEnchantFrame.benillaFilled ~= filled then
|
||||
TemporaryEnchantFrame.benillaFilled = filled
|
||||
TemporaryEnchantFrame:SetWidth(filled * 32)
|
||||
BuffFrame:SetPoint("TOPRIGHT", "TemporaryEnchantFrame", "TOPLEFT", -5, 0)
|
||||
end
|
||||
end
|
||||
|
||||
function BuffFrame_EnchantButton_OnUpdate()
|
||||
if GameTooltip and GameTooltip:IsOwned(this) then
|
||||
BuffFrame_EnchantButton_OnEnter()
|
||||
end
|
||||
end
|
||||
|
||||
-- The enchant hover is the ITEM's tooltip, not an aura's — the plate hangs below-left like
|
||||
-- the buff buttons' (ref l.242-245).
|
||||
function BuffFrame_EnchantButton_OnEnter()
|
||||
if not GameTooltip then return end
|
||||
GameTooltip:SetOwner(this, "ANCHOR_BOTTOMLEFT")
|
||||
GameTooltip:SetInventoryItem("player", this:GetID())
|
||||
end
|
||||
|
||||
-- BuffFrame_UpdateDuration (ref l.247-260), gate and all: the timer draws only when the
|
||||
-- setting is on AND there is a live one. `timeLeft` nil is the enchant row's "no countdown
|
||||
-- known" case. Hidden, not blanked — see the header's deviation note.
|
||||
--
|
||||
-- The POLL stays per-frame (the re-read is load-bearing — decision 0846); only the WRITES
|
||||
-- are gated, on what was last written: shown, the abbreviated text, the warning band, and
|
||||
-- the SHOW_BUFF_DURATIONS value itself (so the settings toggle stays live). Deliberately
|
||||
-- never on a measured extent (1396's trap: a fixture with no font engine measures 0
|
||||
-- forever); the compare is against the STRING this function computed, nothing the host
|
||||
-- had to shape. The benillaDur* memory is voided by whoever else touches the region
|
||||
-- (buffDurationForget), so a stale skip is unrepresentable.
|
||||
function BuffFrame_UpdateDuration(buffButton, timeLeft)
|
||||
local duration = buffButton.durationFS or getglobal(buffButton:GetName() .. "Duration")
|
||||
buffButton.durationFS = duration
|
||||
local setting = SHOW_BUFF_DURATIONS
|
||||
-- Two ways to have no number to draw: the setting is off, or nothing has told us how
|
||||
-- long is left (the enchant row's "no countdown known").
|
||||
if setting ~= "1" or not timeLeft then
|
||||
-- == false, not falsey: nil is "no memory", and the first pass must write.
|
||||
if buffButton.benillaDurShown == false and buffButton.benillaDurSetting == setting then
|
||||
return
|
||||
end
|
||||
buffButton.benillaDurShown = false
|
||||
buffButton.benillaDurText = nil
|
||||
buffButton.benillaDurWarn = nil
|
||||
buffButton.benillaDurSetting = setting
|
||||
duration:Hide()
|
||||
return
|
||||
end
|
||||
local text = SecondsToTimeAbbrev(timeLeft)
|
||||
-- Gold most of the way, white inside the last minute — the number's own warning, a
|
||||
-- half-minute ahead of the icon starting to pulse.
|
||||
local warning = timeLeft < BUFF_DURATION_WARNING_TIME
|
||||
if buffButton.benillaDurShown == true and buffButton.benillaDurText == text
|
||||
and buffButton.benillaDurWarn == warning and buffButton.benillaDurSetting == setting then
|
||||
return
|
||||
end
|
||||
buffButton.benillaDurShown = true
|
||||
buffButton.benillaDurText = text
|
||||
buffButton.benillaDurWarn = warning
|
||||
buffButton.benillaDurSetting = setting
|
||||
duration:SetText(text)
|
||||
local color = NORMAL_FONT_COLOR
|
||||
if warning then
|
||||
color = HIGHLIGHT_FONT_COLOR
|
||||
end
|
||||
duration:SetVertexColor(color.r, color.g, color.b)
|
||||
duration:Show()
|
||||
end
|
||||
]]></Script>
|
||||
|
||||
<!-- 30x30 icon + a bottom-right stack count (ref BuffButtonTemplate, BuffFrame.xml l.4-44).
|
||||
The base template's own filter is the reference's HELPFUL|HARMFUL; the two concrete
|
||||
variants below pin one sign each. -->
|
||||
<Button name="BuffButtonTemplate" virtual="true">
|
||||
<Size><AbsDimension x="30" y="30"/></Size>
|
||||
<Layers>
|
||||
<Layer level="BACKGROUND">
|
||||
<Texture name="$parentIcon"/>
|
||||
<FontString name="$parentCount" inherits="NumberFontNormal">
|
||||
<Anchors>
|
||||
<Anchor point="BOTTOMRIGHT"><Offset><AbsDimension x="-2" y="2"/></Offset></Anchor>
|
||||
</Anchors>
|
||||
</FontString>
|
||||
</Layer>
|
||||
</Layers>
|
||||
<Scripts>
|
||||
<OnLoad>
|
||||
this.buffFilter = "HELPFUL|HARMFUL"
|
||||
BuffButton_OnLoad()
|
||||
</OnLoad>
|
||||
<OnEvent>BuffButton_OnEvent(event)</OnEvent>
|
||||
<OnUpdate>BuffButton_OnUpdate()</OnUpdate>
|
||||
<OnClick>BuffButton_OnClick()</OnClick>
|
||||
<OnEnter>
|
||||
if GameTooltip then
|
||||
GameTooltip:SetOwner(this, "ANCHOR_BOTTOMLEFT")
|
||||
GameTooltip:SetPlayerBuff(this.buffIndex)
|
||||
end
|
||||
</OnEnter>
|
||||
<OnLeave>
|
||||
if GameTooltip then GameTooltip:Hide() end
|
||||
</OnLeave>
|
||||
</Scripts>
|
||||
</Button>
|
||||
|
||||
<!-- Helpful variant: filter HELPFUL, no border (ref l.50-57). -->
|
||||
<Button name="BuffButtonHelpful" inherits="BuffButtonTemplate" virtual="true">
|
||||
<Scripts>
|
||||
<OnLoad>
|
||||
this.buffFilter = "HELPFUL"
|
||||
BuffButton_OnLoad()
|
||||
</OnLoad>
|
||||
</Scripts>
|
||||
</Button>
|
||||
|
||||
<!-- Harmful variant: filter HARMFUL + the dispel-tinted border overlay (ref l.58-82).
|
||||
33x32 UI-Debuff-Overlays, the exact reference TexCoords, tint set at runtime. -->
|
||||
<Button name="BuffButtonHarmful" inherits="BuffButtonTemplate" virtual="true">
|
||||
<Layers>
|
||||
<Layer level="OVERLAY">
|
||||
<Texture name="$parentBorder" file="Interface\Buttons\UI-Debuff-Overlays">
|
||||
<Size><AbsDimension x="33" y="32"/></Size>
|
||||
<Anchors>
|
||||
<Anchor point="CENTER"><Offset><AbsDimension x="0" y="0"/></Offset></Anchor>
|
||||
</Anchors>
|
||||
<TexCoords left="0.296875" right="0.5703125" top="0" bottom="0.515625"/>
|
||||
</Texture>
|
||||
</Layer>
|
||||
</Layers>
|
||||
<Scripts>
|
||||
<OnLoad>
|
||||
this.buffFilter = "HARMFUL"
|
||||
BuffButton_OnLoad()
|
||||
</OnLoad>
|
||||
</Scripts>
|
||||
</Button>
|
||||
|
||||
<!-- Temp-enchant slot: the same 30x30 icon under a 32x32 UI-TempEnchant-Border (ref l.83-117).
|
||||
It carries no filter and no buffIndex — its data is GetWeaponEnchantInfo and its hover is
|
||||
the ITEM's tooltip.
|
||||
|
||||
**STANDALONE, where the reference inherits BuffButtonTemplate and then blanks the four aura
|
||||
handlers with empty `<OnLoad></OnLoad>` / `<OnEvent>` / `<OnClick>` elements.** Our loader
|
||||
does not implement that idiom: `Loader::compile_handler` returns None for an empty body, so
|
||||
`apply_scripts` skips the SetScript entirely and the INHERITED handler survives. Written
|
||||
the reference's way and MEASURED, TempEnchant1 ran `BuffButton_OnLoad`, registered
|
||||
PLAYER_AURAS_CHANGED, and painted the player's first buff in the corner — and a right-click
|
||||
on it would have cancelled that aura. Writing the three regions out removes the dependence
|
||||
on a loader semantic we have not byte-verified; the engine gap is real and worth closing on
|
||||
its own evidence, not silently here. -->
|
||||
<Button name="BuffButtonTempEnchant" virtual="true">
|
||||
<Size><AbsDimension x="30" y="30"/></Size>
|
||||
<Layers>
|
||||
<Layer level="BACKGROUND">
|
||||
<Texture name="$parentIcon"/>
|
||||
<FontString name="$parentCount" inherits="NumberFontNormal">
|
||||
<Anchors>
|
||||
<Anchor point="BOTTOMRIGHT"><Offset><AbsDimension x="-2" y="2"/></Offset></Anchor>
|
||||
</Anchors>
|
||||
</FontString>
|
||||
</Layer>
|
||||
<Layer level="OVERLAY">
|
||||
<Texture name="$parentBorder" file="Interface\Buttons\UI-TempEnchant-Border">
|
||||
<Size><AbsDimension x="32" y="32"/></Size>
|
||||
<Anchors>
|
||||
<Anchor point="CENTER"><Offset><AbsDimension x="0" y="0"/></Offset></Anchor>
|
||||
</Anchors>
|
||||
</Texture>
|
||||
</Layer>
|
||||
</Layers>
|
||||
<Scripts>
|
||||
<OnUpdate>BuffFrame_EnchantButton_OnUpdate()</OnUpdate>
|
||||
<OnEnter>BuffFrame_EnchantButton_OnEnter()</OnEnter>
|
||||
<OnLeave>
|
||||
if GameTooltip then GameTooltip:Hide() end
|
||||
</OnLeave>
|
||||
</Scripts>
|
||||
</Button>
|
||||
|
||||
<!-- The duration text under a button: the ref routes these through a virtual FontString
|
||||
template (BuffButtonDurationTemplate, l.45-49 — GameFontNormalSmall + a bare TOP anchor).
|
||||
Region templates DO exist in the loader now (Loader::expand_region), but chaining to a FONT
|
||||
OBJECT through one does not: `framexml::merge` lets the instance's own `inherits=` win, so
|
||||
the spliced element still reads `inherits="BuffButtonDurationTemplate"` and
|
||||
`apply_fontstring_font` looks that up in the FONT registry — a separate namespace — and
|
||||
misses. Each instance therefore names GameFontNormalSmall directly; the anchor is re-seated
|
||||
by BuffFrame_OnLoad / TemporaryEnchantFrame's OnLoad anyway. Same resolved font, no
|
||||
indirection. -->
|
||||
<!--
|
||||
The bar. Right-aligned under the minimap (UIParent TOPRIGHT -175,-13 — the reference's XML
|
||||
says -205 and lets the first Enchant_OnUpdate snap it here; ours starts where it rests, so
|
||||
the bar is never one frame out of place). 16 helpful buttons in two rows of 8 (ids 0-15),
|
||||
then 8 harmful (ids 0-7). The 24 duration strings live in the frame's ARTWORK layer (over the
|
||||
icons' BACKGROUND), named to pair with their button. BuffButton8's and BuffButton16's static
|
||||
anchors here are the no-timer geometry; BuffButtons_UpdatePositions re-seats both at load.
|
||||
-->
|
||||
<Frame name="BuffFrame" parent="UIParent" frameStrata="LOW" toplevel="true">
|
||||
<Size><AbsDimension x="50" y="50"/></Size>
|
||||
<Anchors>
|
||||
<Anchor point="TOPRIGHT" relativeTo="UIParent" relativePoint="TOPRIGHT">
|
||||
<Offset><AbsDimension x="-175" y="-13"/></Offset>
|
||||
</Anchor>
|
||||
</Anchors>
|
||||
<Layers>
|
||||
<Layer level="ARTWORK">
|
||||
<FontString name="BuffButton0Duration" inherits="GameFontNormalSmall"/>
|
||||
<FontString name="BuffButton1Duration" inherits="GameFontNormalSmall"/>
|
||||
<FontString name="BuffButton2Duration" inherits="GameFontNormalSmall"/>
|
||||
<FontString name="BuffButton3Duration" inherits="GameFontNormalSmall"/>
|
||||
<FontString name="BuffButton4Duration" inherits="GameFontNormalSmall"/>
|
||||
<FontString name="BuffButton5Duration" inherits="GameFontNormalSmall"/>
|
||||
<FontString name="BuffButton6Duration" inherits="GameFontNormalSmall"/>
|
||||
<FontString name="BuffButton7Duration" inherits="GameFontNormalSmall"/>
|
||||
<FontString name="BuffButton8Duration" inherits="GameFontNormalSmall"/>
|
||||
<FontString name="BuffButton9Duration" inherits="GameFontNormalSmall"/>
|
||||
<FontString name="BuffButton10Duration" inherits="GameFontNormalSmall"/>
|
||||
<FontString name="BuffButton11Duration" inherits="GameFontNormalSmall"/>
|
||||
<FontString name="BuffButton12Duration" inherits="GameFontNormalSmall"/>
|
||||
<FontString name="BuffButton13Duration" inherits="GameFontNormalSmall"/>
|
||||
<FontString name="BuffButton14Duration" inherits="GameFontNormalSmall"/>
|
||||
<FontString name="BuffButton15Duration" inherits="GameFontNormalSmall"/>
|
||||
<FontString name="BuffButton16Duration" inherits="GameFontNormalSmall"/>
|
||||
<FontString name="BuffButton17Duration" inherits="GameFontNormalSmall"/>
|
||||
<FontString name="BuffButton18Duration" inherits="GameFontNormalSmall"/>
|
||||
<FontString name="BuffButton19Duration" inherits="GameFontNormalSmall"/>
|
||||
<FontString name="BuffButton20Duration" inherits="GameFontNormalSmall"/>
|
||||
<FontString name="BuffButton21Duration" inherits="GameFontNormalSmall"/>
|
||||
<FontString name="BuffButton22Duration" inherits="GameFontNormalSmall"/>
|
||||
<FontString name="BuffButton23Duration" inherits="GameFontNormalSmall"/>
|
||||
</Layer>
|
||||
</Layers>
|
||||
<Frames>
|
||||
<!-- Row 0: helpful ordinals 0-7, right to left. -->
|
||||
<Button name="BuffButton0" inherits="BuffButtonHelpful" id="0">
|
||||
<Anchors><Anchor point="TOPRIGHT"><Offset><AbsDimension x="0" y="0"/></Offset></Anchor></Anchors>
|
||||
</Button>
|
||||
<Button name="BuffButton1" inherits="BuffButtonHelpful" id="1">
|
||||
<Anchors><Anchor point="RIGHT" relativeTo="BuffButton0" relativePoint="LEFT"><Offset><AbsDimension x="-5" y="0"/></Offset></Anchor></Anchors>
|
||||
</Button>
|
||||
<Button name="BuffButton2" inherits="BuffButtonHelpful" id="2">
|
||||
<Anchors><Anchor point="RIGHT" relativeTo="BuffButton1" relativePoint="LEFT"><Offset><AbsDimension x="-5" y="0"/></Offset></Anchor></Anchors>
|
||||
</Button>
|
||||
<Button name="BuffButton3" inherits="BuffButtonHelpful" id="3">
|
||||
<Anchors><Anchor point="RIGHT" relativeTo="BuffButton2" relativePoint="LEFT"><Offset><AbsDimension x="-5" y="0"/></Offset></Anchor></Anchors>
|
||||
</Button>
|
||||
<Button name="BuffButton4" inherits="BuffButtonHelpful" id="4">
|
||||
<Anchors><Anchor point="RIGHT" relativeTo="BuffButton3" relativePoint="LEFT"><Offset><AbsDimension x="-5" y="0"/></Offset></Anchor></Anchors>
|
||||
</Button>
|
||||
<Button name="BuffButton5" inherits="BuffButtonHelpful" id="5">
|
||||
<Anchors><Anchor point="RIGHT" relativeTo="BuffButton4" relativePoint="LEFT"><Offset><AbsDimension x="-5" y="0"/></Offset></Anchor></Anchors>
|
||||
</Button>
|
||||
<Button name="BuffButton6" inherits="BuffButtonHelpful" id="6">
|
||||
<Anchors><Anchor point="RIGHT" relativeTo="BuffButton5" relativePoint="LEFT"><Offset><AbsDimension x="-5" y="0"/></Offset></Anchor></Anchors>
|
||||
</Button>
|
||||
<Button name="BuffButton7" inherits="BuffButtonHelpful" id="7">
|
||||
<Anchors><Anchor point="RIGHT" relativeTo="BuffButton6" relativePoint="LEFT"><Offset><AbsDimension x="-5" y="0"/></Offset></Anchor></Anchors>
|
||||
</Button>
|
||||
<!-- Row 1: helpful ordinals 8-15. Re-anchored to TempEnchant1 by BuffButtons_UpdatePositions. -->
|
||||
<Button name="BuffButton8" inherits="BuffButtonHelpful" id="8">
|
||||
<Anchors><Anchor point="TOP" relativeTo="BuffButton0" relativePoint="BOTTOM"><Offset><AbsDimension x="0" y="-5"/></Offset></Anchor></Anchors>
|
||||
</Button>
|
||||
<Button name="BuffButton9" inherits="BuffButtonHelpful" id="9">
|
||||
<Anchors><Anchor point="RIGHT" relativeTo="BuffButton8" relativePoint="LEFT"><Offset><AbsDimension x="-5" y="0"/></Offset></Anchor></Anchors>
|
||||
</Button>
|
||||
<Button name="BuffButton10" inherits="BuffButtonHelpful" id="10">
|
||||
<Anchors><Anchor point="RIGHT" relativeTo="BuffButton9" relativePoint="LEFT"><Offset><AbsDimension x="-5" y="0"/></Offset></Anchor></Anchors>
|
||||
</Button>
|
||||
<Button name="BuffButton11" inherits="BuffButtonHelpful" id="11">
|
||||
<Anchors><Anchor point="RIGHT" relativeTo="BuffButton10" relativePoint="LEFT"><Offset><AbsDimension x="-5" y="0"/></Offset></Anchor></Anchors>
|
||||
</Button>
|
||||
<Button name="BuffButton12" inherits="BuffButtonHelpful" id="12">
|
||||
<Anchors><Anchor point="RIGHT" relativeTo="BuffButton11" relativePoint="LEFT"><Offset><AbsDimension x="-5" y="0"/></Offset></Anchor></Anchors>
|
||||
</Button>
|
||||
<Button name="BuffButton13" inherits="BuffButtonHelpful" id="13">
|
||||
<Anchors><Anchor point="RIGHT" relativeTo="BuffButton12" relativePoint="LEFT"><Offset><AbsDimension x="-5" y="0"/></Offset></Anchor></Anchors>
|
||||
</Button>
|
||||
<Button name="BuffButton14" inherits="BuffButtonHelpful" id="14">
|
||||
<Anchors><Anchor point="RIGHT" relativeTo="BuffButton13" relativePoint="LEFT"><Offset><AbsDimension x="-5" y="0"/></Offset></Anchor></Anchors>
|
||||
</Button>
|
||||
<Button name="BuffButton15" inherits="BuffButtonHelpful" id="15">
|
||||
<Anchors><Anchor point="RIGHT" relativeTo="BuffButton14" relativePoint="LEFT"><Offset><AbsDimension x="-5" y="0"/></Offset></Anchor></Anchors>
|
||||
</Button>
|
||||
<!-- Row 2: harmful ordinals 0-7. Re-anchored to TemporaryEnchantFrame by
|
||||
BuffButtons_UpdatePositions. (The reference's static anchor here names
|
||||
relativePoint="BOTTOM" against a TOPRIGHT point — a typo it never notices because
|
||||
UpdatePositions overwrites it; BOTTOMRIGHT is what it meant.) -->
|
||||
<Button name="BuffButton16" inherits="BuffButtonHarmful" id="0">
|
||||
<Anchors><Anchor point="TOPRIGHT" relativeTo="BuffButton8" relativePoint="BOTTOMRIGHT"><Offset><AbsDimension x="0" y="-5"/></Offset></Anchor></Anchors>
|
||||
</Button>
|
||||
<Button name="BuffButton17" inherits="BuffButtonHarmful" id="1">
|
||||
<Anchors><Anchor point="RIGHT" relativeTo="BuffButton16" relativePoint="LEFT"><Offset><AbsDimension x="-5" y="0"/></Offset></Anchor></Anchors>
|
||||
</Button>
|
||||
<Button name="BuffButton18" inherits="BuffButtonHarmful" id="2">
|
||||
<Anchors><Anchor point="RIGHT" relativeTo="BuffButton17" relativePoint="LEFT"><Offset><AbsDimension x="-5" y="0"/></Offset></Anchor></Anchors>
|
||||
</Button>
|
||||
<Button name="BuffButton19" inherits="BuffButtonHarmful" id="3">
|
||||
<Anchors><Anchor point="RIGHT" relativeTo="BuffButton18" relativePoint="LEFT"><Offset><AbsDimension x="-5" y="0"/></Offset></Anchor></Anchors>
|
||||
</Button>
|
||||
<Button name="BuffButton20" inherits="BuffButtonHarmful" id="4">
|
||||
<Anchors><Anchor point="RIGHT" relativeTo="BuffButton19" relativePoint="LEFT"><Offset><AbsDimension x="-5" y="0"/></Offset></Anchor></Anchors>
|
||||
</Button>
|
||||
<Button name="BuffButton21" inherits="BuffButtonHarmful" id="5">
|
||||
<Anchors><Anchor point="RIGHT" relativeTo="BuffButton20" relativePoint="LEFT"><Offset><AbsDimension x="-5" y="0"/></Offset></Anchor></Anchors>
|
||||
</Button>
|
||||
<Button name="BuffButton22" inherits="BuffButtonHarmful" id="6">
|
||||
<Anchors><Anchor point="RIGHT" relativeTo="BuffButton21" relativePoint="LEFT"><Offset><AbsDimension x="-5" y="0"/></Offset></Anchor></Anchors>
|
||||
</Button>
|
||||
<Button name="BuffButton23" inherits="BuffButtonHarmful" id="7">
|
||||
<Anchors><Anchor point="RIGHT" relativeTo="BuffButton22" relativePoint="LEFT"><Offset><AbsDimension x="-5" y="0"/></Offset></Anchor></Anchors>
|
||||
</Button>
|
||||
</Frames>
|
||||
<Scripts>
|
||||
<OnLoad>BuffFrame_OnLoad()</OnLoad>
|
||||
<OnUpdate>BuffFrame_OnUpdate(arg1)</OnUpdate>
|
||||
</Scripts>
|
||||
</Frame>
|
||||
|
||||
<!-- The temporary-weapon-enchant row (ref BuffFrame.xml l.385-431). It owns the top-right
|
||||
corner; BuffFrame hangs off it, which is why the bar's own anchor above is this frame's
|
||||
point. Two slots, right to left, so with both weapons enchanted the main hand sits left of
|
||||
the off hand.
|
||||
|
||||
Declared AFTER BuffFrame so that by the time this OnLoad runs every name
|
||||
BuffButtons_UpdatePositions touches exists — the reference gets away with never calling it
|
||||
at load because its default is the geometry its static anchors already hold; ours is not. -->
|
||||
<Frame name="TemporaryEnchantFrame" parent="UIParent" frameStrata="LOW" toplevel="true">
|
||||
<Size><AbsDimension x="36" y="36"/></Size>
|
||||
<Anchors>
|
||||
<Anchor point="TOPRIGHT" relativeTo="UIParent" relativePoint="TOPRIGHT">
|
||||
<Offset><AbsDimension x="-175" y="-13"/></Offset>
|
||||
</Anchor>
|
||||
</Anchors>
|
||||
<Layers>
|
||||
<Layer level="BACKGROUND">
|
||||
<FontString name="TempEnchant1Duration" inherits="GameFontNormalSmall"/>
|
||||
<FontString name="TempEnchant2Duration" inherits="GameFontNormalSmall"/>
|
||||
</Layer>
|
||||
</Layers>
|
||||
<Frames>
|
||||
<Button name="TempEnchant1" inherits="BuffButtonTempEnchant" hidden="true">
|
||||
<Anchors><Anchor point="TOPRIGHT"><Offset><AbsDimension x="0" y="0"/></Offset></Anchor></Anchors>
|
||||
</Button>
|
||||
<Button name="TempEnchant2" inherits="BuffButtonTempEnchant" hidden="true">
|
||||
<Anchors><Anchor point="RIGHT" relativeTo="TempEnchant1" relativePoint="LEFT"><Offset><AbsDimension x="-5" y="0"/></Offset></Anchor></Anchors>
|
||||
</Button>
|
||||
</Frames>
|
||||
<Scripts>
|
||||
<OnLoad>
|
||||
TempEnchant1Duration:SetPoint("TOP", "TempEnchant1", "BOTTOM", 0, 0)
|
||||
TempEnchant2Duration:SetPoint("TOP", "TempEnchant2", "BOTTOM", 0, 0)
|
||||
BuffButtons_UpdatePositions()
|
||||
</OnLoad>
|
||||
<OnUpdate>BuffFrame_Enchant_OnUpdate(arg1)</OnUpdate>
|
||||
</Scripts>
|
||||
</Frame>
|
||||
</Ui>
|
||||
@@ -2107,94 +2107,11 @@
|
||||
</Anchors>
|
||||
</Button>
|
||||
|
||||
|
||||
<!-- ── The UIMenu kit (ref UIMenu.xml, names kept for the addon surface): a row button
|
||||
with a right-aligned shortcut string, and the tooltip-backdrop menu holding the ref's
|
||||
fixed pool of 32 rows (hidden until AddButton). The label is authored LEFT-anchored:
|
||||
the ref leans on the 1.12 ButtonText default rect spanning the button, which our lazy
|
||||
label slot doesn't reproduce. ─────────────────────────────────────────────────── -->
|
||||
<Button name="UIMenuButtonTemplate" virtual="true">
|
||||
<Size><AbsDimension x="128" y="16"/></Size>
|
||||
<Scripts>
|
||||
<OnLoad>
|
||||
UIMenuButton_OnLoad()
|
||||
</OnLoad>
|
||||
<OnClick>
|
||||
UIMenuButton_OnClick()
|
||||
</OnClick>
|
||||
<OnEnter>
|
||||
UIMenuButton_OnEnter()
|
||||
</OnEnter>
|
||||
<OnLeave>
|
||||
UIMenuButton_OnLeave()
|
||||
</OnLeave>
|
||||
</Scripts>
|
||||
<Layers>
|
||||
<Layer level="ARTWORK">
|
||||
<FontString name="$parentShortcutText" inherits="GameFontHighlight" hidden="true">
|
||||
<Anchors><Anchor point="RIGHT"/></Anchors>
|
||||
</FontString>
|
||||
</Layer>
|
||||
</Layers>
|
||||
<ButtonText name="$parentText">
|
||||
<Anchors><Anchor point="LEFT"/></Anchors>
|
||||
</ButtonText>
|
||||
<NormalFont inherits="GameFontNormal" justifyH="LEFT"/>
|
||||
<HighlightFont inherits="GameFontHighlight" justifyH="LEFT"/>
|
||||
<DisabledFont inherits="GameFontDisable" justifyH="LEFT"/>
|
||||
</Button>
|
||||
<Frame name="UIMenuTemplate" toplevel="true" frameStrata="DIALOG" virtual="true">
|
||||
<Backdrop bgFile="Interface\Tooltips\UI-Tooltip-Background" edgeFile="Interface\Tooltips\UI-Tooltip-Border" tile="true">
|
||||
<BackgroundInsets><AbsInset left="4" right="4" top="4" bottom="4"/></BackgroundInsets>
|
||||
<TileSize><AbsValue val="32"/></TileSize>
|
||||
<EdgeSize><AbsValue val="13"/></EdgeSize>
|
||||
</Backdrop>
|
||||
<Frames>
|
||||
<Button name="$parentButton1" inherits="UIMenuButtonTemplate" id="1"/>
|
||||
<Button name="$parentButton2" inherits="UIMenuButtonTemplate" id="2"/>
|
||||
<Button name="$parentButton3" inherits="UIMenuButtonTemplate" id="3"/>
|
||||
<Button name="$parentButton4" inherits="UIMenuButtonTemplate" id="4"/>
|
||||
<Button name="$parentButton5" inherits="UIMenuButtonTemplate" id="5"/>
|
||||
<Button name="$parentButton6" inherits="UIMenuButtonTemplate" id="6"/>
|
||||
<Button name="$parentButton7" inherits="UIMenuButtonTemplate" id="7"/>
|
||||
<Button name="$parentButton8" inherits="UIMenuButtonTemplate" id="8"/>
|
||||
<Button name="$parentButton9" inherits="UIMenuButtonTemplate" id="9"/>
|
||||
<Button name="$parentButton10" inherits="UIMenuButtonTemplate" id="10"/>
|
||||
<Button name="$parentButton11" inherits="UIMenuButtonTemplate" id="11"/>
|
||||
<Button name="$parentButton12" inherits="UIMenuButtonTemplate" id="12"/>
|
||||
<Button name="$parentButton13" inherits="UIMenuButtonTemplate" id="13"/>
|
||||
<Button name="$parentButton14" inherits="UIMenuButtonTemplate" id="14"/>
|
||||
<Button name="$parentButton15" inherits="UIMenuButtonTemplate" id="15"/>
|
||||
<Button name="$parentButton16" inherits="UIMenuButtonTemplate" id="16"/>
|
||||
<Button name="$parentButton17" inherits="UIMenuButtonTemplate" id="17"/>
|
||||
<Button name="$parentButton18" inherits="UIMenuButtonTemplate" id="18"/>
|
||||
<Button name="$parentButton19" inherits="UIMenuButtonTemplate" id="19"/>
|
||||
<Button name="$parentButton20" inherits="UIMenuButtonTemplate" id="20"/>
|
||||
<Button name="$parentButton21" inherits="UIMenuButtonTemplate" id="21"/>
|
||||
<Button name="$parentButton22" inherits="UIMenuButtonTemplate" id="22"/>
|
||||
<Button name="$parentButton23" inherits="UIMenuButtonTemplate" id="23"/>
|
||||
<Button name="$parentButton24" inherits="UIMenuButtonTemplate" id="24"/>
|
||||
<Button name="$parentButton25" inherits="UIMenuButtonTemplate" id="25"/>
|
||||
<Button name="$parentButton26" inherits="UIMenuButtonTemplate" id="26"/>
|
||||
<Button name="$parentButton27" inherits="UIMenuButtonTemplate" id="27"/>
|
||||
<Button name="$parentButton28" inherits="UIMenuButtonTemplate" id="28"/>
|
||||
<Button name="$parentButton29" inherits="UIMenuButtonTemplate" id="29"/>
|
||||
<Button name="$parentButton30" inherits="UIMenuButtonTemplate" id="30"/>
|
||||
<Button name="$parentButton31" inherits="UIMenuButtonTemplate" id="31"/>
|
||||
<Button name="$parentButton32" inherits="UIMenuButtonTemplate" id="32"/>
|
||||
</Frames>
|
||||
<Scripts>
|
||||
<OnShow>
|
||||
UIMenu_OnShow()
|
||||
PlaySound("UChatScrollButton")
|
||||
this:SetBackdropBorderColor(TOOLTIP_DEFAULT_COLOR.r, TOOLTIP_DEFAULT_COLOR.g, TOOLTIP_DEFAULT_COLOR.b)
|
||||
this:SetBackdropColor(TOOLTIP_DEFAULT_BACKGROUND_COLOR.r, TOOLTIP_DEFAULT_BACKGROUND_COLOR.g, TOOLTIP_DEFAULT_BACKGROUND_COLOR.b)
|
||||
</OnShow>
|
||||
<OnUpdate>
|
||||
UIMenu_OnUpdate(arg1)
|
||||
</OnUpdate>
|
||||
</Scripts>
|
||||
</Frame>
|
||||
<!-- The UIMenu kit (UIMenuTemplate / UIMenuButtonTemplate and the four UIMenuButton_*
|
||||
handlers) was transcribed here and is gone: it is the reference's own
|
||||
`Interface\FrameXML\UIMenu.xml`, sourced far above this file (1751 window 21). Its
|
||||
one recorded divergence went with it — our copy authored the row label LEFT-anchored
|
||||
because the reference leans on 1.12's ButtonText default rect. -->
|
||||
|
||||
<!-- ── The chat menu button (ref ChatFrameMenuButton): the bubble icon atop the scroll
|
||||
column; click opens ChatMenu (ref ChatFrame_OpenMenu). ──────────────────────────── -->
|
||||
|
||||
@@ -1,65 +0,0 @@
|
||||
<Ui>
|
||||
<!--
|
||||
The cinematic frame — what makes InCinematic() true, and the ESC that skips a fly-by.
|
||||
Transcribed from the reference's Interface\FrameXML\CinematicFrame.xml + .lua (decision
|
||||
1602 permits it); the engine half is crate::cinematic.
|
||||
|
||||
This frame is load-bearing for exactly one thing, and it is not a picture: it is the WHOLE
|
||||
skip path. In the reference, StopCinematic (0x48b970) has zero native callers and no
|
||||
Bindings.xml row — the only thing that ever calls it is the OnKeyDown below, reached
|
||||
because this fullscreen frame is keyboard-enabled and showing. No frame on screen means no
|
||||
way out of a 102-second Night Elf intro.
|
||||
|
||||
THE BLACK BARS ARE NOT HERE, deliberately. The reference draws them as two textures on this
|
||||
frame; we draw them as Bevy UI nodes in crate::cinematic instead, because a cinematic hides
|
||||
the HUD through UiHidden — which kills both of ui_pass's quad lanes wholesale, and the
|
||||
FrameXML layer is one of them. Bars declared here would go dark along with the action bars
|
||||
they exist to replace. The bar LAW is unchanged and still the reference's — but it is BOTH
|
||||
halves of it (decision 1701): the Lua's `if width/height > 4/3` guards a *recompute*, not
|
||||
whether there are bars, and below it the XML's own `1024x128` textures stand on the native
|
||||
1024x768 sheet, i.e. height/6 — which is the 2:1 crop evaluated at exactly 4:3. One
|
||||
expression covers both: `min((h - min(w/2, h))/2, h/6)`. See `drive_letterbox`.
|
||||
|
||||
So this frame draws nothing at all. It is kept rather than replaced with an engine-side key
|
||||
handler because the skip path IS a frame in the reference: keeping it means ESC works for
|
||||
the same reason it works there, and an addon that hooks CinematicFrame still finds it.
|
||||
|
||||
RunBinding("SCREENSHOT") is kept as the reference has it: while this frame eats the
|
||||
keyboard, the screenshot key is the one binding that still has to get through.
|
||||
-->
|
||||
<Script><![CDATA[
|
||||
function CinematicFrame_OnLoad()
|
||||
this:RegisterEvent("CINEMATIC_START");
|
||||
this:RegisterEvent("CINEMATIC_STOP");
|
||||
end
|
||||
|
||||
function CinematicFrame_OnEvent()
|
||||
if ( event == "CINEMATIC_START" ) then
|
||||
ShowUIPanel(this, 1);
|
||||
return;
|
||||
end
|
||||
if ( event == "CINEMATIC_STOP" ) then
|
||||
HideUIPanel(this);
|
||||
return;
|
||||
end
|
||||
end
|
||||
]]></Script>
|
||||
<Frame name="CinematicFrame" setAllPoints="true" hidden="true" enableKeyboard="true" enableMouse="true">
|
||||
<Scripts>
|
||||
<OnLoad>
|
||||
CinematicFrame_OnLoad();
|
||||
</OnLoad>
|
||||
<OnEvent>
|
||||
CinematicFrame_OnEvent();
|
||||
</OnEvent>
|
||||
<OnKeyDown>
|
||||
local screenshotKey = GetBindingKey("SCREENSHOT");
|
||||
if ( arg1 == "ESCAPE" ) then
|
||||
StopCinematic();
|
||||
elseif ( screenshotKey and (arg1 == screenshotKey) ) then
|
||||
RunBinding("SCREENSHOT");
|
||||
end
|
||||
</OnKeyDown>
|
||||
</Scripts>
|
||||
</Frame>
|
||||
</Ui>
|
||||
@@ -164,8 +164,10 @@
|
||||
-- ref's titleButton.type dispatch (ref l.53-61), collapsed onto our seam's two intents
|
||||
-- (SelectGossipQuest covers both the ref's Available/Active clicks, gossip.rs's doc comment).
|
||||
function BenillaGossipRow_OnClick(button)
|
||||
if button.kind == "quest" then
|
||||
SelectGossipQuest(button.itemIndex)
|
||||
if button.kind == "available" then
|
||||
SelectGossipAvailableQuest(button.itemIndex)
|
||||
elseif button.kind == "active" then
|
||||
SelectGossipActiveQuest(button.itemIndex)
|
||||
else
|
||||
SelectGossipOption(button.itemIndex)
|
||||
end
|
||||
@@ -173,22 +175,30 @@
|
||||
|
||||
function BenillaGossipFrame_Update()
|
||||
getglobal("GossipGreetingText"):SetText(GetGossipText() or "")
|
||||
local numQuests = GetNumGossipQuests()
|
||||
-- **The reference's own two lists, in the reference's own order** (ref l.27-28:
|
||||
-- available, then active). Each verb returns a flat vararg of (title, level) PAIRS, and
|
||||
-- each click indexes back INTO ITS OWN LIST — which is why the row remembers which
|
||||
-- list it came from rather than one shared position. This replaced
|
||||
-- `GetNumGossipQuests`/`GetGossipQuestInfo`/`SelectGossipQuest`, three names benilla
|
||||
-- invented and 1.12 does not have (1189: a superset is not free).
|
||||
local avail = { GetGossipAvailableQuests() }
|
||||
local active = { GetGossipActiveQuests() }
|
||||
local numAvail, numActive = #avail / 2, #active / 2
|
||||
local numQuests = numAvail + numActive
|
||||
-- GetGossipOptions() returns a flat vararg of (label, type) pairs (the Era shape).
|
||||
local opts = { GetGossipOptions() }
|
||||
local numOptions = #opts / 2
|
||||
for i = 1, BENILLA_GOSSIP_MAX do
|
||||
local button = getglobal("GossipTitleButton" .. i)
|
||||
if i <= numQuests then
|
||||
-- Quest rows fill first (ref l.27-28: available then active, already ordered by
|
||||
-- the app before it reaches GetGossipQuestInfo) — the row's OWN 1-based position
|
||||
-- IS its quest-list index, so no separate local counter is needed.
|
||||
local title, active = GetGossipQuestInfo(i)
|
||||
button.kind = "quest"
|
||||
button.itemIndex = i
|
||||
button.label:SetText(title)
|
||||
local isActive = i > numAvail
|
||||
local listIndex = isActive and (i - numAvail) or i
|
||||
local src = isActive and active or avail
|
||||
button.kind = isActive and "active" or "available"
|
||||
button.itemIndex = listIndex
|
||||
button.label:SetText(src[(listIndex - 1) * 2 + 1])
|
||||
BenillaGossipRow_Resize(button)
|
||||
button.icon:SetTexture(active and BENILLA_GOSSIP_QUEST_ICON_ACTIVE or BENILLA_GOSSIP_QUEST_ICON_AVAILABLE)
|
||||
button.icon:SetTexture(isActive and BENILLA_GOSSIP_QUEST_ICON_ACTIVE or BENILLA_GOSSIP_QUEST_ICON_AVAILABLE)
|
||||
button:Enable()
|
||||
button:Show()
|
||||
elseif i <= numQuests + numOptions then
|
||||
|
||||
@@ -1,256 +0,0 @@
|
||||
<!--
|
||||
Our implementation of the eight `SetItemButton*` globals every 1.12 window uses to paint an item
|
||||
slot: its icon, its stack count, its merchant stock line, and the four vertex-colour verbs. The
|
||||
specification is the reference's `ItemButtonTemplate.lua` — the names, the argument order and the
|
||||
behaviour of every branch are its contract; the code below is ours, and it is factored the way
|
||||
eight near-identical verbs want to be factored (one region lookup, one tint verb) rather than the
|
||||
way eight separately-written ones came out.
|
||||
|
||||
**Why this file exists, and what it cost to be without it.** The director installed Bagnon, hit
|
||||
B, and got a frame with a title and a gold line and NO BAG SLOTS. `Bagnon_Core/core/Item.lua`
|
||||
ends every slot update with
|
||||
|
||||
l.204 SetItemButtonDesaturated(item, locked, 0.5, 0.5, 0.5)
|
||||
l.209 SetItemButtonTexture(item, texture)
|
||||
l.210 SetItemButtonCount(item, itemCount)
|
||||
|
||||
and all three were nil. The raise lands inside `BagnonFrame_AddBag`'s per-slot loop, so the
|
||||
FIRST slot aborts the loop, `frame.size` never advances, `BagnonFrame_Layout` never runs and
|
||||
the window is left exactly as wide as its own border. `ui_script::bagnon_render_tests` is the
|
||||
reproduction.
|
||||
|
||||
These are FrameXML, not engine (decision 1190's split): the real client ships them as Lua in
|
||||
`Interface\FrameXML\ItemButtonTemplate.lua`, so ours belong here in our own interface rather
|
||||
than in a Rust binding. The specification was read off the install's own copy with the
|
||||
`mpqcat` example over `WoW/Data/interface.MPQ` — not from memory.
|
||||
(An XML comment may not contain a double hyphen, which is why no command line is spelled out
|
||||
here; `benilla-mpq/examples/mpqcat.rs`'s own header carries the invocation.)
|
||||
|
||||
**Corpus demand, counted rather than assumed** (the 218-addon vanilla corpus; `grep -rl`, one
|
||||
row per verb, addons/sites):
|
||||
|
||||
SetItemButtonTexture 4 addons / 19 sites
|
||||
SetItemButtonDesaturated 3 addons / 8 sites
|
||||
SetItemButtonCount 3 addons / 7 sites
|
||||
SetItemButtonTextureVertexColor 3 addons / 7 sites
|
||||
SetItemButtonNormalTextureVertexColor 1 addon / 2 sites
|
||||
SetItemButtonNameFrameVertexColor 1 addon / 2 sites
|
||||
SetItemButtonSlotVertexColor 0 / 0
|
||||
SetItemButtonStock 0 / 0
|
||||
|
||||
Six addons in all — Bagnon_Core, CT_MailMod, CT_MasterMod, AdvancedTradeSkillWindow,
|
||||
QuestHistory, SpecialTalentUI. The last two rows have **zero** corpus callers and are here
|
||||
anyway because the family is one contract and, once the tint verb below is shared, they are one
|
||||
line each; that is stated rather than dressed up as demand (1210).
|
||||
|
||||
**The virtual `ItemButtonTemplate` Button IS here now — 1719: a prior measured decision is
|
||||
revisited when the measurement changes, and it changed.** The paragraph below is kept because
|
||||
it records how the number was wrong before it was also incomplete. It used to say
|
||||
"the harness's template demand ranks it at zero on both axes", full stop. The zero was real;
|
||||
the demand was not. pfUI asks for `ContainerFrameItemButtonTemplate` — which the reference
|
||||
declares as `inherits="ItemButtonTemplate"` (ContainerFrame.xml l.4), so it wants this one
|
||||
transitively — through a shape the census could not see:
|
||||
|
||||
local tpl = "ContainerFrameItemButtonTemplate"
|
||||
if bag == -1 then tpl = "BankItemButtonGenericTemplate" end
|
||||
CreateFrame("Button", "pfBag" .. bag .. "item" .. slot, pfUI.bags[bag], tpl)
|
||||
|
||||
The instrument said so at itself the whole time (`missing_templates`' doc named the literal-only
|
||||
bound), which is the difference between an honest under-report and a wrong number — but a bound
|
||||
stated at the function and forgotten at the call site is only half a guard. The census now
|
||||
resolves a local bound to a literal, and a test asserts both what it sees and what it still
|
||||
cannot (concatenation, table fields, parameters).
|
||||
|
||||
That paragraph then weighed the chain as "a real unit of work" and left it. **The weighing was
|
||||
wrong, and wrong for a reason worth recording: it priced the SCRIPTS, which we already have.**
|
||||
`ContainerFrameItemButtonTemplate`'s handlers are `ContainerFrameItemButton_OnLoad/_OnClick/
|
||||
_OnEnter/_OnUpdate` and `KeyRingItemButton_OnClick` — and this client does not transcribe those,
|
||||
it EXECUTES the reference's own `ContainerFrame.lua` off the player's patch chain
|
||||
(`ui_script::reference_ui`, SOURCED). They have been live the whole time. What was missing was
|
||||
never the behaviour; it was two XML declarations, because the reference ships its templates in
|
||||
`ContainerFrame.xml` and that file is not in the sourced set.
|
||||
|
||||
So the chain cost what it actually cost: this Button, and — at the time — a transcribed
|
||||
`ContainerFrameItemButtonTemplate` beside the adapter that already wrapped one of those very
|
||||
handlers. **Decision 1751 then took the last step and made both moot for the windows
|
||||
themselves**: `Interface\FrameXML\ContainerFrame.xml` is a manifest entry now, so the
|
||||
reference's OWN declaration of that template is the live one and the transcription is deleted.
|
||||
This Button stays, because it is the reference's PUBLISHED surface for addons that build their
|
||||
own slots and it has to exist whether or not any window of ours uses it.
|
||||
|
||||
**The bank arm resolves too, since 1751's second window.** `BankItemButtonGenericTemplate`
|
||||
inherits `BankItemButtonTemplate`, whose handlers live in `BankFrame.lua` — and that file
|
||||
arrives through `Interface\FrameXML\BankFrame.xml`'s own `<Script file=>`, off the chain, the
|
||||
same way. This paragraph used to say the arm was a named omission (1134 §4) whose real cost was
|
||||
the unsourced Lua plus `this:GetInventorySlot()`, a widget method we do not bind. Neither is
|
||||
true any more: the Lua is sourced, and `GetInventorySlot` is not a widget method at all — it is
|
||||
a plain field `BankFrameBaseButton_OnLoad` assigns (`this.GetInventorySlot =
|
||||
ButtonInventorySlot`). pfUI reaches the bank arm for `bag == -1`, and it now finds a real
|
||||
template.
|
||||
-->
|
||||
<Ui xmlns="http://www.blizzard.com/wow/ui/">
|
||||
<Script><![CDATA[
|
||||
-- The naming contract every verb below rests on: an "item button" is a Button whose own
|
||||
-- name prefixes its regions — `<name>IconTexture`, `<name>Count`, `<name>Stock`,
|
||||
-- `<name>NormalTexture`, `<name>NameFrame`, `<name>SlotTexture`. That is the reference's
|
||||
-- `$parent…` template shape, and it is why these are free functions taking a button rather
|
||||
-- than widget methods: any button that follows the naming works, whoever built it.
|
||||
--
|
||||
-- TWO DIFFERENT NILS, and the difference is the whole error policy of this file.
|
||||
-- A nil BUTTON is a no-op everywhere: callers hand us whatever their loop is holding, and
|
||||
-- the reference tolerates that in all eight verbs, so we do too. A button that is missing
|
||||
-- the REGION is not tolerated — the lookup yields nil and the very next `:` raises. That
|
||||
-- loudness is deliberate: "this is not an item button" is a real mistake by the caller, and
|
||||
-- 1203's lesson is that the silent version of a failure is the expensive one.
|
||||
|
||||
-- One named region off a button, which is the only address these verbs ever compute. Nil in,
|
||||
-- nil out, so the button guard reads as one expression instead of eight copied early
|
||||
-- returns.
|
||||
local function region(button, suffix)
|
||||
return button and getglobal(button:GetName() .. suffix)
|
||||
end
|
||||
|
||||
-- Four of the eight differ in nothing but which region they tint, so they share the verb
|
||||
-- and name the region. The guard is on the BUTTON and deliberately not on the region it
|
||||
-- resolved: a nil button is the tolerated case, a button carrying no such region is the
|
||||
-- loud one. `texture` is a local rather than an inline call only so that the raise names
|
||||
-- which lookup came back empty.
|
||||
local function tint(button, suffix, r, g, b)
|
||||
local texture = region(button, suffix)
|
||||
if button then
|
||||
texture:SetVertexColor(r, g, b)
|
||||
end
|
||||
end
|
||||
|
||||
function SetItemButtonCount(button, count)
|
||||
if not button then
|
||||
return
|
||||
end
|
||||
count = count or 0
|
||||
button.count = count
|
||||
local label = region(button, "Count")
|
||||
-- `isBag` is the reference's own flag for a bag button, where even a single item
|
||||
-- counts as a number worth showing. Above 999 the slot shows a bare `*` — the stack
|
||||
-- is "more than the label can say" — while `button.count` keeps the real number.
|
||||
if count > 1 or (button.isBag and count > 0) then
|
||||
label:SetText(count > 999 and "*" or count)
|
||||
label:Show()
|
||||
else
|
||||
label:Hide()
|
||||
end
|
||||
end
|
||||
|
||||
function SetItemButtonStock(button, numInStock)
|
||||
if not button then
|
||||
return
|
||||
end
|
||||
-- The reference defaults this to `""` and then compares it with `> 0`, which is a
|
||||
-- string/number comparison Lua refuses — a raise on the only path the default can
|
||||
-- reach. Ours defaults to 0, which is what the branch below actually means. Not aping
|
||||
-- a quirk (method step 3); the behaviour on every reachable path is identical.
|
||||
numInStock = numInStock or 0
|
||||
button.numInStock = numInStock
|
||||
local label = region(button, "Stock")
|
||||
if numInStock > 0 then
|
||||
label:SetText(format(MERCHANT_STOCK, numInStock))
|
||||
label:Show()
|
||||
else
|
||||
label:Hide()
|
||||
end
|
||||
end
|
||||
|
||||
function SetItemButtonTexture(button, texture)
|
||||
if not button then
|
||||
return
|
||||
end
|
||||
local icon = region(button, "IconTexture")
|
||||
-- Visibility tracks the ARGUMENT, not the region's contents, and it is settled before
|
||||
-- the texture is assigned: clearing a slot with a nil texture must leave the region
|
||||
-- hidden, never an empty quad standing where the icon was.
|
||||
if texture then
|
||||
icon:Show()
|
||||
else
|
||||
icon:Hide()
|
||||
end
|
||||
icon:SetTexture(texture)
|
||||
end
|
||||
|
||||
function SetItemButtonTextureVertexColor(button, r, g, b)
|
||||
tint(button, "IconTexture", r, g, b)
|
||||
end
|
||||
|
||||
function SetItemButtonNormalTextureVertexColor(button, r, g, b)
|
||||
tint(button, "NormalTexture", r, g, b)
|
||||
end
|
||||
|
||||
function SetItemButtonNameFrameVertexColor(button, r, g, b)
|
||||
tint(button, "NameFrame", r, g, b)
|
||||
end
|
||||
|
||||
function SetItemButtonSlotVertexColor(button, r, g, b)
|
||||
tint(button, "SlotTexture", r, g, b)
|
||||
end
|
||||
|
||||
function SetItemButtonDesaturated(button, desaturated, r, g, b)
|
||||
-- The one verb whose own nil guard is on the ICON rather than the button: the
|
||||
-- reference checks both, and `region()` collapses them into this single test without
|
||||
-- changing which calls no-op and which raise.
|
||||
local icon = region(button, "IconTexture")
|
||||
if not icon then
|
||||
return
|
||||
end
|
||||
-- The reference asks the texture itself whether the desaturating shader is available
|
||||
-- (`local shaderSupported = icon:SetDesaturated(desaturated)`) and falls through to a
|
||||
-- flat grey tint when it is not — 1.12 ran on fixed-function hardware where that was
|
||||
-- the ordinary answer. Since decision 1327 benilla's answer is YES: the icon greys to
|
||||
-- luminance in the shader and wears the caller's own tint on top, which is what the
|
||||
-- ref's own `(1, 0.65, 0.65, 0.65)` and `(locked, 0.5, 0.5, 0.5)` calls are asking
|
||||
-- for. The `icon.SetDesaturated and` guard stays for a region leaf without the verb;
|
||||
-- the flat-grey arm below is then the ref's own fallback, unchanged.
|
||||
local shaderSupported = icon.SetDesaturated and icon:SetDesaturated(desaturated)
|
||||
-- Undesaturated is full colour whatever the caller passed; desaturated is the caller's
|
||||
-- own tint when they named one AND the shader took it, and the flat grey otherwise.
|
||||
if not desaturated then
|
||||
r, g, b = 1.0, 1.0, 1.0
|
||||
elseif not r or not shaderSupported then
|
||||
r, g, b = 0.5, 0.5, 0.5
|
||||
end
|
||||
icon:SetVertexColor(r, g, b)
|
||||
end
|
||||
]]></Script>
|
||||
|
||||
<!-- Ref `Interface\FrameXML\ItemButtonTemplate.xml` l.4-46, structure verbatim: the region
|
||||
NAMES are the whole point of the file, because they are the contract the eight verbs above
|
||||
and every addon both address. `$parentIconTexture`, `$parentCount`, `$parentStock`,
|
||||
`$parentNormalTexture` — pfUI reads three of those four off a button it built itself
|
||||
(modules/bags.lua l.418-431) and never asks who built it.
|
||||
|
||||
The reference declares NO scripts on this Button, and neither do we; it is a shape, and
|
||||
whoever inherits it brings the behaviour. -->
|
||||
<Button name="ItemButtonTemplate" virtual="true">
|
||||
<Size><AbsDimension x="37" y="37"/></Size>
|
||||
<Layers>
|
||||
<Layer level="BORDER">
|
||||
<Texture name="$parentIconTexture"/>
|
||||
<FontString name="$parentCount" inherits="NumberFontNormal" justifyH="RIGHT" hidden="true">
|
||||
<Anchors>
|
||||
<Anchor point="BOTTOMRIGHT"><Offset><AbsDimension x="-5" y="2"/></Offset></Anchor>
|
||||
</Anchors>
|
||||
</FontString>
|
||||
<FontString name="$parentStock" inherits="NumberFontNormalYellow" justifyH="LEFT" hidden="true">
|
||||
<Anchors>
|
||||
<Anchor point="TOPLEFT"><Offset><AbsDimension x="0" y="-2"/></Offset></Anchor>
|
||||
</Anchors>
|
||||
</FontString>
|
||||
</Layer>
|
||||
</Layers>
|
||||
<NormalTexture name="$parentNormalTexture" file="Interface\Buttons\UI-Quickslot2">
|
||||
<Size><AbsDimension x="64" y="64"/></Size>
|
||||
<Anchors>
|
||||
<Anchor point="CENTER"><Offset><AbsDimension x="0" y="-1"/></Offset></Anchor>
|
||||
</Anchors>
|
||||
</NormalTexture>
|
||||
<PushedTexture file="Interface\Buttons\UI-Quickslot-Depress"/>
|
||||
<HighlightTexture file="Interface\Buttons\ButtonHilight-Square" alphaMode="ADD"/>
|
||||
</Button>
|
||||
</Ui>
|
||||
@@ -190,6 +190,26 @@
|
||||
</Font>
|
||||
|
||||
<Script><![CDATA[
|
||||
-- ── SHOW_BUFF_DURATIONS: the buff bar's timer switch (decision 1139) ──────────────────
|
||||
--
|
||||
-- The reference declares this in `UIOptionsFrame.lua` (l.21/104), whose `default` is
|
||||
-- `"0"`. We have no counterpart to that file, and the row that drives it is on THIS
|
||||
-- window's Interface page — so this is its home, and 1751's eighteenth window is what
|
||||
-- moved it: it lived in our BuffFrame.xml, and the buff bar is the reference's own file
|
||||
-- now, which reads the global and declares nothing.
|
||||
--
|
||||
-- **`"1"` is a stated divergence, not the reference's default.** 0255 shipped the
|
||||
-- durations-shown geometry on the director's call, and 1139 turned it into a setting
|
||||
-- without changing what it ships as. This assignment is the ONLY statement of it: the
|
||||
-- saved chunk executes over it at load (1128), and the Interface page's row captures it as
|
||||
-- its Defaults value at OnLoad, which runs first — so Defaults honours the director's
|
||||
-- choice rather than the reference's table.
|
||||
--
|
||||
-- Read at CALL time by `BuffButton_Update` / `BuffButtons_UpdatePositions` /
|
||||
-- `BuffFrame_UpdateDuration`, so this file sitting far below the bar does not bind.
|
||||
SHOW_BUFF_DURATIONS = "1"
|
||||
RegisterForSave("SHOW_BUFF_DURATIONS")
|
||||
|
||||
-- The static category tree (era Game tab, Classic tocs), in list order. Keys are the row
|
||||
-- name suffixes; the visible label doubles as the page title (the era does the same —
|
||||
-- Settings.RegisterCategory names both from one string).
|
||||
|
||||
@@ -320,6 +320,35 @@
|
||||
table.insert(UIPARENT_MANAGED_POSITION_LISTENERS, fn)
|
||||
end
|
||||
|
||||
-- ── SecondsToTimeAbbrev (ref UIParent.lua l.1034-1049) ───────────────────────────────
|
||||
--
|
||||
-- "2 m", "45 s" — the one-letter duration the buff bar prints under a timed aura. It lived
|
||||
-- in our BuffFrame.xml behind an `if not SecondsToTimeAbbrev` guard, with the four
|
||||
-- GlobalStrings it formats defined there too, on the reasoning that "we load no
|
||||
-- UIParent/GlobalStrings" — which stopped being true a long time ago and is why the guard
|
||||
-- reads as a leftover rather than a decision. It belongs in this file because it belongs
|
||||
-- in `UIParent.lua`, and 1751's eighteenth window made the buff bar the reference's own,
|
||||
-- which reaches for it by name and defines nothing.
|
||||
--
|
||||
-- Verbatim, including the `>` rather than `>=` on all three boundaries: exactly 60 seconds
|
||||
-- prints "60 s", not "1 m". That is the reference's own arithmetic.
|
||||
function SecondsToTimeAbbrev(seconds)
|
||||
local tempTime
|
||||
if ( seconds > 86400 ) then
|
||||
tempTime = ceil(seconds / 86400)
|
||||
return format(DAY_ONELETTER_ABBR, tempTime)
|
||||
end
|
||||
if ( seconds > 3600 ) then
|
||||
tempTime = ceil(seconds / 3600)
|
||||
return format(HOUR_ONELETTER_ABBR, tempTime)
|
||||
end
|
||||
if ( seconds > 60 ) then
|
||||
tempTime = ceil(seconds / 60)
|
||||
return format(MINUTE_ONELETTER_ABBR, tempTime)
|
||||
end
|
||||
return format(SECOND_ONELETTER_ABBR, seconds)
|
||||
end
|
||||
|
||||
-- ── GetMaterialTextColors (ref UIParent.lua l.1880-1888) ─────────────────────────────
|
||||
--
|
||||
-- The page ink and the title ink for a readable's material. Transcribed here rather than
|
||||
|
||||
@@ -55,6 +55,25 @@ Fonts.xml
|
||||
# `LocalizeFrames()` is called from `UIParent_OnEvent`'s VARIABLES_LOADED arm — the reference's own
|
||||
# caller (UIParent.lua l.230-235), added to our UIParent.xml with this window.
|
||||
Interface\FrameXML\Localization.xml
|
||||
# ══ THE LOCALE LOOKUP — the reference's own, off the player's chain (1751, window 19) ══
|
||||
# `GetText(token, gender, ordinal)` and its three helpers (`GetPluralIndex`/`GetPluralTag`/
|
||||
# `GetGenderTag`) plus `GenderTagInfo` / `MAX_GENDER_INDICES` / `MAX_PLURAL_INDICES`.
|
||||
#
|
||||
# It is a `.lua` entry because the reference has no XML for it: 1.12 pulls it in through
|
||||
# `<Script file="LocaleProperties.lua"/>` at the top of `UIParent.xml`, and until that window
|
||||
# migrates this is where it comes from.
|
||||
#
|
||||
# WORTH KNOWING, because it saved building the wrong thing: `GetText` LOOKS like an engine binding
|
||||
# — `reference/1.12-globals.tsv` has it, and the exe's `FrameScript_GetText 0x703bf0` is a real
|
||||
# function — but the exe's one CALLS this Lua global. So there is nothing to implement; the whole
|
||||
# selector is fifty lines of FrameXML, and sourcing them is the entire fix. It composes a suffix
|
||||
# from the gender (`_FEMALE` at 3, and five more tags 1.12 never uses in enGB) and the plural
|
||||
# (`_P1` for any ordinal that is not 1), then falls back suffix-by-suffix to the bare token.
|
||||
#
|
||||
# The reputation page is what needed it — `ReputationFrame.lua:65` reads its standing
|
||||
# words with `GetText("FACTION_STANDING_LABEL"..id, gender)` — and fourteen more call sites across
|
||||
# FriendsFrame, StaticPopup and the quest windows are waiting behind it.
|
||||
Interface\FrameXML\LocaleProperties.lua
|
||||
# ── A reference file sourced ABOVE ours, so OUR bodies win every collision ─────────────────────
|
||||
# This one is not a migrated window: it is here for one family of frame-agnostic globals that
|
||||
# addons call by name, while the window that file also drives is still ours. So it loads first and
|
||||
@@ -93,11 +112,18 @@ BasicControls.xml
|
||||
# mechanism that re-anchors the cast bar / chat over the bottom bars) are called later, applied by
|
||||
# the loader's post-load bootstrap (ui_script/manifest.rs).
|
||||
UIParent.xml
|
||||
# The `SetItemButton*` family (ref ItemButtonTemplate.lua) — script-only, no frames and no
|
||||
# dependencies of its own, so it sits as early as it can be read. Every window that paints an item
|
||||
# slot calls these, ours and third-party alike: their absence is what left the director's Bagnon
|
||||
# window with a title, a gold line and no bag slots at all (see the file's own header).
|
||||
ItemButtonTemplate.xml
|
||||
# ══ THE ITEM-SLOT BASE — the reference's own, off the player's chain (1751, window 24) ══
|
||||
# `ItemButtonTemplate` and the eight `SetItemButton*` verbs out of `ItemButtonTemplate.lua`. It
|
||||
# sits as early as it can be read: no frames, no dependencies of its own, and every window that
|
||||
# paints an item slot inherits or calls it — ours and third-party alike. Their absence is what left
|
||||
# the director's Bagnon window with a title, a gold line and no bag slots at all.
|
||||
#
|
||||
# Our copy called the reference's file "the specification" in its own header and re-implemented the
|
||||
# eight verbs against it; the specification is now the thing that runs. The template's regions and
|
||||
# size are identical either way (`$parentIconTexture` / `$parentCount` / `$parentStock` /
|
||||
# `$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
|
||||
@@ -109,6 +135,21 @@ ItemButtonTemplate.xml
|
||||
# still run on MerchantFrame.xml's BenillaMoney_* slot kit, and MoneyFrame.xml's header is where
|
||||
# the two are reconciled.
|
||||
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
|
||||
# shipped it, and it declares no frames, so this is a pure addition: a template and ten globals.
|
||||
#
|
||||
# THE REFERENCE'S OWN SLOT, straight after MoneyFrame.xml (FrameXML.toc l.9-10) and for the same
|
||||
# reason ours is here — the coin art and `MONEY_ICON_WIDTH` family it lays its boxes out against.
|
||||
#
|
||||
# It is groundwork rather than a window, and the windows it is groundwork FOR are named so the
|
||||
# next session does not have to re-derive it: stock `TradeFrame.xml` and `MailFrame.xml` both build
|
||||
# their amount entry from this template and call `MoneyInputFrame_SetMode`/`_GetCopper`/
|
||||
# `_SetCopper`/`_ClearFocus`/`_SetTextColor`/`_SetOnvalueChangedFunc` — six of the names below —
|
||||
# and neither can be sourced until they exist. (Trade still needs four ENGINE verbs beyond this:
|
||||
# `AddTradeMoney`, `GetCursorMoney`, `GetTradePlayerItemLink`, `GetTradeTargetItemLink`.)
|
||||
Interface\FrameXML\MoneyInputFrame.xml
|
||||
# The UIPanel slot manager (decision 0084 §2): script-only, no frames of its own — must load
|
||||
# before any panel frame (Gossip/Merchant today) so ShowUIPanel/HideUIPanel already exist
|
||||
# when those frames' OnLoad/OnEvent reference them.
|
||||
@@ -124,6 +165,20 @@ GameTooltip.xml
|
||||
# UIDropDownMenuTemplate + DropDownList1/2 + the UIDropDownMenu_* API — before its
|
||||
# first customers (the unit popups below, the world map's pickers, the trainer filter).
|
||||
UIDropDownMenu.xml
|
||||
# ══ THE UIMenu KIT — the reference's own, off the player's chain (1751, window 21) ══
|
||||
# `UIMenuTemplate` and `UIMenuButtonTemplate`, with `UIMenu.lua`'s `UIMenu_OnShow`/`_OnUpdate` and
|
||||
# the four `UIMenuButton_*` handlers arriving through the file's own `<Script file=>`.
|
||||
#
|
||||
# The kit was a block inside our ChatFrame.xml — a transcription that kept the reference's names
|
||||
# for the addon surface and said so, and whose own note recorded a divergence it had to make (the
|
||||
# row label authored LEFT-anchored, because the reference leans on 1.12's ButtonText default rect
|
||||
# and our lazy label slot did not reproduce it). Sourcing the file retires the transcription and
|
||||
# the divergence together.
|
||||
#
|
||||
# THE POSITION is the reference's own (FrameXML.toc l.12, straight after GameTooltip.xml, which it
|
||||
# needs for the tooltip backdrop its menu wears). Far above ChatFrame.xml, which is the one thing
|
||||
# that used it here — resolved at load, and now satisfied earlier rather than in the same file.
|
||||
Interface\FrameXML\UIMenu.xml
|
||||
# The unit right-click popups (0434 phase 5): UnitPopupMenus/Buttons + UnitPopup_ShowMenu
|
||||
# — after the dropdown kit it drives, before the unit/party frames whose DropDown
|
||||
# children's OnLoad initialize into it.
|
||||
@@ -159,6 +214,30 @@ Interface\FrameXML\ItemRef.xml
|
||||
# health/mana numerals are the ref's other users, when that slice lands). The ref's own TOC
|
||||
# loads TextStatusBar right before its unit frames, same seat as here.
|
||||
TextStatusBar.xml
|
||||
# ══ THE UNIT-FRAME KIT — the reference's own, off the player's chain (1751, window 22) ══
|
||||
# `UnitFrame.xml` declares NO frames — it is a `<Script file="UnitFrame.lua"/>` and nothing else —
|
||||
# so this is a pure addition of thirteen globals: `UnitFrame_Initialize`, `_Update`, `_OnEvent`,
|
||||
# `_OnEnter`, `_OnLeave`, `_OnUpdate`, `_UpdateManaType`, the `UnitFrameHealthBar_*` /
|
||||
# `UnitFrameManaBar_*` pairs, and `GetUnitName`.
|
||||
#
|
||||
# ABOVE OURS ON PURPOSE, the `PaperDollFrame.lua` shape (see the top of this file): four of the
|
||||
# thirteen — `UnitFrame_Update`/`_OnEvent`/`_OnEnter`/`_OnLeave` — collide with bodies our own
|
||||
# UnitFrames.xml owns and drives, and the later line wins, so ours stay live. The other nine arrive
|
||||
# fresh, and `UnitFrame_Initialize` is the one that matters most: it is what stock
|
||||
# `PlayerFrame.xml`, `TargetFrame.xml`, `PartyFrame.xml` and `PetFrame.xml` each call in their
|
||||
# OnLoad, and its absence is the single blocker the readiness probe reports for all four.
|
||||
#
|
||||
# So this is groundwork rather than a window: those four are the most visible thing on the screen
|
||||
# and their swap is a look the director has to see. This is the part that can land without one.
|
||||
Interface\FrameXML\UnitFrame.xml
|
||||
# …and its sibling, the same shape and the same reason. `CombatFeedback.xml` declares no frames
|
||||
# either: three timing constants plus `CombatFeedback_Initialize` / `_OnCombatEvent` / `_OnUpdate`,
|
||||
# the floating hit/miss text a unit frame's portrait carries. Two of the three collide with bodies
|
||||
# our UnitFrames.xml owns, so ours win from below; `CombatFeedback_Initialize` is the one that is
|
||||
# new, and it is the second of the two blockers the probe reports for stock `PlayerFrame.xml` and
|
||||
# `PetFrame.xml` (`UnitFrame_Initialize` above being the first). With both here, all four of the
|
||||
# reference's unit windows load clean.
|
||||
Interface\FrameXML\CombatFeedback.xml
|
||||
UnitFrames.xml
|
||||
# ══ THE COMBO-POINT DOTS — the reference's own, off the player's chain (1751, window 10) ══
|
||||
# `ComboFrame`, `ComboPoint1..5`, `ComboPointTemplate` and the `ComboPointsFrame_OnEvent` /
|
||||
@@ -379,7 +458,7 @@ WorldStateFrame.xml
|
||||
# The player buff/debuff bar (decisions 0255/0257 — the player aura arc): the HUD row of
|
||||
# aura icons under the minimap, driven by the UnitAura feed (`crate::ui_aura`). Needs Fonts
|
||||
# (its FontStrings) and ActionBar's `BENILLA_FALLBACK_ICON` global (both already loaded).
|
||||
BuffFrame.xml
|
||||
Interface\FrameXML\BuffFrame.xml
|
||||
# ══ THE DURABILITY ALERT — the reference's own, off the player's chain (1751, window 4) ══
|
||||
# The "armor guy" under the minimap: `DurabilityFrame`, its eleven glyph textures, and
|
||||
# `DurabilityFrame_SetAlerts` / `_OnUpdate` with `INVENTORY_ALERT_STATUS_SLOTS` and
|
||||
@@ -748,7 +827,7 @@ HelpFrame.xml
|
||||
# Needs UiPanels.xml far above for ShowUIPanel/HideUIPanel and its own UIPanelWindows row; needs
|
||||
# nothing else, and nothing needs it — the engine reaches it purely by firing CINEMATIC_START.
|
||||
# Seated late because it is an outermost, screen-owning layer, like the two windows below it.
|
||||
CinematicFrame.xml
|
||||
Interface\FrameXML\CinematicFrame.xml
|
||||
# The Keybindings page MODULE (decision 1008, superseding 0997's standalone window):
|
||||
# the templates + script of the Options window's Keybindings category, over the
|
||||
# engine's binding table (benilla_ui::script::keybind + crate::bindings — the 1.12
|
||||
|
||||
@@ -228,7 +228,7 @@ pub(super) fn seed_ui_fixture(
|
||||
// Quest rows ride above the options (decision 0088) — an available quest (its own
|
||||
// AvailableQuestIcon dot) so the capture covers the quest-row icon/text seating, not
|
||||
// just the option rows.
|
||||
gossip.quests = vec![(783, 0, "Eagan Peltskinner".into())];
|
||||
gossip.quests = vec![(783, 0, 5, "Eagan Peltskinner".into())];
|
||||
// One short option AND four that WRAP — the live shape both gossip bugs came in as
|
||||
// (the director's screenshots), and a menu deliberately TALLER than the parchment so
|
||||
// the capture covers the whole chain: the per-row auto-height
|
||||
|
||||
@@ -45,7 +45,7 @@ pub(super) fn gossip_menu(
|
||||
npc: u64,
|
||||
text_id: u32,
|
||||
options: Vec<GossipOption>,
|
||||
quests: Vec<(u32, u32, String)>,
|
||||
quests: Vec<(u32, u32, u32, String)>,
|
||||
gossip: &mut GossipState,
|
||||
net_commands: &NetCommands,
|
||||
index: &GuidIndex,
|
||||
|
||||
@@ -16,6 +16,7 @@ fn ev(kind: K, text: &str, sender: &str) -> ChatEvent {
|
||||
|
||||
#[test]
|
||||
fn player_lines_link_the_name_except_emote() {
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
// The composer emits the REAL |Hplayer link now (ref ChatFrame.lua l.1451); the renderer
|
||||
// strips the markers and spans the [Name] (the P2 markup law).
|
||||
assert_eq!(
|
||||
@@ -40,6 +41,7 @@ fn player_lines_link_the_name_except_emote() {
|
||||
|
||||
#[test]
|
||||
fn group_prefixed_kinds_wear_their_brackets() {
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
assert_eq!(
|
||||
compose(&ev(K::Party, "inc 3", "Ann"), K::Party, "Common").unwrap(),
|
||||
"[Party] |Hplayer:Ann|h[Ann]|h: inc 3"
|
||||
@@ -56,6 +58,7 @@ fn group_prefixed_kinds_wear_their_brackets() {
|
||||
|
||||
#[test]
|
||||
fn flags_prefix_the_name_and_afk_uses_its_get() {
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
let mut e = ev(K::Say, "brb", "Bob");
|
||||
e.flag = "GM".into();
|
||||
assert_eq!(
|
||||
@@ -71,6 +74,7 @@ fn flags_prefix_the_name_and_afk_uses_its_get() {
|
||||
|
||||
#[test]
|
||||
fn language_header_rides_non_default_tongues() {
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
let mut e = ev(K::Say, "throm-ka", "Grunk");
|
||||
e.language = "Orcish".into();
|
||||
assert_eq!(
|
||||
@@ -87,6 +91,7 @@ fn language_header_rides_non_default_tongues() {
|
||||
|
||||
#[test]
|
||||
fn system_and_loot_lines_are_verbatim() {
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
assert_eq!(
|
||||
compose(
|
||||
&ChatEvent::text_only(K::System, "Additem: Wool Cloth added.".into()),
|
||||
@@ -118,6 +123,7 @@ fn system_and_loot_lines_are_verbatim() {
|
||||
/// into the render.
|
||||
#[test]
|
||||
fn text_emote_lines_are_verbatim_and_never_wear_the_senders_name() {
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
let e = ev(K::TextEmote, "Bob waves at you.", "Bob");
|
||||
assert_eq!(
|
||||
compose(&e, K::TextEmote, "Common").unwrap(),
|
||||
@@ -139,6 +145,7 @@ fn text_emote_lines_are_verbatim_and_never_wear_the_senders_name() {
|
||||
/// intact) is what makes this a gate and not a mute button.
|
||||
#[test]
|
||||
fn emoting_at_your_own_selection_sends_an_untargeted_emote() {
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
use crate::target::Selection;
|
||||
use bevy::prelude::Entity;
|
||||
|
||||
@@ -225,6 +232,7 @@ fn a_received_text_emote_composes_its_sentence_and_names_the_performer() {
|
||||
|
||||
#[test]
|
||||
fn level_up_lines_follow_the_reference_order_and_forms() {
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
use benilla_protocol::messages::LevelUpInfo;
|
||||
|
||||
// A caster ding with talent point + three stat gains: the PLAYER_LEVEL_UP handler's exact
|
||||
@@ -277,6 +285,7 @@ fn level_up_lines_follow_the_reference_order_and_forms() {
|
||||
/// without it.
|
||||
#[test]
|
||||
fn honor_gain_lines_pick_the_reference_form() {
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
assert_eq!(
|
||||
super::feed::honor_gain_line(None, None, 42),
|
||||
"You have been awarded 42 honor points."
|
||||
@@ -307,6 +316,7 @@ fn honor_gain_lines_pick_the_reference_form() {
|
||||
|
||||
#[test]
|
||||
fn xp_gain_lines_pick_the_reference_form() {
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
// COMBATLOG_XPGAIN_FIRSTPERSON / its EXHAUSTION1 rested form / _UNNAMED (GlobalStrings
|
||||
// :801/:789/:804).
|
||||
assert_eq!(
|
||||
@@ -327,6 +337,7 @@ fn xp_gain_lines_pick_the_reference_form() {
|
||||
|
||||
#[test]
|
||||
fn exploration_lines_pick_the_reference_form() {
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
// ERR_ZONE_EXPLORED (GlobalStrings :1925) — the toast, fired on EVERY exploration packet
|
||||
// (UIErrorsFrame); ERR_ZONE_EXPLORED_XP (:1926) — the chat system line that rides
|
||||
// additionally iff xp > 0 (byte-verified branch `0x5e422f`; decisions 0828/0829).
|
||||
@@ -342,6 +353,7 @@ fn exploration_lines_pick_the_reference_form() {
|
||||
|
||||
#[test]
|
||||
fn monster_lines_use_the_bare_inline_name() {
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
assert_eq!(
|
||||
compose(
|
||||
&ev(K::MonsterSay, "Intruders!", "Guard"),
|
||||
@@ -365,6 +377,7 @@ fn monster_lines_use_the_bare_inline_name() {
|
||||
|
||||
#[test]
|
||||
fn channel_line_prefixes_the_stripped_channel() {
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
let mut e = ev(K::Channel, "wts boar livers", "Bob");
|
||||
e.channel = "General - Elwynn Forest".into();
|
||||
assert_eq!(
|
||||
@@ -381,6 +394,7 @@ fn channel_line_prefixes_the_stripped_channel() {
|
||||
/// returned — so speech says "[General]" and the join notice says "[General - Elwynn Forest]".
|
||||
#[test]
|
||||
fn channel_notices_compose_by_the_notice_law() {
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
let mut e = ChatEvent::text_only(K::ChannelNotice, String::new());
|
||||
e.channel = "General - Elwynn Forest".into();
|
||||
e.notice = "2".into(); // YOU_JOINED
|
||||
@@ -427,17 +441,12 @@ fn chat_vm() -> benilla_ui::script::UiScript {
|
||||
"Fonts.xml",
|
||||
"GameTooltip.xml",
|
||||
"UIDropDownMenu.xml",
|
||||
// The UIMenu kit is the reference's own file since 1751 window 21, so this reads both
|
||||
// stores through the one loader that speaks them.
|
||||
"Interface\\FrameXML\\UIMenu.xml",
|
||||
"ChatFrame.xml",
|
||||
] {
|
||||
let text = std::fs::read_to_string(
|
||||
std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
|
||||
.join("assets/ui")
|
||||
.join(file),
|
||||
)
|
||||
.unwrap();
|
||||
let doc = benilla_ui::framexml::parse(&text).unwrap();
|
||||
let report = benilla_ui::loader::load(&s, &doc, &|_| None);
|
||||
assert!(report.errors.is_empty(), "{file}: {:?}", report.errors);
|
||||
crate::ui_script::load_ui_for_test(&s, file);
|
||||
}
|
||||
s.set_screen_size(1600.0, 900.0);
|
||||
s.resolve();
|
||||
@@ -474,6 +483,7 @@ fn lines_in_window(s: &benilla_ui::script::UiScript) -> i64 {
|
||||
/// The spy is a control, not decoration: without it a broken fire would pass this test.
|
||||
#[test]
|
||||
fn an_addon_registering_our_own_chat_frame_does_not_double_print() {
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
let mut s = chat_vm();
|
||||
let mut windows = super::frames::ChatWindows::default();
|
||||
s.run(SPY).unwrap();
|
||||
@@ -510,6 +520,7 @@ fn an_addon_registering_our_own_chat_frame_does_not_double_print() {
|
||||
/// in for ChatFrame1's handler, so it has to run first for the same reason.
|
||||
#[test]
|
||||
fn an_addons_handler_sees_the_line_already_in_the_window() {
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
let mut s = chat_vm();
|
||||
let mut windows = super::frames::ChatWindows::default();
|
||||
s.run(
|
||||
@@ -537,6 +548,7 @@ fn an_addons_handler_sees_the_line_already_in_the_window() {
|
||||
/// line.
|
||||
#[test]
|
||||
fn a_say_line_fires_chat_msg_say_in_the_references_arg_positions() {
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
let mut s = chat_vm();
|
||||
let mut windows = super::frames::ChatWindows::default();
|
||||
s.run(SPY).unwrap();
|
||||
@@ -565,6 +577,7 @@ fn a_say_line_fires_chat_msg_say_in_the_references_arg_positions() {
|
||||
/// would have caught passing nine args instead of ten.
|
||||
#[test]
|
||||
fn a_channel_notice_fires_its_token_and_the_reference_reads_arg7_and_arg10_bare() {
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
let mut s = chat_vm();
|
||||
let mut windows = super::frames::ChatWindows::default();
|
||||
s.run(SPY).unwrap();
|
||||
@@ -618,6 +631,7 @@ fn a_channel_notice_fires_its_token_and_the_reference_reads_arg7_and_arg10_bare(
|
||||
/// now asserts.
|
||||
#[test]
|
||||
fn a_mode_change_notice_never_becomes_an_event() {
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
use benilla_protocol::messages::{channel_notice, ChannelNoticeTail};
|
||||
|
||||
let mut log = super::feed::ChatLog::default();
|
||||
@@ -651,6 +665,7 @@ fn a_mode_change_notice_never_becomes_an_event() {
|
||||
/// (`slot+0x00/+0x04/+0x94/+0x98`), so they are one record here (`chat-msg-event-args.md` §§4, 7-10).
|
||||
#[test]
|
||||
fn a_channel_we_are_not_in_fires_the_bare_name_and_zeroes() {
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
let mut s = chat_vm();
|
||||
let mut windows = super::frames::ChatWindows::default();
|
||||
s.run(SPY).unwrap();
|
||||
@@ -676,6 +691,7 @@ fn a_channel_we_are_not_in_fires_the_bare_name_and_zeroes() {
|
||||
/// display form gets the number prefix, arg9 never does.
|
||||
#[test]
|
||||
fn stamping_a_channel_splits_the_display_form_from_the_base_name() {
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
let mut channels = super::edit::ChannelState::default();
|
||||
channels.claim_slot("World");
|
||||
channels.claim_slot("General - Elwynn Forest");
|
||||
@@ -709,6 +725,7 @@ fn stamping_a_channel_splits_the_display_form_from_the_base_name() {
|
||||
/// quad, because the color that matters is the one that reaches the screen.
|
||||
#[test]
|
||||
fn a_channel_notice_renders_in_the_channels_color_not_the_notice_row() {
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
use benilla_ui::script::QuadContent;
|
||||
|
||||
let mut s = chat_vm();
|
||||
@@ -750,6 +767,7 @@ fn a_channel_notice_renders_in_the_channels_color_not_the_notice_row() {
|
||||
/// though their own rows differ (CHANNEL_JOIN/LEAVE are C08080, the notices C0C0C0).
|
||||
#[test]
|
||||
fn the_channel_color_override_covers_the_family_but_not_its_two_exemptions() {
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
use super::event::resolved_color;
|
||||
|
||||
let chan = [255, 192, 192];
|
||||
@@ -790,6 +808,7 @@ fn the_channel_color_override_covers_the_family_but_not_its_two_exemptions() {
|
||||
/// 0x49bbd0`), so the line is numbered and an addon's handler still sees the channel.
|
||||
#[test]
|
||||
fn a_leave_notice_keeps_its_number_because_the_record_dies_after_the_line() {
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
let mut s = chat_vm();
|
||||
let mut windows = super::frames::ChatWindows::default();
|
||||
let mut channels = super::edit::ChannelState::default();
|
||||
@@ -826,6 +845,7 @@ fn a_leave_notice_keeps_its_number_because_the_record_dies_after_the_line() {
|
||||
/// free one, so a zone hop *renames* a channel and leaves its number alone.
|
||||
#[test]
|
||||
fn a_freed_slot_is_reused_and_the_others_keep_their_numbers() {
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
let mut c = super::edit::ChannelState::default();
|
||||
assert_eq!(c.claim_slot("General - Teldrassil"), Some(1));
|
||||
assert_eq!(c.claim_slot("Trade - City"), Some(2));
|
||||
@@ -877,6 +897,7 @@ fn a_freed_slot_is_reused_and_the_others_keep_their_numbers() {
|
||||
/// new character's. Everything the module remembers across a box open goes with the lines.
|
||||
#[test]
|
||||
fn a_session_end_empties_the_window_and_the_boxs_memory() {
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
let mut s = chat_vm();
|
||||
let mut windows = super::frames::ChatWindows::default();
|
||||
let mut edit = super::edit::ChatEditState::default();
|
||||
@@ -917,6 +938,7 @@ fn a_session_end_empties_the_window_and_the_boxs_memory() {
|
||||
/// `CHAT_<X>_NOTICE` GlobalStrings keys).
|
||||
#[test]
|
||||
fn every_rendered_notice_has_a_token() {
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
for byte in 0x00u8..=0x1F {
|
||||
let mut e = ChatEvent::text_only(K::ChannelNotice, String::new());
|
||||
e.channel = "World".into();
|
||||
@@ -934,6 +956,7 @@ fn every_rendered_notice_has_a_token() {
|
||||
/// exhaustive match at compile time; this is what makes you add it to `ALL` as well.
|
||||
#[test]
|
||||
fn every_kind_is_in_all() {
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
let mut seen: Vec<&str> = K::ALL
|
||||
.iter()
|
||||
.map(|&k| super::event::event_name(k))
|
||||
@@ -947,6 +970,7 @@ fn every_kind_is_in_all() {
|
||||
|
||||
#[test]
|
||||
fn colors_match_the_shipped_table() {
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
assert_eq!(default_color(K::Say), [255, 255, 255]);
|
||||
assert_eq!(default_color(K::System), [255, 255, 0]);
|
||||
assert_eq!(default_color(K::Yell), [255, 64, 64]);
|
||||
@@ -1005,6 +1029,7 @@ fn enter_switch(text: &str) -> Option<(super::edit::TypeSwitch, String)> {
|
||||
|
||||
#[test]
|
||||
fn enter_path_type_switch_converts_and_keeps_the_remainder() {
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
use super::edit::{SendType, TypeSwitch};
|
||||
for (cmd, want) in [
|
||||
("s", SendType::Say),
|
||||
@@ -1034,6 +1059,7 @@ fn enter_path_type_switch_converts_and_keeps_the_remainder() {
|
||||
|
||||
#[test]
|
||||
fn enter_path_whisper_takes_name_then_message() {
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
use super::edit::TypeSwitch;
|
||||
for cmd in ["w", "whisper", "t", "tell", "send"] {
|
||||
let (sw, rest) = enter_switch(&format!("/{cmd} Bob hi there")).expect(cmd);
|
||||
@@ -1051,6 +1077,7 @@ fn enter_path_whisper_takes_name_then_message() {
|
||||
|
||||
#[test]
|
||||
fn live_parse_waits_for_the_delimiting_space() {
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
use super::edit::{parse_type_switch, ChannelState, ChatEditState, TypeSwitch};
|
||||
let mut state = ChatEditState::default();
|
||||
let chans = ChannelState::default();
|
||||
@@ -1076,6 +1103,7 @@ fn live_parse_waits_for_the_delimiting_space() {
|
||||
|
||||
#[test]
|
||||
fn tell_ring_dedups_and_cycles() {
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
let mut state = super::edit::ChatEditState::default();
|
||||
state.remember_tell("Ann");
|
||||
state.remember_tell("Bob");
|
||||
@@ -1090,6 +1118,7 @@ fn tell_ring_dedups_and_cycles() {
|
||||
|
||||
#[test]
|
||||
fn action_commands_parse() {
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
let t = stub_table();
|
||||
let parse_line = |line: &str| super::input::parse_line(&t, line);
|
||||
assert_eq!(
|
||||
@@ -1142,6 +1171,7 @@ fn action_commands_parse() {
|
||||
|
||||
#[test]
|
||||
fn emote_aliases_resolve_through_the_table() {
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
let t = stub_table();
|
||||
let parse_line = |line: &str| super::input::parse_line(&t, line);
|
||||
assert_eq!(parse_line("/wave"), ParsedChat::TextEmote(101));
|
||||
@@ -1156,6 +1186,7 @@ fn emote_aliases_resolve_through_the_table() {
|
||||
|
||||
#[test]
|
||||
fn logout_and_camp_parse() {
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
let t = stub_table();
|
||||
let parse_line = |line: &str| super::input::parse_line(&t, line);
|
||||
for line in ["/logout", "/camp", "/LOGOUT", "/logout now"] {
|
||||
@@ -1166,6 +1197,7 @@ fn logout_and_camp_parse() {
|
||||
|
||||
#[test]
|
||||
fn one_line_reference_bodies_run_in_the_vm() {
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
let t = stub_table();
|
||||
let parse_line = |line: &str| super::input::parse_line(&t, line);
|
||||
// `/trade` is the ref's `InitiateTrade("target")`, verbatim.
|
||||
@@ -1192,6 +1224,7 @@ fn one_line_reference_bodies_run_in_the_vm() {
|
||||
/// it happens to run in. (It assumed, until 1180's `player-tests` gate ran it the other way.)
|
||||
#[test]
|
||||
fn castvis_parses_id_and_phase() {
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
use crate::creature_anim::CastEventKind;
|
||||
let t = stub_table();
|
||||
let parse_line = |line: &str| super::input::parse_line(&t, line);
|
||||
@@ -1243,6 +1276,7 @@ fn castvis_parses_id_and_phase() {
|
||||
|
||||
#[test]
|
||||
fn unknown_slash_command_is_dropped_not_said_aloud() {
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
let t = stub_table();
|
||||
let parse_line = |line: &str| super::input::parse_line(&t, line);
|
||||
// The regression this grammar exists to fix: `/yell` used to literally SAY "/yell hello" —
|
||||
@@ -1451,12 +1485,14 @@ const LAUGH: u32 = 0x0980;
|
||||
|
||||
#[test]
|
||||
fn seated_stand_required_emotes_are_suppressed() {
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
assert!(!emote_send_eligible(BOW, 1, false)); // 0x4801 has 0x1 (requires STAND)
|
||||
assert!(!emote_send_eligible(RUDE, 1, false));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn seated_non_stand_emotes_pass() {
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
assert!(emote_send_eligible(APPLAUD, 1, false));
|
||||
assert!(emote_send_eligible(CHEER, 1, false));
|
||||
assert!(emote_send_eligible(LAUGH, 1, false));
|
||||
@@ -1465,12 +1501,14 @@ fn seated_non_stand_emotes_pass() {
|
||||
|
||||
#[test]
|
||||
fn swimming_suppresses_only_the_0x80_emotes() {
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
assert!(!emote_send_eligible(LAUGH, 0, true)); // 0x0980 has 0x80
|
||||
assert!(emote_send_eligible(CHEER, 0, true));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn standing_and_dry_everyone_is_eligible() {
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
for flags in [BOW, RUDE, APPLAUD, CHEER, SALUTE, LAUGH] {
|
||||
assert!(emote_send_eligible(flags, 0, false), "flags {flags:#x}");
|
||||
}
|
||||
@@ -1478,6 +1516,7 @@ fn standing_and_dry_everyone_is_eligible() {
|
||||
|
||||
#[test]
|
||||
fn unconditional_and_sleep_dead_rules() {
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
assert!(!emote_send_eligible(0x0400, 0, false)); // unconditional suppress
|
||||
assert!(!emote_send_eligible(0, 3, false)); // SLEEP without the allow bit
|
||||
assert!(!emote_send_eligible(0, 7, false)); // DEAD without the allow bit
|
||||
@@ -1492,6 +1531,7 @@ fn unconditional_and_sleep_dead_rules() {
|
||||
/// opening the box lands in the same type the player's own keypress would.
|
||||
#[test]
|
||||
fn a_sticky_whose_group_is_gone_opens_as_say() {
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
use super::edit::{sticky_on_open, SendType};
|
||||
use crate::ui_party::GroupState;
|
||||
|
||||
@@ -1530,6 +1570,7 @@ fn a_sticky_whose_group_is_gone_opens_as_say() {
|
||||
/// branch on either way.
|
||||
#[test]
|
||||
fn an_inbound_addon_line_splits_on_the_first_tab_only() {
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
// **Imported, not hand-copied.** These read `0x03`/`0x04`/`0x18` for RAID/GUILD/BATTLEGROUND,
|
||||
// which are all wrong — and because the test carried the SAME wrong bytes as the code under
|
||||
// test, it agreed with the defect instead of catching it. A test that restates the value it is
|
||||
@@ -1579,6 +1620,7 @@ fn an_inbound_addon_line_splits_on_the_first_tab_only() {
|
||||
/// four positionally, so a reordering fails on the values rather than on a count.
|
||||
#[test]
|
||||
fn the_addon_event_reaches_lua_with_four_arguments_in_order() {
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
let mut s = benilla_ui::script::UiScript::new().unwrap();
|
||||
s.run(
|
||||
r#"
|
||||
@@ -1644,6 +1686,7 @@ fn the_addon_event_reaches_lua_with_four_arguments_in_order() {
|
||||
/// only the first, so the payload must come back with its own tabs intact.
|
||||
#[test]
|
||||
fn an_addon_message_survives_its_own_send_and_receive() {
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
let mut s = benilla_ui::script::UiScript::new().unwrap();
|
||||
s.run(r#"SendAddonMessage("oRA", "SYNC\t1\t2", "PARTY")"#)
|
||||
.unwrap();
|
||||
@@ -1683,6 +1726,7 @@ fn an_addon_message_survives_its_own_send_and_receive() {
|
||||
/// so a fully-understood foreign line still carries its tag.
|
||||
#[test]
|
||||
fn the_language_header_suppresses_only_the_frames_own_default_tongue() {
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
let orcish = ChatEvent {
|
||||
language: "Orcish".into(),
|
||||
..ev(K::Say, "lok'tar", "Grom")
|
||||
@@ -1749,6 +1793,7 @@ fn the_language_header_suppresses_only_the_frames_own_default_tongue() {
|
||||
/// text here and the laugh silently becomes a plain talk.
|
||||
#[test]
|
||||
fn the_talk_gesture_reads_the_plaintext_not_the_garbled_line() {
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
use crate::creature_anim::{select_gesture, Gesture};
|
||||
use benilla_protocol::messages::CHAT_MSG_SAY;
|
||||
|
||||
@@ -1788,6 +1833,7 @@ fn the_talk_gesture_reads_the_plaintext_not_the_garbled_line() {
|
||||
/// useless. This asserts the text.
|
||||
#[test]
|
||||
fn an_addon_sees_the_combat_log_line_it_registers_for() {
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
let mut s = chat_vm();
|
||||
let mut windows = super::frames::ChatWindows::default();
|
||||
s.run(SPY).unwrap();
|
||||
@@ -1823,6 +1869,7 @@ fn an_addon_sees_the_combat_log_line_it_registers_for() {
|
||||
/// and would otherwise be found by a player's damage meter months later.
|
||||
#[test]
|
||||
fn every_combat_log_kind_reaches_an_addon() {
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
for kind in K::ALL.iter().copied().filter(|k| k.is_combat_log()) {
|
||||
let name = super::event::event_name(kind);
|
||||
let mut s = chat_vm();
|
||||
@@ -1867,6 +1914,7 @@ fn every_combat_log_kind_reaches_an_addon() {
|
||||
/// this guards against is the player/monster branch's decorations leaking onto a combat line.
|
||||
#[test]
|
||||
fn a_combat_log_line_renders_verbatim() {
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
let default_language = String::from("Common");
|
||||
for kind in K::ALL.iter().copied().filter(|k| k.is_combat_log()) {
|
||||
let mut e = ChatEvent::text_only(kind, "You hit Kobold Vermin for 5.".into());
|
||||
@@ -1891,6 +1939,7 @@ fn a_combat_log_line_renders_verbatim() {
|
||||
/// behaviour and the reason a damage meter works without touching the windows.
|
||||
#[test]
|
||||
fn the_combat_log_lands_in_window_two_only() {
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
let windows = super::frames::ChatWindows::default();
|
||||
for kind in [
|
||||
K::CombatSelfHits,
|
||||
@@ -1926,6 +1975,7 @@ fn the_combat_log_lands_in_window_two_only() {
|
||||
/// the broken half).
|
||||
#[test]
|
||||
fn both_dock_tabs_exist_and_select_their_window() {
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
let s = chat_vm();
|
||||
for id in [1, 2] {
|
||||
assert!(
|
||||
@@ -1977,6 +2027,7 @@ fn the_dock_tab_labels_come_from_the_install() {
|
||||
/// pixels".
|
||||
#[test]
|
||||
fn the_combat_log_window_has_the_docks_rect() {
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
let s = chat_vm();
|
||||
let edge = |frame: &str, get: &str| {
|
||||
s.eval::<Option<f64>>(&format!("return {frame}:{get}()"))
|
||||
@@ -2035,6 +2086,7 @@ fn the_combat_log_window_has_the_docks_rect() {
|
||||
/// both halves of each piece: the texture the fade paints and the button that grabs it.
|
||||
#[test]
|
||||
fn both_dock_windows_carry_the_same_chrome() {
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
let s = chat_vm();
|
||||
// The reference's own `CHAT_FRAME_TEXTURES`, which is also the list the file declares — read
|
||||
// out of the VM rather than restated here, so a piece that leaves the list cannot leave this
|
||||
@@ -2108,6 +2160,7 @@ fn both_dock_windows_carry_the_same_chrome() {
|
||||
/// `<Layers>`, `<Frames>` or `<Scripts>` block on any instance is the defect, whatever is in it.
|
||||
#[test]
|
||||
fn the_seven_chat_windows_are_one_declaration() {
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
use benilla_ui::framexml::TopLevel;
|
||||
let xml = std::fs::read_to_string(
|
||||
std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("assets/ui/ChatFrame.xml"),
|
||||
@@ -2154,6 +2207,7 @@ fn the_seven_chat_windows_are_one_declaration() {
|
||||
/// ChatFrame1, and a hidden frame gets no `OnUpdate`, so the whole dock froze on first use.
|
||||
#[test]
|
||||
fn the_dock_driver_does_not_ride_a_dock_window() {
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
let xml = std::fs::read_to_string(
|
||||
std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("assets/ui/ChatFrame.xml"),
|
||||
)
|
||||
@@ -2201,6 +2255,7 @@ fn the_dock_driver_does_not_ride_a_dock_window() {
|
||||
/// defect, whatever it contains.
|
||||
#[test]
|
||||
fn no_docked_chat_window_is_managed_by_uiparent() {
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
let xml = std::fs::read_to_string(
|
||||
std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("assets/ui/UIParent.xml"),
|
||||
)
|
||||
|
||||
@@ -56,10 +56,14 @@ pub(crate) struct GossipState {
|
||||
/// The selectable option rows (wire `GossipOption`: `index` echoed on select, `icon`, `coded`,
|
||||
/// `message`).
|
||||
pub(crate) options: Vec<GossipOption>,
|
||||
/// The quest rows riding the same packet — `(quest_id, dialog-status icon, title)`. The gossip
|
||||
/// The quest rows riding the same packet — `(quest_id, dialog-status icon, level, title)`.
|
||||
/// The level joined the tuple with 1751 window 21: the reference's own
|
||||
/// `GetGossipAvailableQuests`/`GetGossipActiveQuests` return `(title, level)` PAIRS and stock
|
||||
/// `GossipFrame.lua` strides its walk by 2 over them, so a dropped level is a broken stride
|
||||
/// rather than a missing number. It was on the wire all along. The gossip
|
||||
/// window lists them above the options; a click sends `CMSG_QUESTGIVER_QUERY_QUEST` /
|
||||
/// `_COMPLETE_QUEST` (decision 0088).
|
||||
pub(crate) quests: Vec<(u32, u32, String)>,
|
||||
pub(crate) quests: Vec<(u32, u32, u32, String)>,
|
||||
/// Ask-once NPC-text record cache keyed by `text_id` — the 8 undrawn blocks.
|
||||
records: HashMap<u32, Vec<NpcTextBlock>>,
|
||||
}
|
||||
@@ -93,7 +97,7 @@ impl GossipState {
|
||||
npc: u64,
|
||||
text_id: u32,
|
||||
options: Vec<GossipOption>,
|
||||
quests: Vec<(u32, u32, String)>,
|
||||
quests: Vec<(u32, u32, u32, String)>,
|
||||
npc_gender: u8,
|
||||
) -> bool {
|
||||
self.npc = Some(npc);
|
||||
@@ -268,8 +272,9 @@ fn snapshot(state: &GossipState) -> Option<GossipMenu> {
|
||||
quests: state
|
||||
.quests
|
||||
.iter()
|
||||
.map(|(_id, icon, title)| GossipQuestRow {
|
||||
.map(|(_id, icon, level, title)| GossipQuestRow {
|
||||
title: title.clone(),
|
||||
level: *level,
|
||||
active: row_is_active(*icon),
|
||||
})
|
||||
.collect(),
|
||||
@@ -422,7 +427,7 @@ fn drain_gossip(
|
||||
.checked_sub(1)
|
||||
.and_then(|i| state.quests.get(i as usize))
|
||||
{
|
||||
Some((quest_id, icon, _title)) => {
|
||||
Some((quest_id, icon, _level, _title)) => {
|
||||
let (quest, icon) = (*quest_id, *icon);
|
||||
let active = row_is_active(icon);
|
||||
// Same opcode law as the greeting panel: an active row is always a turn-in; an
|
||||
|
||||
@@ -222,6 +222,7 @@ fn resolve_item(
|
||||
item_id: item.entry,
|
||||
stats,
|
||||
link,
|
||||
max_stack: template.map(|t| t.stackable.max(1)),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -286,6 +287,10 @@ fn resolve_buyback(
|
||||
// click carries no ctrl/shift branch at all — it is a bare `BuybackItem(this:GetID())`
|
||||
// (`MerchantFrame.lua:358-361`). Nothing reads it, so nothing builds it (decision 1059).
|
||||
link: None,
|
||||
// …and no max stack, for the same shape of reason: `GetMerchantItemMaxStack` indexes the
|
||||
// MERCHANT list, and a buyback row is bought whole rather than by the stackful. Nothing
|
||||
// asks, so nothing is answered.
|
||||
max_stack: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,24 +4,7 @@
|
||||
|
||||
use benilla_ui::script::{ScriptValue, UiScript};
|
||||
|
||||
/// Load one shipped `assets/ui/<file>` into `s`, panicking on any loader error (the duel tests'
|
||||
/// loader, duplicated so this file is self-contained).
|
||||
fn load_xml(s: &UiScript, file: &str) -> usize {
|
||||
let text = std::fs::read_to_string(
|
||||
std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
|
||||
.join("assets/ui")
|
||||
.join(file),
|
||||
)
|
||||
.unwrap();
|
||||
let doc = benilla_ui::framexml::parse(&text).unwrap();
|
||||
let report = benilla_ui::loader::load(s, &doc, &|_| None);
|
||||
assert!(
|
||||
report.errors.is_empty(),
|
||||
"{file}: loader errors: {:?}",
|
||||
report.errors
|
||||
);
|
||||
report.frames
|
||||
}
|
||||
use super::test_ui::load_ui as load_xml;
|
||||
|
||||
/// The app's own pre-state: a question is pending and in range, which is what
|
||||
/// `CheckBinderDist()` reports while the dialog is up.
|
||||
|
||||
@@ -22,10 +22,34 @@ use super::test_ui::load_ui as load_xml;
|
||||
fn harness() -> UiScript {
|
||||
let mut s = UiScript::new().unwrap();
|
||||
s.set_screen_size(1024.0, 768.0);
|
||||
// The one-letter duration strings (DAY/HOUR/MINUTE/SECOND_ONELETTER_ABBR) the bar prints
|
||||
// under a timed aura. Our copy carried them as `X = X or "%d s"` fallbacks; the reference's
|
||||
// file formats them straight and `format(nil, …)` raises.
|
||||
load_xml(&s, "Interface\\FrameXML\\GlobalStrings.lua");
|
||||
load_xml(&s, "Fonts.xml"); // NORMAL/HIGHLIGHT_FONT_COLOR + the FontStrings' faces
|
||||
// `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, "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, "UiPanels.xml");
|
||||
load_xml(&s, "UIParent.xml");
|
||||
load_xml(&s, "Cooldown.xml");
|
||||
load_xml(&s, "ActionBar.xml"); // BENILLA_FALLBACK_ICON (the unknown-icon fallback)
|
||||
load_xml(&s, "BuffFrame.xml");
|
||||
// The timer switch, seeded BEFORE the bar loads. 1.12 declares it in UIOptionsFrame.lua
|
||||
// (default "0"); we have no counterpart to that file, so it lives with the row that drives it
|
||||
// (OptionsFrame.xml) and ships as "1" — the director's call, 0255/1139. Order matters: the
|
||||
// reference's `BuffFrame_OnLoad` calls `BuffButtons_UpdatePositions`, which seats the debuff
|
||||
// row 20px differently depending on this value, so setting it afterwards leaves the bar laid
|
||||
// out for the wrong one.
|
||||
s.run("SHOW_BUFF_DURATIONS = \"1\"").unwrap();
|
||||
load_xml(&s, "Interface\\FrameXML\\BuffFrame.xml");
|
||||
// …and APPLIED, the way the app applies it (`manifest::apply_buff_durations`).
|
||||
// `BuffFrame_OnLoad` does not call `BuffButtons_UpdatePositions`: in 1.12 that is
|
||||
// `UIOptionsFrame.lua`'s job and we have no counterpart to that file. A session that skips it
|
||||
// is laid out for durations-OFF while the setting says on.
|
||||
super::manifest::apply_buff_durations(&s).unwrap();
|
||||
s
|
||||
}
|
||||
|
||||
@@ -101,6 +125,7 @@ fn tex_quad<'a>(quads: &'a [ExtractedQuad], leaf: &str) -> Option<&'a ExtractedQ
|
||||
|
||||
#[test]
|
||||
fn buffs_and_debuffs_fill_their_own_rows_with_counts_and_the_dispel_tint() {
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
let mut s = harness();
|
||||
push(
|
||||
&mut s,
|
||||
@@ -177,6 +202,7 @@ fn buffs_and_debuffs_fill_their_own_rows_with_counts_and_the_dispel_tint() {
|
||||
|
||||
#[test]
|
||||
fn a_timed_aura_counts_down_and_a_permanent_one_shows_no_timer() {
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
let mut s = harness();
|
||||
push(
|
||||
&mut s,
|
||||
@@ -222,6 +248,7 @@ fn a_timed_aura_counts_down_and_a_permanent_one_shows_no_timer() {
|
||||
|
||||
#[test]
|
||||
fn the_warning_flash_pulses_a_low_aura_but_leaves_a_long_one_solid() {
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
let mut s = harness();
|
||||
push(
|
||||
&mut s,
|
||||
@@ -270,6 +297,7 @@ fn the_warning_flash_pulses_a_low_aura_but_leaves_a_long_one_solid() {
|
||||
|
||||
#[test]
|
||||
fn right_clicking_a_cancelable_buff_queues_its_spell_cancel() {
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
let mut s = harness();
|
||||
push(
|
||||
&mut s,
|
||||
@@ -300,6 +328,7 @@ fn right_clicking_a_cancelable_buff_queues_its_spell_cancel() {
|
||||
|
||||
#[test]
|
||||
fn an_emptied_bar_hides_every_button() {
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
let mut s = harness();
|
||||
push(
|
||||
&mut s,
|
||||
@@ -341,6 +370,7 @@ fn an_emptied_bar_hides_every_button() {
|
||||
/// `lua left -9.9s`).
|
||||
#[test]
|
||||
fn a_refreshed_duration_reaches_the_bar_with_no_aura_event() {
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
let mut s = harness();
|
||||
let mark = |expiry: f64| {
|
||||
aura(
|
||||
@@ -412,6 +442,7 @@ fn a_refreshed_duration_reaches_the_bar_with_no_aura_event() {
|
||||
/// about to drop, and the reference pulses from `BuffButton_OnUpdate` regardless of the setting.
|
||||
#[test]
|
||||
fn the_duration_switch_hides_the_timers_and_closes_their_gutter() {
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
let mut s = harness();
|
||||
push(
|
||||
&mut s,
|
||||
@@ -532,6 +563,7 @@ fn mixed_bar() -> Vec<AuraState> {
|
||||
/// too or the whole thing silently halves.
|
||||
#[test]
|
||||
fn the_buttons_and_the_corpus_walk_agree_on_the_cache_positions() {
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
let mut s = harness();
|
||||
push(&mut s, mixed_bar());
|
||||
frame(&mut s, 0.1);
|
||||
@@ -613,6 +645,7 @@ fn the_buttons_and_the_corpus_walk_agree_on_the_cache_positions() {
|
||||
/// it is structural, because a harmful button's handle simply names a harmful record.
|
||||
#[test]
|
||||
fn right_clicking_cancels_the_aura_under_the_cursor_by_its_cache_position() {
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
let mut s = harness();
|
||||
push(&mut s, mixed_bar());
|
||||
frame(&mut s, 0.1);
|
||||
@@ -651,6 +684,7 @@ fn right_clicking_cancels_the_aura_under_the_cursor_by_its_cache_position() {
|
||||
/// clear it — while rows 2 and 3, which hang off TempEnchant1 and TemporaryEnchantFrame, stay put.
|
||||
#[test]
|
||||
fn the_temporary_enchant_row_shows_a_weapon_enchant_and_moves_the_top_row_aside() {
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
let mut s = harness();
|
||||
// A LOADED bar, deliberately: the enchant slots must be driven by GetWeaponEnchantInfo and by
|
||||
// nothing else. The reference builds them from `BuffButtonTemplate` and blanks the four aura
|
||||
@@ -669,18 +703,34 @@ fn the_temporary_enchant_row_shows_a_weapon_enchant_and_moves_the_top_row_aside(
|
||||
"the enchant slots are not buff buttons: no aura may reach them"
|
||||
);
|
||||
assert!(!shown(&s, "TempEnchant2"));
|
||||
// The direct probe: none of the three aura handlers is even REGISTERED on an enchant slot.
|
||||
// This is what the reference's empty-element idiom is supposed to achieve and what our loader
|
||||
// does not do, so it is asserted rather than assumed.
|
||||
// The direct probe, and its exact shape is the point. `BuffButtonTempEnchant` inherits
|
||||
// `BuffButtonTemplate` and blanks its three aura handlers with `<OnLoad>`↵`</OnLoad>` — a
|
||||
// WHITESPACE body, which 1.12 compiles into a valid empty function rather than storing nil
|
||||
// (`SetScript 0x7025c0` tests `text[0]`, and nothing trims — wow-5875-re
|
||||
// `xml-script-empty-element.md`). So each handler is still a FUNCTION here, and it is the
|
||||
// reference's own no-op rather than `BuffButton_OnLoad`.
|
||||
//
|
||||
// Asserting `== nil` instead would be asserting a bug: an addon reading
|
||||
// `TempEnchant1:GetScript("OnLoad")` gets a function on a real client.
|
||||
for handler in ["OnLoad", "OnEvent", "OnClick"] {
|
||||
assert!(
|
||||
s.eval::<bool>(&format!(
|
||||
"return TempEnchant1:GetScript(\"{handler}\") == nil"
|
||||
"return type(TempEnchant1:GetScript(\"{handler}\")) == \"function\""
|
||||
))
|
||||
.unwrap(),
|
||||
"an enchant slot must carry no {handler} — it is not a buff button"
|
||||
"an enchant slot's {handler} is the reference's compiled no-op, not nil"
|
||||
);
|
||||
}
|
||||
// …and it is NOT the buff-button body: an aura event reaching it would paint the first buff
|
||||
// into the corner and arm a right-click cancel, which is what a naive "empty means absent"
|
||||
// loader produces.
|
||||
assert!(
|
||||
s.eval::<bool>(
|
||||
"return TempEnchant1:GetScript(\"OnLoad\") ~= BuffButton0:GetScript(\"OnLoad\")"
|
||||
)
|
||||
.unwrap(),
|
||||
"the blank displaces the inherited handler rather than sharing it"
|
||||
);
|
||||
s.resolve();
|
||||
let resting = s.eval::<f64>("return BuffFrame:GetRight()").unwrap();
|
||||
let row2 = s.eval::<f64>("return BuffButton16:GetRight()").unwrap();
|
||||
@@ -715,6 +765,14 @@ fn the_temporary_enchant_row_shows_a_weapon_enchant_and_moves_the_top_row_aside(
|
||||
);
|
||||
|
||||
// The top buff row slid left by one icon plus the 5px gutter; the debuff row did not move.
|
||||
//
|
||||
// That is true only because the harness has APPLIED the durations setting, and the
|
||||
// qualification is worth keeping: `BuffButton16`'s XML anchor is `TOPRIGHT` to `BuffButton8`'s
|
||||
// `BOTTOM` — inside BuffFrame, so it would travel with it — and the only thing that re-anchors
|
||||
// it to `TemporaryEnchantFrame` is `BuffButtons_UpdatePositions`, which `BuffFrame_OnLoad`
|
||||
// does NOT call. A bar that nobody has applied the setting to therefore moves BOTH rows. The
|
||||
// window's own file cannot reach that state in production (our Interface page applies it), and
|
||||
// finding it was what showed the harness had to apply it too.
|
||||
s.resolve();
|
||||
let shifted = s.eval::<f64>("return BuffFrame:GetRight()").unwrap();
|
||||
assert!(
|
||||
@@ -723,7 +781,8 @@ fn the_temporary_enchant_row_shows_a_weapon_enchant_and_moves_the_top_row_aside(
|
||||
);
|
||||
assert!(
|
||||
(s.eval::<f64>("return BuffButton16:GetRight()").unwrap() - row2).abs() < 1e-3,
|
||||
"the debuff row hangs off TemporaryEnchantFrame and stays put"
|
||||
"the debuff row hangs off TemporaryEnchantFrame and stays put: {row2} -> {}",
|
||||
s.eval::<f64>("return BuffButton16:GetRight()").unwrap()
|
||||
);
|
||||
|
||||
// And back: the enchant drops, the row hides and the bar returns to its resting point.
|
||||
@@ -735,19 +794,27 @@ fn the_temporary_enchant_row_shows_a_weapon_enchant_and_moves_the_top_row_aside(
|
||||
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
|
||||
}
|
||||
|
||||
/// **The idle enchant row writes nothing** (decision 1396's class, the audit's item on
|
||||
/// `TemporaryEnchantFrame:OnUpdate` — 1396 measured it at ~14 µs/frame as the post-fix lead). With
|
||||
/// neither hand enchanted, `BuffFrame_Enchant_OnUpdate` used to clear both slots and re-park the
|
||||
/// bar every frame of every session; now the no-enchant branch latches on `benillaCleared` after
|
||||
/// its first pass.
|
||||
/// **The idle enchant row rewrites the bar every frame, because the reference's does** — and this
|
||||
/// pins that rather than the write-gating it replaced.
|
||||
///
|
||||
/// The probes are sentinels the branch would overwrite if it still ran: the bar re-anchored 41px
|
||||
/// left of its park, and slot 1 shown. Ten ticks later both survive. Then the row is DRIVEN — both
|
||||
/// hands at once — proving the latch releases, the straight-line hand packer keeps the reference's
|
||||
/// off-hand-first order, and the `filled`-gated width/anchor writes fire on the 0→2 edge; the drop
|
||||
/// back to empty re-parks the bar (the first pass after a transition always writes).
|
||||
/// Our own `BuffFrame.xml` latched the no-enchant branch on a `benillaCleared` flag after its first
|
||||
/// pass; decision 1396 measured that branch at ~14 µs/frame and took the gate as its post-fix lead.
|
||||
/// 1751's eighteenth window made the bar `Interface\FrameXML\BuffFrame.xml`, whose
|
||||
/// `BuffFrame_Enchant_OnUpdate` opens with an unconditional `TempEnchant1:Hide()` … `BuffFrame:
|
||||
/// SetPoint(…)` and returns. So the writes are back, they are the reference's, and 1751 §2 takes
|
||||
/// them: an optimisation is not a divergence we get to keep silently once the file is theirs.
|
||||
///
|
||||
/// **1396's finding is not withdrawn** — the cost is real and the same measurement would find it
|
||||
/// again. If it is ever worth paying down, the fix is an adapter over this one handler with a
|
||||
/// record beside it, not a re-transcription of the window.
|
||||
///
|
||||
/// The sentinels below are inverted from what they were: the branch overwrites both, every frame.
|
||||
/// The rest of the test is unchanged and is the part that always mattered — the row DRIVEN with
|
||||
/// both hands keeps the reference's off-hand-first packing order, and the drop back to empty
|
||||
/// re-parks the bar.
|
||||
#[test]
|
||||
fn an_idle_enchant_row_stops_rewriting_the_bar() {
|
||||
fn an_idle_enchant_row_rewrites_the_bar_as_the_reference_does() {
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
let mut s = harness();
|
||||
push(&mut s, mixed_bar());
|
||||
frame(&mut s, 0.1); // first pass: the clear + park write once and latch
|
||||
@@ -766,13 +833,13 @@ fn an_idle_enchant_row_stops_rewriting_the_bar() {
|
||||
}
|
||||
assert!(s.errors().is_empty(), "{:?}", s.errors());
|
||||
assert!(
|
||||
shown(&s, "TempEnchant1"),
|
||||
"a latched idle row must not re-hide the slot every frame"
|
||||
!shown(&s, "TempEnchant1"),
|
||||
"the reference's idle branch re-hides the slot on every tick"
|
||||
);
|
||||
let displaced = s.eval::<f64>("return BuffFrame:GetRight()").unwrap();
|
||||
assert!(
|
||||
(displaced - (resting - 41.0)).abs() < 1e-3,
|
||||
"a latched idle row must not re-park the bar every frame — got {displaced}, resting {resting}"
|
||||
(displaced - resting).abs() < 1e-3,
|
||||
"…and re-parks the bar with it: got {displaced}, resting {resting}"
|
||||
);
|
||||
|
||||
// The control: both hands enchant. Off hand packs slot 1 (the reference's order — main hand
|
||||
@@ -823,16 +890,23 @@ fn an_idle_enchant_row_stops_rewriting_the_bar() {
|
||||
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
|
||||
}
|
||||
|
||||
/// **A settled buff button writes nothing per frame** (the same 1396 class, one row down): the
|
||||
/// poll (`GetPlayerBuffTimeLeft` every frame — load-bearing, decision 0846) stays, but the alpha
|
||||
/// and the duration-region writes are gated on what was last written. The probes are sentinels the
|
||||
/// handler can never produce: alpha 0.42 (it writes only 1.0 or the warning ramp, and at 5 min out
|
||||
/// the ramp is not running) and the text "X" (it writes only `SecondsToTimeAbbrev` strings — and
|
||||
/// the compare is against the computed STRING, never a measured extent, which is 1396's fixture
|
||||
/// trap). The controls: the minute rollover still rewrites the text, the warning band still turns
|
||||
/// the number white, and inside the last 31s the pulse still writes a fresh alpha per tick.
|
||||
/// **A settled buff button rewrites its alpha and its duration every frame, because the
|
||||
/// reference's does** — the sibling of the enchant-row test above, and the same trade.
|
||||
///
|
||||
/// Our copy gated both writes on what was last written (the 1396 class, one row down); the
|
||||
/// reference's `BuffButton_OnUpdate` polls `GetPlayerBuffTimeLeft` and then writes
|
||||
/// `SetAlpha` and `BuffFrame_UpdateDuration` unconditionally, every tick. So the sentinels below —
|
||||
/// alpha 0.42 and the text "X", neither of which the handler can produce — are overwritten within
|
||||
/// one frame, and that is what is pinned.
|
||||
///
|
||||
/// **The poll itself was never the divergence** (`GetPlayerBuffTimeLeft` every frame is
|
||||
/// load-bearing, decision 0846) and the controls below are unchanged, because they are the part
|
||||
/// that describes the WINDOW rather than our implementation of it: the minute rollover drops "5 m"
|
||||
/// to "4 m", the warning band turns the number white, and inside the last 31s the pulse ramps the
|
||||
/// alpha. Those three are the reference's behaviour and they still hold.
|
||||
#[test]
|
||||
fn a_settled_buff_button_stops_rewriting_alpha_and_duration() {
|
||||
fn a_settled_buff_button_rewrites_alpha_and_duration_as_the_reference_does() {
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
let mut s = harness();
|
||||
push(
|
||||
&mut s,
|
||||
@@ -858,14 +932,14 @@ fn a_settled_buff_button_stops_rewriting_alpha_and_duration() {
|
||||
}
|
||||
assert!(s.errors().is_empty(), "{:?}", s.errors());
|
||||
assert!(
|
||||
(alpha(&s, "BuffButton0") - 0.42).abs() < 1e-6,
|
||||
"a settled button must not rewrite its alpha every frame — got {}",
|
||||
(alpha(&s, "BuffButton0") - 1.0).abs() < 1e-6,
|
||||
"the reference's OnUpdate writes a fresh alpha every tick — got {}",
|
||||
alpha(&s, "BuffButton0")
|
||||
);
|
||||
assert_eq!(
|
||||
text(&s, "BuffButton0Duration"),
|
||||
"X",
|
||||
"a settled button must not rewrite its unchanged duration text every frame"
|
||||
"5 m",
|
||||
"…and re-writes the duration with it, unchanged or not"
|
||||
);
|
||||
|
||||
// Control 1: a REAL text change flows — the abbreviation drops to "4 m" past the boundary.
|
||||
|
||||
@@ -384,6 +384,7 @@ fn managed_positions_track_the_bottom_bar_stack() {
|
||||
load_xml(&s, "Fonts.xml");
|
||||
load_xml(&s, "UIParent.xml");
|
||||
load_xml(&s, "CastingBar.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\UIMenu.xml"); // the kit the chat menus build from
|
||||
load_xml(&s, "ChatFrame.xml");
|
||||
|
||||
// The loader's post-load bootstrap, replayed with no bars in existence: the bare bases.
|
||||
|
||||
@@ -48,6 +48,7 @@ fn chat_with_menu() -> UiScript {
|
||||
"ScrollTemplates.xml",
|
||||
"UIPanelTemplates.xml",
|
||||
"Interface\\FrameXML\\ColorPickerFrame.xml",
|
||||
"Interface\\FrameXML\\UIMenu.xml", // the kit ChatMenu/EmoteMenu/VoiceMacroMenu build from
|
||||
"ChatFrame.xml",
|
||||
] {
|
||||
load_xml(&s, file);
|
||||
|
||||
@@ -26,6 +26,7 @@ fn chat_ui() -> UiScript {
|
||||
"ScrollTemplates.xml",
|
||||
"UIPanelTemplates.xml",
|
||||
"Interface\\FrameXML\\ColorPickerFrame.xml",
|
||||
"Interface\\FrameXML\\UIMenu.xml", // the kit ChatMenu/EmoteMenu/VoiceMacroMenu build from
|
||||
"ChatFrame.xml",
|
||||
] {
|
||||
load_xml(&s, file);
|
||||
|
||||
@@ -21,6 +21,7 @@ fn chat_frame() -> UiScript {
|
||||
// the dropdown kit's own dependency — its MenuBackdrop reads `TOOLTIP_DEFAULT_COLOR`.)
|
||||
load_xml(&s, "GameTooltip.xml");
|
||||
load_xml(&s, "UIDropDownMenu.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\UIMenu.xml"); // the kit the chat menus build from
|
||||
load_xml(&s, "ChatFrame.xml");
|
||||
s.set_screen_size(1600.0, 900.0);
|
||||
s.resolve();
|
||||
@@ -245,6 +246,7 @@ fn chat_click_dismisses_a_stuck_spell_but_not_an_item() {
|
||||
"UiPanels.xml",
|
||||
"GameTooltip.xml",
|
||||
"Cooldown.xml",
|
||||
"Interface\\FrameXML\\UIMenu.xml", // the kit ChatMenu/EmoteMenu/VoiceMacroMenu build from
|
||||
"ChatFrame.xml",
|
||||
"SpellBookFrame.xml",
|
||||
] {
|
||||
|
||||
@@ -26,7 +26,7 @@ fn ui_with_the_cinematic_frame() -> UiScript {
|
||||
load_xml(&s, "UIParent.xml");
|
||||
load_xml(&s, "MoneyFrame.xml"); // StaticPopup's money row, or UiPanels errors at load
|
||||
load_xml(&s, "UiPanels.xml");
|
||||
load_xml(&s, "CinematicFrame.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\CinematicFrame.xml");
|
||||
s.resolve();
|
||||
s
|
||||
}
|
||||
|
||||
@@ -5,24 +5,7 @@
|
||||
|
||||
use benilla_ui::script::{DeathAction, DeathUiState, ScriptValue, UiScript};
|
||||
|
||||
/// Load one shipped `assets/ui/<file>` into `s`, panicking on any loader error (the delete-item
|
||||
/// tests' loader, duplicated so this file is self-contained).
|
||||
fn load_xml(s: &UiScript, file: &str) -> usize {
|
||||
let text = std::fs::read_to_string(
|
||||
std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
|
||||
.join("assets/ui")
|
||||
.join(file),
|
||||
)
|
||||
.unwrap();
|
||||
let doc = benilla_ui::framexml::parse(&text).unwrap();
|
||||
let report = benilla_ui::loader::load(s, &doc, &|_| None);
|
||||
assert!(
|
||||
report.errors.is_empty(),
|
||||
"{file}: loader errors: {:?}",
|
||||
report.errors
|
||||
);
|
||||
report.frames
|
||||
}
|
||||
use super::test_ui::load_ui as load_xml;
|
||||
|
||||
fn setup() -> UiScript {
|
||||
let mut s = UiScript::new().unwrap();
|
||||
|
||||
@@ -34,6 +34,7 @@ const ROOM_UI: &[&str] = &[
|
||||
"MerchantFrame.xml",
|
||||
"Interface\\FrameXML\\StackSplitFrame.xml",
|
||||
"DressUpFrame.xml",
|
||||
"Interface\\FrameXML\\UIMenu.xml", // the kit ChatMenu/EmoteMenu/VoiceMacroMenu build from
|
||||
"ChatFrame.xml",
|
||||
];
|
||||
|
||||
@@ -244,6 +245,7 @@ fn the_paper_doll_slots_preview_and_post_what_you_wear() {
|
||||
"GameTooltip.xml",
|
||||
"CharacterFrame.xml",
|
||||
"DressUpFrame.xml",
|
||||
"Interface\\FrameXML\\UIMenu.xml", // the kit ChatMenu/EmoteMenu/VoiceMacroMenu build from
|
||||
"ChatFrame.xml",
|
||||
] {
|
||||
load_xml(&s, file);
|
||||
@@ -310,6 +312,7 @@ fn shift_clicking_an_unresolved_slot_posts_nothing_and_never_raises() {
|
||||
"GameTooltip.xml",
|
||||
"CharacterFrame.xml",
|
||||
"DressUpFrame.xml",
|
||||
"Interface\\FrameXML\\UIMenu.xml", // the kit ChatMenu/EmoteMenu/VoiceMacroMenu build from
|
||||
"ChatFrame.xml",
|
||||
] {
|
||||
load_xml(&s, file);
|
||||
|
||||
@@ -5,24 +5,7 @@
|
||||
|
||||
use benilla_ui::script::{DuelRequest, ScriptValue, UiScript};
|
||||
|
||||
/// Load one shipped `assets/ui/<file>` into `s`, panicking on any loader error (the death tests'
|
||||
/// loader, duplicated so this file is self-contained).
|
||||
fn load_xml(s: &UiScript, file: &str) -> usize {
|
||||
let text = std::fs::read_to_string(
|
||||
std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
|
||||
.join("assets/ui")
|
||||
.join(file),
|
||||
)
|
||||
.unwrap();
|
||||
let doc = benilla_ui::framexml::parse(&text).unwrap();
|
||||
let report = benilla_ui::loader::load(s, &doc, &|_| None);
|
||||
assert!(
|
||||
report.errors.is_empty(),
|
||||
"{file}: loader errors: {:?}",
|
||||
report.errors
|
||||
);
|
||||
report.frames
|
||||
}
|
||||
use super::test_ui::load_ui as load_xml;
|
||||
|
||||
fn setup() -> UiScript {
|
||||
let mut s = UiScript::new().unwrap();
|
||||
|
||||
@@ -143,6 +143,7 @@ fn escape_is_consumed_by_a_focused_editbox_and_leaves_windows_open() {
|
||||
load_xml(&s, file);
|
||||
}
|
||||
load_xml(&s, "MerchantFrame.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\UIMenu.xml"); // the kit the chat menus build from
|
||||
load_xml(&s, "ChatFrame.xml");
|
||||
s.set_money(0);
|
||||
s.set_container(0, Some(one_item_backpack()));
|
||||
|
||||
@@ -7,24 +7,7 @@ use benilla_ui::script::{
|
||||
DressUpIntent, ExtractedQuad, LootRollEntry, LootRollsState, QuadContent, ScriptValue, UiScript,
|
||||
};
|
||||
|
||||
/// Load one shipped `assets/ui/<file>` into `s` (the loot tests' loader, duplicated here so this
|
||||
/// file is self-contained), panicking on any loader error and returning the frame count.
|
||||
fn load_xml(s: &UiScript, file: &str) -> usize {
|
||||
let text = std::fs::read_to_string(
|
||||
std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
|
||||
.join("assets/ui")
|
||||
.join(file),
|
||||
)
|
||||
.unwrap();
|
||||
let doc = benilla_ui::framexml::parse(&text).unwrap();
|
||||
let report = benilla_ui::loader::load(s, &doc, &|_| None);
|
||||
assert!(
|
||||
report.errors.is_empty(),
|
||||
"{file}: loader errors: {:?}",
|
||||
report.errors
|
||||
);
|
||||
report.frames
|
||||
}
|
||||
use super::test_ui::load_ui as load_xml;
|
||||
|
||||
/// Like [`load_xml`], but also demands zero loader WARNINGS — the bar this file's own assignment
|
||||
/// set for `GroupLootFrame.xml` itself (a stale "unknown template" warning, e.g. the kind
|
||||
@@ -555,6 +538,7 @@ fn ctrl_and_shift_on_the_roll_icon_preview_and_post_its_link() {
|
||||
load_xml(&s, "GroupLootFrame.xml");
|
||||
load_xml(&s, "UIParent.xml"); // BenillaChatEdit_InsertLink, the shared shift-insert helper
|
||||
load_xml(&s, "DressUpFrame.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\UIMenu.xml"); // the kit the chat menus build from
|
||||
load_xml(&s, "ChatFrame.xml");
|
||||
s.set_loot_rolls(rolls());
|
||||
|
||||
|
||||
@@ -11,24 +11,7 @@
|
||||
|
||||
use benilla_ui::script::{GmTicketIntent, GmTicketWrite, ScriptValue, UiScript};
|
||||
|
||||
/// Load one shipped `assets/ui/<file>` into `s`, panicking on any loader error (the binder tests'
|
||||
/// loader, duplicated so this file is self-contained).
|
||||
fn load_xml(s: &UiScript, file: &str) -> usize {
|
||||
let text = std::fs::read_to_string(
|
||||
std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
|
||||
.join("assets/ui")
|
||||
.join(file),
|
||||
)
|
||||
.unwrap();
|
||||
let doc = benilla_ui::framexml::parse(&text).unwrap();
|
||||
let report = benilla_ui::loader::load(s, &doc, &|_| None);
|
||||
assert!(
|
||||
report.errors.is_empty(),
|
||||
"{file}: loader errors: {:?}",
|
||||
report.errors
|
||||
);
|
||||
report.frames
|
||||
}
|
||||
use super::test_ui::load_ui as load_xml;
|
||||
|
||||
/// The window, its dependencies, and the catalog the app pushes — the real ten `GMTicketCategory`
|
||||
/// rows, so a test that walks the list is walking the shipped data.
|
||||
|
||||
@@ -3,24 +3,7 @@ use benilla_ui::script::{
|
||||
SoundRequest, UiScript,
|
||||
};
|
||||
|
||||
/// Load one shipped `assets/ui/<file>` into `s` (the panel tests' loader, duplicated here so this
|
||||
/// file is self-contained), panicking on any loader error and returning the frame count.
|
||||
fn load_xml(s: &UiScript, file: &str) -> usize {
|
||||
let text = std::fs::read_to_string(
|
||||
std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
|
||||
.join("assets/ui")
|
||||
.join(file),
|
||||
)
|
||||
.unwrap();
|
||||
let doc = benilla_ui::framexml::parse(&text).unwrap();
|
||||
let report = benilla_ui::loader::load(s, &doc, &|_| None);
|
||||
assert!(
|
||||
report.errors.is_empty(),
|
||||
"{file}: loader errors: {:?}",
|
||||
report.errors
|
||||
);
|
||||
report.frames
|
||||
}
|
||||
use super::test_ui::load_ui as load_xml;
|
||||
|
||||
/// A bare frame's own rect via its `QuadContent::Frame` entry (every frame emits one at its resolved
|
||||
/// rect). The re-skinned loot window has no solid-colour fill (the UI-LootPanel slab is opaque), so
|
||||
@@ -516,6 +499,7 @@ fn shipped_loot_pushed_to_center_by_merchant() {
|
||||
item_id: 159,
|
||||
stats: None,
|
||||
link: None,
|
||||
max_stack: Some(1),
|
||||
}],
|
||||
..Default::default()
|
||||
}));
|
||||
@@ -630,6 +614,7 @@ fn ctrl_and_shift_on_a_loot_row_preview_and_post_without_looting() {
|
||||
"UIDropDownMenu.xml", // LootFrame owns GroupLootDropDown, initialized at its OnLoad
|
||||
"LootFrame.xml",
|
||||
"DressUpFrame.xml",
|
||||
"Interface\\FrameXML\\UIMenu.xml", // the kit ChatMenu/EmoteMenu/VoiceMacroMenu build from
|
||||
"ChatFrame.xml",
|
||||
] {
|
||||
load_xml(&s, file);
|
||||
|
||||
@@ -70,9 +70,33 @@ fn bootstrap_positions(script: &UiScript) -> Vec<String> {
|
||||
error!("ui_script: durability re-seat hook: {e}");
|
||||
return vec![format!("durability re-seat hook: {e}")];
|
||||
}
|
||||
if let Err(e) = apply_buff_durations(script) {
|
||||
error!("ui_script: buff-duration layout: {e}");
|
||||
return vec![format!("buff-duration layout: {e}")];
|
||||
}
|
||||
Vec::new()
|
||||
}
|
||||
|
||||
/// **Apply `SHOW_BUFF_DURATIONS` to the buff bar once, at load** (1751 window 18).
|
||||
///
|
||||
/// The reference's `BuffFrame_OnLoad` seats the duration FontStrings and nothing else — the row
|
||||
/// pitch and the debuff row's anchor come from `BuffButtons_UpdatePositions`, which it never calls.
|
||||
/// In 1.12 that is applied by `UIOptionsFrame.lua`, the file that owns the setting; we have no
|
||||
/// counterpart to it (our row lives on `OptionsFrame.xml`, whose `applyFunc` fires on a CHANGE),
|
||||
/// so without this the bar ships laid out for durations-OFF while the setting says on — a 10px
|
||||
/// pitch error on every buff and a debuff row anchored to the wrong frame.
|
||||
///
|
||||
/// Here rather than in a FrameXML file for the same two reasons as the durability hook above:
|
||||
/// `assets/ui` does not grow (1779), and this is our stand-in for a file the reference has and we
|
||||
/// do not, which makes Rust the durable home.
|
||||
///
|
||||
/// Guarded, because the font-registry-only load has no buff bar.
|
||||
pub(super) fn apply_buff_durations(script: &UiScript) -> Result<(), String> {
|
||||
script
|
||||
.run("if BuffButtons_UpdatePositions then BuffButtons_UpdatePositions() end")
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
/// **A stated divergence from the reference, installed rather than edited in** (1751 window 4).
|
||||
///
|
||||
/// The reference seats `DurabilityFrame` 20 further in when one of its three side glyphs is up
|
||||
|
||||
@@ -91,6 +91,7 @@ fn shipped_merchant_frame_drives_end_to_end() {
|
||||
item_id: 159,
|
||||
stats: None,
|
||||
link: None,
|
||||
max_stack: Some(1),
|
||||
},
|
||||
MerchantItem {
|
||||
name: Some("Linen Bandage".into()),
|
||||
@@ -101,6 +102,7 @@ fn shipped_merchant_frame_drives_end_to_end() {
|
||||
item_id: 1251,
|
||||
stats: None,
|
||||
link: None,
|
||||
max_stack: Some(1),
|
||||
},
|
||||
],
|
||||
..Default::default()
|
||||
@@ -595,6 +597,7 @@ fn shipped_merchant_hover_scopes_highlight_and_anchors_item_tooltip() {
|
||||
sell_price: 0,
|
||||
}),
|
||||
link: None,
|
||||
max_stack: Some(1),
|
||||
},
|
||||
MerchantItem {
|
||||
name: Some("Chipped Buckler".into()),
|
||||
@@ -618,6 +621,7 @@ fn shipped_merchant_hover_scopes_highlight_and_anchors_item_tooltip() {
|
||||
sell_price: 0,
|
||||
}),
|
||||
link: None,
|
||||
max_stack: Some(1),
|
||||
},
|
||||
],
|
||||
..Default::default()
|
||||
@@ -803,6 +807,7 @@ fn money_display_shrinks_to_content_and_stays_flush() {
|
||||
item_id: 159,
|
||||
stats: None,
|
||||
link: None,
|
||||
max_stack: Some(1),
|
||||
},
|
||||
MerchantItem {
|
||||
name: Some("Linen Bandage".into()),
|
||||
@@ -813,6 +818,7 @@ fn money_display_shrinks_to_content_and_stays_flush() {
|
||||
item_id: 1251,
|
||||
stats: None,
|
||||
link: None,
|
||||
max_stack: Some(1),
|
||||
},
|
||||
],
|
||||
..Default::default()
|
||||
@@ -949,6 +955,7 @@ fn merchant_tabs_drive_buyback_page_and_repair_pair() {
|
||||
item_id: 159,
|
||||
stats: None,
|
||||
link: None,
|
||||
max_stack: Some(1),
|
||||
}],
|
||||
buyback: vec![
|
||||
buyback_item("Bandit Cloak", 116),
|
||||
@@ -1109,6 +1116,7 @@ fn shipped_merchant_frame_arms_the_buy_cursor_on_hover() {
|
||||
item_id: 159,
|
||||
stats: None,
|
||||
link: None,
|
||||
max_stack: Some(1),
|
||||
},
|
||||
MerchantItem {
|
||||
name: Some("Linen Bandage".into()),
|
||||
@@ -1119,6 +1127,7 @@ fn shipped_merchant_frame_arms_the_buy_cursor_on_hover() {
|
||||
item_id: 1251,
|
||||
stats: None,
|
||||
link: None,
|
||||
max_stack: Some(1),
|
||||
},
|
||||
],
|
||||
..Default::default()
|
||||
@@ -1241,6 +1250,7 @@ fn ctrl_and_shift_on_a_vendor_row_preview_and_post_without_buying() {
|
||||
"GameTooltip.xml",
|
||||
"MerchantFrame.xml",
|
||||
"DressUpFrame.xml",
|
||||
"Interface\\FrameXML\\UIMenu.xml", // the kit ChatMenu/EmoteMenu/VoiceMacroMenu build from
|
||||
"ChatFrame.xml",
|
||||
] {
|
||||
load_xml(&s, file);
|
||||
@@ -1258,6 +1268,7 @@ fn ctrl_and_shift_on_a_vendor_row_preview_and_post_without_buying() {
|
||||
stats: None,
|
||||
// Fed exactly as `ui_merchant.rs` builds it off the row's template answer.
|
||||
link: Some(WATER_LINK.into()),
|
||||
max_stack: Some(1),
|
||||
}],
|
||||
..Default::default()
|
||||
}));
|
||||
|
||||
@@ -73,6 +73,11 @@ fn harness_on(mut s: UiScript) -> UiScript {
|
||||
);
|
||||
}
|
||||
}
|
||||
// The app's own post-load pass, at the app's own moment: everything is loaded, so
|
||||
// `SHOW_BUFF_DURATIONS` (declared by OptionsFrame.xml, just above) is real by the time the
|
||||
// buff bar's row pitch is settled from it. `BuffFrame_OnLoad` does not do this and 1.12 leaves
|
||||
// it to UIOptionsFrame.lua, which we have no counterpart to (`manifest::apply_buff_durations`).
|
||||
super::manifest::apply_buff_durations(&s).unwrap();
|
||||
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
|
||||
s
|
||||
}
|
||||
@@ -687,19 +692,9 @@ fn combat_harness() -> UiScript {
|
||||
/// exactly the ordering the real manifest guarantees.
|
||||
fn load_definers(s: &UiScript, files: &[&str]) {
|
||||
for file in files {
|
||||
let text = std::fs::read_to_string(
|
||||
std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
|
||||
.join("assets/ui")
|
||||
.join(file),
|
||||
)
|
||||
.unwrap();
|
||||
let doc = benilla_ui::framexml::parse(&text).unwrap();
|
||||
let report = benilla_ui::loader::load(s, &doc, &|_| None);
|
||||
assert!(
|
||||
report.errors.is_empty(),
|
||||
"{file}: loader errors: {:?}",
|
||||
report.errors
|
||||
);
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -738,7 +733,7 @@ fn interface_harness() -> UiScript {
|
||||
"Cooldown.xml",
|
||||
"ActionBar.xml",
|
||||
"ScrollTemplates.xml",
|
||||
"BuffFrame.xml",
|
||||
"Interface\\FrameXML\\BuffFrame.xml",
|
||||
"MerchantFrame.xml",
|
||||
"QuestFrame.xml",
|
||||
"QuestLogFrame.xml",
|
||||
@@ -765,6 +760,7 @@ fn chat_harness() -> UiScript {
|
||||
"UIParent.xml",
|
||||
"GameTooltip.xml",
|
||||
"UIDropDownMenu.xml",
|
||||
"Interface\\FrameXML\\UIMenu.xml", // the kit ChatMenu/EmoteMenu/VoiceMacroMenu build from
|
||||
"ChatFrame.xml",
|
||||
],
|
||||
);
|
||||
|
||||
@@ -238,10 +238,12 @@ fn shipped_gossip_frame_renders_quest_rows_above_options() {
|
||||
quests: vec![
|
||||
GossipQuestRow {
|
||||
title: "Report to Goldshire".into(),
|
||||
level: 5,
|
||||
active: true,
|
||||
},
|
||||
GossipQuestRow {
|
||||
title: "A Threat Within".into(),
|
||||
level: 7,
|
||||
active: false,
|
||||
},
|
||||
],
|
||||
@@ -273,8 +275,13 @@ fn shipped_gossip_frame_renders_quest_rows_above_options() {
|
||||
GossipTitleButton4:IsVisible()",
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!((r1_text.as_str(), r1_vis), ("Report to Goldshire", true));
|
||||
assert_eq!((r2_text.as_str(), r2_vis), ("A Threat Within", true));
|
||||
// **AVAILABLE quests lead, then ACTIVE** — the reference's own order (`GossipFrameUpdate` calls
|
||||
// `GossipFrameAvailableQuestsUpdate` before `…ActiveQuestsUpdate`, ref l.27-28), and ours since
|
||||
// this window started reading the reference's two list verbs instead of the single ordered
|
||||
// `GetGossipQuestInfo` benilla invented. The packet listed the active quest first; the window
|
||||
// does not.
|
||||
assert_eq!((r1_text.as_str(), r1_vis), ("A Threat Within", true));
|
||||
assert_eq!((r2_text.as_str(), r2_vis), ("Report to Goldshire", true));
|
||||
assert_eq!(
|
||||
(r3_text.as_str(), r3_vis),
|
||||
("Let me browse your goods.", true),
|
||||
@@ -291,11 +298,11 @@ fn shipped_gossip_frame_renders_quest_rows_above_options() {
|
||||
matches!(&q.content, QuadContent::Texture { path: Some(p), .. } if p.contains(needle))
|
||||
})
|
||||
};
|
||||
assert!(has_icon("ActiveQuestIcon"), "row 1 (active) icon renders");
|
||||
assert!(
|
||||
has_icon("AvailableQuestIcon"),
|
||||
"row 2 (available) icon renders"
|
||||
"row 1 (available) icon renders"
|
||||
);
|
||||
assert!(has_icon("ActiveQuestIcon"), "row 2 (active) icon renders");
|
||||
|
||||
// The window's rect places row 3 (the option) strictly below row 2 (the last quest row) — the
|
||||
// static anchor chain, not a runtime SetPoint, closes the gap between the two sections. Found
|
||||
@@ -311,19 +318,22 @@ fn shipped_gossip_frame_renders_quest_rows_above_options() {
|
||||
.unwrap_or_else(|| panic!("no text quad for {needle:?}"))
|
||||
.top
|
||||
};
|
||||
let row2_top = label_top("A Threat Within");
|
||||
let row2_top = label_top("Report to Goldshire");
|
||||
let row3_top = label_top("Let me browse your goods.");
|
||||
assert!(
|
||||
row3_top < row2_top,
|
||||
"row 3 (option) sits below row 2 (last quest row): row2 top {row2_top}, row3 top {row3_top}"
|
||||
);
|
||||
|
||||
// Click quest row 2 (available, "A Threat Within") → SelectGossipQuest(2) queues the seam's
|
||||
// 1-based quest-row position; the app maps it to the quest id + guid and sends
|
||||
// CMSG_QUESTGIVER_QUERY_QUEST (decision 0088 §3).
|
||||
// Click quest row 2 — the ACTIVE quest "Report to Goldshire", which the packet listed FIRST.
|
||||
// The row calls `SelectGossipActiveQuest(1)`: 1 because it is the first row of the active list,
|
||||
// which is the index the reference's own `GossipTitleButton_OnClick` passes. The binding maps
|
||||
// that back to the whole-menu position the app's queue speaks — 1, the packet's own order. The
|
||||
// two numbers being different for the same click is exactly what the single-list
|
||||
// `SelectGossipQuest` we retired could not express.
|
||||
s.run("BenillaGossipRow_OnClick(GossipTitleButton2)")
|
||||
.unwrap();
|
||||
assert_eq!(s.take_gossip_quest_selects(), vec![2]);
|
||||
assert_eq!(s.take_gossip_quest_selects(), vec![1]);
|
||||
assert!(
|
||||
s.take_gossip_selects().is_empty(),
|
||||
"a quest-row click never queues SelectGossipOption"
|
||||
@@ -541,6 +551,7 @@ fn shipped_panel_slot_replaces_gossip_with_merchant() {
|
||||
// Not this test's subject (the slot manager is) — a row with no template answer yet
|
||||
// carries no link (decision 1059).
|
||||
link: None,
|
||||
max_stack: Some(1),
|
||||
}],
|
||||
..Default::default()
|
||||
}));
|
||||
@@ -676,6 +687,7 @@ fn shipped_panel_slot_pushable_promotes_to_center() {
|
||||
// Not this test's subject (the slot manager is) — a row with no template answer yet
|
||||
// carries no link (decision 1059).
|
||||
link: None,
|
||||
max_stack: Some(1),
|
||||
}],
|
||||
..Default::default()
|
||||
}));
|
||||
@@ -729,7 +741,7 @@ fn gossip_bank_option_hands_the_left_slot_to_the_bank() {
|
||||
let mut s = UiScript::new().unwrap();
|
||||
s.set_screen_size(1024.0, 768.0);
|
||||
load_xml(&s, "Fonts.xml");
|
||||
load_xml(&s, "ItemButtonTemplate.xml"); // the reference bank's slot buttons inherit it
|
||||
load_xml(&s, "Interface\\FrameXML\\ItemButtonTemplate.xml"); // the reference bank's slot buttons inherit it
|
||||
load_xml(&s, "MoneyFrame.xml");
|
||||
load_xml(&s, "UiPanels.xml");
|
||||
load_xml(&s, "Cooldown.xml");
|
||||
|
||||
@@ -107,6 +107,7 @@ fn setup() -> UiScript {
|
||||
// bought. Ours guarded it; the reference does not.
|
||||
load_xml(&s, "GameTooltip.xml"); // TOOLTIP_DEFAULT_COLOR, read by the dropdown backdrops
|
||||
load_xml(&s, "UIDropDownMenu.xml"); // ChatFrame's seven dropdowns inherit its template
|
||||
load_xml(&s, "Interface\\FrameXML\\UIMenu.xml"); // the kit the chat menus build from
|
||||
load_xml(&s, "ChatFrame.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\GuildRegistrarFrame.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\PetitionFrame.xml");
|
||||
|
||||
@@ -8,24 +8,7 @@
|
||||
|
||||
use benilla_ui::script::{ScriptValue, UiScript};
|
||||
|
||||
/// Load one shipped `assets/ui/<file>` into `s`, panicking on any loader error (the duel tests'
|
||||
/// loader, duplicated so this file is self-contained).
|
||||
fn load_xml(s: &UiScript, file: &str) -> usize {
|
||||
let text = std::fs::read_to_string(
|
||||
std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
|
||||
.join("assets/ui")
|
||||
.join(file),
|
||||
)
|
||||
.unwrap();
|
||||
let doc = benilla_ui::framexml::parse(&text).unwrap();
|
||||
let report = benilla_ui::loader::load(s, &doc, &|_| None);
|
||||
assert!(
|
||||
report.errors.is_empty(),
|
||||
"{file}: loader errors: {:?}",
|
||||
report.errors
|
||||
);
|
||||
report.frames
|
||||
}
|
||||
use super::test_ui::load_ui as load_xml;
|
||||
|
||||
fn setup() -> UiScript {
|
||||
let mut s = UiScript::new().unwrap();
|
||||
|
||||
@@ -659,6 +659,7 @@ fn reward_rows_preview_and_post_without_selecting_the_choice() {
|
||||
load_xml(&s, "QuestFrame.xml");
|
||||
load_xml(&s, "UIParent.xml"); // BenillaChatEdit_InsertLink lives here
|
||||
load_xml(&s, "DressUpFrame.xml"); // DressUpItemLink lives here
|
||||
load_xml(&s, "Interface\\FrameXML\\UIMenu.xml"); // the kit its menus build from
|
||||
load_xml(&s, "ChatFrame.xml"); // ChatFrameEditBox lives here
|
||||
|
||||
const SWORD: &str = "|cffffffff|Hitem:2299:0:0:0|h[Worn Sword]|h|r";
|
||||
|
||||
@@ -1125,6 +1125,7 @@ fn reward_rows_preview_and_post_and_a_plain_click_stays_inert() {
|
||||
load_xml(&s, "QuestLogFrame.xml");
|
||||
load_xml(&s, "UIParent.xml"); // BenillaChatEdit_InsertLink lives here
|
||||
load_xml(&s, "DressUpFrame.xml"); // DressUpItemLink lives here
|
||||
load_xml(&s, "Interface\\FrameXML\\UIMenu.xml"); // the kit its menus build from
|
||||
load_xml(&s, "ChatFrame.xml"); // ChatFrameEditBox lives here
|
||||
|
||||
s.set_quest_log(eight_entries());
|
||||
@@ -1203,6 +1204,7 @@ fn shift_click_on_a_title_posts_the_quest_name_with_chat_open_and_watches_with_i
|
||||
load_xml(&s, "UIPanelTemplates.xml");
|
||||
load_xml(&s, "QuestLogFrame.xml");
|
||||
load_xml(&s, "UIParent.xml"); // BenillaChatEdit_InsertLink lives here
|
||||
load_xml(&s, "Interface\\FrameXML\\UIMenu.xml"); // the kit its menus build from
|
||||
load_xml(&s, "ChatFrame.xml"); // ChatFrameEditBox lives here
|
||||
|
||||
s.set_quest_log(eight_entries());
|
||||
|
||||
@@ -143,6 +143,11 @@ fn chain() -> Option<&'static Chain> {
|
||||
.as_ref()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn is_word(c: char) -> bool {
|
||||
c.is_alphanumeric() || c == '_'
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use benilla_ui::script::UiScript;
|
||||
@@ -231,9 +236,26 @@ mod tests {
|
||||
/// **Loading on top of the manifest, not instead of it**, because that is the position a
|
||||
/// migrated file occupies — every template it inherits is declared by an earlier entry, and
|
||||
/// asking whether `ContainerFrame.xml` loads *alone* only measures that it has predecessors.
|
||||
/// Names it collides with are simply overwritten in the probe VM, which is thrown away; the
|
||||
/// probe is asking about load failures, not about who wins a collision (the manifest's order is
|
||||
/// what settles those, and it is settled per-window when the swap is made).
|
||||
///
|
||||
/// ## The false positive this method has, and how to recognise it
|
||||
///
|
||||
/// A candidate whose frame NAMES our own shipped file already owns produces failures that are
|
||||
/// artefacts of the probe, not of the window. `publish_global` is deliberately non-overwriting
|
||||
/// (RF-0023), so the second frame to claim a name gets a wrapper that `_G` never points at —
|
||||
/// and any reference body using the `getglobal(this:GetName())` idiom then reads a DIFFERENT
|
||||
/// table than the `this` it just wrote to.
|
||||
///
|
||||
/// That is exactly what the money frames look like: stock `TradeFrame.xml` reports
|
||||
/// `MoneyFrame_Update: attempt to index local 'info'`, because `MoneyFrame_SetType` set
|
||||
/// `this.info` on the new frame and `MoneyFrame_Update` read it back off OUR TradeFrame's
|
||||
/// same-named one. Delete our counterpart — which is what migrating the window does — and the
|
||||
/// collision goes with it. The same shape covers `MailFrame` and `QuestLogFrame`.
|
||||
///
|
||||
/// **So a failure inside a name our own manifest also declares is suspect and has to be
|
||||
/// re-measured with the counterpart removed.** A failure naming something nothing of ours
|
||||
/// declares (`CreateFrame: unknown frame type 'LootButton'`, `attempt to call global
|
||||
/// 'UnitFrame_Initialize'`) is real. The probe does not tell the two apart for you; the
|
||||
/// question to ask of every line is "does our tree already own this name?".
|
||||
///
|
||||
/// Ignored because it stands up ~90 fresh VMs and each one loads the entire interface; it is an
|
||||
/// instrument you run when choosing the next window, not a gate.
|
||||
@@ -331,6 +353,204 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
/// **The readiness probe's companion: not "does it load" but "what would I have to BUILD".**
|
||||
///
|
||||
/// ```text
|
||||
/// cargo test -p benilla-app --lib chain_gap_report -- --ignored --nocapture
|
||||
/// ```
|
||||
///
|
||||
/// [`chain_readiness_report`] answers one question well and is silent on the next one. A window
|
||||
/// it calls CLEAN can still be a week of work (its verbs are only reached by a click, which
|
||||
/// loading never makes), and a window it reports failing may be blocked on a single name. When
|
||||
/// the migration ran out of drop-in windows, "which of these is actually cheap" became the
|
||||
/// question, and the probe could not answer it.
|
||||
///
|
||||
/// So this one reads the calls instead of running them. For every stock window not yet in the
|
||||
/// manifest it collects the `Name(` sites across the file and its `.lua`, subtracts what the
|
||||
/// file defines itself and what this client already has, and splits the remainder against
|
||||
/// **the reference's own `_G`** (`reference/1.12-globals.tsv`, captured from the running
|
||||
/// client):
|
||||
///
|
||||
/// * `engine=` — the reference has it as an engine binding and we do not. **Real work**, and
|
||||
/// the only column worth planning from.
|
||||
/// * `fx=` — the reference has it as a FrameXML function. Cheap by comparison: it lives in
|
||||
/// some stock file, and the name beside it says which, so sourcing that file may be the whole
|
||||
/// fix. `GetText` looked like an engine binding for an hour and turned out to be
|
||||
/// `LocaleProperties.lua`; this column is that lesson, mechanised.
|
||||
/// * A name in NEITHER is dropped, and that is the load-bearing filter: 1.12's widget methods
|
||||
/// do not live in `_G`, so `SetText(` and `Hide(` and their two hundred siblings would
|
||||
/// otherwise drown the report. Anything the reference's own global table does not carry is
|
||||
/// not a global.
|
||||
///
|
||||
/// **What "already has" means, and why it is asked of a LOADED VM.** An earlier hand-rolled
|
||||
/// version of this compared against a bare `UiScript::new()`, which is the ENGINE surface
|
||||
/// alone — so every FrameXML function our own interface defines (`ShowUIPanel`,
|
||||
/// `StaticPopup_Visible`, `UpdateMicroButtons`, …) read as missing, and the `fx=` column was
|
||||
/// mostly noise. Here the manifest is loaded first and `_G` is read after, so the answer is
|
||||
/// what this client *actually* answers to.
|
||||
///
|
||||
/// Two crudenesses, stated because they decide how to read the output. The `Name(` scan counts
|
||||
/// a call inside an XML comment (1.12 comments out whole blocks — `GuildRegistrarFrame`'s
|
||||
/// tabard button is one), so it can over-report; and it cannot see a name reached through
|
||||
/// `getglobal`, so it can under-report. It is for ranking work, not for proving a window done —
|
||||
/// [`chain_readiness_report`] and the window's own tests are that.
|
||||
#[test]
|
||||
#[ignore = "instrument: run by hand when choosing what to build next"]
|
||||
fn chain_gap_report() {
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
|
||||
// The reference's own global table, with each name's origin.
|
||||
let tsv = concat!(
|
||||
env!("CARGO_MANIFEST_DIR"),
|
||||
"/../../reference/1.12-globals.tsv"
|
||||
);
|
||||
let text = std::fs::read_to_string(tsv).expect("the reference surface");
|
||||
let mut origin: std::collections::HashMap<&str, &str> = std::collections::HashMap::new();
|
||||
for line in text.lines().filter(|l| !l.starts_with('#')) {
|
||||
let mut f = line.split('\t');
|
||||
if let (Some(name), Some(_kind), Some(from)) = (f.next(), f.next(), f.next()) {
|
||||
origin.insert(name, from);
|
||||
}
|
||||
}
|
||||
|
||||
// What this client answers to with its whole interface up — engine bindings AND every
|
||||
// global our own FrameXML defines.
|
||||
let mut s = UiScript::new().expect("VM");
|
||||
s.set_screen_size(1024.0, 768.0);
|
||||
let failures = super::super::manifest::load_default_ui(&s);
|
||||
assert!(failures.is_empty(), "the shipped manifest: {failures:#?}");
|
||||
let have: std::collections::HashSet<String> = s
|
||||
.eval::<Vec<String>>(
|
||||
"local t = {} for k in pairs(_G) do table.insert(t, k) end return t",
|
||||
)
|
||||
.expect("dump _G")
|
||||
.into_iter()
|
||||
.collect();
|
||||
|
||||
let migrated: std::collections::HashSet<String> = super::super::addons::Addon::builtin()
|
||||
.toc
|
||||
.files
|
||||
.iter()
|
||||
.filter(|f| super::is_chain_entry(f))
|
||||
.filter_map(|f| f.rsplit(['\\', '/']).next().map(str::to_string))
|
||||
.collect();
|
||||
|
||||
let toc = String::from_utf8_lossy(
|
||||
&super::read("Interface\\FrameXML\\FrameXML.toc").expect("the reference's own toc"),
|
||||
)
|
||||
.into_owned();
|
||||
let stock: Vec<String> = toc
|
||||
.lines()
|
||||
.map(str::trim)
|
||||
.filter(|l| !l.is_empty() && !l.starts_with('#') && l.ends_with(".xml"))
|
||||
.map(str::to_string)
|
||||
.collect();
|
||||
|
||||
// `function Name(` across the whole corpus, so an fx gap can name the file that holds it.
|
||||
let mut home: std::collections::HashMap<String, String> = std::collections::HashMap::new();
|
||||
for f in &stock {
|
||||
for cand in [f.clone(), format!("{}.lua", &f[..f.len() - 4])] {
|
||||
let Some(bytes) = super::read(&format!("Interface\\FrameXML\\{cand}")) else {
|
||||
continue;
|
||||
};
|
||||
for line in String::from_utf8_lossy(&bytes).lines() {
|
||||
if let Some(rest) = line.trim_start().strip_prefix("function ") {
|
||||
let name: String = rest
|
||||
.chars()
|
||||
.take_while(|c| c.is_alphanumeric() || *c == '_')
|
||||
.collect();
|
||||
if !name.is_empty() {
|
||||
home.entry(name).or_insert_with(|| cand.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let called = |text: &str| -> std::collections::HashSet<String> {
|
||||
let b: Vec<char> = text.chars().collect();
|
||||
let mut out = std::collections::HashSet::new();
|
||||
let mut i = 0;
|
||||
while i < b.len() {
|
||||
if b[i].is_ascii_uppercase() && (i == 0 || !super::is_word(b[i - 1])) {
|
||||
let mut j = i;
|
||||
while j < b.len() && super::is_word(b[j]) {
|
||||
j += 1;
|
||||
}
|
||||
let mut k = j;
|
||||
while k < b.len() && b[k] == ' ' {
|
||||
k += 1;
|
||||
}
|
||||
if k < b.len() && b[k] == '(' {
|
||||
out.insert(b[i..j].iter().collect::<String>());
|
||||
}
|
||||
i = j;
|
||||
continue;
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
out
|
||||
};
|
||||
|
||||
println!("\n=== 1751 gap report — what each unmigrated window would cost ===");
|
||||
let mut rows: Vec<(usize, String, Vec<String>, Vec<String>)> = Vec::new();
|
||||
for f in &stock {
|
||||
if migrated.contains(f) {
|
||||
continue;
|
||||
}
|
||||
let mut text = String::new();
|
||||
for cand in [f.clone(), format!("{}.lua", &f[..f.len() - 4])] {
|
||||
if let Some(b) = super::read(&format!("Interface\\FrameXML\\{cand}")) {
|
||||
text.push_str(&String::from_utf8_lossy(&b));
|
||||
}
|
||||
}
|
||||
let own: std::collections::HashSet<String> = text
|
||||
.lines()
|
||||
.filter_map(|l| l.trim_start().strip_prefix("function "))
|
||||
.map(|r| {
|
||||
r.chars()
|
||||
.take_while(|c| c.is_alphanumeric() || *c == '_')
|
||||
.collect::<String>()
|
||||
})
|
||||
.collect();
|
||||
let (mut eng, mut fx) = (Vec::new(), Vec::new());
|
||||
let mut names: Vec<String> = called(&text)
|
||||
.into_iter()
|
||||
.filter(|c| !own.contains(c) && !have.contains(c))
|
||||
.collect();
|
||||
names.sort();
|
||||
for c in names {
|
||||
match origin.get(c.as_str()).copied() {
|
||||
Some("engine") => eng.push(c),
|
||||
Some(_) => {
|
||||
let h = home.get(&c).cloned().unwrap_or_else(|| "?".into());
|
||||
fx.push(format!("{c}<{h}>"));
|
||||
}
|
||||
None => {} // a widget method, not a global — the reference's _G would have it
|
||||
}
|
||||
}
|
||||
rows.push((eng.len(), f.clone(), eng, fx));
|
||||
}
|
||||
rows.sort_by(|a, b| (a.0, &a.1).cmp(&(b.0, &b.1)));
|
||||
for (n, f, eng, fx) in &rows {
|
||||
println!("{n:>3} engine {f}");
|
||||
if !eng.is_empty() {
|
||||
println!(" engine: {}", eng.join(" "));
|
||||
}
|
||||
if !fx.is_empty() {
|
||||
println!(" fx: {}", fx.join(" "));
|
||||
}
|
||||
}
|
||||
let free: Vec<&String> = rows.iter().filter(|r| r.0 == 0).map(|r| &r.1).collect();
|
||||
println!(
|
||||
"\n=== {} windows need NO engine work at all ===",
|
||||
free.len()
|
||||
);
|
||||
for f in free {
|
||||
println!(" {f}");
|
||||
}
|
||||
}
|
||||
|
||||
/// A path is a chain entry; a bare name is ours. The one-line rule the manifest rests on.
|
||||
#[test]
|
||||
fn a_separator_is_what_makes_an_entry_the_players_own_file() {
|
||||
|
||||
@@ -5,22 +5,7 @@
|
||||
|
||||
use benilla_ui::script::UiScript;
|
||||
|
||||
fn load_xml(s: &UiScript, file: &str) -> usize {
|
||||
let text = std::fs::read_to_string(
|
||||
std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
|
||||
.join("assets/ui")
|
||||
.join(file),
|
||||
)
|
||||
.unwrap();
|
||||
let doc = benilla_ui::framexml::parse(&text).unwrap();
|
||||
let report = benilla_ui::loader::load(s, &doc, &|_| None);
|
||||
assert!(
|
||||
report.errors.is_empty(),
|
||||
"{file}: loader errors: {:?}",
|
||||
report.errors
|
||||
);
|
||||
report.frames
|
||||
}
|
||||
use super::test_ui::load_ui as load_xml;
|
||||
|
||||
fn setup() -> UiScript {
|
||||
let mut s = UiScript::new().unwrap();
|
||||
|
||||
@@ -19,24 +19,7 @@ fn player(in_combat: bool) -> UnitState {
|
||||
}
|
||||
}
|
||||
|
||||
/// Load one shipped `assets/ui/<file>` into `s`, panicking on any loader error (the binder tests'
|
||||
/// loader, duplicated so this file is self-contained).
|
||||
fn load_xml(s: &UiScript, file: &str) -> usize {
|
||||
let text = std::fs::read_to_string(
|
||||
std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
|
||||
.join("assets/ui")
|
||||
.join(file),
|
||||
)
|
||||
.unwrap();
|
||||
let doc = benilla_ui::framexml::parse(&text).unwrap();
|
||||
let report = benilla_ui::loader::load(s, &doc, &|_| None);
|
||||
assert!(
|
||||
report.errors.is_empty(),
|
||||
"{file}: loader errors: {:?}",
|
||||
report.errors
|
||||
);
|
||||
report.frames
|
||||
}
|
||||
use super::test_ui::load_ui as load_xml;
|
||||
|
||||
/// The app's own pre-state: a live offer from Twomage, out of Stormwind City, with the server's
|
||||
/// full two-minute window still on the clock.
|
||||
|
||||
@@ -4,24 +4,7 @@
|
||||
|
||||
use benilla_ui::script::{ScriptValue, UiScript};
|
||||
|
||||
/// Load one shipped `assets/ui/<file>` into `s`, panicking on any loader error (the binder tests'
|
||||
/// loader, duplicated so this file is self-contained).
|
||||
fn load_xml(s: &UiScript, file: &str) -> usize {
|
||||
let text = std::fs::read_to_string(
|
||||
std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
|
||||
.join("assets/ui")
|
||||
.join(file),
|
||||
)
|
||||
.unwrap();
|
||||
let doc = benilla_ui::framexml::parse(&text).unwrap();
|
||||
let report = benilla_ui::loader::load(s, &doc, &|_| None);
|
||||
assert!(
|
||||
report.errors.is_empty(),
|
||||
"{file}: loader errors: {:?}",
|
||||
report.errors
|
||||
);
|
||||
report.frames
|
||||
}
|
||||
use super::test_ui::load_ui as load_xml;
|
||||
|
||||
/// The app's own pre-state: a question is pending and in range, which is what
|
||||
/// `CheckTalentMasterDist()` reports while the dialog is up. MoneyFrame.xml loads FIRST because
|
||||
|
||||
@@ -23,7 +23,7 @@ fn harness() -> UiScript {
|
||||
load_xml(&s, "Cooldown.xml");
|
||||
load_xml(&s, "ActionBar.xml"); // BENILLA_FALLBACK_ICON
|
||||
load_xml(&s, "UnitFrames.xml"); // loads BEFORE BuffFrame, as in the app
|
||||
load_xml(&s, "BuffFrame.xml"); // DebuffTypeColor
|
||||
load_xml(&s, "Interface\\FrameXML\\BuffFrame.xml"); // DebuffTypeColor
|
||||
s
|
||||
}
|
||||
|
||||
|
||||
@@ -116,7 +116,7 @@ pub(super) const BAG_UI: &[&str] = &[
|
||||
// `OpenAllBags` opens with `if not UIParent:IsVisible() then return end`. Without it the
|
||||
// windows fall out of the cascade and the reference's own layout pass has nothing to measure.
|
||||
"UIParent.xml",
|
||||
"ItemButtonTemplate.xml",
|
||||
"Interface\\FrameXML\\ItemButtonTemplate.xml",
|
||||
"MoneyFrame.xml",
|
||||
"UiPanels.xml",
|
||||
"GameTooltip.xml",
|
||||
@@ -143,6 +143,7 @@ pub(super) const BAG_UI: &[&str] = &[
|
||||
// …nor is the chat edit box. The reference's SHIFT arm opens with
|
||||
// `if ( ChatFrameEditBox:IsShown() )` (ContainerFrame.lua l.569) to decide between posting the
|
||||
// item's link and splitting the stack, so a VM without it raises before either.
|
||||
"Interface\\FrameXML\\UIMenu.xml", // the kit ChatMenu/EmoteMenu/VoiceMacroMenu build from
|
||||
"ChatFrame.xml",
|
||||
// Our adapters over the reference's container files — the keyring tooltip wrapper, the three
|
||||
// bag verbs 0561 shadows (`OpenBackpack`/`CloseBackpack`/`CloseAllBags`), and the item-push
|
||||
|
||||
@@ -310,7 +310,11 @@ fn ubertooltips_off_seats_stance_plates_beside_the_button() {
|
||||
/// BOTTOMLEFT.
|
||||
#[test]
|
||||
fn buff_hover_hangs_below_left_of_the_button() {
|
||||
let mut s = harness(&["Cooldown.xml", "ActionBar.xml", "BuffFrame.xml"]);
|
||||
let mut s = harness(&[
|
||||
"Cooldown.xml",
|
||||
"ActionBar.xml",
|
||||
"Interface\\FrameXML\\BuffFrame.xml",
|
||||
]);
|
||||
s.set_auras(
|
||||
"player",
|
||||
Some(vec![AuraState {
|
||||
|
||||
@@ -56,7 +56,7 @@ fn load_tot() -> UiScript {
|
||||
load_xml(&s, "Cooldown.xml");
|
||||
load_xml(&s, "ActionBar.xml");
|
||||
load_xml(&s, "UnitFrames.xml");
|
||||
load_xml(&s, "BuffFrame.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\BuffFrame.xml");
|
||||
s.set_unit("player", Some(unit("Tri", 0x100, 100)));
|
||||
s.set_unit("target", Some(unit("Kobold Miner", 0x200, 80)));
|
||||
s.set_unit("targettarget", Some(unit("Tri", 0x100, 100)));
|
||||
|
||||
@@ -13,24 +13,7 @@ use benilla_ui::script::{
|
||||
TrainerServiceCategory, TrainerSkillReq, TrainerState, UiScript,
|
||||
};
|
||||
|
||||
/// Load one shipped `assets/ui/<file>` into `s`, panicking on any loader error and returning the
|
||||
/// frame count it materialized.
|
||||
fn load_xml(s: &UiScript, file: &str) -> usize {
|
||||
let text = std::fs::read_to_string(
|
||||
std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
|
||||
.join("assets/ui")
|
||||
.join(file),
|
||||
)
|
||||
.unwrap();
|
||||
let doc = benilla_ui::framexml::parse(&text).unwrap();
|
||||
let report = benilla_ui::loader::load(s, &doc, &|_| None);
|
||||
assert!(
|
||||
report.errors.is_empty(),
|
||||
"{file}: loader errors: {:?}",
|
||||
report.errors
|
||||
);
|
||||
report.frames
|
||||
}
|
||||
use super::test_ui::load_ui as load_xml;
|
||||
|
||||
/// Load the trainer window + all its deps into a fresh script, screen sized, with every state filter
|
||||
/// ON (the XML defaults "Already Known" off — the tests want the full tree, deterministic indices).
|
||||
|
||||
@@ -508,6 +508,12 @@ fn resolve_slot(
|
||||
let texture = icons
|
||||
.and_then(|i| i.catalog.get(item.display_id))
|
||||
.and_then(|d| d.icon.clone());
|
||||
// The same link the merchant rows carry, built by the same helper — nil until the template
|
||||
// answers, because the string embeds both the name and the quality colour.
|
||||
let link = match (name.as_deref(), quality) {
|
||||
(Some(n), Some(q)) => Some(crate::ui_items::item_link(item.entry, n, q)),
|
||||
_ => None,
|
||||
};
|
||||
TradeSlotItem {
|
||||
item_id: item.entry,
|
||||
name,
|
||||
@@ -515,6 +521,7 @@ fn resolve_slot(
|
||||
count: item.count.max(1),
|
||||
quality,
|
||||
enchantment: None,
|
||||
link,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -15,16 +15,17 @@
|
||||
//! one (decision 1397: the ref Lua subtracts `posBuff`/`negBuff` itself, so a pre-subtracted first
|
||||
//! return deducts the buff twice — a defect the f32 bug was hiding).
|
||||
|
||||
use benilla_ui::script::{UiScript, UnitCombatStats, UnitState};
|
||||
mod common;
|
||||
|
||||
const UI_DIR: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/assets/ui");
|
||||
use benilla_ui::script::{UiScript, UnitCombatStats, UnitState};
|
||||
|
||||
/// The character window's load prefix, in `assets/ui/benilla.toc` order. The .toc's own entry says
|
||||
/// it "depends only on Fonts/UiPanels/GameTooltip"; `ItemButtonTemplate.xml` joins them because the
|
||||
/// doll's equipment slots are `SetItemButton*` buttons.
|
||||
const FILES: [&str; 6] = [
|
||||
"Fonts.xml",
|
||||
"ItemButtonTemplate.xml",
|
||||
// The reference's own since 1751 window 24 — `common::load_ui` speaks both stores.
|
||||
"Interface\\FrameXML\\ItemButtonTemplate.xml",
|
||||
"MoneyFrame.xml",
|
||||
"UiPanels.xml",
|
||||
"GameTooltip.xml",
|
||||
@@ -32,26 +33,8 @@ const FILES: [&str; 6] = [
|
||||
];
|
||||
|
||||
fn load_ui(script: &UiScript) {
|
||||
let dir = std::path::Path::new(UI_DIR);
|
||||
// The app's provider shape: backslash paths, dir-relative, basename fallback.
|
||||
let provider = |req: &str| -> Option<Vec<u8>> {
|
||||
let norm = req.replace('\\', "/");
|
||||
let base = norm.rsplit('/').next().unwrap_or(&norm);
|
||||
std::fs::read(dir.join(&norm))
|
||||
.or_else(|_| std::fs::read(dir.join(base)))
|
||||
.ok()
|
||||
};
|
||||
for file in FILES {
|
||||
let text = std::fs::read_to_string(dir.join(file))
|
||||
.unwrap_or_else(|e| panic!("reading {file}: {e}"));
|
||||
let doc =
|
||||
benilla_ui::framexml::parse(&text).unwrap_or_else(|e| panic!("parsing {file}: {e}"));
|
||||
let report = benilla_ui::loader::load(script, &doc, &provider);
|
||||
assert!(
|
||||
report.errors.is_empty(),
|
||||
"{file} loaded with errors: {:#?}",
|
||||
report.errors
|
||||
);
|
||||
common::load_ui(script, file);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -119,6 +102,7 @@ fn seated() -> UiScript {
|
||||
|
||||
#[test]
|
||||
fn a_geared_stat_renders_green_and_its_tooltip_names_the_unbuffed_base() {
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
let mut s = seated();
|
||||
painted(&mut s);
|
||||
|
||||
@@ -143,6 +127,7 @@ fn a_geared_stat_renders_green_and_its_tooltip_names_the_unbuffed_base() {
|
||||
|
||||
#[test]
|
||||
fn a_debuffed_stat_renders_red_and_an_untouched_one_stays_plain() {
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
let mut s = seated();
|
||||
painted(&mut s);
|
||||
|
||||
@@ -174,6 +159,7 @@ fn a_debuffed_stat_renders_red_and_an_untouched_one_stays_plain() {
|
||||
|
||||
#[test]
|
||||
fn resistance_rows_colour_by_which_half_is_bigger() {
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
let mut s = seated();
|
||||
painted(&mut s);
|
||||
|
||||
@@ -210,6 +196,7 @@ fn resistance_rows_colour_by_which_half_is_bigger() {
|
||||
/// shape and worth pinning as such.
|
||||
#[test]
|
||||
fn armor_with_no_buff_split_renders_plain() {
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
let mut s = seated();
|
||||
painted(&mut s);
|
||||
assert_eq!(text_of(&mut s, "CharacterArmorFrameStatText"), "2965");
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
//! **The integration tests' interface loader — one copy, both stores** (decision 1751).
|
||||
//!
|
||||
//! The in-crate sibling of `ui_script::test_ui::load_ui`, which integration tests cannot reach:
|
||||
//! they link this crate as a library, so its `#[cfg(test)]` items are not compiled for them. Each
|
||||
//! `tests/*.rs` therefore grew its own reader off `assets/ui`, and every one of them broke the
|
||||
//! first time a file it names became the reference's own.
|
||||
//!
|
||||
//! The rule is the manifest's, verbatim: **a bare filename is a file we ship, a path is the
|
||||
//! reference's own off the player's installed chain.** Our shipped tree is flat, so a separator
|
||||
//! decides, and nothing here needs to know which windows have migrated.
|
||||
//!
|
||||
//! The provider half matters as much as the loop. A sourced document pulls its Lua through its own
|
||||
//! `<Script file="X.lua"/>`, which the loader resolves against the *including document's*
|
||||
//! directory — `Interface\FrameXML\X.lua`, a chain path. A disk-only provider leaves every one of
|
||||
//! those globals nil and the failures land nowhere near the cause.
|
||||
//!
|
||||
//! **A chain entry needs client data**, so a test that names one opens with
|
||||
//! `benilla_formats::wow_data_or_skip!()`, like every other archive-backed test.
|
||||
|
||||
use benilla_ui::script::UiScript;
|
||||
|
||||
/// Load one manifest entry into `script`, panicking on any loader error.
|
||||
///
|
||||
/// A `.lua` entry is run as a chunk rather than parsed as a document — `GlobalStrings.lua` and
|
||||
/// `LocaleProperties.lua` are entries of that shape in the real manifest too. Bytes, not text: a
|
||||
/// chunk goes to Lua as it sits in the archive and only an XML parse decodes (1193).
|
||||
pub fn load_ui(script: &UiScript, entry: &str) {
|
||||
let dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("assets/ui");
|
||||
let chain = |req: &str| -> Option<Vec<u8>> {
|
||||
let data = benilla_formats::wow_data()?;
|
||||
benilla_formats::open_chain(&data).ok()?.read(req).ok()
|
||||
};
|
||||
let read = |req: &str| -> Option<Vec<u8>> {
|
||||
if req.contains('\\') || req.contains('/') {
|
||||
return chain(req);
|
||||
}
|
||||
std::fs::read(dir.join(req)).ok()
|
||||
};
|
||||
|
||||
let bytes = read(entry).unwrap_or_else(|| panic!("{entry}: not found"));
|
||||
if entry.to_ascii_lowercase().ends_with(".lua") {
|
||||
script
|
||||
.run_chunk_named(&bytes, &format!("@{entry}"))
|
||||
.unwrap_or_else(|e| panic!("{entry}: {e}"));
|
||||
return;
|
||||
}
|
||||
let doc = benilla_ui::framexml::parse(&benilla_ui::source::decode(&bytes))
|
||||
.unwrap_or_else(|e| panic!("{entry}: {e}"));
|
||||
let report = benilla_ui::loader::load_in(script, &doc, &entry.replace('\\', "/"), &read);
|
||||
assert!(
|
||||
report.errors.is_empty(),
|
||||
"{entry}: loader errors: {:#?}",
|
||||
report.errors
|
||||
);
|
||||
}
|
||||
@@ -9,20 +9,20 @@
|
||||
//! depths: the Lua-visible state (`TALENT_BRANCH_ARRAY`), the logical frame state (`IsShown`),
|
||||
//! and the extract the renderer actually draws (branch/arrow quads with their atlas coords).
|
||||
|
||||
mod common;
|
||||
|
||||
use benilla_ui::script::{
|
||||
QuadContent, SpellTooltipView, TalentPrereqView, TalentTabView, TalentUiState, TalentView,
|
||||
UiScript,
|
||||
};
|
||||
|
||||
const UI_DIR: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/assets/ui");
|
||||
|
||||
/// The talent window's load prefix — the app's own order (`assets/ui/benilla.toc`), members only.
|
||||
/// `ItemButtonTemplate.xml` is the `SetItemButton*` family the talent buttons grey through (the
|
||||
/// reference's own verb; see TalentFrame.xml's header) — it sits at .toc line 32, above every
|
||||
/// other entry here bar `Fonts.xml`.
|
||||
const FILES: [&str; 7] = [
|
||||
"Fonts.xml",
|
||||
"ItemButtonTemplate.xml",
|
||||
"Interface\\FrameXML\\ItemButtonTemplate.xml",
|
||||
"MoneyFrame.xml",
|
||||
"UiPanels.xml",
|
||||
"GameTooltip.xml",
|
||||
@@ -31,28 +31,8 @@ const FILES: [&str; 7] = [
|
||||
];
|
||||
|
||||
fn load_ui(script: &UiScript) {
|
||||
let dir = std::path::Path::new(UI_DIR);
|
||||
// The app's provider shape: backslash paths, dir-relative, basename fallback.
|
||||
let provider = |req: &str| -> Option<Vec<u8>> {
|
||||
let norm = req.replace('\\', "/");
|
||||
let base = norm.rsplit('/').next().unwrap_or(&norm);
|
||||
std::fs::read(dir.join(&norm))
|
||||
.or_else(|_| std::fs::read(dir.join(base)))
|
||||
.ok()
|
||||
};
|
||||
for file in FILES {
|
||||
let text = std::fs::read_to_string(dir.join(file)).unwrap_or_else(|e| {
|
||||
panic!("reading {file}: {e}");
|
||||
});
|
||||
let doc = benilla_ui::framexml::parse(&text).unwrap_or_else(|e| {
|
||||
panic!("parsing {file}: {e}");
|
||||
});
|
||||
let report = benilla_ui::loader::load(script, &doc, &provider);
|
||||
assert!(
|
||||
report.errors.is_empty(),
|
||||
"{file} loaded with errors: {:#?}",
|
||||
report.errors
|
||||
);
|
||||
common::load_ui(script, file);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -125,6 +105,7 @@ fn open_window(points_spent: u32) -> UiScript {
|
||||
|
||||
#[test]
|
||||
fn branch_lines_draw_for_a_same_column_prereq() {
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
let mut script = open_window(10);
|
||||
// Depth 1 — the Lua machinery: Update ran, the branch array carries the vertical chain
|
||||
// (tier 1's down-edge, the empty tier-2 cell's up+down, the button's own top arrow).
|
||||
@@ -182,6 +163,7 @@ fn branch_lines_draw_for_a_same_column_prereq() {
|
||||
|
||||
#[test]
|
||||
fn tooltip_is_complete_on_the_first_hover() {
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
let mut script = open_window(10);
|
||||
// The app's feed owes the store every talent spell view up front (the spellbook's own
|
||||
// arrival-driven contract) — with the views in place, the FIRST OnEnter must render the
|
||||
@@ -202,6 +184,7 @@ fn tooltip_is_complete_on_the_first_hover() {
|
||||
|
||||
#[test]
|
||||
fn tooltip_miss_still_shows_the_rank_line() {
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
let mut script = open_window(10);
|
||||
// The store is EMPTY (no views pushed): the ask-once fallback must still show a box with
|
||||
// the talent head — never a blank hover — and record the ask for the app's resolver.
|
||||
@@ -224,6 +207,7 @@ fn tooltip_miss_still_shows_the_rank_line() {
|
||||
|
||||
#[test]
|
||||
fn a_locked_tier_still_draws_the_branch_gray() {
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
// The director's own day-one state: 2 points spent, tier 3 locked — the reference draws
|
||||
// the chain anyway, gray (the `-1` keys of the texcoord tables; a typo'd negative key
|
||||
// would error the draw loop and hide every line while the grid stays perfect).
|
||||
@@ -258,6 +242,7 @@ fn a_locked_tier_still_draws_the_branch_gray() {
|
||||
/// greys the whole tree passes the first assertion and fails this one.
|
||||
#[test]
|
||||
fn an_unavailable_talent_reaches_the_renderer_desaturated() {
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
let mut script = open_window(2);
|
||||
script.resolve();
|
||||
let icon = |quads: &[benilla_ui::script::ExtractedQuad], leaf: &str| -> (bool, [f32; 4]) {
|
||||
@@ -304,6 +289,7 @@ fn an_unavailable_talent_reaches_the_renderer_desaturated() {
|
||||
|
||||
#[test]
|
||||
fn a_wheel_spin_over_the_grid_scrolls_the_tree() {
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
// The wheel's whole chain, driven from the pointer entry point the app feeds: hit-test on
|
||||
// a talent button, bubble to the ScrollFrame's OnMouseWheel, step the Slider, whose
|
||||
// OnValueChanged pans the frame. This is the chain the 2051f4f8 rename broke silently —
|
||||
|
||||
@@ -55,6 +55,7 @@ fn item(item_id: u32, name: &str, count: u32, quality: u32) -> TradeSlotItem {
|
||||
count,
|
||||
quality: Some(quality),
|
||||
enchantment: None,
|
||||
link: Some(format!("|cffffffff|Hitem:{item_id}:0:0:0|h[{name}]|h|r")),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -839,7 +839,7 @@ pub enum SessionEvent {
|
||||
npc: u64,
|
||||
text_id: u32,
|
||||
options: Vec<GossipOption>,
|
||||
quests: Vec<(u32, u32, String)>,
|
||||
quests: Vec<(u32, u32, u32, String)>,
|
||||
},
|
||||
/// A questgiver dialog status for one NPC (`SMSG_QUESTGIVER_STATUS`) — the `!`/`?` marker's
|
||||
/// [`crate::messages::dialog_status`] value. Stored per guid now; the world marker is a later
|
||||
|
||||
@@ -318,7 +318,7 @@ pub fn decode(packet: ServerPacket) -> Vec<SessionEvent> {
|
||||
options,
|
||||
quests: quests
|
||||
.into_iter()
|
||||
.map(|q| (q.quest_id, q.icon, q.title))
|
||||
.map(|q| (q.quest_id, q.icon, q.level, q.title))
|
||||
.collect(),
|
||||
}],
|
||||
ServerPacket::QuestGiverStatus { npc, status } => {
|
||||
|
||||
@@ -20,8 +20,40 @@ impl Loader<'_> {
|
||||
for scripts in children_named(el, "Scripts") {
|
||||
for handler in &scripts.children {
|
||||
let name = handler.tag.clone();
|
||||
let Some(func) = self.compile_handler(handler, &name, dbg) else {
|
||||
continue;
|
||||
// **AN EMPTY BODY IS A CLEAR, AND WHITESPACE IS NOT EMPTY** — the two byte
|
||||
// tests at `SetScript 0x7025c0`, reproduced rather than approximated (wow-5875-re
|
||||
// `system/ui/scratch/xml-script-empty-element.md`, a 3-worker cross-check):
|
||||
//
|
||||
// 7025ec call 0x702670 ; unref the PREVIOUS handler, unconditionally
|
||||
// 7025f4 test ebx,ebx
|
||||
// 7025f6 je 0x702655 ; text == NULL -> store 0 (reads back as nil)
|
||||
// 7025f8 cmp byte ptr [ebx],0
|
||||
// 7025fb je 0x702655 ; text == "" -> same
|
||||
//
|
||||
// The XMLTree chardata handler (`0x6f29d0`) appends every run with `len > 0` and
|
||||
// nothing trims, so `0x7025f8` tests the FIRST BYTE of the raw body.
|
||||
//
|
||||
// That distinction is the whole finding, and it is not academic. 1.12's own
|
||||
// FrameXML has **zero** `<OnX/>` and **zero** `<OnX></OnX>`; its seven blanking
|
||||
// sites (BuffFrame ×3, BankFrame, PaperDollFrame ×2, UIPanelTemplates) are all
|
||||
// `<OnLoad>`↵`</OnLoad>` — a body of `"\n\t\t\t"`, whose first byte is `0x0A`.
|
||||
// Both tests fail and it **compiles to a valid empty function**. So
|
||||
// `TempEnchant1:GetScript("OnLoad")` answers a FUNCTION in the real client, not
|
||||
// nil, and the inherited `BuffButton_OnLoad` is displaced rather than removed.
|
||||
// A `.trim()` here would produce the right window and the wrong answer to an addon.
|
||||
//
|
||||
// The clear path is still real (an empty body, or a `function=` element with no
|
||||
// body — 5875 never reads that attribute at all, so it takes the NULL leg), and it
|
||||
// matters that it is a stored nil: a `0` registry ref is skipped by the dispatch
|
||||
// guard entirely, where the compiled no-op is entered and returns.
|
||||
let cleared = handler.body.is_empty() && handler.attr("function").is_none();
|
||||
let func = if cleared {
|
||||
None
|
||||
} else {
|
||||
match self.compile_handler(handler, &name, dbg) {
|
||||
Some(f) => Some(f),
|
||||
None => continue,
|
||||
}
|
||||
};
|
||||
// SetScript stores it; an unsupported name errors — surface as a gap, don't drop hard.
|
||||
if let Err(e) = wrapper.call_method::<()>("SetScript", (name.clone(), func.clone()))
|
||||
@@ -63,7 +95,10 @@ impl Loader<'_> {
|
||||
self.call(wrapper, "EnableKeyboard", true, dbg);
|
||||
}
|
||||
if name.eq_ignore_ascii_case("OnLoad") {
|
||||
onload = Some(func);
|
||||
// A blanked `<OnLoad/>` leaves nothing to fire — and it must also UNSET a
|
||||
// handle a template's own OnLoad put here, or the caller fires the very body
|
||||
// this element exists to remove.
|
||||
onload = func;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -109,7 +144,11 @@ impl Loader<'_> {
|
||||
name: &str,
|
||||
dbg: &str,
|
||||
) -> Option<Function> {
|
||||
let body = handler.body.trim();
|
||||
// **The raw body, NOT a trimmed one** — 1.12 hands `node->text` straight to
|
||||
// `luaL_loadbuffer` and tests only its first byte, so a whitespace-only body is a real
|
||||
// (empty) chunk there and must be one here. See `apply_scripts` for the bytes and for the
|
||||
// seven stock files that depend on it.
|
||||
let body = handler.body.as_str();
|
||||
if !body.is_empty() {
|
||||
let src = format!(
|
||||
"return function(self, ...) if self == nil then self = this end\n{body}\nend"
|
||||
|
||||
@@ -47,6 +47,13 @@ pub struct GossipOptionView {
|
||||
#[derive(Clone, Debug, Default, PartialEq)]
|
||||
pub struct GossipQuestRow {
|
||||
pub title: String,
|
||||
/// The quest's level — `SMSG_GOSSIP_MESSAGE`'s own third field per quest row.
|
||||
///
|
||||
/// The app parsed it and dropped it until 1751's twenty-first window, because the invented
|
||||
/// `GetGossipQuestInfo` had nowhere to put it. The reference's two verbs return the rows as
|
||||
/// `(title, level)` PAIRS, and stock `GossipFrame.lua` strides its walk by 2 over them — so
|
||||
/// the level is load-bearing for the stride even though 1.12's own FrameXML never reads it.
|
||||
pub level: u32,
|
||||
pub active: bool,
|
||||
}
|
||||
|
||||
@@ -165,44 +172,89 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
})?,
|
||||
)?;
|
||||
|
||||
// → the number of quest rows the open menu carries (0 when none / no menu).
|
||||
g.set(
|
||||
"GetNumGossipQuests",
|
||||
lua.create_function(|lua, ()| {
|
||||
// ══ THE TWO QUEST LISTS, and why there are two ══════════════════════════════════════════
|
||||
//
|
||||
// 1.12 splits the gossip packet's quest rows by their WIRE ICON — `{3,4}` are "active", every
|
||||
// other value "available" (decision 0758) — and publishes each list through its own vararg
|
||||
// verb. The reference runs that same test lazily behind these two bindings
|
||||
// (`0x4e2430`/`0x4e2580`); we run it at parse time and keep the answer on the row.
|
||||
//
|
||||
// **Each row is a PAIR: `(title, level)`.** Stock `GossipFrameAvailableQuestsUpdate` /
|
||||
// `…ActiveQuestsUpdate` walk `for i = 1, arg.n, 2` and read only `arg[i]`, so the level is
|
||||
// never displayed in 1.12 — but it is what makes the stride land, and an addon reading the
|
||||
// second slot gets the number the server sent rather than a nil.
|
||||
//
|
||||
// These four REPLACE `GetNumGossipQuests` / `GetGossipQuestInfo` / `SelectGossipQuest`, which
|
||||
// were benilla's own single-list shape and appear nowhere in `reference/1.12-globals.tsv`.
|
||||
// 1189's rule: a superset is not free — an addon that feature-detects on a name the client
|
||||
// does not have takes a branch we cannot honour.
|
||||
fn quest_rows(lua: &Lua, active: bool) -> mlua::Result<MultiValue> {
|
||||
let rows: Vec<(String, u32)> = {
|
||||
let model = lua.app_data_ref::<Model>().expect("model app_data");
|
||||
Ok(model.gossip.as_ref().map_or(0, |m| m.quests.len()) as i64)
|
||||
})?,
|
||||
model.gossip.as_ref().map_or_else(Vec::new, |m| {
|
||||
m.quests
|
||||
.iter()
|
||||
.filter(|q| q.active == active)
|
||||
.map(|q| (q.title.clone(), q.level))
|
||||
.collect()
|
||||
})
|
||||
};
|
||||
let mut out = Vec::with_capacity(rows.len() * 2);
|
||||
for (title, level) in rows {
|
||||
out.push(Value::String(lua.create_string(&title)?));
|
||||
out.push(Value::Integer(i64::from(level)));
|
||||
}
|
||||
Ok(MultiValue::from_vec(out))
|
||||
}
|
||||
|
||||
g.set(
|
||||
"GetGossipAvailableQuests",
|
||||
lua.create_function(|lua, ()| quest_rows(lua, false))?,
|
||||
)?;
|
||||
g.set(
|
||||
"GetGossipActiveQuests",
|
||||
lua.create_function(|lua, ()| quest_rows(lua, true))?,
|
||||
)?;
|
||||
|
||||
// GetGossipQuestInfo(i) → title, isActive (1-based; out of range → nil).
|
||||
// The two selects. Each index is 1-based **within its own list**, which is what
|
||||
// `GossipTitleButton_OnClick` passes: the reference re-numbers the rows per list as it lays
|
||||
// them out (`titleIndex`), so a menu with two available and one active quest hands the active
|
||||
// row a 1, not a 3.
|
||||
//
|
||||
// The queue the app drains is one list, so the index is mapped back to the WHOLE row order
|
||||
// here — the app's own `take_gossip_quest_selects` contract is unchanged, and the two verbs
|
||||
// stay a pure re-expression of it rather than a second drain to wire up.
|
||||
fn select_quest(lua: &Lua, active: bool, index: usize) {
|
||||
let whole = {
|
||||
let model = lua.app_data_ref::<Model>().expect("model app_data");
|
||||
model.gossip.as_ref().and_then(|m| {
|
||||
m.quests
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(_, q)| q.active == active)
|
||||
.nth(index.wrapping_sub(1))
|
||||
.map(|(n, _)| n as u32 + 1)
|
||||
})
|
||||
};
|
||||
if let Some(n) = whole {
|
||||
lua.app_data_mut::<Model>()
|
||||
.expect("model app_data")
|
||||
.gossip_quest_selects
|
||||
.push(n);
|
||||
}
|
||||
}
|
||||
|
||||
g.set(
|
||||
"GetGossipQuestInfo",
|
||||
"SelectGossipAvailableQuest",
|
||||
lua.create_function(|lua, i: usize| {
|
||||
let row = {
|
||||
let model = lua.app_data_ref::<Model>().expect("model app_data");
|
||||
model
|
||||
.gossip
|
||||
.as_ref()
|
||||
.and_then(|m| i.checked_sub(1).and_then(|n| m.quests.get(n)))
|
||||
.cloned()
|
||||
};
|
||||
match row {
|
||||
Some(r) => Ok(MultiValue::from_vec(vec![
|
||||
Value::String(lua.create_string(&r.title)?),
|
||||
Value::Boolean(r.active),
|
||||
])),
|
||||
None => Ok(MultiValue::from_vec(vec![Value::Nil])),
|
||||
}
|
||||
select_quest(lua, false, i);
|
||||
Ok(())
|
||||
})?,
|
||||
)?;
|
||||
|
||||
// SelectGossipQuest(i) — queue the 1-based quest row; the app maps it to the quest id + guid and
|
||||
// sends CMSG_QUESTGIVER_QUERY_QUEST.
|
||||
g.set(
|
||||
"SelectGossipQuest",
|
||||
lua.create_function(|lua, i: u32| {
|
||||
let mut model = lua.app_data_mut::<Model>().expect("model app_data");
|
||||
model.gossip_quest_selects.push(i);
|
||||
"SelectGossipActiveQuest",
|
||||
lua.create_function(|lua, i: usize| {
|
||||
select_quest(lua, true, i);
|
||||
Ok(())
|
||||
})?,
|
||||
)?;
|
||||
@@ -281,40 +333,84 @@ mod tests {
|
||||
assert!(!s.take_gossip_close(), "drained");
|
||||
}
|
||||
|
||||
/// **The two quest lists the reference publishes, and the two selects that index INTO them.**
|
||||
///
|
||||
/// This replaces a test of `GetNumGossipQuests`/`GetGossipQuestInfo`/`SelectGossipQuest` —
|
||||
/// benilla's own single-list shape, and three names that appear nowhere in
|
||||
/// `reference/1.12-globals.tsv`. 1189's rule is why they had to go rather than sit beside the
|
||||
/// real four: an addon that feature-detects on a name this client does not have takes a branch
|
||||
/// we cannot honour.
|
||||
///
|
||||
/// The row shape is a `(title, level)` PAIR because stock `GossipFrameAvailableQuestsUpdate`
|
||||
/// walks `for i = 1, arg.n, 2` and reads `arg[i]`. 1.12 never displays the level; it is the
|
||||
/// stride that needs it, and an addon reading the second slot should get the server's number.
|
||||
#[test]
|
||||
fn gossip_quest_rows_read_and_select() {
|
||||
fn the_two_gossip_quest_lists_split_by_active_and_select_within_themselves() {
|
||||
use super::GossipQuestRow;
|
||||
let mut s = UiScript::new().unwrap();
|
||||
// No menu → zero rows.
|
||||
assert_eq!(s.eval::<i64>("return GetNumGossipQuests()").unwrap(), 0);
|
||||
// No menu → both lists are empty, and that is `arg.n == 0`, not a nil.
|
||||
assert_eq!(
|
||||
s.eval::<i64>("return select('#', GetGossipAvailableQuests())")
|
||||
.unwrap(),
|
||||
0
|
||||
);
|
||||
assert_eq!(
|
||||
s.eval::<i64>("return select('#', GetGossipActiveQuests())")
|
||||
.unwrap(),
|
||||
0
|
||||
);
|
||||
|
||||
let mut m = menu();
|
||||
m.quests = vec![
|
||||
GossipQuestRow {
|
||||
title: "Report to Goldshire".into(),
|
||||
level: 5,
|
||||
active: true,
|
||||
},
|
||||
GossipQuestRow {
|
||||
title: "A Threat Within".into(),
|
||||
level: 7,
|
||||
active: false,
|
||||
},
|
||||
GossipQuestRow {
|
||||
title: "Kobold Camp Cleanup".into(),
|
||||
level: 9,
|
||||
active: false,
|
||||
},
|
||||
];
|
||||
s.set_gossip(Some(m));
|
||||
assert_eq!(s.eval::<i64>("return GetNumGossipQuests()").unwrap(), 2);
|
||||
let (title, active) = s
|
||||
.eval::<(String, bool)>("return GetGossipQuestInfo(1)")
|
||||
.unwrap();
|
||||
assert_eq!((title.as_str(), active), ("Report to Goldshire", true));
|
||||
assert!(s
|
||||
.eval::<bool>(
|
||||
"local t, a = GetGossipQuestInfo(2) return t == 'A Threat Within' and a == false"
|
||||
)
|
||||
.unwrap());
|
||||
assert!(s
|
||||
.eval::<bool>("return GetGossipQuestInfo(9) == nil")
|
||||
.unwrap());
|
||||
|
||||
s.run("SelectGossipQuest(2)").unwrap();
|
||||
assert_eq!(s.take_gossip_quest_selects(), vec![2]);
|
||||
// Two available, in menu order, each a pair.
|
||||
assert_eq!(
|
||||
s.eval::<(String, i64, String, i64)>("return GetGossipAvailableQuests()")
|
||||
.unwrap(),
|
||||
(
|
||||
"A Threat Within".to_string(),
|
||||
7,
|
||||
"Kobold Camp Cleanup".to_string(),
|
||||
9
|
||||
)
|
||||
);
|
||||
// One active, and the active row does NOT appear in the available list even though it
|
||||
// comes first in the menu — the split is the wire icon, not the position.
|
||||
assert_eq!(
|
||||
s.eval::<(String, i64)>("return GetGossipActiveQuests()")
|
||||
.unwrap(),
|
||||
("Report to Goldshire".to_string(), 5)
|
||||
);
|
||||
|
||||
// **Each select is 1-based within its OWN list**, which is what
|
||||
// `GossipTitleButton_OnClick` passes (the reference re-numbers per list as it lays the
|
||||
// rows out). Available #2 is "Kobold Camp Cleanup", the THIRD row of the menu — and the
|
||||
// queue the app drains is still whole-menu positions, so it must come back as 3.
|
||||
s.run("SelectGossipAvailableQuest(2)").unwrap();
|
||||
assert_eq!(s.take_gossip_quest_selects(), vec![3]);
|
||||
// …and the single active row is #1 in its list, the FIRST of the menu.
|
||||
s.run("SelectGossipActiveQuest(1)").unwrap();
|
||||
assert_eq!(s.take_gossip_quest_selects(), vec![1]);
|
||||
// Out of range queues nothing rather than a wrong row.
|
||||
s.run("SelectGossipAvailableQuest(9) SelectGossipActiveQuest(0)")
|
||||
.unwrap();
|
||||
assert!(s.take_gossip_quest_selects().is_empty(), "drained");
|
||||
}
|
||||
|
||||
|
||||
@@ -59,6 +59,15 @@ pub struct MerchantItem {
|
||||
/// `GetBuybackItemLink` — the reference's buyback arm is an unmodified `BuybackItem(this:GetID())`
|
||||
/// with no ctrl/shift branch at all (`MerchantFrame.lua:358-361`), so nothing would read it.
|
||||
pub link: Option<String>,
|
||||
/// `Stackable` from the item's own template — what one slot can hold, `1` for an item that
|
||||
/// does not stack. `None` while the ask-once template query is in flight, like `name`.
|
||||
///
|
||||
/// This is `GetMerchantItemMaxStack`'s answer and it exists for one caller: the reference's
|
||||
/// `MerchantItemButton_OnClick` asks it before a shift-click, and opens the stack-split
|
||||
/// spinner only when it is above 1 (`MerchantFrame.lua:313-326`, and again at :340 for the
|
||||
/// buy-in-bulk arm). The wire has carried it all along — `item_template.stackable`, parsed and
|
||||
/// then dropped, because nothing on the Lua side could ask.
|
||||
pub max_stack: Option<u32>,
|
||||
}
|
||||
|
||||
/// An item template's tooltip stat head (the app's `ItemInfo` view, resolved per row): what the
|
||||
@@ -249,6 +258,31 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
})?,
|
||||
)?;
|
||||
|
||||
// GetMerchantItemMaxStack(index) → the stack size ONE slot can hold for that row's item, or
|
||||
// nil while its template answer is in flight / the index is out of range.
|
||||
//
|
||||
// Its only caller in 1.12 is the reference's own `MerchantItemButton_OnClick`, which asks it
|
||||
// twice — once on the right-click arm (`MerchantFrame.lua:313`) and once on shift-click
|
||||
// (`:340`) — and takes the plain single-buy path whenever the answer is `<= 1`, opening the
|
||||
// stack-split spinner otherwise. So the number decides whether a vendor click asks "how many?"
|
||||
// at all.
|
||||
//
|
||||
// The value was on the wire and parsed the whole time (`item_template.stackable`); nothing on
|
||||
// the Lua side could reach it until now, which is why the reference's merchant window listed
|
||||
// this as one of its two missing verbs.
|
||||
g.set(
|
||||
"GetMerchantItemMaxStack",
|
||||
lua.create_function(|lua, index: usize| {
|
||||
let model = lua.app_data_ref::<Model>().expect("model app_data");
|
||||
Ok(model
|
||||
.merchant
|
||||
.as_ref()
|
||||
.and_then(|m| index.checked_sub(1).and_then(|n| m.items.get(n)))
|
||||
.and_then(|it| it.max_stack)
|
||||
.map_or(Value::Nil, |n| Value::Integer(i64::from(n))))
|
||||
})?,
|
||||
)?;
|
||||
|
||||
// BenillaGetMerchantItemStats(index) → quality, invType, class, subclass, dmgMin, dmgMax,
|
||||
// dmgType, delayMs, armor, block — the tooltip stat head ([`ItemStatsHead`]), or nil while
|
||||
// the row's template answer is in flight / the index is out of range. Benilla-named: 1.12's Lua
|
||||
@@ -540,6 +574,7 @@ mod tests {
|
||||
..Default::default()
|
||||
}),
|
||||
link: Some("|cffffffff|Hitem:159:0:0:0|h[Refreshing Spring Water]|h|r".into()),
|
||||
max_stack: Some(20),
|
||||
},
|
||||
// An in-flight row: the vendor list arrived, the item-template answer hasn't.
|
||||
MerchantItem {
|
||||
@@ -551,6 +586,7 @@ mod tests {
|
||||
item_id: 4540,
|
||||
stats: None,
|
||||
link: None,
|
||||
max_stack: None, // in flight, like `name` above
|
||||
},
|
||||
],
|
||||
..Default::default()
|
||||
@@ -605,6 +641,25 @@ mod tests {
|
||||
assert!(s
|
||||
.eval::<bool>("return GetMerchantItemLink(9) == nil")
|
||||
.unwrap());
|
||||
|
||||
// **GetMerchantItemMaxStack** — the one number that decides whether a vendor click asks
|
||||
// "how many?". The reference's `MerchantItemButton_OnClick` reads it twice
|
||||
// (`MerchantFrame.lua:313` on the right-click arm and `:340` on shift-click) and takes the
|
||||
// plain single-buy path whenever it is `<= 1`, opening the stack-split spinner otherwise.
|
||||
//
|
||||
// Nil while the row's template answer is in flight, and nil out of range — the same two
|
||||
// absences `GetMerchantItemLink` above has, and for the same reason: the value is the
|
||||
// template's `stackable`, which the wire has carried all along.
|
||||
assert_eq!(
|
||||
s.eval::<i64>("return GetMerchantItemMaxStack(1)").unwrap(),
|
||||
20
|
||||
);
|
||||
assert!(s
|
||||
.eval::<bool>("return GetMerchantItemMaxStack(2) == nil")
|
||||
.unwrap());
|
||||
assert!(s
|
||||
.eval::<bool>("return GetMerchantItemMaxStack(9) == nil")
|
||||
.unwrap());
|
||||
}
|
||||
|
||||
/// The isUsable leg end-to-end: the gate reads the shared template store + the player req
|
||||
|
||||
@@ -67,15 +67,18 @@ fn allowed_beyond_1_12() -> HashSet<&'static str> {
|
||||
// Every one of these predates 1189 and is used by our own transcribed FrameXML today.
|
||||
// Resolving each means either replacing it with its 1.12 equivalent (`UnitPower` →
|
||||
// `UnitMana`, which 1.12 has and we do not) or recording why it stays.
|
||||
//
|
||||
// Three left with 1751's gossip work: `GetNumGossipQuests`, `GetGossipQuestInfo` and
|
||||
// `SelectGossipQuest` were benilla's own single-list shape over the gossip packet's quest
|
||||
// rows, and 1.12 has none of them — it publishes `GetGossipAvailableQuests` /
|
||||
// `GetGossipActiveQuests` and a select for each, which is what this client answers now.
|
||||
// The window that needed them is ours until it migrates; it calls the reference's four.
|
||||
"CancelUnitBuff",
|
||||
"GetCursorInfo",
|
||||
"GetGossipQuestInfo",
|
||||
"GetInventoryItemID",
|
||||
"GetNumGossipQuests",
|
||||
"GetPlayerFacing",
|
||||
"GetTradePartnerName",
|
||||
"IsGossipOptionCoded",
|
||||
"SelectGossipQuest",
|
||||
"SubmitChatInput",
|
||||
"UnitAura",
|
||||
"UnitIsAFK",
|
||||
|
||||
@@ -55,6 +55,15 @@ pub struct TradeSlotItem {
|
||||
/// The enchant-slot spell name (slot 7 only) — `None` in P1 (the applied-spell path is
|
||||
/// decision 0592 P3).
|
||||
pub enchantment: Option<String>,
|
||||
/// The slot's full escaped `|cff…|Hitem:…|h[Name]|h|r` link — `GetTradePlayerItemLink` /
|
||||
/// `GetTradeTargetItemLink`'s answer, and `None` while the ask-once template answer is in
|
||||
/// flight (the link embeds the name and the quality colour).
|
||||
///
|
||||
/// The same shape as [`super::merchant::MerchantItem::link`] and for the same reason: 1.12's
|
||||
/// own `TradeFrame.lua` hands it straight to `DressUpItemLink` on a ctrl-click and to
|
||||
/// `ChatFrameEditBox:Insert` on a shift-click, so it is a string the app already knows rather
|
||||
/// than anything the engine composes.
|
||||
pub link: Option<String>,
|
||||
}
|
||||
|
||||
/// One side of the trade window — the seven slots (1-based in the API) plus the gold offered on
|
||||
@@ -229,6 +238,37 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
})?,
|
||||
)?;
|
||||
|
||||
// GetTradePlayerItemLink(id) / GetTradeTargetItemLink(id) → the slot's escaped item link, or
|
||||
// nil for an empty slot / one whose template answer is still in flight.
|
||||
//
|
||||
// 1.12's `TradeFrame.lua` reaches for these on a modified click of a trade slot — ctrl hands
|
||||
// the link to `DressUpItemLink`, shift inserts it into the chat box — the same pair of arms
|
||||
// the merchant rows have, which is why this mirrors `GetMerchantItemLink` exactly rather than
|
||||
// composing anything here.
|
||||
for (name, side) in [
|
||||
("GetTradePlayerItemLink", true),
|
||||
("GetTradeTargetItemLink", false),
|
||||
] {
|
||||
g.set(
|
||||
name,
|
||||
lua.create_function(move |lua, id: usize| {
|
||||
let link = {
|
||||
let model = lua.app_data_ref::<Model>().expect("model app_data");
|
||||
let slot = if side {
|
||||
slot_at(&model, id, |t| &t.player)
|
||||
} else {
|
||||
slot_at(&model, id, |t| &t.target)
|
||||
};
|
||||
slot.and_then(|s| s.link.clone())
|
||||
};
|
||||
match link {
|
||||
Some(link) => Ok(Value::String(lua.create_string(&link)?)),
|
||||
None => Ok(Value::Nil),
|
||||
}
|
||||
})?,
|
||||
)?;
|
||||
}
|
||||
|
||||
// GetTradeTargetItemInfo(id) → name, texture, numItems, quality, isUsable, enchantment (the
|
||||
// partner's offer; TradeFrame.lua l.84 — the extra `quality` is the recipient-only colour).
|
||||
g.set(
|
||||
@@ -389,6 +429,9 @@ mod tests {
|
||||
count,
|
||||
quality: Some(quality),
|
||||
enchantment: None,
|
||||
link: Some(format!(
|
||||
"|cffffffff|Hitem:{item_id}:0:0:0|h[Linen Cloth]|h|r"
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -411,6 +454,58 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
/// **The two link verbs**, and the two absences they share with every other item-link reader
|
||||
/// here: an empty slot, and a slot whose ask-once template answer has not landed.
|
||||
///
|
||||
/// 1.12's `TradeFrame.lua` reaches for these on a modified click of a trade slot — ctrl hands
|
||||
/// the link to `DressUpItemLink`, shift inserts it into the chat box. They were two of the four
|
||||
/// engine verbs the readiness probe reports against stock `TradeFrame.xml`.
|
||||
#[test]
|
||||
fn the_trade_slots_answer_their_item_links() {
|
||||
let mut s = UiScript::new().unwrap();
|
||||
// No trade open: both sides answer nil rather than raising.
|
||||
assert!(s
|
||||
.eval::<bool>("return GetTradePlayerItemLink(1) == nil")
|
||||
.unwrap());
|
||||
assert!(s
|
||||
.eval::<bool>("return GetTradeTargetItemLink(1) == nil")
|
||||
.unwrap());
|
||||
|
||||
let mut st = state();
|
||||
// A slot whose template is still in flight carries no link — the string embeds the name.
|
||||
st.target.slots[1] = Some(TradeSlotItem {
|
||||
item_id: 4306,
|
||||
name: None,
|
||||
texture: None,
|
||||
count: 1,
|
||||
quality: None,
|
||||
enchantment: None,
|
||||
link: None,
|
||||
});
|
||||
s.set_trade(Some(st));
|
||||
|
||||
assert_eq!(
|
||||
s.eval::<String>("return GetTradePlayerItemLink(1)")
|
||||
.unwrap(),
|
||||
"|cffffffff|Hitem:2589:0:0:0|h[Linen Cloth]|h|r"
|
||||
);
|
||||
assert_eq!(
|
||||
s.eval::<String>("return GetTradeTargetItemLink(1)")
|
||||
.unwrap(),
|
||||
"|cffffffff|Hitem:4306:0:0:0|h[Linen Cloth]|h|r"
|
||||
);
|
||||
// …the in-flight slot, and an empty one, and one past the seven.
|
||||
assert!(s
|
||||
.eval::<bool>("return GetTradeTargetItemLink(2) == nil")
|
||||
.unwrap());
|
||||
assert!(s
|
||||
.eval::<bool>("return GetTradePlayerItemLink(7) == nil")
|
||||
.unwrap());
|
||||
assert!(s
|
||||
.eval::<bool>("return GetTradePlayerItemLink(9) == nil")
|
||||
.unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn player_and_target_item_info_read_the_reference_tuples() {
|
||||
let mut s = UiScript::new().unwrap();
|
||||
|
||||
Reference in New Issue
Block a user