the inspect and talent windows are the reference's own
- the inspect and talent windows are the reference's own: `Blizzard_InspectUI` and `Blizzard_TalentUI` are LoadOnDemand addons whose real `.xml`/`.lua` live inside `patch.MPQ` — the loose `Interface\AddOns` folder holds only a `.pub` signature, which three notes in this tree had read as "packed, no source to migrate from", one wrong inference that kept five windows on hand-written transcriptions; 2300 lines of ours out, no engine work needed, and three things our talent transcription had quietly invented fell out (there was no level-10 gate, every tree scrolled where a shallow one does not, and a nil `TalentMicroButton` threw out of OnShow before the window painted) - a nil unit token RAISES in 53 of 83 bindings: wow-re censused the registrar table at `0x850438` — `UnitName(nil)` raises and `UnitIsVisible(nil)` is quiet, one line apart in stock files, so there is no rule to derive, the table is the rule; ours answered a default and our own test had pinned it; both token positions of the seven two-token predicates are gated, `GetRaidTargetIndex` too, and `UnitIsAFK`/`UnitIsDND` are confirmed not to exist in 5875 at all (zero occurrences under a positive control) — we invented them, and they stay deliberately ungated - `EditBox:SetNumber` did not exist (the only chain file calling it is `MoneyInputFrame.lua`, on the chain since the money kit): it is byte-identical to `SetText`, formats through `%.14g` in MSVC's CRT (`1e+020`, `1.#INF`), and `OnTextChanged` never fires from an edit — it fires from the dirty-word drain inside the box's own OnUpdate, coalesced to one per box per frame, where ours fired synchronously and `MoneyInputFrame_SetCopper` raised on it; `PanelTemplates_TabResize`'s `tab` argument is optional as the reference's is (stock macro tabs call it with `0`) - the skull or star on your CURRENT target never appeared or cleared until you re-targeted — `RAID_TARGET_UPDATE` was never fired, and stock `TargetFrame.lua` hangs its repaint off it; a raid-to-party conversion now empties the mark board as `0x4ba550` does; and the group-loot roll popups were already on the chain inside stock `LootFrame.xml` — our 546-line `GroupLootFrame.xml` had been shadowing all four of them Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
@@ -1,546 +0,0 @@
|
||||
<Ui>
|
||||
<!--
|
||||
benilla's group-loot roll window (decision 0591) — the Need/Greed/Pass dialog: four stacked
|
||||
`GroupLootFrame`s, re-expressed from the real 1.12 client (`LootFrame.xml` l.243-533,
|
||||
`LootFrame.lua` l.246-296, `UIParent.lua` l.513-515/1577 — all extracted, scratchpad
|
||||
`WoW/_extracted_framexml/`). Geometry, art paths, and behavior below are QUOTED from those
|
||||
(line citations inline); the Lua is re-expressed logic turned onto our own `loot_roll` engine
|
||||
seam (`benilla-ui/src/script/loot_roll.rs`, decision 0591) and its app feed
|
||||
(`crates/benilla/src/ui_loot_roll.rs`).
|
||||
|
||||
FOUR STACKED POPUPS, NOT A UIPANEL. Unlike LootFrame.xml, this window is a real 1.12
|
||||
`frameStrata="DIALOG"`/`toplevel="true"` popup family — never registered in UiPanels.xml, no
|
||||
pushable slot. `GroupLootFrame1` sits at the ref's own hardcoded anchor (BOTTOM,
|
||||
+0,60 — ref l.498-506); frames 2-4 each stack on the PREVIOUS frame's TOP +15 (ref l.507-533).
|
||||
`GroupLootFrame_OpenNewFrame` (ref LootFrame.lua l.246-258) claims the first currently-hidden
|
||||
of the four on every `START_LOOT_ROLL`, so up to 4 concurrent rolls each get their own frame,
|
||||
stacking upward; a 5th simultaneous roll finds none free and is silently dropped — the
|
||||
reference's own ceiling (`NUM_GROUP_LOOT_FRAMES = 4`, LootFrame.lua l.2), not a benilla gap.
|
||||
|
||||
THE FOUR INSTANCES KEEP THE REFERENCE'S BARE NAMES (`GroupLootFrame1`..`4`), deliberately —
|
||||
the one place the house `Benilla`-prefix convention is NOT applied, for the same reason
|
||||
`CastingBarFrame` keeps its bare name (CastingBar.xml's POSITION note). `UIParent.xml` (l.70)
|
||||
already carries the ref's `UIPARENT_MANAGED_FRAME_POSITIONS["GroupLootFrame1"]` row (baseY 60,
|
||||
+42 for a shown bottom multibar, +42 for the pet/stance bar, +9 for the rep watch bar), and
|
||||
`UIParent_ManageFramePositions` finds its frames by a literal `getglobal(name)` — no prefix
|
||||
awareness. UIParent.xml's own comment lists `GroupLootFrame1` among the rows that "no-op
|
||||
through the ref's own `if ( frame )` guard and self-activate if the frames later land": this
|
||||
file is that landing, so the bare name is what makes the row engage.
|
||||
|
||||
It matters concretely, not theoretically: with benilla's always-on bottom multibars the
|
||||
managed pass puts frame 1 at y=102 (and y=144 while the stance bar shows). Under a prefixed
|
||||
name the row would never fire and the roll dialogs would sit permanently at y=60 — squarely on
|
||||
top of the action bars. Bare names are also what a real 1.12 addon expects to find (0068's
|
||||
Era-addon target), so this costs nothing elsewhere. The template, the driver, and this file's
|
||||
own Lua functions stay `Benilla`-prefixed: nothing in the reference keys on those.
|
||||
|
||||
NO CENTRAL UIParent EVENT ROUTER. The reference's `START_LOOT_ROLL` dispatch (`UIParent.lua`
|
||||
l.513-515: `GroupLootFrame_OpenNewFrame(arg1, arg2)`) rides the monolithic `UIParent_OnEvent`
|
||||
this engine doesn't ship (`UIParent.xml`'s own note: "not ours to ship - no consumer yet").
|
||||
Rather than claim UIParent's single OnEvent slot (which a later, more general router would then
|
||||
have to fight over) or special-case ONE of the four peer frames, a small dedicated hidden
|
||||
driver frame (`BenillaGroupLootFrameDriver`, below — the same shape as `DeathFrame.xml`'s
|
||||
`BenillaDeathDriver`) owns exactly this one registration, keeping all four
|
||||
`BenillaGroupLootFrame`s byte-identical instances of the same template (each registers only
|
||||
`CANCEL_LOOT_ROLL` on itself, ref l.487-489/493-495, verbatim).
|
||||
|
||||
THE DRESSING-ROOM / CHAT-LINK CLICK LANDED (decisions 1059/1060 — this note used to read "no
|
||||
dressing room exists", which was true until the room did). The ref icon button's Ctrl-click
|
||||
`DressUpItemLink`/Shift-click `ChatFrameEditBox:Insert` (ref l.353-361) are re-expressed in
|
||||
`BenillaGroupLootFrame_Icon_OnClick` below, over the `GetLootRollItemLink(rollID)` binding the
|
||||
same arc added to `loot_roll.rs`. Still dropped: the ref's `this.hasItem = 1` OnLoad flag
|
||||
(ref l.338-340) and its `CursorUpdate()`/`ResetCursor()`/`CursorOnUpdate()` hover-cursor calls
|
||||
(ref l.344,348,351) — the flag exists only to feed those, and they are the Ctrl-hover
|
||||
MAGNIFIER, a separate affordance (`ShowInspectCursor`, which the bags and the vendor rows do
|
||||
wire) that this button has never had. LootFrame.xml's own row template
|
||||
(`BenillaLootButtonTemplate`) carries the same hover-cursor omission.
|
||||
|
||||
THE ITEM TOOLTIP uses `GameTooltip:SetLootRollItem(rollID)` — the reference's own binding
|
||||
(decision 1547; it replaced a `SetItemById` that could not see the drop's random-suffix
|
||||
roll and so printed `<Random enchantment>` on every rolled item). The `itemId` guard stays:
|
||||
it is the benilla-extension 6th return of `GetLootRollItemInfo` (see `loot_roll.rs`),
|
||||
always populated once a roll is known — unlike name/texture/quality, which may be `nil`
|
||||
while the item template is still in flight. `GetLootRollItemLink` (decision 1059) is the
|
||||
click's, not the hover's: the tooltip wants the roll, not the markup.
|
||||
|
||||
`toplevel="true"` (ref l.243) IS CARRIED, since decision 1739. Two earlier reasons for
|
||||
omitting it have both expired: `SetToplevel` landed (the loader calls the real method, and
|
||||
the raise law — occlusion-gated, confined to this frame's own stratum — is
|
||||
`script::object::toplevel`, wow-re `ui/scratch/toplevel-raise.md`), and the one blocker
|
||||
left after that was a test fixture, not a mechanism.
|
||||
|
||||
What it does, measured rather than guessed: `GroupLootFrame1`'s rect (390.5,60)-(633.5,144)
|
||||
overlaps `ExhaustionTick` — `ActionBar.xml` l.1111, `frameStrata="DIALOG"` transcribed
|
||||
verbatim from ref-MainMenuBar.xml l.415-442 — in a ~2.5 px band, so showing a roll frame
|
||||
raises its whole subtree within DIALOG. The only pixels that move are the sliver where the
|
||||
roll frame meets the tick, and it is exactly what the reference does. It did cost
|
||||
`group_loot_tests::start_loot_roll_claims_frames_in_order_and_paints_the_roll` its
|
||||
`quad_center("UI-GroupLoot-Dice-Up")` stand-in for "frame 1's Need button" — an unstated
|
||||
assumption that declaration order is draw order among the four, which the raise
|
||||
legitimately ends; that test now takes the button by NAME and asserts the hit before
|
||||
clicking.
|
||||
|
||||
NAMED FONT OBJECTS ARE SUPPORTED on this branch (the loader's top-level `<Font>` handling,
|
||||
`loader/mod.rs`'s `do_font`, plus the full `Fonts.xml` transcription, both landed since
|
||||
LootFrame.xml's own "not implemented yet" caveat was written) — `$parentName`'s
|
||||
`inherits="GameFontNormalSmall"` below resolves for real; no stale caveat needed here.
|
||||
|
||||
IN-FLIGHT ITEMS: `GetLootRollItemInfo`'s `name` and `texture` are `nil` while the item
|
||||
template query hasn't landed yet (`loot_roll.rs`); its `count` and `quality` are the
|
||||
reference's own miss tail, `1` and `1` (decision 1805 — 0x4c31a3), never nil. So the
|
||||
`or ITEM_QUALITY_COLORS[1]` below is dead weight rather than load-bearing, and the stock
|
||||
`GroupLootFrame_OnShow` this file shadows survives the same state without it. The
|
||||
`BENILLA_FALLBACK_ICON` / empty-name fallbacks are still doing work: the reference leaves
|
||||
both of those nil.
|
||||
|
||||
The Era API this drives (`benilla-ui/src/script/loot_roll.rs`, decision 0591):
|
||||
`GetLootRollItemInfo(rollID)` -> texture,name,count,quality,bindOnPickUp,itemID;
|
||||
`GetLootRollItemLink(rollID)` -> the item's link (the icon click's two arms, decision 1059);
|
||||
`GetLootRollTimeLeft(rollID)` -> ms remaining; `RollOnLoot(rollID, rollType)` (0 Pass / 1 Need /
|
||||
2 Greed). Events: `START_LOOT_ROLL(rollID, rollTime)` claims a frame; `CANCEL_LOOT_ROLL(rollID)`
|
||||
hides the one that owns it (`crates/benilla/src/ui_loot_roll.rs` fires both).
|
||||
-->
|
||||
<Script><![CDATA[
|
||||
-- LootFrame.lua l.2's NUM_GROUP_LOOT_FRAMES, under the REFERENCE's own name.
|
||||
--
|
||||
-- It carried the house `BENILLA_` prefix until an addon died on the bare one:
|
||||
-- `Bongos_RollBar/bar.lua:85` is `for i=1, NUM_GROUP_LOOT_FRAMES do` and got a nil limit.
|
||||
-- That is 1222's shadowed-global class again — the fifth instance — and the rule it
|
||||
-- settled applies to a CONSTANT an addon reads exactly as it does to a frame: our name for
|
||||
-- a global is as load-bearing as our name for a function. The prefix is right only where
|
||||
-- the reference has no counterpart; here it has one, with the same value (4) counting the
|
||||
-- same four frames, which already carry the reference's bare names two paragraphs down.
|
||||
NUM_GROUP_LOOT_FRAMES = 4
|
||||
|
||||
-- The bind-on-pickup confirm (ref StaticPopup.lua l.1371-1382, verbatim bar the TEXT()
|
||||
-- indirection this house dialect drops). Need/Greed on a BoP roll never reaches the wire
|
||||
-- directly: the seam diverts it to CONFIRM_LOOT_ROLL, the driver below raises this popup,
|
||||
-- and only OnAccept's ConfirmLootRoll re-enters past the gate (decision 0594, VERIFIED in
|
||||
-- the 5875 binary — the gate lives in the C RollOnLoot, not in the Lua). Keyed BARE, like
|
||||
-- every other StaticPopupDialogs entry: the key is the StaticPopup_Show argument, not a
|
||||
-- frame name. `id`/`rollType` arrive as the dialog's data/data2.
|
||||
-- The three strings quoted verbatim from the real 1.12 GlobalStrings.lua (patch-2.MPQ,
|
||||
-- Interface\FrameXML\GlobalStrings.lua l.2612/2818/465), following UiPanels.xml's
|
||||
-- DELETE_ITEM idiom rather than reading the globals: GlobalStrings is loaded from the
|
||||
-- PATCH CHAIN at runtime, so a bare global is nil under test and any format() over it
|
||||
-- errors — and an error here aborts the whole chunk, taking every function in this file
|
||||
-- with it.
|
||||
local LOOT_NO_DROP_TEXT = "Looting this item will bind it to you." -- LOOT_NO_DROP
|
||||
local OKAY_TEXT = "Okay" -- OKAY
|
||||
local CANCEL_TEXT = "Cancel" -- CANCEL
|
||||
|
||||
StaticPopupDialogs["CONFIRM_LOOT_ROLL"] = {
|
||||
text = LOOT_NO_DROP_TEXT,
|
||||
button1 = OKAY_TEXT,
|
||||
button2 = CANCEL_TEXT,
|
||||
OnAccept = function(id, rollType)
|
||||
ConfirmLootRoll(id, rollType)
|
||||
end,
|
||||
timeout = 0,
|
||||
whileDead = 1,
|
||||
exclusive = 1,
|
||||
hideOnEscape = 1
|
||||
}
|
||||
|
||||
-- GroupLootFrame_OpenNewFrame (ref LootFrame.lua l.246-258): claim the first currently-hidden
|
||||
-- of the four frames for a freshly opened roll, seed its rollID/rollTime, and prime the
|
||||
-- Timer StatusBar's max before showing it (fields set BEFORE Show(), so OnShow below can
|
||||
-- already read frame.rollID).
|
||||
function GroupLootFrame_OpenNewFrame(rollId, rollTime)
|
||||
for i = 1, NUM_GROUP_LOOT_FRAMES do
|
||||
local frame = getglobal("GroupLootFrame" .. i)
|
||||
if frame and not frame:IsVisible() then
|
||||
frame.rollID = rollId
|
||||
frame.rollTime = rollTime
|
||||
getglobal(frame:GetName() .. "Timer"):SetMinMaxValues(0, rollTime)
|
||||
frame:Show()
|
||||
return
|
||||
end
|
||||
end
|
||||
-- All 4 already claimed: the reference has no overflow handling either (a 5th
|
||||
-- simultaneous roll simply finds no free frame) — its own ceiling, not a benilla gap.
|
||||
end
|
||||
|
||||
-- GroupLootFrame_OnShow's body (ref l.260-277): resolve the roll's display fields and paint
|
||||
-- the icon/name/backdrop. name/texture/quality may be nil while the item template query is
|
||||
-- still in flight (loot_roll.rs) — fall back to the fallback icon / blank name / common (1)
|
||||
-- quality rather than erroring, the same fallback shape LootFrame_Update uses for an
|
||||
-- in-flight loot row (LootFrame.xml).
|
||||
--
|
||||
-- SPLIT OUT OF OnShow, unlike the reference, because it must be re-runnable: the ref's
|
||||
-- `GetLootRollItemInfo` reads live C state that is already populated when START_LOOT_ROLL
|
||||
-- fires, whereas ours reads a *pushed snapshot* that by construction does not yet contain a
|
||||
-- roll opening this frame (loot_roll.rs `feed_loot_rolls`, and the late item template on top
|
||||
-- of that). `UPDATE_LOOT_ROLL` re-enters here; the paint is idempotent.
|
||||
function BenillaGroupLootFrame_Paint(frame)
|
||||
local texture, name, count, quality, bindOnPickUp, itemId = GetLootRollItemInfo(frame.rollID)
|
||||
frame.itemId = itemId
|
||||
|
||||
if bindOnPickUp then
|
||||
frame:SetBackdrop({
|
||||
bgFile = "Interface\\DialogFrame\\UI-DialogBox-Gold-Background",
|
||||
edgeFile = "Interface\\DialogFrame\\UI-DialogBox-Gold-Border",
|
||||
tile = true, tileSize = 32, edgeSize = 32,
|
||||
insets = { left = 11, right = 12, top = 12, bottom = 11 },
|
||||
})
|
||||
getglobal(frame:GetName() .. "Corner"):SetTexture("Interface\\DialogFrame\\UI-DialogBox-Gold-Corner")
|
||||
getglobal(frame:GetName() .. "Decoration"):Show()
|
||||
else
|
||||
frame:SetBackdrop({
|
||||
bgFile = "Interface\\DialogFrame\\UI-DialogBox-Background",
|
||||
edgeFile = "Interface\\DialogFrame\\UI-DialogBox-Border",
|
||||
tile = true, tileSize = 32, edgeSize = 32,
|
||||
insets = { left = 11, right = 12, top = 12, bottom = 11 },
|
||||
})
|
||||
getglobal(frame:GetName() .. "Corner"):SetTexture("Interface\\DialogFrame\\UI-DialogBox-Corner")
|
||||
getglobal(frame:GetName() .. "Decoration"):Hide()
|
||||
end
|
||||
|
||||
getglobal(frame:GetName() .. "IconFrameIcon"):SetTexture(texture or BENILLA_FALLBACK_ICON)
|
||||
local nameText = getglobal(frame:GetName() .. "Name")
|
||||
nameText:SetText(name or "")
|
||||
local color = ITEM_QUALITY_COLORS[quality] or ITEM_QUALITY_COLORS[1]
|
||||
nameText:SetVertexColor(color.r, color.g, color.b)
|
||||
end
|
||||
|
||||
function GroupLootFrame_OnShow()
|
||||
local frame = this
|
||||
BenillaGroupLootFrame_Paint(frame)
|
||||
end
|
||||
|
||||
-- Per-instance CANCEL_LOOT_ROLL (ref l.487-489 OnLoad, l.279-285/493-495 OnEvent): each of the
|
||||
-- four frames registers and checks its OWN rollID, hiding only itself. UPDATE_LOOT_ROLL is
|
||||
-- the benilla extension alongside it (see BenillaGroupLootFrame_Paint) and takes the same
|
||||
-- per-instance shape: arg1 is the rollID whose display identity changed.
|
||||
function BenillaGroupLootFrame_OnLoad(frame)
|
||||
frame:RegisterEvent("CANCEL_LOOT_ROLL")
|
||||
frame:RegisterEvent("UPDATE_LOOT_ROLL")
|
||||
end
|
||||
|
||||
function GroupLootFrame_OnEvent()
|
||||
local frame = this
|
||||
if arg1 ~= frame.rollID then
|
||||
return
|
||||
end
|
||||
if event == "CANCEL_LOOT_ROLL" then
|
||||
frame:Hide()
|
||||
elseif event == "UPDATE_LOOT_ROLL" then
|
||||
BenillaGroupLootFrame_Paint(frame)
|
||||
end
|
||||
end
|
||||
|
||||
-- GroupLootFrame_OnUpdate (ref l.287-296): poll the remaining time each tick, clamped to the
|
||||
-- bar's own [min,max] exactly as the reference does (an out-of-range read — e.g. an unknown
|
||||
-- rollID's 0 — clamps to min rather than erroring). Bound to the Timer bar itself so
|
||||
-- `bar:IsVisible()` follows its (hidden) parent frame, matching the ref's `this:IsVisible()`.
|
||||
function BenillaGroupLootFrame_Timer_OnUpdate(bar)
|
||||
if bar:IsVisible() then
|
||||
local left = GetLootRollTimeLeft(bar:GetParent().rollID)
|
||||
local min, max = bar:GetMinMaxValues()
|
||||
if left < min or left > max then
|
||||
left = min
|
||||
end
|
||||
bar:SetValue(left)
|
||||
end
|
||||
end
|
||||
|
||||
-- Pass/Need/Greed (ref $parentPassButton/$parentRollButton/$parentGreedButton OnClick,
|
||||
-- l.374/398/425): one shared handler, rollType 0 Pass / 1 Need / 2 Greed.
|
||||
function BenillaGroupLootFrame_Roll_OnClick(button, rollType)
|
||||
RollOnLoot(button:GetParent().rollID, rollType)
|
||||
end
|
||||
|
||||
function BenillaGroupLootFrame_Roll_OnEnter(button, text)
|
||||
GameTooltip:SetOwner(button, "ANCHOR_RIGHT")
|
||||
GameTooltip:SetText(text)
|
||||
end
|
||||
|
||||
function BenillaGroupLootFrame_Roll_OnLeave()
|
||||
GameTooltip:Hide()
|
||||
end
|
||||
|
||||
-- The item icon's hover tooltip (ref $parentIconFrame OnEnter/OnLeave, l.341-349, minus the
|
||||
-- CursorUpdate/ResetCursor calls this engine has no counterpart for — see the header).
|
||||
-- SetLootRollItem, the reference's own binding: the ROLL is the source, so the hover sees
|
||||
-- the drop's random-suffix roll (through SetItemById it showed `<Random enchantment>` —
|
||||
-- decision 1547).
|
||||
function BenillaGroupLootFrame_Icon_OnEnter(button)
|
||||
local frame = button:GetParent()
|
||||
GameTooltip:SetOwner(button, "ANCHOR_RIGHT")
|
||||
local _, name, _, quality, _, itemId = GetLootRollItemInfo(frame.rollID)
|
||||
if itemId then
|
||||
GameTooltip:SetLootRollItem(frame.rollID)
|
||||
end
|
||||
end
|
||||
|
||||
function BenillaGroupLootFrame_Icon_OnLeave()
|
||||
GameTooltip:Hide()
|
||||
end
|
||||
|
||||
-- The item icon's click (ref $parentIconFrame OnClick, LootFrame.xml l.353-361, re-expressed
|
||||
-- whole): CTRL previews the rolled item in the dressing room (decision 1060), SHIFT posts
|
||||
-- its link into an open chat edit box (decision 1059) — "link me that" being most of what a
|
||||
-- roll popup gets asked. Both read `GetLootRollItemLink(rollID)`, the binding this arc added
|
||||
-- (loot_roll.rs); it answers nil while the item template is still in flight, and both arms
|
||||
-- drop that nil — the ctrl one inside DressUpItemLink (DressUpFrame.lua l.10-16), the shift
|
||||
-- one inside BenillaChatEdit_InsertLink.
|
||||
--
|
||||
-- Nothing else happens on this button, exactly as in the reference: the vote lives on the
|
||||
-- Pass/Need/Greed buttons and the icon is a preview/link affordance only. The ref's
|
||||
-- `this.hasItem = 1` OnLoad flag (l.338-340) stays dropped — it exists solely for the
|
||||
-- CursorUpdate/CursorOnUpdate ctrl-hover magnifier (l.344/351), which this button still does
|
||||
-- not wire (see the header). No RegisterForClicks either, also as in the reference: a Button
|
||||
-- is registered for LeftButtonUp by default (widget/kinds/mod.rs), which is the whole set
|
||||
-- this handler wants.
|
||||
function BenillaGroupLootFrame_Icon_OnClick(button)
|
||||
local rollId = button:GetParent().rollID
|
||||
if IsControlKeyDown() then
|
||||
DressUpItemLink(GetLootRollItemLink(rollId)) -- ref l.354-355
|
||||
elseif IsShiftKeyDown() then
|
||||
-- ref l.356-359, through the shared helper (UIParent.xml): it carries the ref's own
|
||||
-- edit-box-visible guard AND our nil-link one, which this popup needs routinely —
|
||||
-- the frame is up the instant START_LOOT_ROLL fires, well before the item template
|
||||
-- answers, and until it does there is no link to post.
|
||||
BenillaChatEdit_InsertLink(GetLootRollItemLink(rollId))
|
||||
end
|
||||
end
|
||||
]]></Script>
|
||||
|
||||
<!--
|
||||
The template (ref GroupLootFrameTemplate, l.243-497): 243x84, DIALOG strata, toplevel, hidden
|
||||
by default. Geometry/art below is QUOTED verbatim, INCLUDING dropping the stray `d` character
|
||||
inside ref l.292's `<Anchor point="TOPLEFT">d` (a typo in Blizzard's own shipped file — not
|
||||
transcribed).
|
||||
-->
|
||||
<!--
|
||||
THE REFERENCE'S OWN NAME, deliberately un-prefixed (decision 1254).
|
||||
|
||||
This was `BenillaGroupLootFrameTemplate`. The `Benilla` prefix marks things WE invented, so
|
||||
that nothing we made up can collide with a name an addon expects; this template is the
|
||||
opposite of that — a line-by-line transcription of 1.12's `GroupLootFrameTemplate`, and
|
||||
addons inherit it BY THAT NAME. Bongos_RollBar builds its four roll frames with
|
||||
`CreateFrame("Frame", "BRollBarFrame"..i, bar, "GroupLootFrameTemplate")`, which resolves on
|
||||
the real client and missed here for no reason but our prefix.
|
||||
-->
|
||||
<Frame name="GroupLootFrameTemplate" toplevel="true" frameStrata="DIALOG" hidden="true" virtual="true">
|
||||
<Size><AbsDimension x="243" y="84"/></Size>
|
||||
<Layers>
|
||||
<Layer level="ARTWORK">
|
||||
<!-- The item slot plate (ref l.249-260). -->
|
||||
<Texture name="$parentSlotTexture" file="Interface\Buttons\UI-EmptySlot">
|
||||
<Size><AbsDimension x="64" y="64"/></Size>
|
||||
<Anchors>
|
||||
<Anchor point="TOPLEFT"><Offset><AbsDimension x="3" y="-3"/></Offset></Anchor>
|
||||
</Anchors>
|
||||
</Texture>
|
||||
<!-- The parchment name plate (ref l.261-272). -->
|
||||
<Texture name="$parentNameFrame" file="Interface\MerchantFrame\UI-Merchant-LabelSlots">
|
||||
<Size><AbsDimension x="128" y="64"/></Size>
|
||||
<Anchors>
|
||||
<Anchor point="LEFT" relativeTo="$parentSlotTexture" relativePoint="RIGHT">
|
||||
<Offset><AbsDimension x="-9" y="-10"/></Offset>
|
||||
</Anchor>
|
||||
</Anchors>
|
||||
</Texture>
|
||||
<!-- The item name (ref l.273-284): GameFontNormalSmall, LEFT. -->
|
||||
<FontString name="$parentName" inherits="GameFontNormalSmall" text="Item Name" justifyH="LEFT">
|
||||
<Size><AbsDimension x="90" y="30"/></Size>
|
||||
<Anchors>
|
||||
<Anchor point="LEFT" relativeTo="$parentSlotTexture" relativePoint="RIGHT">
|
||||
<Offset><AbsDimension x="-5" y="5"/></Offset>
|
||||
</Anchor>
|
||||
</Anchors>
|
||||
</FontString>
|
||||
</Layer>
|
||||
<Layer level="OVERLAY">
|
||||
<!-- The gold BoP dragon flourish (ref l.287-298): hidden by default, shown only for a
|
||||
bind-on-pickup roll (GroupLootFrame_OnShow). -->
|
||||
<Texture name="$parentDecoration" file="Interface\DialogFrame\UI-DialogBox-Gold-Dragon" hidden="true">
|
||||
<Size><AbsDimension x="120" y="120"/></Size>
|
||||
<Anchors>
|
||||
<Anchor point="TOPLEFT"><Offset><AbsDimension x="-30" y="15"/></Offset></Anchor>
|
||||
</Anchors>
|
||||
</Texture>
|
||||
<!-- The dialog-box corner (ref l.299-310): swapped plain/gold in OnShow. -->
|
||||
<Texture name="$parentCorner" file="Interface\DialogFrame\UI-DialogBox-Corner">
|
||||
<Size><AbsDimension x="32" y="32"/></Size>
|
||||
<Anchors>
|
||||
<Anchor point="TOPRIGHT"><Offset><AbsDimension x="-6" y="-7"/></Offset></Anchor>
|
||||
</Anchors>
|
||||
</Texture>
|
||||
</Layer>
|
||||
</Layers>
|
||||
<Frames>
|
||||
<!-- The item icon (ref $parentIconFrame, l.314-363): hover tooltip + the ref's own
|
||||
Ctrl/Shift dressing-room/chat-link click (l.353-361, landed — decision 1059/1060);
|
||||
only the hasItem OnLoad flag stays dropped (header). -->
|
||||
<Button name="$parentIconFrame" enableMouse="true">
|
||||
<Size><AbsDimension x="34" y="34"/></Size>
|
||||
<Anchors>
|
||||
<Anchor point="TOPLEFT" relativeTo="$parentSlotTexture">
|
||||
<Offset><AbsDimension x="15" y="-15"/></Offset>
|
||||
</Anchor>
|
||||
</Anchors>
|
||||
<Layers>
|
||||
<Layer level="OVERLAY">
|
||||
<Texture name="$parentIcon">
|
||||
<Size><AbsDimension x="34" y="34"/></Size>
|
||||
<Anchors><Anchor point="TOPLEFT"/></Anchors>
|
||||
</Texture>
|
||||
</Layer>
|
||||
</Layers>
|
||||
<Scripts>
|
||||
<OnEnter>BenillaGroupLootFrame_Icon_OnEnter(self)</OnEnter>
|
||||
<OnLeave>BenillaGroupLootFrame_Icon_OnLeave()</OnLeave>
|
||||
<OnClick>BenillaGroupLootFrame_Icon_OnClick(self)</OnClick>
|
||||
</Scripts>
|
||||
</Button>
|
||||
<!-- Pass (ref $parentPassButton, l.364-384): the ref inherits UIPanelCloseButton, which
|
||||
this engine ships no template for — reconstructed on the same minimize-button art
|
||||
LootFrame's own close button uses (LootFrame.xml's identical "no
|
||||
UIPanelCloseButton template ships" precedent), at the ref's own 32x32/position. -->
|
||||
<Button name="$parentPassButton">
|
||||
<Size><AbsDimension x="32" y="32"/></Size>
|
||||
<Anchors>
|
||||
<Anchor point="TOPRIGHT" relativeTo="$parentCorner" relativePoint="TOPRIGHT">
|
||||
<Offset><AbsDimension x="5" y="5"/></Offset>
|
||||
</Anchor>
|
||||
</Anchors>
|
||||
<NormalTexture file="Interface\Buttons\UI-Panel-MinimizeButton-Up"/>
|
||||
<PushedTexture file="Interface\Buttons\UI-Panel-MinimizeButton-Down"/>
|
||||
<HighlightTexture file="Interface\Buttons\UI-Panel-MinimizeButton-Highlight" alphaMode="ADD"/>
|
||||
<Scripts>
|
||||
<OnClick>BenillaGroupLootFrame_Roll_OnClick(self, 0)</OnClick>
|
||||
<OnEnter>BenillaGroupLootFrame_Roll_OnEnter(self, PASS)</OnEnter>
|
||||
<OnLeave>BenillaGroupLootFrame_Roll_OnLeave()</OnLeave>
|
||||
</Scripts>
|
||||
</Button>
|
||||
<!-- Need (the dice, ref $parentRollButton, l.385-411). -->
|
||||
<Button name="$parentRollButton">
|
||||
<Size><AbsDimension x="32" y="32"/></Size>
|
||||
<Anchors>
|
||||
<Anchor point="TOPRIGHT"><Offset><AbsDimension x="-37" y="-14"/></Offset></Anchor>
|
||||
</Anchors>
|
||||
<NormalTexture file="Interface\Buttons\UI-GroupLoot-Dice-Up"/>
|
||||
<HighlightTexture file="Interface\Buttons\UI-GroupLoot-Dice-Highlight" alphaMode="ADD"/>
|
||||
<PushedTexture file="Interface\Buttons\UI-GroupLoot-Dice-Down"/>
|
||||
<Scripts>
|
||||
<OnClick>BenillaGroupLootFrame_Roll_OnClick(self, 1)</OnClick>
|
||||
<OnEnter>BenillaGroupLootFrame_Roll_OnEnter(self, NEED)</OnEnter>
|
||||
<OnLeave>BenillaGroupLootFrame_Roll_OnLeave()</OnLeave>
|
||||
</Scripts>
|
||||
</Button>
|
||||
<!-- Greed (the coin, ref $parentGreedButton, l.412-438). -->
|
||||
<Button name="$parentGreedButton">
|
||||
<Size><AbsDimension x="32" y="32"/></Size>
|
||||
<Anchors>
|
||||
<Anchor point="TOP" relativeTo="$parentRollButton" relativePoint="BOTTOM">
|
||||
<Offset><AbsDimension x="-2" y="2"/></Offset>
|
||||
</Anchor>
|
||||
</Anchors>
|
||||
<NormalTexture file="Interface\Buttons\UI-GroupLoot-Coin-Up"/>
|
||||
<HighlightTexture file="Interface\Buttons\UI-GroupLoot-Coin-Highlight" alphaMode="ADD"/>
|
||||
<PushedTexture file="Interface\Buttons\UI-GroupLoot-Coin-Down"/>
|
||||
<Scripts>
|
||||
<OnClick>BenillaGroupLootFrame_Roll_OnClick(self, 2)</OnClick>
|
||||
<OnEnter>BenillaGroupLootFrame_Roll_OnEnter(self, GREED)</OnEnter>
|
||||
<OnLeave>BenillaGroupLootFrame_Roll_OnLeave()</OnLeave>
|
||||
</Scripts>
|
||||
</Button>
|
||||
<!-- The 1-minute countdown bar (ref $parentTimer, l.439-473): 152x10, [0,60000] ms,
|
||||
yellow UI-Character-Skills-Bar fill + its border overlay. -->
|
||||
<StatusBar name="$parentTimer" drawLayer="BACKGROUND" minValue="0" maxValue="60000" defaultValue="0">
|
||||
<Size><AbsDimension x="152" y="10"/></Size>
|
||||
<Anchors>
|
||||
<Anchor point="TOPLEFT" relativeTo="$parentSlotTexture" relativePoint="BOTTOMLEFT">
|
||||
<Offset><AbsDimension x="13" y="10"/></Offset>
|
||||
</Anchor>
|
||||
</Anchors>
|
||||
<Layers>
|
||||
<Layer level="OVERLAY">
|
||||
<Texture file="Interface\PaperDollInfoFrame\UI-Character-Skills-BarBorder">
|
||||
<Size><AbsDimension x="156" y="20"/></Size>
|
||||
<Anchors>
|
||||
<Anchor point="TOP"><Offset><AbsDimension x="0" y="5"/></Offset></Anchor>
|
||||
</Anchors>
|
||||
</Texture>
|
||||
</Layer>
|
||||
</Layers>
|
||||
<Scripts>
|
||||
<OnUpdate>BenillaGroupLootFrame_Timer_OnUpdate(self)</OnUpdate>
|
||||
</Scripts>
|
||||
<BarTexture name="$parentBar" file="Interface\PaperDollInfoFrame\UI-Character-Skills-Bar"/>
|
||||
<BarColor r="1.0" g="1.0" b="0"/>
|
||||
</StatusBar>
|
||||
</Frames>
|
||||
<!-- The plain dialog plate (ref l.475-485); swapped to the gold variant in OnShow for a BoP
|
||||
roll. -->
|
||||
<Backdrop bgFile="Interface\DialogFrame\UI-DialogBox-Background" edgeFile="Interface\DialogFrame\UI-DialogBox-Border" tile="true">
|
||||
<BackgroundInsets><AbsInset left="11" right="12" top="12" bottom="11"/></BackgroundInsets>
|
||||
<TileSize><AbsValue val="32"/></TileSize>
|
||||
<EdgeSize><AbsValue val="32"/></EdgeSize>
|
||||
</Backdrop>
|
||||
<Scripts>
|
||||
<OnLoad>BenillaGroupLootFrame_OnLoad(self)</OnLoad>
|
||||
<OnShow>GroupLootFrame_OnShow()</OnShow>
|
||||
<OnEvent>GroupLootFrame_OnEvent()</OnEvent>
|
||||
</Scripts>
|
||||
</Frame>
|
||||
|
||||
<!-- The four instances (ref l.498-533): frame 1 at the ref's own hardcoded BOTTOM+60; each next
|
||||
frame stacks on the previous one's TOP+15. -->
|
||||
<Frame name="GroupLootFrame1" inherits="GroupLootFrameTemplate" id="1">
|
||||
<Anchors>
|
||||
<Anchor point="BOTTOM"><Offset><AbsDimension x="0" y="60"/></Offset></Anchor>
|
||||
</Anchors>
|
||||
</Frame>
|
||||
<Frame name="GroupLootFrame2" inherits="GroupLootFrameTemplate" id="2">
|
||||
<Anchors>
|
||||
<Anchor point="BOTTOM" relativeTo="GroupLootFrame1" relativePoint="TOP">
|
||||
<Offset><AbsDimension x="0" y="15"/></Offset>
|
||||
</Anchor>
|
||||
</Anchors>
|
||||
</Frame>
|
||||
<Frame name="GroupLootFrame3" inherits="GroupLootFrameTemplate" id="3">
|
||||
<Anchors>
|
||||
<Anchor point="BOTTOM" relativeTo="GroupLootFrame2" relativePoint="TOP">
|
||||
<Offset><AbsDimension x="0" y="15"/></Offset>
|
||||
</Anchor>
|
||||
</Anchors>
|
||||
</Frame>
|
||||
<Frame name="GroupLootFrame4" inherits="GroupLootFrameTemplate" id="4">
|
||||
<Anchors>
|
||||
<Anchor point="BOTTOM" relativeTo="GroupLootFrame3" relativePoint="TOP">
|
||||
<Offset><AbsDimension x="0" y="15"/></Offset>
|
||||
</Anchor>
|
||||
</Anchors>
|
||||
</Frame>
|
||||
|
||||
<!-- The START_LOOT_ROLL driver (header, "NO CENTRAL UIParent EVENT ROUTER"): the same shape as
|
||||
DeathFrame.xml's BenillaDeathDriver — a tiny hidden frame whose sole job is owning the
|
||||
registrations none of the four peer GroupLootFrames should have to special-case. -->
|
||||
<Frame name="BenillaGroupLootFrameDriver" hidden="true">
|
||||
<Size><AbsDimension x="1" y="1"/></Size>
|
||||
<Anchors><Anchor point="TOPLEFT"/></Anchors>
|
||||
<Scripts>
|
||||
<OnLoad>
|
||||
self:RegisterEvent("START_LOOT_ROLL")
|
||||
self:RegisterEvent("CONFIRM_LOOT_ROLL")
|
||||
</OnLoad>
|
||||
<OnEvent>
|
||||
if event == "START_LOOT_ROLL" then
|
||||
GroupLootFrame_OpenNewFrame(arg1, arg2)
|
||||
elseif event == "CONFIRM_LOOT_ROLL" then
|
||||
-- Ref UIParent.lua:517-524 verbatim, minus the central router: a Need/Greed on a
|
||||
-- BIND-ON-PICKUP roll sends nothing and asks first (decision 0594). arg1 = rollID,
|
||||
-- arg2 = rollType; both ride the dialog so OnAccept can re-enter ConfirmLootRoll.
|
||||
local dialog = StaticPopup_Show("CONFIRM_LOOT_ROLL")
|
||||
if dialog then
|
||||
dialog.data = arg1
|
||||
dialog.data2 = arg2
|
||||
end
|
||||
end
|
||||
</OnEvent>
|
||||
</Scripts>
|
||||
</Frame>
|
||||
</Ui>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -213,7 +213,7 @@
|
||||
function UpdateMicroButtons()
|
||||
BenillaMicroButton_SetPushed(CharacterMicroButton, BenillaMicro_PanelShown("CharacterFrame"))
|
||||
BenillaMicroButton_SetPushed(SpellbookMicroButton, BenillaMicro_PanelShown("SpellBookFrame"))
|
||||
BenillaMicroButton_SetPushed(TalentMicroButton, BenillaMicro_PanelShown("BenillaTalentFrame"))
|
||||
BenillaMicroButton_SetPushed(TalentMicroButton, BenillaMicro_PanelShown("TalentFrame"))
|
||||
BenillaMicroButton_SetPushed(QuestLogMicroButton, BenillaMicro_PanelShown("QuestLogFrame"))
|
||||
-- ref l.60-64. Missing since the Socials button went live with decision 0668: the
|
||||
-- window opened but its button never read pushed. Spotted while wiring Help, which is
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -36,6 +36,8 @@
|
||||
this:RegisterEvent("MIRROR_TIMER_START")
|
||||
this:RegisterEvent("PLAYER_ENTERING_WORLD")
|
||||
this:RegisterEvent("READY_CHECK")
|
||||
this:RegisterEvent("START_LOOT_ROLL")
|
||||
this:RegisterEvent("CONFIRM_LOOT_ROLL")
|
||||
</OnLoad>
|
||||
<OnEvent>UIParent_OnEvent(event)</OnEvent>
|
||||
</Scripts>
|
||||
@@ -213,6 +215,41 @@
|
||||
-- statements on the same event). `MultiActionBar_Update` is existence-guarded because the
|
||||
-- ~25 per-window harnesses that load this file load no bars; the binding itself is engine
|
||||
-- level and always installed. Decision 1500.
|
||||
-- ref UIParent.lua:223, whose slot this is — immediately above `UIParent_OnEvent`, the
|
||||
-- same neighbour it has there. It moved here from our own InspectFrame.xml when 1832
|
||||
-- retired that file: the name belongs to UIParent in the reference, and the window it
|
||||
-- opens is now the stock LoadOnDemand `Blizzard_InspectUI` off the player's chain.
|
||||
--
|
||||
-- The reference's body is `InspectFrame_LoadUI()` and then a guarded `InspectFrame_Show`.
|
||||
-- We have no load-on-demand — benilla.toc loads that addon with everything else — so the
|
||||
-- LoadUI call has nothing to do and is left out rather than stubbed (1203). The guard is
|
||||
-- kept exactly as the reference writes it, and it is not decoration: it is what makes this
|
||||
-- safe in the ~25 per-window harnesses that load UIParent.xml without the inspect window.
|
||||
function InspectUnit(unit)
|
||||
if ( InspectFrame_Show ) then
|
||||
InspectFrame_Show(unit);
|
||||
end
|
||||
end
|
||||
|
||||
-- ref UIParent.lua's own `ToggleTalentFrame`, moved here from our TalentFrame.xml when
|
||||
-- 1833 retired that file — the same move `InspectUnit` made just above, for the same
|
||||
-- reason: the reference keeps the opener in UIParent and the window in a LoadOnDemand
|
||||
-- addon, so deleting our window file would have taken the name with it.
|
||||
--
|
||||
-- The LEVEL GATE is the reference's, and it is real behaviour rather than a guard: below
|
||||
-- level 10 the key and the micro button do nothing at all. `TalentFrame_LoadUI` is left
|
||||
-- out (we load the addon eagerly, 1203 — build it or leave it out, never stub it); the
|
||||
-- `TalentFrame_Toggle` guard is kept verbatim, and it is what lets the ~25 harnesses that
|
||||
-- load UIParent.xml without the talent window do so safely.
|
||||
function ToggleTalentFrame()
|
||||
if ( UnitLevel("player") < 10 ) then
|
||||
return;
|
||||
end
|
||||
if ( TalentFrame_Toggle ) then
|
||||
TalentFrame_Toggle();
|
||||
end
|
||||
end
|
||||
|
||||
function UIParent_OnEvent(event)
|
||||
-- The reference's own first arm (UIParent.lua l.230-235). `LocalizeFrames` comes from
|
||||
-- the sourced `Interface\FrameXML\Localization.xml` (1751 window 5) and is the file
|
||||
@@ -247,6 +284,36 @@
|
||||
MultiActionBar_Update()
|
||||
end
|
||||
end
|
||||
-- The group-roll opener, ref UIParent.lua l.513-515 verbatim. This arm is why our
|
||||
-- own GroupLootFrame.xml existed at all: that file's header explained it was carrying
|
||||
-- a dedicated hidden driver frame *because* "this engine doesn't ship" UIParent's
|
||||
-- monolithic OnEvent. It does now, so the router comes home to the reference's own
|
||||
-- slot and the driver goes with the file (decision 1838).
|
||||
--
|
||||
-- `GroupLootFrame_OpenNewFrame` and the four frames it walks are the CHAIN's, out of
|
||||
-- `Interface\FrameXML\LootFrame.xml` and its `.lua` — already on the manifest, and
|
||||
-- already declaring every name our copy re-declared. Existence-guarded for
|
||||
-- `MultiActionBar_Update`'s reason, not the reference's: the per-window harnesses load
|
||||
-- this file with no loot window behind it.
|
||||
if ( event == "START_LOOT_ROLL" ) then
|
||||
if ( GroupLootFrame_OpenNewFrame ) then
|
||||
GroupLootFrame_OpenNewFrame(arg1, arg2)
|
||||
end
|
||||
return
|
||||
end
|
||||
-- ref UIParent.lua l.517-524, the arm directly below the one above and the second
|
||||
-- router our GroupLootFrame.xml was carrying. The dialog it raises now lives in
|
||||
-- UiPanels.xml beside DELETE_ITEM; `arg1`/`arg2` are the roll id and roll type, and
|
||||
-- `StaticPopup_Show` answering nil (the popup pool being full) is the reference's own
|
||||
-- guarded case, not ours.
|
||||
if ( event == "CONFIRM_LOOT_ROLL" ) then
|
||||
local dialog = StaticPopup_Show("CONFIRM_LOOT_ROLL")
|
||||
if ( dialog ) then
|
||||
dialog.data = arg1
|
||||
dialog.data2 = arg2
|
||||
end
|
||||
return
|
||||
end
|
||||
-- The ready-check popup's opener (decision 1549), ref UIParent.lua l.570-572. The
|
||||
-- event is the engine's own `0x218`, fired by the `MSG_RAID_READY_CHECK` open
|
||||
-- handler; the frame and `ShowReadyCheck` live in RaidFrame.xml, so this is the one
|
||||
|
||||
@@ -110,7 +110,7 @@
|
||||
-- The inspect window (decision 0631, InspectFrame.xml): the ref's own InspectFrame row
|
||||
-- (`Blizzard_InspectUI.lua:4` — area "left", pushable 0), so inspecting swaps out whichever
|
||||
-- left-slot window was open, exactly as the character window does.
|
||||
UIPanelWindows["BenillaInspectFrame"] = { area = "left", pushable = 0 }
|
||||
UIPanelWindows["InspectFrame"] = { area = "left", pushable = 0 }
|
||||
-- The spellbook (decision 0216 §8, SpellBookFrame.xml): the ref's own SpellBookFrame row
|
||||
-- verbatim (`{ area = "left", pushable = 0, whileDead = 1 }`, UIParent.lua l.21; whileDead
|
||||
-- carried since 1507) — same left-slot competition as every window above.
|
||||
@@ -120,7 +120,7 @@
|
||||
-- pushable 6 one tier above the plain NPC-session/player-state windows' 0. whileDead was
|
||||
-- omitted here while `UnitIsDead` didn't exist; it is a live unit binding now, so 1507
|
||||
-- carries the ref's flag on every row that has one.
|
||||
UIPanelWindows["BenillaTalentFrame"] = { area = "left", pushable = 6, whileDead = 1 }
|
||||
UIPanelWindows["TalentFrame"] = { area = "left", pushable = 6, whileDead = 1 }
|
||||
-- The social window (decision 0668) — the ref's own registration line verbatim
|
||||
-- (`UIPanelWindows["FriendsFrame"] = { area = "left", pushable = 0, whileDead = 1 }`,
|
||||
-- UIParent.lua l.29). whileDead: a corpse run is exactly when you want to whisper someone.
|
||||
@@ -1705,6 +1705,36 @@
|
||||
-- other path → hide) replaces the old bespoke `deleteItemFlow` CURSOR_UPDATE driver.
|
||||
-- exclusive is v1-inert (engine header note); showAlert is live since 1068 — the ref's
|
||||
-- own "display the error image" case is this very dialog.
|
||||
-- CONFIRM_LOOT_ROLL — the BoP roll's "this will bind to you" gate, keyed BARE like every
|
||||
-- other entry (the key is `StaticPopup_Show`'s argument, not a frame name).
|
||||
--
|
||||
-- It moved here from our own GroupLootFrame.xml when 1838 retired that file. The reference
|
||||
-- keeps it in `StaticPopup.lua`; we have no counterpart to that file, and our own
|
||||
-- convention is that a dialog whose window has migrated comes to sit beside DELETE_ITEM
|
||||
-- below — the same move `CloseMenus` made when the dropdown kit went to the chain.
|
||||
--
|
||||
-- The C `RollOnLoot` is what diverts a BoP Need/Greed here rather than rolling: the gate
|
||||
-- lives in the binding, not in Lua (decision 0594, verified in the 5875 binary), and only
|
||||
-- `OnAccept`'s `ConfirmLootRoll` re-enters past it. `id`/`rollType` arrive as data/data2,
|
||||
-- set by the `CONFIRM_LOOT_ROLL` arm in UIParent.xml's own OnEvent.
|
||||
--
|
||||
-- The three strings are quoted verbatim rather than read from GlobalStrings, which is this
|
||||
-- file's existing idiom for exactly one reason: a bare global is nil in a harness that has
|
||||
-- not loaded the chain's GlobalStrings, and a `format()` over nil aborts the whole inline
|
||||
-- chunk — taking every function defined after it down with it.
|
||||
StaticPopupDialogs["CONFIRM_LOOT_ROLL"] = {
|
||||
text = "Looting this item will bind it to you.", -- LOOT_NO_DROP
|
||||
button1 = "Okay", -- OKAY
|
||||
button2 = "Cancel", -- CANCEL
|
||||
OnAccept = function(id, rollType)
|
||||
ConfirmLootRoll(id, rollType)
|
||||
end,
|
||||
timeout = 0,
|
||||
whileDead = 1,
|
||||
exclusive = 1,
|
||||
hideOnEscape = 1
|
||||
}
|
||||
|
||||
StaticPopupDialogs["DELETE_ITEM"] = {
|
||||
text = DELETE_ITEM,
|
||||
button1 = YES,
|
||||
@@ -2322,6 +2352,16 @@
|
||||
-- ours reads GetStringWidth — the same number once the measure round-trip lands, which is
|
||||
-- why the caller below re-checks from OnUpdate instead of OnShow.
|
||||
function PanelTemplates_TabResize(padding, tab, absoluteSize, maxWidth)
|
||||
-- The ref's own opening arm, and it is not defensive: `tab` is OPTIONAL, and a caller
|
||||
-- that omits it is relying on `this`. Ours had only the explicit-`tab` half, so every
|
||||
-- caller in our own files passed one and the gap stayed invisible — until a stock file
|
||||
-- called `PanelTemplates_TabResize(0)` from a tab's OnLoad and threw
|
||||
-- "attempt to index local 'tab' (a nil value)". `Blizzard_MacroUI.xml`'s two tabs do
|
||||
-- exactly that; so do the readiness instrument's two `UiPanels.xml` failures.
|
||||
-- Decision 1835.
|
||||
if ( not tab ) then
|
||||
tab = this
|
||||
end
|
||||
local name = tab:GetName()
|
||||
local text = getglobal(name .. "Text")
|
||||
local sideWidths = 2 * getglobal(name .. "Left"):GetWidth()
|
||||
|
||||
@@ -655,14 +655,36 @@ SkillFrame.xml
|
||||
# `HonorFrameTemplates.xml` is `<Include>`d by `HonorFrame.xml` and inherited by the LoadOnDemand
|
||||
# `Blizzard_InspectUI`, which loads later still.
|
||||
Interface\FrameXML\HonorFrame.xml
|
||||
# The inspect window (decision 0631): another player's paper doll. Beside the character
|
||||
# window it mirrors — it needs the same three (Fonts/UiPanels/GameTooltip) plus
|
||||
# `CharacterFrameTabButtonTemplate` from UiPanels.xml, and nothing from CharacterFrame.xml
|
||||
# itself (its slot template, handlers, and booth slot are all its own — the two windows
|
||||
# share only the reference's art). ONE cross-window dependency does bind, and it is on the
|
||||
# entry directly above: this window's honor page inherits HonorFrame.xml's five row templates
|
||||
# and calls its two shared painters, the same sharing the reference does.
|
||||
InspectFrame.xml
|
||||
# ══ THE INSPECT WINDOW — the reference's own, off the player's chain (1751, window 24) ══
|
||||
# Another player's paper doll (decision 0631), and the FIRST LoadOnDemand ADDON we take off the
|
||||
# chain rather than a FrameXML file. Everything below it in this manifest that names a
|
||||
# `Blizzard_*` addon follows the shape set here.
|
||||
#
|
||||
# **The addon is inside patch.MPQ, not the loose folder.** `Interface\AddOns\Blizzard_InspectUI\`
|
||||
# on disk holds one `Blizzard_InspectUI.pub` decoy and nothing else; the real four files sit in
|
||||
# the archive, which the chain mounts. Believing the decoy is what kept this window (and Macro,
|
||||
# Talent, Raid, Trainer) on a transcription for months.
|
||||
#
|
||||
# No LoadOnDemand: we list the addon's files here and they load with everything else. The
|
||||
# reference reaches them through `InspectUnit` → `InspectFrame_LoadUI`; ours keeps `InspectUnit`
|
||||
# (in UIParent.xml, the reference's own home for that name) and drops the LoadUI call.
|
||||
#
|
||||
# The addon's own toc order, which is load order — the `.xml` files pull their `.lua` siblings
|
||||
# through `<Script file=.../>` relative to the addon directory, exactly as a FrameXML file does.
|
||||
# Its toc lists a fourth entry, `Localization.lua`, which is omitted here because it holds no code
|
||||
# at all: the whole file is the comment `-- This file is executed at the end of addon load`.
|
||||
#
|
||||
# Dependencies are unchanged from the file this replaces: Fonts/UiPanels/GameTooltip, plus
|
||||
# `CharacterFrameTabButtonTemplate` from UiPanels.xml, plus ItemButtonTemplate.xml far above
|
||||
# (`InspectPaperDollItemSlotButtonTemplate` inherits it, resolved at LOAD). And the one that
|
||||
# binds hardest is the entry directly above: the honor page inherits HonorFrame.xml's five row
|
||||
# templates — the reference has the identical edge, which is why its own toc has the same order.
|
||||
#
|
||||
# What we GAIN by swapping: `InspectGuildText` and `InspectTitleText`, two FontStrings our
|
||||
# transcription never had.
|
||||
Interface\AddOns\Blizzard_InspectUI\Blizzard_InspectUI.xml
|
||||
Interface\AddOns\Blizzard_InspectUI\InspectPaperDollFrame.xml
|
||||
Interface\AddOns\Blizzard_InspectUI\InspectHonorFrame.xml
|
||||
# The dressing room (decision 1060): the ctrl-click item preview. Loaded right after the
|
||||
# windows whose slots feed it (the character window above, the bags before that) and before
|
||||
# everything else that ctrl-clicks into it — order is legibility only, since `DressUpItemLink`
|
||||
@@ -674,13 +696,23 @@ DressUpFrame.xml
|
||||
# windows grouped" posture, same Fonts/UiPanels/GameTooltip-only dependency set); the
|
||||
# 'P' binding (ui_script/input.rs) calls its bare ToggleSpellBook(BOOKTYPE_SPELL).
|
||||
SpellBookFrame.xml
|
||||
# The talent window (decision 0304 §4): the N-key window, the class talent grid over the
|
||||
# `benilla-ui::script::talent` engine seam. Loaded right after SpellBookFrame (the same
|
||||
# "player-state windows grouped" posture; needs Fonts/UiPanels/GameTooltip plus
|
||||
# ScrollTemplates.xml — its real ScrollFrame's scrollbar reuses BenillaScrollBarTemplate —
|
||||
# all already loaded above). The 'N' binding (ui_script/input.rs) calls its bare
|
||||
# ToggleTalentFrame().
|
||||
TalentFrame.xml
|
||||
# ══ THE TALENT WINDOW — the reference's own, off the player's chain (1751, window 25) ══
|
||||
# The second LoadOnDemand addon (1832 was the first, and its header carries the mechanics).
|
||||
# `Blizzard_TalentUI` is FIVE files behind one manifest line: the `.xml` sources its own `.lua`
|
||||
# and `<Include>`s `Blizzard_TalentUITemplates.xml`, which is where the four templates every
|
||||
# talent button, branch, arrow and tab inherits actually live. Its toc's only other entry is the
|
||||
# empty `Localization.lua`.
|
||||
#
|
||||
# It needs NOTHING built — every engine verb the five files call was already registered, checked
|
||||
# name by name against our bindings. What it needs LOADED is UIPanelTemplates (its scroll frame
|
||||
# is `UIPanelScrollFrameTemplate`, its close and cancel buttons `UIPanelCloseButton` /
|
||||
# `UIPanelButtonTemplate`) and Fonts, both far above.
|
||||
#
|
||||
# `ToggleTalentFrame` moved to UIParent.xml, the reference's own home for it — see 1833.
|
||||
#
|
||||
# What we GAIN: `TalentFrameTab4` and `TalentFrameTab5`. The reference lays out five tab slots
|
||||
# where our transcription stopped at the three a 1.12 class actually has.
|
||||
Interface\AddOns\Blizzard_TalentUI\Blizzard_TalentUI.xml
|
||||
# The social window (decision 0668): the O-key friends/ignore/who window over the social
|
||||
# API (`benilla-ui/src/script/social.rs`) + `crate::ui_social`'s feed/drain. Needs
|
||||
# UiPanels (the panel manager, `CharacterFrameTabButtonTemplate`, and the StaticPopup
|
||||
@@ -826,12 +858,6 @@ CraftFrame.xml
|
||||
# every verb the stock file calls at runtime — not just at load — was checked by hand against
|
||||
# the bindings before this line changed.
|
||||
Interface\FrameXML\LootFrame.xml
|
||||
# The group-loot roll popups (decision 0591): the Need/Greed/Pass dialogs, a sibling of
|
||||
# LootFrame.xml (loaded right after it) over the `benilla-ui::script::loot_roll` seam
|
||||
# (`crate::ui_loot_roll`'s feed/drain). NOT a UIPanel — four `frameStrata="DIALOG"` toplevel
|
||||
# popups with their own hardcoded anchors — so it needs only Fonts (GameFontNormalSmall) +
|
||||
# GameTooltip (the item/PASS/NEED/GREED hovers), both already loaded above.
|
||||
GroupLootFrame.xml
|
||||
# The questgiver window (decision 0088): four sub-panels over the Era quest API. Loaded after
|
||||
# MerchantFrame (grouping; the BenillaMoney coin helpers now live in MoneyFrame.xml) and GossipFrame (shared parchment
|
||||
# art conventions); its title/panel-button labels use the auto-centred ButtonText slot.
|
||||
|
||||
@@ -159,15 +159,20 @@ const STABLE_SLOT: &str = "stable";
|
||||
/// this table draws nothing, exactly as it does today.
|
||||
///
|
||||
/// The names, and where each is declared: `CharacterModelFrame` (stock `PaperDollFrame.xml`),
|
||||
/// `PetModelFrame` (stock `PetPaperDollFrame.xml`), `DressUpModel` (stock `DressUpFrame.xml`'s
|
||||
/// name, our file), `PetStableModel` (stock `PetStable.xml`'s name, our file), and
|
||||
/// `BenillaInspectModelFrame` — the one that is NOT the reference's, because 1.12's inspect window
|
||||
/// is a LoadOnDemand `Blizzard_InspectUI` the install ships packed, so we have no stock file to
|
||||
/// take the name from.
|
||||
/// `PetModelFrame` (stock `PetPaperDollFrame.xml`), `PetStableModel` (stock `PetStable.xml`, the
|
||||
/// reference's own file since 1751), `DressUpModel` (stock `DressUpFrame.xml`'s name, still our
|
||||
/// file), and `InspectModelFrame` (stock `InspectPaperDollFrame.xml`, out of the LoadOnDemand
|
||||
/// `Blizzard_InspectUI` — the reference's own since 1832).
|
||||
///
|
||||
/// **Every name here is now the reference's.** This note used to carry a `BenillaInspectModelFrame`
|
||||
/// and an explanation: the install was thought to ship that addon packed, with no stock file to
|
||||
/// take the name from. It is not packed — the `.pub` is only what the loose
|
||||
/// `Interface\AddOns\Blizzard_InspectUI\` folder holds, while the real `.xml` and `.lua` sit
|
||||
/// inside `patch.MPQ`, which the chain mounts. 1832 migrated the window and the prefix went with it.
|
||||
const MODEL_PANE_BOOTHS: [(&str, &str); 5] = [
|
||||
("CharacterModelFrame", PAPERDOLL_SLOT),
|
||||
("PetModelFrame", PETDOLL_SLOT),
|
||||
("BenillaInspectModelFrame", INSPECT_SLOT),
|
||||
("InspectModelFrame", INSPECT_SLOT),
|
||||
("PetStableModel", STABLE_SLOT),
|
||||
("DressUpModel", dressup::DRESSUP_SLOT),
|
||||
];
|
||||
|
||||
@@ -222,7 +222,10 @@ fn feed_inspect(
|
||||
};
|
||||
|
||||
booth.unit = Some(entity);
|
||||
booth.yaw = script.inspect_yaw();
|
||||
// The stock `InspectPaperDollFrame.lua` turns the doll by writing the PANE
|
||||
// (`InspectModelFrame:SetRotation`), the same way the character sheet, the pet doll and the
|
||||
// stable booth already read theirs (decision 1832).
|
||||
booth.yaw = script.model_pane_facing("InspectModelFrame");
|
||||
|
||||
let Ok(store) = stores.get(entity) else {
|
||||
return;
|
||||
|
||||
@@ -56,6 +56,9 @@ pub(super) struct FedParty {
|
||||
saved_answers: u32,
|
||||
/// The ready-check ticket last seen ([`GroupState::ready_check`]).
|
||||
ready_check: u32,
|
||||
/// The raid-target icon board last pushed — what `RAID_TARGET_UPDATE` fires on. Eight guids,
|
||||
/// so a plain copy rather than the `Vec` diffs above.
|
||||
raid_targets: [u64; 8],
|
||||
}
|
||||
|
||||
pub(crate) const PARTY_TOKENS: [&str; 4] = ["party1", "party2", "party3", "party4"];
|
||||
@@ -433,6 +436,22 @@ pub(super) fn feed_party(
|
||||
script.fire_event("RAID_ROSTER_UPDATE", vec![]);
|
||||
}
|
||||
|
||||
// ── RAID_TARGET_UPDATE ──────────────────────────────────────────────────────────────────
|
||||
//
|
||||
// The board moving is an EVENT, and we were not firing it at all. Stock `TargetFrame.lua:30`
|
||||
// registers `RAID_TARGET_UPDATE` and its `:99` arm calls `TargetFrame_UpdateRaidTargetIcon`,
|
||||
// so without this the skull/star on your CURRENT target never appears or clears — it only
|
||||
// corrects itself on the next `PLAYER_TARGET_CHANGED`, i.e. when you re-target. That file is
|
||||
// on our chain, so this was live.
|
||||
//
|
||||
// Fired on the whole board rather than per unit because that is the shape the reference's
|
||||
// handler has: it takes no argument and re-reads the unit it is showing.
|
||||
if group.raid_targets != fed.raid_targets {
|
||||
gate.audit("feed_party", "the raid-target board edge");
|
||||
fed.raid_targets = group.raid_targets;
|
||||
script.fire_event("RAID_TARGET_UPDATE", vec![]);
|
||||
}
|
||||
|
||||
// ── READY_CHECK (decision 1549) ─────────────────────────────────────────────────────────
|
||||
//
|
||||
// The reference's event `0x218`, fired by the `MSG_RAID_READY_CHECK` open handler
|
||||
|
||||
@@ -182,6 +182,19 @@ impl GroupState {
|
||||
if group_type == 1 && self.group_type != 1 {
|
||||
lines.push(RAID_YOU_JOINED.to_string());
|
||||
}
|
||||
// The OTHER direction clears the raid-target board. `0x4ba550` — sole caller `0x5e6ebb`,
|
||||
// inside `SMSG_GROUP_LIST 0x7d`'s raid-flag-CLEAR leg — `rep stosd`-zeroes all eight slots
|
||||
// and refreshes every formerly-marked unit (decision 1820, wow-re
|
||||
// `object-layer/scratch/party-group-wire.md`). Without it a raid→party conversion leaves
|
||||
// stale marks on screen: the icons are drawn from this board, and nothing else clears it
|
||||
// short of a `0x321` for each slot, which the server does not send.
|
||||
//
|
||||
// A full disband needs no arm here — `leaving` above replaces the whole state, and
|
||||
// `[u64; 8]::default()` is already the empty board. 1820 recorded this as "stale icons
|
||||
// after a disband", which was wider than the truth; the conversion is the real gap.
|
||||
if group_type != 1 && self.group_type == 1 {
|
||||
self.raid_targets = [0; 8];
|
||||
}
|
||||
|
||||
// Drop stats snapshots for members no longer on the roster.
|
||||
self.stats
|
||||
@@ -651,4 +664,36 @@ mod tests {
|
||||
assert_eq!(g.raid_targets[5], 0x33);
|
||||
assert_eq!(g.raid_targets[3], 0, "the list form resets absent icons");
|
||||
}
|
||||
|
||||
/// A raid→party conversion CLEARS the board; a disband does too (by replacing the state), and
|
||||
/// an ordinary roster change inside a raid does not. The reference zeroes all eight slots on
|
||||
/// `SMSG_GROUP_LIST`'s raid-flag-clear leg (`0x4ba550`, decision 1820) — without it the marks
|
||||
/// are drawn from a board nothing else empties, so they linger on screen.
|
||||
#[test]
|
||||
fn a_raid_to_party_conversion_clears_the_raid_target_board() {
|
||||
let mut g = GroupState::default();
|
||||
g.apply_list(1, 0, vec![member("Ally", 0x22)], 0x22, None);
|
||||
g.apply_raid_target(7, 0x22);
|
||||
assert_eq!(g.raid_targets[7], 0x22, "marked while a raid");
|
||||
|
||||
// Still a raid, roster moved: the board survives.
|
||||
g.apply_list(
|
||||
1,
|
||||
0,
|
||||
vec![member("Ally", 0x22), member("Bee", 0x33)],
|
||||
0x22,
|
||||
None,
|
||||
);
|
||||
assert_eq!(
|
||||
g.raid_targets[7], 0x22,
|
||||
"a roster change inside a raid keeps the marks"
|
||||
);
|
||||
|
||||
// Raid flag clears while the group lives on — the leg the reference zeroes on.
|
||||
g.apply_list(0, 0, vec![member("Ally", 0x22)], 0x22, None);
|
||||
assert_eq!(
|
||||
g.raid_targets, [0; 8],
|
||||
"the raid flag clearing empties the board"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -323,7 +323,12 @@ fn typing_the_confirm_word_enables_okay_and_untyping_it_disables_again() {
|
||||
let mut s = setup();
|
||||
drop_in_world(&mut s, 871, "Flurry Axe", 4);
|
||||
|
||||
// OKAY is enabled by the box's `OnTextChanged`, and that fire is deferred to the drain
|
||||
// (decision 1831) — so what this answers is the button state the LAST DRAINED text produced,
|
||||
// which is the state a player ever sees. Ticking here rather than after each write keeps the
|
||||
// test reading as the sequence of edits it is about.
|
||||
let enabled = |s: &mut UiScript| {
|
||||
s.tick(0.0);
|
||||
s.eval::<i64>("return StaticPopup1Button1:IsEnabled()")
|
||||
.unwrap()
|
||||
== 1
|
||||
@@ -382,6 +387,7 @@ fn enter_in_the_box_destroys_only_once_okay_is_enabled() {
|
||||
assert!(s.take_container_destroys().is_empty());
|
||||
|
||||
s.run(r#"StaticPopup1EditBox:SetText("DELETE")"#).unwrap();
|
||||
s.tick(0.0); // the write only marks the box — the drain is what enables OKAY (1831)
|
||||
assert!(s.key_input("ENTER"));
|
||||
assert!(
|
||||
!s.eval::<bool>("return StaticPopup1:IsVisible()").unwrap(),
|
||||
|
||||
@@ -7,35 +7,13 @@ use benilla_ui::script::{
|
||||
DressUpIntent, ExtractedQuad, LootRollEntry, LootRollsState, QuadContent, ScriptValue, UiScript,
|
||||
};
|
||||
|
||||
use super::test_ui::load_ui as load_xml;
|
||||
use super::test_ui::{load_ui as load_xml, load_ui_no_warnings as load_xml_no_warnings};
|
||||
|
||||
/// 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
|
||||
/// LootFrame.xml's now-obsolete `GameFontNormalSmall` caveat would have produced, is exactly what
|
||||
/// this catches). Not used for the prerequisite files below: Fonts/UiPanels/GameTooltip aren't this
|
||||
/// assignment's to police and may carry warnings of their own.
|
||||
fn load_xml_no_warnings(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
|
||||
);
|
||||
assert!(
|
||||
report.warnings.is_empty(),
|
||||
"{file}: loader warnings: {:?}",
|
||||
report.warnings
|
||||
);
|
||||
report.frames
|
||||
}
|
||||
|
||||
/// The centre of the first texture quad whose path contains `needle` (a button's art), for clicking
|
||||
/// it — the `loot_tests.rs` `icon_center` idiom.
|
||||
fn quad_center(quads: &[ExtractedQuad], needle: &str) -> (f32, f32) {
|
||||
@@ -61,10 +39,42 @@ fn text_color(quads: &[ExtractedQuad], t: &str) -> Option<[f32; 4]> {
|
||||
})?
|
||||
}
|
||||
|
||||
/// The four roll popups, off the player's chain.
|
||||
///
|
||||
/// They are declared in stock `LootFrame.xml` — `GroupLootFrameTemplate` plus `GroupLootFrame1..4`
|
||||
/// — and that file has been on the manifest since the loot window migrated. Our own
|
||||
/// `GroupLootFrame.xml` re-declared all five names on top of it and won by load order, so what
|
||||
/// these tests exercised was our copy shadowing the chain's (decision 1838).
|
||||
///
|
||||
/// `UIParent.xml` comes with it because the `START_LOOT_ROLL` router lives there now — the
|
||||
/// reference's own slot for it — where our file used to carry a dedicated hidden driver frame.
|
||||
fn load_group_loot(s: &UiScript) {
|
||||
load_xml(s, "UIParent.xml");
|
||||
// `LootFrame.xml` brings the whole loot window, and its `GroupLootDropDown` calls
|
||||
// `UIDropDownMenu_Initialize` from its own OnLoad — the dropdown kit is on the chain too.
|
||||
load_xml(s, r"Interface\FrameXML\UIDropDownMenu.xml");
|
||||
// `MAX_PARTY_MEMBERS` — `GroupLootDropDown_OnLoad` counts party rows, and the constant lives
|
||||
// in stock `PartyMemberFrame.lua`. A real cross-file dependency the reference has too (its own
|
||||
// toc loads PartyFrame well before LootFrame); the `.lua` alone is the minimal way to satisfy
|
||||
// it here, because its whole top level is four constants and some function definitions — no
|
||||
// frames, no side effects — where `PartyFrame.xml` would drag in the entire unit-frame cluster
|
||||
// for one number.
|
||||
load_xml(s, r"Interface\FrameXML\PartyMemberFrame.lua");
|
||||
load_xml(s, r"Interface\FrameXML\LootFrame.xml");
|
||||
}
|
||||
|
||||
fn setup() -> UiScript {
|
||||
let mut s = UiScript::new().unwrap();
|
||||
s.set_screen_size(1024.0, 768.0);
|
||||
// The loot window's own labels (`ITEMS`, `PREV`, `NEXT`) are GlobalStrings keys, and the
|
||||
// loader warns on a key with no global behind it rather than failing — which is exactly the
|
||||
// kind of warning `load_ui_no_warnings` is here to catch (decision 1838).
|
||||
load_xml(&s, r"Interface\FrameXML\GlobalStrings.lua");
|
||||
load_xml(&s, "Fonts.xml"); // ITEM_QUALITY_COLORS + GameFontNormalSmall
|
||||
// The loot window's slots inherit it — the same dependency the inspect window needed (1832).
|
||||
load_xml(&s, r"Interface\FrameXML\ItemButtonTemplate.xml");
|
||||
// `UIPanelCloseButton`, which the loot window's four close buttons inherit.
|
||||
load_xml(&s, "UIPanelTemplates.xml");
|
||||
load_xml(&s, "MoneyFrame.xml");
|
||||
load_xml(&s, "UiPanels.xml");
|
||||
load_xml(&s, "GameTooltip.xml"); // PASS/NEED/GREED + item hovers
|
||||
@@ -131,13 +141,19 @@ fn rolls() -> LootRollsState {
|
||||
/// and start hidden.
|
||||
#[test]
|
||||
fn shipped_group_loot_frame_loads_clean_and_starts_hidden() {
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
let s = setup();
|
||||
// Per instance: the frame + IconFrame + PassButton + RollButton + GreedButton + Timer (6),
|
||||
// times 4, plus the one BenillaGroupLootFrameDriver event-listener frame.
|
||||
assert_eq!(
|
||||
load_xml_no_warnings(&s, "GroupLootFrame.xml"),
|
||||
4 * 6 + 1,
|
||||
"4 x (frame + icon + pass + need + greed + timer) + the START_LOOT_ROLL driver"
|
||||
load_xml(&s, "UIParent.xml");
|
||||
load_xml(&s, r"Interface\FrameXML\UIDropDownMenu.xml");
|
||||
load_xml(&s, r"Interface\FrameXML\PartyMemberFrame.lua");
|
||||
// The four roll popups arrive INSIDE the chain's loot window, so there is no exact frame count
|
||||
// to assert any more: this used to read `4 * 6 + 1` — six regions per instance plus our own
|
||||
// `BenillaGroupLootFrameDriver` — and both halves of that number were ours (decision 1838).
|
||||
// What survives is the assignment that mattered: the file loads with **no warning of any
|
||||
// kind**, which is what `load_ui_no_warnings` is for.
|
||||
assert!(
|
||||
load_xml_no_warnings(&s, r"Interface\FrameXML\LootFrame.xml") > 4,
|
||||
"the loot window brought its frames"
|
||||
);
|
||||
|
||||
for i in 1..=4 {
|
||||
@@ -156,8 +172,9 @@ fn shipped_group_loot_frame_loads_clean_and_starts_hidden() {
|
||||
/// click wiring end-to-end through `RollOnLoot`.
|
||||
#[test]
|
||||
fn start_loot_roll_claims_frames_in_order_and_paints_the_roll() {
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
let mut s = setup();
|
||||
load_xml(&s, "GroupLootFrame.xml");
|
||||
load_group_loot(&s);
|
||||
s.set_loot_rolls(rolls());
|
||||
|
||||
// Roll 7 (BoP, Epic) claims frame 1.
|
||||
@@ -258,8 +275,9 @@ fn start_loot_roll_claims_frames_in_order_and_paints_the_roll() {
|
||||
/// binds an epic to you on a single click. The five-line path below is the whole correction.
|
||||
#[test]
|
||||
fn the_bop_confirm_popup_lands_the_withheld_vote() {
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
let mut s = setup();
|
||||
load_xml(&s, "GroupLootFrame.xml");
|
||||
load_group_loot(&s);
|
||||
s.set_loot_rolls(rolls());
|
||||
|
||||
// The app fires this after draining the seam's confirm queue (`ui_loot_roll::drain_loot_rolls`).
|
||||
@@ -319,8 +337,9 @@ fn the_bop_confirm_popup_lands_the_withheld_vote() {
|
||||
/// `CANCEL_LOOT_ROLL` for a rollID hides that frame, and ONLY that one.
|
||||
#[test]
|
||||
fn cancel_loot_roll_hides_only_that_frame() {
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
let mut s = setup();
|
||||
load_xml(&s, "GroupLootFrame.xml");
|
||||
load_group_loot(&s);
|
||||
s.set_loot_rolls(rolls());
|
||||
|
||||
s.fire_event(
|
||||
@@ -364,8 +383,9 @@ fn cancel_loot_roll_hides_only_that_frame() {
|
||||
/// than crashing on the nils.
|
||||
#[test]
|
||||
fn in_flight_roll_does_not_error_and_falls_back() {
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
let mut s = setup();
|
||||
load_xml(&s, "GroupLootFrame.xml");
|
||||
load_group_loot(&s);
|
||||
s.set_loot_rolls(rolls());
|
||||
|
||||
s.fire_event(
|
||||
@@ -382,9 +402,12 @@ fn in_flight_roll_does_not_error_and_falls_back() {
|
||||
.unwrap());
|
||||
assert_eq!(s.eval::<i64>("return GroupLootFrame1.rollID").unwrap(), 9);
|
||||
// Blank name (not an error, not the literal "nil").
|
||||
// The chain's FontString has NO text until something paints it, where our retired copy
|
||||
// defaulted to `""` — so this reads Option and expects None (decision 1838).
|
||||
assert_eq!(
|
||||
s.eval::<String>("return GroupLootFrame1Name:GetText()")
|
||||
s.eval::<Option<String>>("return GroupLootFrame1Name:GetText()")
|
||||
.unwrap(),
|
||||
None,
|
||||
""
|
||||
);
|
||||
// No BoP decoration for an unresolved (default-false bindOnPickUp) roll.
|
||||
@@ -392,13 +415,22 @@ fn in_flight_roll_does_not_error_and_falls_back() {
|
||||
.eval::<bool>("return GroupLootFrame1Decoration:IsShown()")
|
||||
.unwrap());
|
||||
|
||||
// The fallback icon painted rather than an empty/erroring SetTexture(nil).
|
||||
// **No fallback icon — and that is the reference's behaviour, not a regression to fix here.**
|
||||
// Our retired file painted `BENILLA_FALLBACK_ICON` (a question mark) whenever the texture was
|
||||
// nil. The chain's `GroupLootFrame_OnShow` just calls `SetTexture(texture)` with whatever
|
||||
// `GetLootRollItemInfo` gave it, so an unresolved item leaves the icon empty. The migration
|
||||
// reverts our embellishment; whether that reads acceptably is a look call, recorded in 1838
|
||||
// rather than re-authored back in.
|
||||
s.resolve();
|
||||
let has_fallback_icon = s.extract().iter().any(|q| {
|
||||
matches!(&q.content, QuadContent::Texture { path: Some(p), .. }
|
||||
if p.contains("INV_Misc_QuestionMark"))
|
||||
});
|
||||
assert!(has_fallback_icon, "in-flight roll shows the fallback icon");
|
||||
assert!(
|
||||
!has_fallback_icon,
|
||||
"the reference paints no placeholder for an unresolved item"
|
||||
);
|
||||
// What DOES still hold: no error, and the dialog is up and usable.
|
||||
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
|
||||
}
|
||||
|
||||
@@ -415,9 +447,10 @@ fn in_flight_roll_does_not_error_and_falls_back() {
|
||||
/// identity changes under an open frame — the snapshot finally arriving, or a late item template —
|
||||
/// `UPDATE_LOOT_ROLL(rollID)` repaints it.
|
||||
#[test]
|
||||
fn a_roll_that_opens_before_its_snapshot_repaints_when_it_lands() {
|
||||
fn a_roll_that_opens_before_its_snapshot_stays_blank() {
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
let mut s = setup();
|
||||
load_xml(&s, "GroupLootFrame.xml");
|
||||
load_group_loot(&s);
|
||||
|
||||
// The app's order: the event first, against a model that has no such roll at all.
|
||||
s.fire_event(
|
||||
@@ -428,10 +461,12 @@ fn a_roll_that_opens_before_its_snapshot_repaints_when_it_lands() {
|
||||
assert!(s
|
||||
.eval::<bool>("return GroupLootFrame1:IsVisible()")
|
||||
.unwrap());
|
||||
// The chain's FontString has NO text until something paints it, where our retired copy
|
||||
// defaulted to `""` — so this reads Option and expects None (decision 1838).
|
||||
assert_eq!(
|
||||
s.eval::<String>("return GroupLootFrame1Name:GetText()")
|
||||
s.eval::<Option<String>>("return GroupLootFrame1Name:GetText()")
|
||||
.unwrap(),
|
||||
"",
|
||||
None,
|
||||
"nothing to paint yet — this is the state the director saw"
|
||||
);
|
||||
|
||||
@@ -440,33 +475,44 @@ fn a_roll_that_opens_before_its_snapshot_repaints_when_it_lands() {
|
||||
s.fire_event("UPDATE_LOOT_ROLL", vec![ScriptValue::Int(7)]);
|
||||
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
|
||||
|
||||
// **The repaint does not happen, and this is the migration's one real loss.** Our retired file
|
||||
// split the paint out of `OnShow` precisely so `UPDATE_LOOT_ROLL` could re-enter it; the
|
||||
// reference has no such seam, because its `GetLootRollItemInfo` reads live C state that is
|
||||
// already populated when `START_LOOT_ROLL` fires. Ours reads a pushed snapshot, and while
|
||||
// `feed_loot_rolls` does push it BEFORE firing, the item template can still be in flight.
|
||||
//
|
||||
// The fix is app-side ordering, not a Lua shim: hold the roll until its template resolves.
|
||||
// Decision 1838 carries that, and the engine question an adapter ran into on the way — a
|
||||
// `Hide()`/`Show()` round trip re-fires `OnShow` from a plain chunk but not from inside an
|
||||
// event handler.
|
||||
assert_eq!(
|
||||
s.eval::<String>("return GroupLootFrame1Name:GetText()")
|
||||
s.eval::<Option<String>>("return GroupLootFrame1Name:GetText()")
|
||||
.unwrap(),
|
||||
"Staff of Jordan",
|
||||
"the repaint fills in the name the OnShow could not know"
|
||||
None,
|
||||
"the name stays blank: the reference has no repaint path"
|
||||
);
|
||||
assert!(
|
||||
s.eval::<bool>("return GroupLootFrame1Decoration:IsShown()")
|
||||
!s.eval::<bool>("return GroupLootFrame1Decoration:IsShown()")
|
||||
.unwrap(),
|
||||
"and the BoP gold decoration, which OnShow also painted from nothing"
|
||||
"and so does the BoP decoration"
|
||||
);
|
||||
s.resolve();
|
||||
let quads = s.extract();
|
||||
assert!(
|
||||
quads.iter().any(|q| {
|
||||
!quads.iter().any(|q| {
|
||||
matches!(&q.content, QuadContent::Texture { path: Some(p), .. }
|
||||
if p.contains("INV_Staff_12"))
|
||||
}),
|
||||
"and the real icon replaced the fallback"
|
||||
"and no icon arrives either — same missing repaint, same one cause"
|
||||
);
|
||||
// An update for a roll this frame does not hold leaves it alone.
|
||||
// An update for a roll this frame does not hold still leaves it alone and still does not
|
||||
// error — the half of this test that survives the migration intact.
|
||||
s.fire_event("UPDATE_LOOT_ROLL", vec![ScriptValue::Int(999)]);
|
||||
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
|
||||
assert_eq!(
|
||||
s.eval::<String>("return GroupLootFrame1Name:GetText()")
|
||||
s.eval::<Option<String>>("return GroupLootFrame1Name:GetText()")
|
||||
.unwrap(),
|
||||
"Staff of Jordan"
|
||||
None
|
||||
);
|
||||
}
|
||||
|
||||
@@ -482,6 +528,7 @@ fn a_roll_that_opens_before_its_snapshot_repaints_when_it_lands() {
|
||||
/// 42, pet 42.
|
||||
#[test]
|
||||
fn managed_positions_engage_for_the_bare_frame_name() {
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
let mut s = UiScript::new().unwrap();
|
||||
s.set_screen_size(1024.0, 768.0);
|
||||
load_xml(&s, "Fonts.xml");
|
||||
@@ -492,7 +539,10 @@ fn managed_positions_engage_for_the_bare_frame_name() {
|
||||
// taking every BenillaGroupLootFrame_* function down with it, not just the popup.
|
||||
load_xml(&s, "MoneyFrame.xml");
|
||||
load_xml(&s, "UiPanels.xml");
|
||||
load_xml(&s, "GroupLootFrame.xml");
|
||||
// `TOOLTIP_DEFAULT_COLOR`, which the chain's dropdown backdrops read in their OnLoad — the
|
||||
// dropdown kit rides in with the loot window now (1838), so this bespoke setup needs it too.
|
||||
load_xml(&s, "GameTooltip.xml");
|
||||
load_group_loot(&s);
|
||||
|
||||
let bottom = |s: &UiScript| s.eval::<f64>("return GroupLootFrame1:GetBottom()").unwrap();
|
||||
|
||||
@@ -534,8 +584,9 @@ fn managed_positions_engage_for_the_bare_frame_name() {
|
||||
/// (or a swallowed one from the dice) is exactly the regression worth catching.
|
||||
#[test]
|
||||
fn ctrl_and_shift_on_the_roll_icon_preview_and_post_its_link() {
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
let mut s = setup();
|
||||
load_xml(&s, "GroupLootFrame.xml");
|
||||
load_group_loot(&s);
|
||||
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
|
||||
@@ -618,8 +669,9 @@ fn ctrl_and_shift_on_the_roll_icon_preview_and_post_its_link() {
|
||||
/// to be nested inside the `IconFrame` **button**, not hung off the template root.
|
||||
#[test]
|
||||
fn the_roll_template_carries_the_reference_name_and_the_parts_addons_reach_for() {
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
let s = setup();
|
||||
load_xml(&s, "GroupLootFrame.xml");
|
||||
load_group_loot(&s);
|
||||
|
||||
// The reference's name resolves — and, since 1253, a miss would RAISE rather than hand back a
|
||||
// bare frame, so this call failing is this test failing.
|
||||
@@ -664,6 +716,7 @@ fn the_roll_template_carries_the_reference_name_and_the_parts_addons_reach_for()
|
||||
/// the eventual GroupLootFrame migration from shipping the loot bug a second time.
|
||||
#[test]
|
||||
fn the_stock_group_loot_frame_survives_an_in_flight_roll() {
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
let mut s = UiScript::new().unwrap();
|
||||
s.set_screen_size(1024.0, 768.0);
|
||||
for f in super::test_ui::LOOT_UI {
|
||||
|
||||
@@ -367,13 +367,21 @@ fn shown_inspect_honor_page() -> UiScript {
|
||||
s.run(RANK_GLOBALS).unwrap();
|
||||
for file in [
|
||||
"Fonts.xml",
|
||||
// `TEXT`, the stock level line's formatter.
|
||||
"BasicControls.xml",
|
||||
"MoneyFrame.xml",
|
||||
"UiPanels.xml",
|
||||
// `InspectUnit`'s home since 1832.
|
||||
"UIParent.xml",
|
||||
"GameTooltip.xml",
|
||||
// Before InspectFrame.xml — the honor page inherits this file's row templates and calls
|
||||
// Before the inspect addon — its honor page inherits this file's row templates and calls
|
||||
// its two shared painters, and `inherits=` resolves at load (the manifest's own order).
|
||||
// The stock inspect slot buttons inherit this (1832).
|
||||
"Interface\\FrameXML\\ItemButtonTemplate.xml",
|
||||
"Interface\\FrameXML\\HonorFrame.xml",
|
||||
"InspectFrame.xml",
|
||||
"Interface\\AddOns\\Blizzard_InspectUI\\Blizzard_InspectUI.xml",
|
||||
"Interface\\AddOns\\Blizzard_InspectUI\\InspectPaperDollFrame.xml",
|
||||
"Interface\\AddOns\\Blizzard_InspectUI\\InspectHonorFrame.xml",
|
||||
] {
|
||||
load_xml(&s, file);
|
||||
}
|
||||
@@ -390,8 +398,7 @@ fn shown_inspect_honor_page() -> UiScript {
|
||||
s.set_unit_reach(HashMap::from([("target".to_string(), reach(16.0))]));
|
||||
s.set_inspect_honor(Some(inspect_reply()));
|
||||
s.run(r#"InspectUnit("target")"#).unwrap();
|
||||
s.run(r#"ToggleInspect("BenillaInspectHonorFrame")"#)
|
||||
.unwrap();
|
||||
s.run(r#"ToggleInspect("InspectHonorFrame")"#).unwrap();
|
||||
s.resolve();
|
||||
s
|
||||
}
|
||||
@@ -403,36 +410,30 @@ fn the_inspect_page_paints_the_reply_it_holds() {
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
let mut s = shown_inspect_honor_page();
|
||||
for (frame, want) in [
|
||||
("BenillaInspectHonorFrameCurrentHKValue", "17"),
|
||||
("BenillaInspectHonorFrameCurrentDKValue", "2"),
|
||||
("BenillaInspectHonorFrameYesterdayHKValue", "41"),
|
||||
("BenillaInspectHonorFrameYesterdayContributionValue", "640"),
|
||||
("BenillaInspectHonorFrameThisWeekHKValue", "123"),
|
||||
("BenillaInspectHonorFrameThisWeekContributionValue", "1250"),
|
||||
("BenillaInspectHonorFrameLastWeekHKValue", "420"),
|
||||
("BenillaInspectHonorFrameLastWeekContributionValue", "8431"),
|
||||
("BenillaInspectHonorFrameLastWeekStandingValue", "57"),
|
||||
("BenillaInspectHonorFrameLifeTimeHKValue", "3907"),
|
||||
("BenillaInspectHonorFrameLifeTimeDKValue", "12"),
|
||||
(
|
||||
"BenillaInspectHonorFrameLifeTimeRankValue",
|
||||
"Lieutenant Commander",
|
||||
),
|
||||
("InspectHonorFrameCurrentHKValue", "17"),
|
||||
("InspectHonorFrameCurrentDKValue", "2"),
|
||||
("InspectHonorFrameYesterdayHKValue", "41"),
|
||||
("InspectHonorFrameYesterdayContributionValue", "640"),
|
||||
("InspectHonorFrameThisWeekHKValue", "123"),
|
||||
("InspectHonorFrameThisWeekContributionValue", "1250"),
|
||||
("InspectHonorFrameLastWeekHKValue", "420"),
|
||||
("InspectHonorFrameLastWeekContributionValue", "8431"),
|
||||
("InspectHonorFrameLastWeekStandingValue", "57"),
|
||||
("InspectHonorFrameLifeTimeHKValue", "3907"),
|
||||
("InspectHonorFrameLifeTimeDKValue", "12"),
|
||||
("InspectHonorFrameLifeTimeRankValue", "Lieutenant Commander"),
|
||||
] {
|
||||
assert_eq!(text(&mut s, frame), want, "{frame}");
|
||||
}
|
||||
// The rank block: the target's CURRENT rank (12 → "Knight-Captain", badge 08), and the bar off
|
||||
// the reply's own byte rather than the player's.
|
||||
assert_eq!(
|
||||
text(&mut s, "BenillaInspectHonorFrameCurrentPVPTitle"),
|
||||
text(&mut s, "InspectHonorFrameCurrentPVPTitle"),
|
||||
"Knight-Captain"
|
||||
);
|
||||
assert_eq!(
|
||||
text(&mut s, "BenillaInspectHonorFrameCurrentPVPRank"),
|
||||
"(Rank 8)"
|
||||
);
|
||||
assert_eq!(text(&mut s, "InspectHonorFrameCurrentPVPRank"), "(Rank 8)");
|
||||
let bar = s
|
||||
.eval::<f64>("return BenillaInspectHonorFrameProgressBar:GetValue()")
|
||||
.eval::<f64>("return InspectHonorFrameProgressBar:GetValue()")
|
||||
.unwrap();
|
||||
assert!(
|
||||
(bar - 191.0 / 255.0).abs() < 1e-6,
|
||||
@@ -461,7 +462,7 @@ fn the_page_asks_only_when_it_holds_nothing() {
|
||||
// Cleared — the app drops the reply when the inspected player changes. Re-showing the page
|
||||
// asks, and paints nothing until an answer lands.
|
||||
s.set_inspect_honor(None);
|
||||
s.run(r#"BenillaInspectHonorFrame_OnShow()"#).unwrap();
|
||||
s.run(r#"InspectHonorFrame_OnShow()"#).unwrap();
|
||||
s.resolve();
|
||||
assert_eq!(
|
||||
s.take_inspect_honor_requests(),
|
||||
|
||||
@@ -111,15 +111,39 @@ fn drawn(s: &mut UiScript, needle: &str) -> bool {
|
||||
fn armed() -> UiScript {
|
||||
let mut s = UiScript::new().unwrap();
|
||||
s.set_screen_size(1024.0, 768.0);
|
||||
// `PLAYER_LEVEL`, the template the stock level line formats through — the reference keeps its
|
||||
// strings here, and our retired file had the sentence written into it (1832).
|
||||
load_xml(&s, "Interface\\FrameXML\\GlobalStrings.lua");
|
||||
load_xml(&s, "Fonts.xml");
|
||||
// `TEXT`, which the stock `InspectPaperDollFrame_SetLevel` formats its level line through.
|
||||
load_xml(&s, "BasicControls.xml");
|
||||
load_xml(&s, "MoneyFrame.xml");
|
||||
load_xml(&s, "UiPanels.xml");
|
||||
// `InspectUnit` lives here now — the reference's own home for that name, and no longer in the
|
||||
// window's own file (decision 1832). The manifest's order, and 49 other harnesses'.
|
||||
load_xml(&s, "UIParent.xml");
|
||||
load_xml(&s, "GameTooltip.xml");
|
||||
// Before InspectFrame.xml, and required rather than tidy: this window's honor page inherits
|
||||
// HonorFrame.xml's five row templates and `inherits=` resolves at LOAD, so without it the
|
||||
// twelve honor rows materialize bare (decision 1512; the manifest states the same order).
|
||||
// The stock slot buttons inherit `ItemButtonTemplate` — where their `$parentIconTexture`
|
||||
// comes from, which `InspectPaperDollItemSlotButton_OnLoad` indexes unconditionally. Our own
|
||||
// slot template declared its icon inline and needed no such file; the manifest has carried
|
||||
// this entry far above the inspect window all along (decision 1832).
|
||||
load_xml(&s, "Interface\\FrameXML\\ItemButtonTemplate.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\HonorFrame.xml");
|
||||
load_xml(&s, "InspectFrame.xml");
|
||||
load_xml(
|
||||
&s,
|
||||
"Interface\\AddOns\\Blizzard_InspectUI\\Blizzard_InspectUI.xml",
|
||||
);
|
||||
load_xml(
|
||||
&s,
|
||||
"Interface\\AddOns\\Blizzard_InspectUI\\InspectPaperDollFrame.xml",
|
||||
);
|
||||
load_xml(
|
||||
&s,
|
||||
"Interface\\AddOns\\Blizzard_InspectUI\\InspectHonorFrame.xml",
|
||||
);
|
||||
s.set_unit("target", Some(target_unit()));
|
||||
s.set_inspect(Some(inspect_view("target")));
|
||||
// 4 yards away (d² = 16) — comfortably inside the verified 100.0.
|
||||
@@ -132,25 +156,51 @@ fn armed() -> UiScript {
|
||||
fn shipped_inspect_frame_loads_clean() {
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
let s = UiScript::new().unwrap();
|
||||
// `PLAYER_LEVEL`, the template the stock level line formats through — the reference keeps its
|
||||
// strings here, and our retired file had the sentence written into it (1832).
|
||||
load_xml(&s, "Interface\\FrameXML\\GlobalStrings.lua");
|
||||
load_xml(&s, "Fonts.xml");
|
||||
// `TEXT`, which the stock `InspectPaperDollFrame_SetLevel` formats its level line through.
|
||||
load_xml(&s, "BasicControls.xml");
|
||||
load_xml(&s, "MoneyFrame.xml");
|
||||
load_xml(&s, "UiPanels.xml");
|
||||
// `InspectUnit` lives here now — the reference's own home for that name, and no longer in the
|
||||
// window's own file (decision 1832). The manifest's order, and 49 other harnesses'.
|
||||
load_xml(&s, "UIParent.xml");
|
||||
load_xml(&s, "GameTooltip.xml");
|
||||
// Before InspectFrame.xml, and required rather than tidy: this window's honor page inherits
|
||||
// HonorFrame.xml's five row templates and `inherits=` resolves at LOAD, so without it the
|
||||
// twelve honor rows materialize bare (decision 1512; the manifest states the same order).
|
||||
// The stock slot buttons inherit `ItemButtonTemplate` — where their `$parentIconTexture`
|
||||
// comes from, which `InspectPaperDollItemSlotButton_OnLoad` indexes unconditionally. Our own
|
||||
// slot template declared its icon inline and needed no such file; the manifest has carried
|
||||
// this entry far above the inspect window all along (decision 1832).
|
||||
load_xml(&s, "Interface\\FrameXML\\ItemButtonTemplate.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\HonorFrame.xml");
|
||||
load_xml(&s, "InspectFrame.xml");
|
||||
load_xml(
|
||||
&s,
|
||||
"Interface\\AddOns\\Blizzard_InspectUI\\Blizzard_InspectUI.xml",
|
||||
);
|
||||
load_xml(
|
||||
&s,
|
||||
"Interface\\AddOns\\Blizzard_InspectUI\\InspectPaperDollFrame.xml",
|
||||
);
|
||||
load_xml(
|
||||
&s,
|
||||
"Interface\\AddOns\\Blizzard_InspectUI\\InspectHonorFrame.xml",
|
||||
);
|
||||
// All 19 slots exist and carry their GetInventorySlotInfo id (1..=19, no ammo slot).
|
||||
// The stock `InspectPaperDollItemSlotButton_OnLoad` puts it on the frame's own ID
|
||||
// (`this:SetID(id)`); our retired file kept it in an `invSlotId` field of its own (1832).
|
||||
for (name, id) in [
|
||||
("BenillaInspectHeadSlot", 1),
|
||||
("BenillaInspectBackSlot", 15),
|
||||
("BenillaInspectMainHandSlot", 16),
|
||||
("BenillaInspectRangedSlot", 18),
|
||||
("BenillaInspectTabardSlot", 19),
|
||||
("InspectHeadSlot", 1),
|
||||
("InspectBackSlot", 15),
|
||||
("InspectMainHandSlot", 16),
|
||||
("InspectRangedSlot", 18),
|
||||
("InspectTabardSlot", 19),
|
||||
] {
|
||||
assert_eq!(
|
||||
s.eval::<i64>(&format!("return {name}.invSlotId")).unwrap(),
|
||||
s.eval::<i64>(&format!("return {name}:GetID()")).unwrap(),
|
||||
id,
|
||||
"{name} inventory slot id"
|
||||
);
|
||||
@@ -169,8 +219,7 @@ fn inspect_unit_refuses_out_of_range() {
|
||||
s.run(r#"InspectUnit("target")"#).unwrap();
|
||||
assert!(s.errors().is_empty(), "errors: {:?}", s.errors());
|
||||
assert!(
|
||||
!s.eval::<bool>("return BenillaInspectFrame:IsVisible()")
|
||||
.unwrap(),
|
||||
!s.eval::<bool>("return InspectFrame:IsVisible()").unwrap(),
|
||||
"out of range: the window must not open"
|
||||
);
|
||||
assert!(
|
||||
@@ -182,8 +231,7 @@ fn inspect_unit_refuses_out_of_range() {
|
||||
s.set_unit_reach(HashMap::from([("target".to_string(), reach(99.9))]));
|
||||
s.run(r#"InspectUnit("target")"#).unwrap();
|
||||
assert!(
|
||||
s.eval::<bool>("return BenillaInspectFrame:IsVisible()")
|
||||
.unwrap(),
|
||||
s.eval::<bool>("return InspectFrame:IsVisible()").unwrap(),
|
||||
"in range: the window opens"
|
||||
);
|
||||
assert_eq!(
|
||||
@@ -330,22 +378,18 @@ fn shipped_inspect_frame_drives_end_to_end() {
|
||||
let mut s = armed();
|
||||
|
||||
// Hidden at load, and no sound queued (never transitions on startup).
|
||||
assert!(!s
|
||||
.eval::<bool>("return BenillaInspectFrame:IsVisible()")
|
||||
.unwrap());
|
||||
assert!(!s.eval::<bool>("return InspectFrame:IsVisible()").unwrap());
|
||||
assert!(s.take_sounds().is_empty());
|
||||
// The pane's OnLoad set the ref's own default facing (InspectModelFrame_OnLoad, 0.61).
|
||||
assert!(
|
||||
(s.inspect_yaw() - 0.61).abs() < 0.0001,
|
||||
(s.model_pane_facing("InspectModelFrame") - 0.61).abs() < 0.0001,
|
||||
"default facing 0.61, got {}",
|
||||
s.inspect_yaw()
|
||||
s.model_pane_facing("InspectModelFrame")
|
||||
);
|
||||
|
||||
s.run(r#"InspectUnit("target")"#).unwrap();
|
||||
assert!(s.errors().is_empty(), "open errors: {:?}", s.errors());
|
||||
assert!(s
|
||||
.eval::<bool>("return BenillaInspectFrame:IsVisible()")
|
||||
.unwrap());
|
||||
assert!(s.eval::<bool>("return InspectFrame:IsVisible()").unwrap());
|
||||
assert_eq!(
|
||||
s.take_sounds(),
|
||||
vec![SoundRequest::KitName("igCharacterInfoOpen".into())],
|
||||
@@ -353,13 +397,13 @@ fn shipped_inspect_frame_drives_end_to_end() {
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
s.eval::<String>("return BenillaInspectNameText:GetText()")
|
||||
s.eval::<String>("return InspectNameText:GetText()")
|
||||
.unwrap(),
|
||||
"Thargrim",
|
||||
"the name line reads UnitName of the inspected token"
|
||||
);
|
||||
assert_eq!(
|
||||
s.eval::<String>("return BenillaInspectLevelText:GetText()")
|
||||
s.eval::<String>("return InspectLevelText:GetText()")
|
||||
.unwrap(),
|
||||
"Level 34 Dwarf Paladin",
|
||||
"the level line reads the inspected unit, not the player"
|
||||
@@ -406,21 +450,18 @@ fn shipped_inspect_frame_drives_end_to_end() {
|
||||
|
||||
// Rotate: the ref's sign convention (left subtracts, right adds 0.03) onto the booth yaw, with
|
||||
// the kit each click.
|
||||
s.run("BenillaInspectModelFrameRotateLeftButton:Click()")
|
||||
.unwrap();
|
||||
s.run("InspectModelRotateLeftButton:Click()").unwrap();
|
||||
assert!(
|
||||
(s.inspect_yaw() - 0.58).abs() < 0.0001,
|
||||
(s.model_pane_facing("InspectModelFrame") - 0.58).abs() < 0.0001,
|
||||
"rotate-left subtracts 0.03, got {}",
|
||||
s.inspect_yaw()
|
||||
s.model_pane_facing("InspectModelFrame")
|
||||
);
|
||||
s.run("BenillaInspectModelFrameRotateRightButton:Click()")
|
||||
.unwrap();
|
||||
s.run("BenillaInspectModelFrameRotateRightButton:Click()")
|
||||
.unwrap();
|
||||
s.run("InspectModelRotateRightButton:Click()").unwrap();
|
||||
s.run("InspectModelRotateRightButton:Click()").unwrap();
|
||||
assert!(
|
||||
(s.inspect_yaw() - 0.64).abs() < 0.0001,
|
||||
(s.model_pane_facing("InspectModelFrame") - 0.64).abs() < 0.0001,
|
||||
"rotate-right adds 0.03, got {}",
|
||||
s.inspect_yaw()
|
||||
s.model_pane_facing("InspectModelFrame")
|
||||
);
|
||||
assert_eq!(
|
||||
s.take_sounds(),
|
||||
@@ -438,10 +479,8 @@ fn shipped_inspect_frame_drives_end_to_end() {
|
||||
!s.take_inspect_clear(),
|
||||
"ClearInspectPlayer not called while open"
|
||||
);
|
||||
s.run(r#"HideUIPanel(BenillaInspectFrame)"#).unwrap();
|
||||
assert!(!s
|
||||
.eval::<bool>("return BenillaInspectFrame:IsVisible()")
|
||||
.unwrap());
|
||||
s.run(r#"HideUIPanel(InspectFrame)"#).unwrap();
|
||||
assert!(!s.eval::<bool>("return InspectFrame:IsVisible()").unwrap());
|
||||
assert_eq!(
|
||||
s.take_sounds(),
|
||||
vec![SoundRequest::KitName("igCharacterInfoClose".into())],
|
||||
@@ -471,30 +510,69 @@ fn the_active_tab_is_inert_and_toggle_inspect_closes() {
|
||||
|
||||
// NB this engine's `IsEnabled` returns a plain bool, not the live API's 1/nil.
|
||||
assert!(
|
||||
!s.eval::<bool>("return BenillaInspectFrameTab1:IsEnabled() ~= 0")
|
||||
!s.eval::<bool>("return InspectFrameTab1:IsEnabled() ~= 0")
|
||||
.unwrap(),
|
||||
"the selected tab is disabled (PanelTemplates_SelectTab), so its click is inert"
|
||||
);
|
||||
s.run("BenillaInspectFrameTab1:Click()").unwrap();
|
||||
s.run("InspectFrameTab1:Click()").unwrap();
|
||||
assert!(
|
||||
s.eval::<bool>("return BenillaInspectFrame:IsVisible()")
|
||||
.unwrap(),
|
||||
s.eval::<bool>("return InspectFrame:IsVisible()").unwrap(),
|
||||
"clicking the disabled active tab changes nothing"
|
||||
);
|
||||
|
||||
// The reachable path: ToggleInspect on the page already showing closes the window.
|
||||
s.run(r#"ToggleInspect("BenillaInspectPaperDollFrame")"#)
|
||||
.unwrap();
|
||||
s.run(r#"ToggleInspect("InspectPaperDollFrame")"#).unwrap();
|
||||
assert!(
|
||||
!s.eval::<bool>("return BenillaInspectFrame:IsVisible()")
|
||||
.unwrap(),
|
||||
!s.eval::<bool>("return InspectFrame:IsVisible()").unwrap(),
|
||||
"ToggleInspect on the showing page closes the window (ref ToggleInspect)"
|
||||
);
|
||||
// And again re-opens it, the other branch of the same function.
|
||||
s.run(r#"ToggleInspect("BenillaInspectPaperDollFrame")"#)
|
||||
.unwrap();
|
||||
assert!(s
|
||||
.eval::<bool>("return BenillaInspectFrame:IsVisible()")
|
||||
.unwrap());
|
||||
assert!(s.errors().is_empty(), "tab errors: {:?}", s.errors());
|
||||
}
|
||||
|
||||
/// The other branch of `ToggleInspect` — re-opening after a close — **errors, and that is the
|
||||
/// reference's own behaviour** (decision 1834 settled what 1832 had to leave open).
|
||||
///
|
||||
/// `InspectFrame_OnHide` sets `this.unit = nil`, so a re-open runs `InspectFrame_OnShow` with no
|
||||
/// token. wow-re censused the whole binding table: `SetPortraitTexture 0x519ef0` gates its unit
|
||||
/// argument at `0x519fb4` with `lua_isstring` and raises `Usage: SetPortraitTexture(texture,
|
||||
/// "unit")`, and `luaL_error` does not return — so the reference raises here too, on its own
|
||||
/// shipped code. The widget clear sits six instructions PAST the gate, so the portrait is not even
|
||||
/// blanked; it is left exactly as it was.
|
||||
///
|
||||
/// In the reference the raise unwinds to the handler's `lua_pcall`, abandons the rest of `OnShow`,
|
||||
/// and pops the ScriptErrors dialog through `_ERRORMESSAGE`. Ours collects it into `errors` and
|
||||
/// abandons the handler the same way. So: the window shows, the portrait and name do not update,
|
||||
/// and there is exactly one error — which is what this now asserts.
|
||||
///
|
||||
/// One correction from that census worth keeping: stock 1.12 ships **no** inspect keybind (0 of 234
|
||||
/// `Binding name=` entries) and no caller of `ToggleInspect` outside the addon's own tab buttons,
|
||||
/// which are children of the hidden `InspectFrame`. So on the stock UI's own paths this is
|
||||
/// unreachable — only an addon or a macro gets here. It is still ours to reproduce.
|
||||
#[test]
|
||||
fn toggle_inspect_reopens_after_a_close_and_raises_like_the_reference() {
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
let s = armed();
|
||||
s.run(r#"InspectUnit("target")"#).unwrap();
|
||||
s.run(r#"ToggleInspect("InspectPaperDollFrame")"#).unwrap();
|
||||
assert!(!s.eval::<bool>("return InspectFrame:IsVisible()").unwrap());
|
||||
let _ = s.errors();
|
||||
|
||||
s.run(r#"ToggleInspect("InspectPaperDollFrame")"#).unwrap();
|
||||
assert!(
|
||||
s.eval::<bool>("return InspectFrame:IsVisible()").unwrap(),
|
||||
"the window still shows — Show() runs before OnShow raises"
|
||||
);
|
||||
// TWO raises, not one: `InspectFrame` and `InspectPaperDollFrame` each have their own OnShow,
|
||||
// each fired under its own pcall, so each abandons independently — the first at the portrait's
|
||||
// gate, the second at `UnitLevel`'s. Both carry the reference's own `Usage:` text.
|
||||
let errors = s.errors();
|
||||
assert_eq!(errors.len(), 2, "one raise per OnShow handler: {errors:?}");
|
||||
assert!(
|
||||
errors[0].contains(r#"Usage: SetPortraitTexture(texture, "unit")"#),
|
||||
"the portrait's gate, before the name line is ever reached: {errors:?}"
|
||||
);
|
||||
assert!(
|
||||
errors[1].contains(r#"Usage: UnitLevel("unit")"#),
|
||||
"and the paper doll's own handler, dying at its first getter: {errors:?}"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -387,6 +387,7 @@ fn search_surfaces_bindings_as_live_rows_under_the_redirect_head() {
|
||||
// with the match painted LIVE on the search pool (the era reflows its real rows the
|
||||
// same way).
|
||||
s.run(r#"OptionsFrameSearchBox:SetText("jump")"#).unwrap();
|
||||
s.tick(0.0); // the deferred OnTextChanged drains here (decision 1831)
|
||||
assert!(s
|
||||
.eval::<bool>("return OptionsFrameContainerBodySearchHeadKeybindings:IsVisible()")
|
||||
.unwrap());
|
||||
@@ -419,6 +420,7 @@ fn search_surfaces_bindings_as_live_rows_under_the_redirect_head() {
|
||||
// The head is the era redirect: clicking it ends the search on the Keybindings page.
|
||||
s.run("OptionsFrameContainerBodySearchHeadKeybindings:Click()")
|
||||
.unwrap();
|
||||
s.tick(0.0); // the deferred OnTextChanged drains here (decision 1831)
|
||||
assert_eq!(
|
||||
s.eval::<String>("return OptionsFrame.selectedCategory")
|
||||
.unwrap(),
|
||||
@@ -691,6 +693,7 @@ fn the_pet_lane_is_registered_under_the_action_bar_header() {
|
||||
// Page side: search finds it and paints a live row with the capsule filled.
|
||||
s.run(r#"OptionsFrameSearchBox:SetText("bonusactionbutton1")"#)
|
||||
.unwrap();
|
||||
s.tick(0.0); // the deferred OnTextChanged drains here (decision 1831)
|
||||
assert!(s
|
||||
.eval::<bool>("return OptionsFrameContainerBodySearchHeadKeybindings:IsVisible()")
|
||||
.unwrap());
|
||||
|
||||
@@ -140,11 +140,14 @@ fn new_then_pick_an_icon_then_okay_creates_the_macro() {
|
||||
/// A regression here silently discards everything the player typed.
|
||||
#[test]
|
||||
fn typing_a_body_and_closing_the_window_commits_it() {
|
||||
let s = harness();
|
||||
let mut s = harness();
|
||||
s.run(r#"CreateMacro("Ambush", 1, "")"#).unwrap();
|
||||
s.run("ShowMacroFrame()").unwrap();
|
||||
s.run(r#"BenillaMacroFrameText:SetText("/cast Ambush\n/say pew")"#)
|
||||
.unwrap();
|
||||
// The window's dirty flag comes from the box's `OnTextChanged`, which the drain owes until the
|
||||
// next frame (decision 1831).
|
||||
s.tick(0.0);
|
||||
no_errors(&s, "type");
|
||||
assert!(
|
||||
s.eval::<bool>("return BenillaMacroFrame.textChanged == 1")
|
||||
@@ -172,13 +175,14 @@ fn typing_a_body_and_closing_the_window_commits_it() {
|
||||
/// point: `MacroFrame.macroBase` is 0 or MAX_MACROS, and every binding takes `macroBase + i`.
|
||||
#[test]
|
||||
fn the_character_tab_creates_in_the_second_index_range_and_switching_saves() {
|
||||
let s = harness();
|
||||
let mut s = harness();
|
||||
s.run(r#"CreateMacro("Acct", 1, "")"#).unwrap();
|
||||
s.run("ShowMacroFrame()").unwrap();
|
||||
|
||||
// Type into the account macro, then switch tabs WITHOUT any explicit save.
|
||||
s.run(r#"BenillaMacroFrameText:SetText("/say account")"#)
|
||||
.unwrap();
|
||||
s.tick(0.0); // as above — the edit marks, the drain notifies (1831)
|
||||
s.run("BenillaMacroFrameTab2:Click()").unwrap();
|
||||
no_errors(&s, "tab 2");
|
||||
assert_eq!(
|
||||
|
||||
@@ -361,3 +361,70 @@ fn the_money_type_table_carries_all_seven_reference_types() {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// **`MoneyInputFrame.lua` is `EditBox:SetNumber`'s only caller on the whole chain**, and the verb
|
||||
/// did not exist until decision 1831 — so the three-box amount editor could not put a number in a
|
||||
/// box at all. This drives the reference's own file: split an amount, read it back, round-trip.
|
||||
///
|
||||
/// The boxes are `numeric="true"`, which is why the value→text law matters here specifically: a
|
||||
/// non-digit abandons the insert wholesale and leaves the box EMPTY. Every value this path passes
|
||||
/// is a non-negative integer, so `%.14g` yields plain digits and the gate never trips — but the
|
||||
/// zero case below is the one that would show if it ever did.
|
||||
#[test]
|
||||
fn the_chains_money_input_frame_splits_an_amount_across_its_three_boxes() {
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
let s = UiScript::new().unwrap();
|
||||
load_xml(&s, "Fonts.xml");
|
||||
load_xml(&s, "MoneyFrame.xml"); // COPPER_PER_GOLD / COPPER_PER_SILVER live here
|
||||
// The manifest's own order: the `.lua` brings the ten verbs, the `.xml` the template.
|
||||
load_xml(&s, r"Interface\FrameXML\MoneyInputFrame.lua");
|
||||
load_xml(&s, r"Interface\FrameXML\MoneyInputFrame.xml");
|
||||
|
||||
let doc = benilla_ui::framexml::parse(
|
||||
r#"<Ui>
|
||||
<Frame name="TestAmount" inherits="MoneyInputFrameTemplate">
|
||||
<Anchors><Anchor point="TOPLEFT"/></Anchors>
|
||||
</Frame>
|
||||
</Ui>"#,
|
||||
)
|
||||
.unwrap();
|
||||
let report = benilla_ui::loader::load_in(&s, &doc, "test", &|_: &str| None);
|
||||
assert!(report.errors.is_empty(), "{:?}", report.errors);
|
||||
|
||||
// 1234g 56s 78c.
|
||||
s.run("MoneyInputFrame_SetCopper(TestAmount, 12345678)")
|
||||
.unwrap();
|
||||
for (box_name, want) in [("Gold", "1234"), ("Silver", "56"), ("Copper", "78")] {
|
||||
assert_eq!(
|
||||
s.eval::<String>(&format!("return TestAmount{box_name}:GetText()"))
|
||||
.unwrap(),
|
||||
want,
|
||||
"TestAmount{box_name}"
|
||||
);
|
||||
}
|
||||
assert_eq!(
|
||||
s.eval::<f64>("return MoneyInputFrame_GetCopper(TestAmount)")
|
||||
.unwrap(),
|
||||
12345678.0,
|
||||
"the amount round-trips back out of the three boxes"
|
||||
);
|
||||
|
||||
// Zero is the reference's quiet case: `GetNumber()` on an empty box is already 0, so the
|
||||
// equality test short-circuits and `SetNumber` is never reached — the boxes stay EMPTY rather
|
||||
// than reading "0", and the total still comes back 0.
|
||||
s.run("MoneyInputFrame_ResetMoney(TestAmount)").unwrap();
|
||||
s.run("MoneyInputFrame_SetCopper(TestAmount, 0)").unwrap();
|
||||
for box_name in ["Gold", "Silver", "Copper"] {
|
||||
assert_eq!(
|
||||
s.eval::<String>(&format!("return TestAmount{box_name}:GetText()"))
|
||||
.unwrap(),
|
||||
"",
|
||||
"TestAmount{box_name} stays empty at zero"
|
||||
);
|
||||
}
|
||||
assert_eq!(
|
||||
s.eval::<f64>("return MoneyInputFrame_GetCopper(TestAmount)")
|
||||
.unwrap(),
|
||||
0.0
|
||||
);
|
||||
}
|
||||
|
||||
@@ -383,6 +383,8 @@ fn search_reflows_live_rows_and_restores_the_page() {
|
||||
.unwrap();
|
||||
|
||||
s.run("OptionsFrameSearchBox:SetText(\"music\")").unwrap();
|
||||
// The search reflows off the box's `OnTextChanged`, which is deferred to the drain (1831).
|
||||
s.tick(0.0);
|
||||
assert_eq!(
|
||||
s.eval::<String>("return OptionsFrameContainerTitle:GetText()")
|
||||
.unwrap(),
|
||||
@@ -440,6 +442,7 @@ fn search_reflows_live_rows_and_restores_the_page() {
|
||||
|
||||
// Clearing restores the page view and the authored chain EXACTLY.
|
||||
s.run("OptionsFrameSearchBox:SetText(\"\")").unwrap();
|
||||
s.tick(0.0); // the clear reflows on the drain, like the search itself (1831)
|
||||
assert_eq!(
|
||||
s.eval::<String>("return OptionsFrameContainerTitle:GetText()")
|
||||
.unwrap(),
|
||||
@@ -489,12 +492,16 @@ fn the_phrase_match_outranks_its_words() {
|
||||
/// the box) and a no-match query (the section-header "no results" line, nothing chained).
|
||||
#[test]
|
||||
fn a_head_click_ends_the_search_and_a_miss_shows_no_results() {
|
||||
let s = harness();
|
||||
let mut s = harness();
|
||||
s.run("ShowUIPanel(OptionsFrame)").unwrap();
|
||||
|
||||
s.run("OptionsFrameSearchBox:SetText(\"volume\")").unwrap();
|
||||
s.tick(0.0); // the head being clicked below is built by the search reflow, so drain first (1831)
|
||||
s.run("OptionsFrameContainerBodySearchHeadAudio:Click()")
|
||||
.unwrap();
|
||||
// The click clears the box synchronously; restoring the page rides that clear's
|
||||
// `OnTextChanged`, which the drain owes (1831).
|
||||
s.tick(0.0);
|
||||
assert_eq!(
|
||||
s.eval::<String>("return OptionsFrameSearchBox:GetText()")
|
||||
.unwrap(),
|
||||
@@ -517,6 +524,7 @@ fn a_head_click_ends_the_search_and_a_miss_shows_no_results() {
|
||||
|
||||
s.run("OptionsFrameSearchBox:SetText(\"flibbertigibbet\")")
|
||||
.unwrap();
|
||||
s.tick(0.0); // the miss renders on the drain too (1831)
|
||||
assert!(s
|
||||
.eval::<bool>("return OptionsFrameContainerBodyNoResults:IsVisible()")
|
||||
.unwrap());
|
||||
@@ -530,6 +538,7 @@ fn a_head_click_ends_the_search_and_a_miss_shows_no_results() {
|
||||
);
|
||||
}
|
||||
s.run("OptionsFrameSearchBox:SetText(\"\")").unwrap();
|
||||
s.tick(0.0); // and so does clearing it (1831)
|
||||
assert!(!s
|
||||
.eval::<bool>("return OptionsFrameContainerBodyNoResults:IsVisible()")
|
||||
.unwrap());
|
||||
|
||||
@@ -466,3 +466,40 @@ fn every_template_these_files_declare_is_a_real_1_12_name() {
|
||||
"only {checked} templates swept — the sweep, not the files, is what broke"
|
||||
);
|
||||
}
|
||||
|
||||
/// **`PanelTemplates_TabResize`'s `tab` argument is OPTIONAL, and omitting it means `this`.**
|
||||
///
|
||||
/// The reference opens with `if ( tab ) then tabName = tab:GetName(); else tabName =
|
||||
/// this:GetName(); tab = this; end`. Ours had only the first half, so `tab:GetName()` on a nil
|
||||
/// threw — and nothing in our tree noticed, because every caller we wrote passed a tab explicitly.
|
||||
/// The stock files do not: `Blizzard_MacroUI.xml`'s two tabs each call
|
||||
/// `PanelTemplates_TabResize(0)` from their own `OnLoad` and nothing else, so both threw at load
|
||||
/// and the window came up with unsized tabs. Decision 1835.
|
||||
///
|
||||
/// Driven through a real `OnLoad` rather than a direct call, because `this` is exactly what is
|
||||
/// under test: a plain function call would not set it.
|
||||
#[test]
|
||||
fn tab_resize_falls_back_to_this_when_no_tab_is_passed() {
|
||||
let mut s = harness();
|
||||
s.set_text_measurer(Box::new(super::FixedWidthFont(6.0)));
|
||||
let doc = benilla_ui::framexml::parse(
|
||||
r#"<Ui>
|
||||
<Button name="ProbeTab" inherits="CharacterFrameTabButtonTemplate" text="Macros">
|
||||
<Anchors><Anchor point="TOPLEFT"/></Anchors>
|
||||
<Scripts><OnLoad>PanelTemplates_TabResize(0)</OnLoad></Scripts>
|
||||
</Button>
|
||||
</Ui>"#,
|
||||
)
|
||||
.unwrap();
|
||||
let report = benilla_ui::loader::load_in(&s, &doc, "test", &|_: &str| None);
|
||||
assert!(
|
||||
report.errors.is_empty(),
|
||||
"the OnLoad must not throw: {:?}",
|
||||
report.errors
|
||||
);
|
||||
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
|
||||
// It sized the tab it was never handed: wider than the bare label, because the end slices are
|
||||
// added on top.
|
||||
let width = s.eval::<f64>("return ProbeTab:GetWidth()").unwrap();
|
||||
assert!(width > 0.0, "the tab got a width, got {width}");
|
||||
}
|
||||
|
||||
@@ -1125,7 +1125,7 @@ fn the_1507_registry_rows_match_the_reference_bytes() {
|
||||
// The whileDead flags the ref authors and 1507 carried (l.21, l.25, Blizzard_TalentUI:71).
|
||||
"UIPanelWindows['SpellBookFrame'].whileDead == 1",
|
||||
"UIPanelWindows['QuestLogFrame'].whileDead == 1",
|
||||
"UIPanelWindows['BenillaTalentFrame'].whileDead == 1",
|
||||
"UIPanelWindows['TalentFrame'].whileDead == 1",
|
||||
// UIChildWindows — UIParent.lua l.44-50 verbatim, all four shipped.
|
||||
"table.getn(UIChildWindows) == 4",
|
||||
"UIChildWindows[1] == 'OpenMailFrame'",
|
||||
|
||||
@@ -610,9 +610,17 @@ mod tests {
|
||||
///
|
||||
/// **A remaining `<?>` in the `fx=` column is usually a LoadOnDemand addon**, not something to
|
||||
/// build. `ClassTrainerFrame_Show`, `CraftFrame_Show`, `MacroFrame_SaveMacro`,
|
||||
/// `InspectFrame_Show`, `TalentFrame_Toggle` and their siblings live in `Blizzard_*` addons the
|
||||
/// install ships **packed as `.pub`**, so no amount of scanning the extracted FrameXML finds
|
||||
/// them. They arrive when that addon does, exactly like an `fx=` name with a home.
|
||||
/// `InspectFrame_Show`, `TalentFrame_Toggle` and their siblings live in `Blizzard_*` addons, so
|
||||
/// no amount of scanning the extracted **FrameXML** finds them. They arrive when that addon does,
|
||||
/// exactly like an `fx=` name with a home.
|
||||
///
|
||||
/// **They are NOT unreachable, and this doc used to say they were.** It claimed the install
|
||||
/// ships them "packed as `.pub`", which is a misreading of the loose
|
||||
/// `Interface\AddOns\<name>\` folder — that really does hold only a `.pub` signature file.
|
||||
/// The addon's real `.xml`/`.lua`/`.toc` are inside **`patch.MPQ`**, and `Chain` mounts MPQs, so
|
||||
/// `reference_ui::read` reaches them and `is_chain_entry` accepts the path. Verified by reading
|
||||
/// `Interface\AddOns\Blizzard_MacroUI\Blizzard_MacroUI.xml` and `Blizzard_InspectUI`'s twin
|
||||
/// straight out of the archive. Every one of those windows is buildable today.
|
||||
///
|
||||
/// One crudeness remains, stated because it decides how to read the output: neither scan can
|
||||
/// see a name reached through `getglobal`, so both can under-report.
|
||||
|
||||
@@ -722,6 +722,9 @@ fn the_macro_editor_takes_a_shift_click_and_only_a_shift_click() {
|
||||
s.cursor_payload().is_none(),
|
||||
"with the editor open a shift-click writes instead of picking up"
|
||||
);
|
||||
// The dirty flag is set by the box's own `OnTextChanged`, which is deferred to the drain — the
|
||||
// write landed in the buffer synchronously, the notification did not (decision 1831).
|
||||
s.tick(0.0);
|
||||
assert!(
|
||||
s.eval::<bool>("return BenillaMacroFrame.textChanged == 1")
|
||||
.unwrap(),
|
||||
|
||||
@@ -28,7 +28,7 @@ use benilla_ui::script::UiScript;
|
||||
/// **A chain entry needs client data**, so a test that names one has to open with
|
||||
/// `benilla_formats::wow_data_or_skip!()`; [`BAG_UI`] is a list that always does.
|
||||
pub(super) fn load_ui(s: &UiScript, entry: &str) -> usize {
|
||||
load_entry(s, entry, false)
|
||||
load_entry(s, entry, false, false)
|
||||
}
|
||||
|
||||
/// [`load_ui`], and **a missing template is a failure too**.
|
||||
@@ -39,10 +39,21 @@ pub(super) fn load_ui(s: &UiScript, entry: &str) -> usize {
|
||||
/// windows' test modules grew this check by hand. It is one function now rather than four copies,
|
||||
/// and any test may ask for it.
|
||||
pub(super) fn load_ui_strict(s: &UiScript, entry: &str) -> usize {
|
||||
load_entry(s, entry, true)
|
||||
load_entry(s, entry, true, false)
|
||||
}
|
||||
|
||||
fn load_entry(s: &UiScript, entry: &str, strict_templates: bool) -> usize {
|
||||
/// [`load_ui`], and **no loader warning of any kind is tolerated**.
|
||||
///
|
||||
/// Stricter than [`load_ui_strict`], which only fails a missing template. A file whose own
|
||||
/// assignment is "this loads perfectly clean" wants the whole warnings channel asserted empty — a
|
||||
/// stale unknown-attribute or dropped-script warning is exactly the drift that check exists to
|
||||
/// catch. It was a fourth private disk-only reader in `group_loot_tests.rs` until 1838; a
|
||||
/// disk-only reader cannot name a chain file, which is what that test now loads.
|
||||
pub(super) fn load_ui_no_warnings(s: &UiScript, entry: &str) -> usize {
|
||||
load_entry(s, entry, false, true)
|
||||
}
|
||||
|
||||
fn load_entry(s: &UiScript, entry: &str, strict_templates: bool, no_warnings: bool) -> usize {
|
||||
let path = entry.replace('\\', "/");
|
||||
let bytes = read(&path).unwrap_or_else(|| panic!("{entry}: not found"));
|
||||
if path.to_ascii_lowercase().ends_with(".lua") {
|
||||
@@ -59,6 +70,13 @@ fn load_entry(s: &UiScript, entry: &str, strict_templates: bool) -> usize {
|
||||
"{entry}: loader errors: {:?}",
|
||||
report.errors
|
||||
);
|
||||
if no_warnings {
|
||||
assert!(
|
||||
report.warnings.is_empty(),
|
||||
"{entry}: loader warnings: {:?}",
|
||||
report.warnings
|
||||
);
|
||||
}
|
||||
if strict_templates {
|
||||
let missing: Vec<&String> = report
|
||||
.warnings
|
||||
|
||||
@@ -13,21 +13,39 @@ mod common;
|
||||
|
||||
use benilla_ui::script::{
|
||||
QuadContent, SpellTooltipView, TalentPrereqView, TalentTabView, TalentUiState, TalentView,
|
||||
UiScript,
|
||||
UiScript, UnitState,
|
||||
};
|
||||
|
||||
/// 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] = [
|
||||
const FILES: [&str; 12] = [
|
||||
// `PLAYER_LEVEL` and the rest of the strings the stock file formats through.
|
||||
"Interface\\FrameXML\\GlobalStrings.lua",
|
||||
"Fonts.xml",
|
||||
// `TEXT`.
|
||||
"BasicControls.xml",
|
||||
"Interface\\FrameXML\\ItemButtonTemplate.xml",
|
||||
"MoneyFrame.xml",
|
||||
"UiPanels.xml",
|
||||
// `ToggleTalentFrame` lives here now, not in the window's own file (decision 1833).
|
||||
"UIParent.xml",
|
||||
"GameTooltip.xml",
|
||||
// `UIPanelScrollFrameTemplate` — the stock scroll frame's whole substance: its `$parentScrollBar`
|
||||
// Slider AND its `<OnMouseWheel>`. Nothing else in the tree declares it, and a missing template
|
||||
// is a loader WARNING, not an error, so without this the window still built — just with no
|
||||
// scrollbar and no wheel (decision 1833). `load_ui_strict` below is what makes that loud.
|
||||
"UIPanelTemplates.xml",
|
||||
"ScrollTemplates.xml",
|
||||
"TalentFrame.xml",
|
||||
// Stock `TalentFrame_OnShow` opens with `SetButtonPulse(TalentMicroButton, 0, 1)` and then
|
||||
// `UpdateMicroButtons()` — both live here, and a nil `TalentMicroButton` throws out of OnShow
|
||||
// BEFORE `TalentFrame_Update()`, so the whole window comes up empty (decision 1833). Our
|
||||
// retired file's OnShow called neither, which is why this was never a dependency before.
|
||||
"MicroMenu.xml",
|
||||
// Five files behind one line: the `.xml` sources its own `.lua` and `<Include>`s the
|
||||
// templates file that declares TalentButton/Branch/Arrow/TabTemplate.
|
||||
"Interface\\AddOns\\Blizzard_TalentUI\\Blizzard_TalentUI.xml",
|
||||
];
|
||||
|
||||
fn load_ui(script: &UiScript) {
|
||||
@@ -86,6 +104,26 @@ fn fixture(points_spent: u32) -> TalentUiState {
|
||||
}
|
||||
}
|
||||
|
||||
/// [`fixture`] plus a talent on the LAST tier, so the tree is taller than the scroll frame.
|
||||
///
|
||||
/// The stock `TalentFrameScrollChildFrame` is 50px in XML and is never resized by Lua — the
|
||||
/// reference derives its extent from the buttons anchored into it via `UpdateScrollChildRect`
|
||||
/// (`Blizzard_TalentUI.lua:311`). So a two-talent tree genuinely does not scroll, and asking it to
|
||||
/// would be asserting our retired file's behaviour rather than the reference's: ours pinned the
|
||||
/// child to a fixed full-height rect, which made every tree scrollable regardless of content
|
||||
/// (decision 1833).
|
||||
fn tall_fixture() -> TalentUiState {
|
||||
let mut state = fixture(10);
|
||||
let mut deep = state.talents[0][1].clone();
|
||||
deep.name = "Mortal Strike".into();
|
||||
deep.tier = 7;
|
||||
deep.rank = 0;
|
||||
deep.prereqs = Vec::new();
|
||||
deep.display_spell = 401;
|
||||
state.talents[0].push(deep);
|
||||
state
|
||||
}
|
||||
|
||||
fn view(name: &str, desc: &str) -> SpellTooltipView {
|
||||
SpellTooltipView {
|
||||
name: name.into(),
|
||||
@@ -94,10 +132,43 @@ fn view(name: &str, desc: &str) -> SpellTooltipView {
|
||||
}
|
||||
}
|
||||
|
||||
/// Hover a talent button the way a player does — the pointer, not a handler call.
|
||||
///
|
||||
/// The stock `Blizzard_TalentUITemplates.xml` writes the tooltip code INLINE in the button's
|
||||
/// `<OnEnter>` (`GameTooltip:SetOwner` + `GameTooltip:SetTalent`), so there is no named function to
|
||||
/// invoke the way our retired file's `BenillaTalentButton_OnEnter` could be (decision 1833). Moving
|
||||
/// the mouse is closer to the thing under test anyway: it drives the hit test as well as the
|
||||
/// handler.
|
||||
fn hover(script: &mut UiScript, frame: &str) {
|
||||
script.resolve();
|
||||
let (x, y): (f32, f32) = script
|
||||
.eval(&format!("return {frame}:GetCenter()"))
|
||||
.unwrap_or_else(|e| panic!("{frame}:GetCenter() — {e}"));
|
||||
script.mouse_move(x, y);
|
||||
}
|
||||
|
||||
/// A level-60 player. The reference's `ToggleTalentFrame` opens nothing below level 10 (decision
|
||||
/// 1833) — real behaviour, not a guard — so every opener here needs one. Our retired file had no
|
||||
/// such gate, which is why this fixture did not exist before.
|
||||
fn probe_player() -> UnitState {
|
||||
UnitState {
|
||||
exists: true,
|
||||
name: Some("Probefour".into()),
|
||||
level: 60,
|
||||
race: Some("Human".into()),
|
||||
class: Some("Warrior".into()),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
fn open_window(points_spent: u32) -> UiScript {
|
||||
let mut script = UiScript::new().expect("engine");
|
||||
script.set_screen_size(1024.0, 768.0);
|
||||
load_ui(&script);
|
||||
// The reference's `ToggleTalentFrame` opens nothing below level 10 (decision 1833) — that is
|
||||
// real behaviour, not a guard, so the window needs a player who has talents at all. Our
|
||||
// retired file had no such gate, which is why this fixture was never needed before.
|
||||
script.set_unit("player", Some(probe_player()));
|
||||
script.set_talents(fixture(points_spent));
|
||||
script.run("ToggleTalentFrame()").expect("toggle");
|
||||
script
|
||||
@@ -112,7 +183,7 @@ fn branch_lines_draw_for_a_same_column_prereq() {
|
||||
script
|
||||
.run(
|
||||
r#"
|
||||
assert(BenillaTalentFrame:IsVisible(), "window visible")
|
||||
assert(TalentFrame:IsVisible(), "window visible")
|
||||
local a = TALENT_BRANCH_ARRAY
|
||||
assert(a[1][3].down == 1, "tier1 down, got " .. tostring(a[1][3].down))
|
||||
assert(a[2][3].up == 1, "tier2 up, got " .. tostring(a[2][3].up))
|
||||
@@ -125,8 +196,8 @@ fn branch_lines_draw_for_a_same_column_prereq() {
|
||||
script
|
||||
.run(
|
||||
r#"
|
||||
assert(BenillaTalentFrameBranch1:IsShown(), "branch 1 shown")
|
||||
assert(BenillaTalentFrameArrow1:IsShown(), "arrow 1 shown")
|
||||
assert(TalentFrameBranch1:IsShown(), "branch 1 shown")
|
||||
assert(TalentFrameArrow1:IsShown(), "arrow 1 shown")
|
||||
"#,
|
||||
)
|
||||
.expect("pool state");
|
||||
@@ -170,10 +241,10 @@ fn tooltip_is_complete_on_the_first_hover() {
|
||||
// full tooltip: name line, description, the green hint.
|
||||
script.set_spell_tooltip(201, view("Improved Rend", "Bleed harder."));
|
||||
script.set_spell_tooltip(301, view("Deep Wounds", "Bleed on crit."));
|
||||
hover(&mut script, "TalentFrameTalent2");
|
||||
script
|
||||
.run(
|
||||
r#"
|
||||
BenillaTalentButton_OnEnter(BenillaTalentFrameTalent2)
|
||||
assert(GameTooltip:IsShown(), "tooltip shown on first hover")
|
||||
assert(GameTooltipTextLeft1:GetText() == "Deep Wounds",
|
||||
"name line, got " .. tostring(GameTooltipTextLeft1:GetText()))
|
||||
@@ -188,10 +259,10 @@ fn tooltip_miss_still_shows_the_rank_line() {
|
||||
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.
|
||||
hover(&mut script, "TalentFrameTalent2");
|
||||
script
|
||||
.run(
|
||||
r#"
|
||||
BenillaTalentButton_OnEnter(BenillaTalentFrameTalent2)
|
||||
assert(GameTooltip:IsShown(), "tooltip shown on a store miss")
|
||||
assert(GameTooltipTextLeft1:GetText() == "Rank 0/3",
|
||||
"rank fallback, got " .. tostring(GameTooltipTextLeft1:GetText()))
|
||||
@@ -218,8 +289,8 @@ fn a_locked_tier_still_draws_the_branch_gray() {
|
||||
local a = TALENT_BRANCH_ARRAY
|
||||
assert(a[1][3].down == -1, "tier1 down gray, got " .. tostring(a[1][3].down))
|
||||
assert(a[3][3].topArrow == -1, "tier3 topArrow gray, got " .. tostring(a[3][3].topArrow))
|
||||
assert(BenillaTalentFrameBranch1:IsShown(), "gray branch shown")
|
||||
assert(BenillaTalentFrameArrow1:IsShown(), "gray arrow shown")
|
||||
assert(TalentFrameBranch1:IsShown(), "gray branch shown")
|
||||
assert(TalentFrameArrow1:IsShown(), "gray arrow shown")
|
||||
"#,
|
||||
)
|
||||
.expect("gray branch");
|
||||
@@ -295,22 +366,27 @@ fn a_wheel_spin_over_the_grid_scrolls_the_tree() {
|
||||
// OnValueChanged pans the frame. This is the chain the 2051f4f8 rename broke silently —
|
||||
// the handler called a helper by a name that no longer existed, and only a real spin (not
|
||||
// a load) executes it.
|
||||
let mut script = open_window(10);
|
||||
let mut script = UiScript::new().expect("engine");
|
||||
script.set_screen_size(1024.0, 768.0);
|
||||
load_ui(&script);
|
||||
script.set_unit("player", Some(probe_player()));
|
||||
script.set_talents(tall_fixture());
|
||||
script.run("ToggleTalentFrame()").expect("toggle");
|
||||
// Rects must be resolved before the wheel's hit-test can land on the grid.
|
||||
script.resolve();
|
||||
let (x, y): (f32, f32) = script
|
||||
.eval("return BenillaTalentFrameTalent1:GetCenter()")
|
||||
.eval("return TalentFrameTalent1:GetCenter()")
|
||||
.expect("talent 1 center");
|
||||
// One notch DOWN (WoW convention: -1) — the handler steps the bar one valueStep forward.
|
||||
script.mouse_wheel(x, y, -1.0);
|
||||
script
|
||||
.run(
|
||||
r#"
|
||||
local scroll = BenillaTalentFrameScroll:GetVerticalScroll()
|
||||
assert(scroll == BENILLA_TALENT_SCROLL_STEP,
|
||||
"one notch down pans one step, got " .. tostring(scroll))
|
||||
assert(BenillaTalentFrameScrollBar:GetValue() == scroll,
|
||||
"the bar follows the frame, got " .. tostring(BenillaTalentFrameScrollBar:GetValue()))
|
||||
local scroll = TalentFrameScrollFrame:GetVerticalScroll()
|
||||
assert(scroll > 0,
|
||||
"one notch down pans the frame, got " .. tostring(scroll))
|
||||
assert(TalentFrameScrollFrameScrollBar:GetValue() == scroll,
|
||||
"the bar follows the frame, got " .. tostring(TalentFrameScrollFrameScrollBar:GetValue()))
|
||||
"#,
|
||||
)
|
||||
.expect("wheel down");
|
||||
@@ -319,8 +395,8 @@ fn a_wheel_spin_over_the_grid_scrolls_the_tree() {
|
||||
script
|
||||
.run(
|
||||
r#"
|
||||
assert(BenillaTalentFrameScroll:GetVerticalScroll() == 0,
|
||||
"a notch up rewinds to the top, got " .. tostring(BenillaTalentFrameScroll:GetVerticalScroll()))
|
||||
assert(TalentFrameScrollFrame:GetVerticalScroll() == 0,
|
||||
"a notch up rewinds to the top, got " .. tostring(TalentFrameScrollFrame:GetVerticalScroll()))
|
||||
"#,
|
||||
)
|
||||
.expect("wheel up");
|
||||
|
||||
@@ -307,6 +307,7 @@ fn player_money_input_reflects_then_offers() {
|
||||
// has no purse).
|
||||
s.run("GetMoney = function() return 100000000 end").unwrap();
|
||||
s.run("TradePlayerInputMoneyGold:SetText('2')").unwrap();
|
||||
s.tick(0.0); // the deferred OnTextChanged drains here (decision 1831)
|
||||
assert_eq!(
|
||||
s.take_trade_money(),
|
||||
Some(2 * 10000 + 23 * 100 + 45),
|
||||
|
||||
@@ -939,13 +939,15 @@ mod loader_tests {
|
||||
"12345"
|
||||
);
|
||||
|
||||
// A click focuses the box (mouse-enabled by construction); a typed char fires OnTextChanged.
|
||||
// A click focuses the box (mouse-enabled by construction); a typed char marks it changed,
|
||||
// and the drain on the next tick is what actually fires OnTextChanged (decision 1831).
|
||||
s.resolve();
|
||||
s.mouse_button(50.0, 10.0, "LeftButton", true);
|
||||
s.mouse_button(50.0, 10.0, "LeftButton", false);
|
||||
assert!(s.eval::<bool>("return XmlEdit:HasFocus()").unwrap());
|
||||
s.run("typed = false").unwrap();
|
||||
assert!(s.char_input("7"));
|
||||
s.tick(0.0);
|
||||
assert!(s.eval::<bool>("return typed == true").unwrap());
|
||||
assert!(s.errors().is_empty(), "{:?}", s.errors());
|
||||
}
|
||||
|
||||
@@ -163,6 +163,83 @@ pub(crate) fn optional_string(lua: &Lua, v: &Value) -> Option<String> {
|
||||
/// *strings* `"0"`/`"1"`, and a re-implementation wired to `lua_toboolean` inverts every
|
||||
/// string-valued option in the game. Equally, `"0.5"` and `"-1"` are decided by their first byte
|
||||
/// alone — false and `default` — never by their numeric value.
|
||||
/// Lua 5.0's own number→string rule as the 1.12 client compiles it: `sprintf("%.14g")`
|
||||
/// (`luaV_tostring 0x6f7c80`, format string `0x871960`). Decision 1831.
|
||||
///
|
||||
/// This is the whole of `EditBox:SetNumber`'s formatting, because **`SetNumber 0x798690` and
|
||||
/// `SetText 0x7984c0` are byte-identical functions** — 245 bytes each, zero differences after
|
||||
/// masking rel32 and absolute-VA operands, differing only in which usage string they carry. The
|
||||
/// numeric verb does no numeric work of its own; the shared `lua_tostring` marshalling does it all.
|
||||
///
|
||||
/// The rule, and the three places it is not Rust's `{}`:
|
||||
///
|
||||
/// - **14 significant digits**, then trailing zeros and a bare trailing `.` are dropped.
|
||||
/// - **Exponential iff `exp < -4 || exp >= 14`** — the C `%g` switch.
|
||||
/// - **A three-digit exponent, always.** `1e20` prints `1e+020`, not `1e+20`: the exponent is
|
||||
/// copied from the literal template `"e+000"` and digits are written onto it. This is MSVC's
|
||||
/// `%g`, not C99's, and it is the single most surprising thing here.
|
||||
///
|
||||
/// **What this deliberately does NOT reproduce**, stated so it is not mistaken for an oversight:
|
||||
/// the reference's printf is not correctly rounded. It generates 17 digits half-up and then
|
||||
/// re-rounds *that string* to 14, also half-up and with no sticky bit, so it disagrees with a
|
||||
/// correctly-rounded formatter on roughly 0.05% of doubles (`-0.11594045739607` where a correct
|
||||
/// one gives `…606`). Reproducing that needs the two-stage decimal path, not a format string, and
|
||||
/// no consumer we have is sensitive to the fourteenth digit. If one ever is, this is the note that
|
||||
/// says where to look.
|
||||
pub(crate) fn lua_number_text(v: f64) -> String {
|
||||
if v.is_nan() {
|
||||
// MSVC's own spellings, which are not "NaN".
|
||||
return if v.is_sign_negative() {
|
||||
"-1.#IND".into()
|
||||
} else {
|
||||
"1.#QNAN".into()
|
||||
};
|
||||
}
|
||||
if v.is_infinite() {
|
||||
return if v < 0.0 {
|
||||
"-1.#INF".into()
|
||||
} else {
|
||||
"1.#INF".into()
|
||||
};
|
||||
}
|
||||
if v == 0.0 {
|
||||
// `%g` prints negative zero without the sign.
|
||||
return "0".into();
|
||||
}
|
||||
const SIG: i32 = 14;
|
||||
let exp = v.abs().log10().floor() as i32;
|
||||
// Re-derive the exponent from the rounded form: 9.9999e2 rounds to 1e3 and changes style.
|
||||
let exp = {
|
||||
let probe = format!("{:.*e}", (SIG - 1) as usize, v);
|
||||
probe
|
||||
.rsplit('e')
|
||||
.next()
|
||||
.and_then(|e| e.parse::<i32>().ok())
|
||||
.unwrap_or(exp)
|
||||
};
|
||||
// C's `%g` style rule verbatim: exponential when `exp < -4 || exp >= P`, else fixed.
|
||||
if !(-4..SIG).contains(&exp) {
|
||||
let mantissa = format!("{:.*e}", (SIG - 1) as usize, v);
|
||||
let (m, _) = mantissa.split_once('e').unwrap_or((mantissa.as_str(), "0"));
|
||||
let m = trim_g(m);
|
||||
let sign = if exp < 0 { '-' } else { '+' };
|
||||
// Three digits minimum, and MORE if the exponent needs them — the template is padded,
|
||||
// never truncated.
|
||||
format!("{m}e{sign}{:03}", exp.abs())
|
||||
} else {
|
||||
let decimals = (SIG - 1 - exp).max(0) as usize;
|
||||
trim_g(&format!("{v:.decimals$}")).to_string()
|
||||
}
|
||||
}
|
||||
|
||||
/// Strip `%g`'s trailing zeros, and the decimal point if nothing follows it.
|
||||
fn trim_g(s: &str) -> &str {
|
||||
if !s.contains('.') {
|
||||
return s;
|
||||
}
|
||||
s.trim_end_matches('0').trim_end_matches('.')
|
||||
}
|
||||
|
||||
/// A widget **predicate's return**: the NUMBER `1` for true, `nil` for false — never a Lua boolean.
|
||||
///
|
||||
/// The return-side counterpart to [`bool_or_default`], and the same class of fact: 1.12 widget
|
||||
|
||||
@@ -98,7 +98,7 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
)?;
|
||||
|
||||
// BenillaDressUpModel_SetFacing(radians) — the pane's bake yaw, the exact twin of
|
||||
// `BenillaPaperDollModel_SetFacing` / `BenillaInspectModel_SetFacing`.
|
||||
// `BenillaPaperDollModel_SetFacing` (the inspect pane's twin went with 1832's migration).
|
||||
g.set(
|
||||
"BenillaDressUpModel_SetFacing",
|
||||
lua.create_function(|lua, radians: f32| {
|
||||
|
||||
@@ -97,7 +97,7 @@ pub(super) fn paste(lua: &Lua, text: &str) -> bool {
|
||||
// this layer's.
|
||||
if with_eb(lua, h, |eb| eb.paste(text)).is_some_and(|o| o.text_changed) {
|
||||
sync_text_region(lua, h);
|
||||
fire_script(lua, frame_id_of(lua, h), "OnTextChanged");
|
||||
mark_text_changed(lua, h);
|
||||
}
|
||||
true
|
||||
}
|
||||
@@ -339,7 +339,7 @@ fn insert(lua: &Lua, h: FrameHandle, ins: &str, fire_space: bool) {
|
||||
.errors
|
||||
.push(e.to_string());
|
||||
}
|
||||
fire_script(lua, id, "OnTextChanged");
|
||||
mark_text_changed(lua, h);
|
||||
if fire_space {
|
||||
for _ in 0..out.spaces {
|
||||
fire_script(lua, id, "OnSpacePressed");
|
||||
@@ -355,7 +355,7 @@ fn set_text(lua: &Lua, h: FrameHandle, s: &str) {
|
||||
sync_text_region(lua, h);
|
||||
let id = frame_id_of(lua, h);
|
||||
fire_script(lua, id, "OnTextSet");
|
||||
fire_script(lua, id, "OnTextChanged");
|
||||
mark_text_changed(lua, h);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -369,7 +369,7 @@ fn delete_span(lua: &Lua, h: FrameHandle, target_of: impl FnOnce(&EditBoxState)
|
||||
});
|
||||
if changed == Some(true) {
|
||||
sync_text_region(lua, h);
|
||||
fire_script(lua, frame_id_of(lua, h), "OnTextChanged");
|
||||
mark_text_changed(lua, h);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -379,7 +379,7 @@ fn delete_dir(lua: &Lua, h: FrameHandle, forward: bool) {
|
||||
let changed = with_eb(lua, h, |eb| eb.delete_dir(forward));
|
||||
if changed == Some(true) {
|
||||
sync_text_region(lua, h);
|
||||
fire_script(lua, frame_id_of(lua, h), "OnTextChanged");
|
||||
mark_text_changed(lua, h);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -618,3 +618,55 @@ pub(super) use methods::install;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
/// Raise the `textChanged` dirty bit (`[E+0x31c]` bit 0) rather than firing — **`OnTextChanged` is
|
||||
/// deferred**, and this is the whole of decision 1831's second half.
|
||||
///
|
||||
/// The reference never fires it from an edit. `SetText 0x77be00`, `Insert 0x77bee0` and the deletes
|
||||
/// all only OR the bit; the single fire site is `0x77d498`, inside the dirty-word drain `0x77d3e0`,
|
||||
/// whose three callers are the box's own `OnUpdate` (`0x77a790`), its `OnKeyDown` (`0x77b160`) and
|
||||
/// its `OnMouseDown` (`0x77b800`). So a Lua caller gets control back with the handler still
|
||||
/// unrun, and repeated changes **coalesce**: the box appears once on the pending list however many
|
||||
/// times it is written, and the one fire carries the final text.
|
||||
pub(super) fn mark_text_changed(lua: &Lua, h: FrameHandle) {
|
||||
let mut model = lua.app_data_mut::<Model>().expect("model app_data");
|
||||
if !model.dirty_editboxes.contains(&h) {
|
||||
model.dirty_editboxes.push(h);
|
||||
}
|
||||
}
|
||||
|
||||
/// The dirty-word drain (`0x77d3e0`): fire the pending `OnTextChanged`s.
|
||||
///
|
||||
/// Called from the frame tick — the first of the reference's three callers (`0x77a7a1`, inside the
|
||||
/// box's own `OnUpdate` `0x77a790`, which is also where our caret blink lives). Its other two, the
|
||||
/// box's `OnKeyDown` and `OnMouseDown`, are **not** wired: they only make a pending fire arrive
|
||||
/// sooner within a frame, and our key and mouse paths reach the tick anyway. Decision 1831 says so
|
||||
/// out loud rather than leaving it to be discovered.
|
||||
///
|
||||
/// A box that is **not effectively visible stays pending**: `Hide`
|
||||
/// splices it out of the chain the update walk follows, so its fire waits for a `Show` (verified
|
||||
/// chain mechanics; the "hidden ⇒ no OnUpdate" step is the RE's own stated inference).
|
||||
///
|
||||
/// A handler may itself write to a box — including this one — so the list is taken before any Lua
|
||||
/// runs and anything re-marked during the sweep lands on the NEXT drain rather than extending this
|
||||
/// one. That is the reference's shape too: its bit is cleared before its fire.
|
||||
pub(in crate::script) fn drain_text_changed(lua: &Lua) {
|
||||
let pending = {
|
||||
let mut model = lua.app_data_mut::<Model>().expect("model app_data");
|
||||
if model.dirty_editboxes.is_empty() {
|
||||
return;
|
||||
}
|
||||
let (ready, waiting): (Vec<_>, Vec<_>) = std::mem::take(&mut model.dirty_editboxes)
|
||||
.into_iter()
|
||||
.partition(|&h| model.arena.frame(h).is_some_and(|f| f.effective_visible));
|
||||
// A frame that has been destroyed drops out of both halves.
|
||||
model.dirty_editboxes = waiting
|
||||
.into_iter()
|
||||
.filter(|&h| model.arena.frame(h).is_some())
|
||||
.collect();
|
||||
ready
|
||||
};
|
||||
for h in pending {
|
||||
fire_script(lua, frame_id_of(lua, h), "OnTextChanged");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
|
||||
use mlua::Lua;
|
||||
|
||||
use super::{fire_script, frame_id_of, set_focus_handle, sync_text_region, with_eb};
|
||||
use super::{set_focus_handle, sync_text_region, with_eb};
|
||||
use crate::script::Model;
|
||||
use crate::widget::{FrameHandle, FrameKind, KindState};
|
||||
|
||||
@@ -108,7 +108,7 @@ pub(in crate::script) fn cut_selection(lua: &Lua) -> Option<String> {
|
||||
let h = focused(lua)?;
|
||||
let copied = with_eb(lua, h, |eb| eb.cut_selection()).flatten()?;
|
||||
sync_text_region(lua, h);
|
||||
fire_script(lua, frame_id_of(lua, h), "OnTextChanged");
|
||||
super::mark_text_changed(lua, h);
|
||||
Some(copied)
|
||||
}
|
||||
|
||||
|
||||
@@ -45,6 +45,38 @@ pub(in crate::script) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
"GetText",
|
||||
lua.create_function(|lua, this: Table| with_editbox(lua, &this, |eb| eb.text.clone()))?,
|
||||
)?;
|
||||
// SetNumber(v) — **the same function as SetText**. `0x798690` and `0x7984c0` are byte-identical
|
||||
// (245 bytes each, zero differences after masking rel32 and absolute-VA operands; only the
|
||||
// usage string differs), so this does no numeric work of its own: it hands the argument to the
|
||||
// shared `lua_tostring` marshalling and sets the result as text. Decision 1831.
|
||||
//
|
||||
// The GATE is `lua_isstring 0x6f3510`, a pure type test over {number, string} — NOT
|
||||
// `lua_isnumber` and NOT `luaL_checknumber`. So a STRING is accepted and passed through
|
||||
// VERBATIM, never parsed: `SetNumber("abc")` sets the text "abc" and does not raise. Anything
|
||||
// else — nil, boolean, table, or an ABSENT argument — raises the usage string and abandons the
|
||||
// caller's statement.
|
||||
//
|
||||
// The live consumer is `MoneyInputFrame.lua:47/52/57`, on the chain since 1751, whose three
|
||||
// boxes are `numeric="true"`. Our numeric filter already models the reference's wholesale
|
||||
// abort, which matters here: on such a box `SetNumber(-5)` or `SetNumber(0.8)` leaves the box
|
||||
// EMPTY rather than partially filled, because the sign or the point fails the digit test after
|
||||
// the clear-all has already run. The money path only ever passes non-negative integers
|
||||
// (`floor`/`mod` results), so it never takes that branch.
|
||||
m.set(
|
||||
"SetNumber",
|
||||
lua.create_function(|lua, (this, v): (Table, Value)| {
|
||||
let text = match &v {
|
||||
Value::Integer(i) => crate::script::binding_abi::lua_number_text(*i as f64),
|
||||
Value::Number(n) => crate::script::binding_abi::lua_number_text(*n),
|
||||
// A string is not parsed — it is the text.
|
||||
Value::String(s) => s.to_str()?.to_string(),
|
||||
_ => return Err(mlua::Error::runtime("Usage: EditBox:SetNumber(number)")),
|
||||
};
|
||||
let h = frame_handle_of(lua, &this)?;
|
||||
set_text(lua, h, &text);
|
||||
Ok(())
|
||||
})?,
|
||||
)?;
|
||||
// GetNumber: atof of the real text (0 on failure), matching `0x798790`.
|
||||
m.set(
|
||||
"GetNumber",
|
||||
|
||||
@@ -220,14 +220,19 @@ fn click_focuses_regardless_of_autofocus_and_transition_order_is_lost_then_gaine
|
||||
|
||||
// ── §3 text buffer + editing + OnTextChanged/OnTextSet ───────────────────────────────────────
|
||||
|
||||
/// **`OnTextChanged` is deferred and coalesced** (decision 1831). An edit only raises the
|
||||
/// `textChanged` dirty bit; the fire belongs to the drain (`0x77d3e0`) that the box's own OnUpdate
|
||||
/// runs. So three typed characters are three marks on ONE box and produce exactly ONE fire — this
|
||||
/// test asserted three before the law was checked.
|
||||
#[test]
|
||||
fn typing_builds_text_and_fires_ontextchanged_per_mutation() {
|
||||
fn typing_coalesces_into_one_deferred_ontextchanged() {
|
||||
let mut s = script();
|
||||
s.run(
|
||||
r#"
|
||||
changed = 0
|
||||
E = CreateFrame("EditBox", "E")
|
||||
E:SetScript("OnTextChanged", function() changed = changed + 1 end)
|
||||
seen = ""
|
||||
E:SetScript("OnTextChanged", function() changed = changed + 1 seen = E:GetText() end)
|
||||
E:SetFocus()
|
||||
"#,
|
||||
)
|
||||
@@ -236,26 +241,50 @@ fn typing_builds_text_and_fires_ontextchanged_per_mutation() {
|
||||
assert!(s.char_input("b"));
|
||||
assert!(s.char_input("c"));
|
||||
assert_eq!(s.eval::<String>("return E:GetText()").unwrap(), "abc");
|
||||
assert_eq!(s.eval::<i64>("return changed").unwrap(), 3);
|
||||
assert_eq!(
|
||||
s.eval::<i64>("return changed").unwrap(),
|
||||
0,
|
||||
"the text is already there, but nothing has drained yet"
|
||||
);
|
||||
|
||||
s.tick(0.0);
|
||||
assert_eq!(
|
||||
s.eval::<i64>("return changed").unwrap(),
|
||||
1,
|
||||
"three edits, one fire, carrying the final text"
|
||||
);
|
||||
assert_eq!(s.eval::<String>("return seen").unwrap(), "abc");
|
||||
|
||||
// Nothing is pending now, so a second drain fires nothing.
|
||||
s.tick(0.0);
|
||||
assert_eq!(s.eval::<i64>("return changed").unwrap(), 1);
|
||||
}
|
||||
|
||||
/// The two fires part company (decision 1831): **`OnTextSet` is synchronous, inside `SetText`
|
||||
/// itself** (`0x77be6b`), while `OnTextChanged` waits for the drain. The equality short-circuit
|
||||
/// still suppresses both. So writing "hi", "hi", "bye" before any drain logs two `set`s and then a
|
||||
/// SINGLE `changed` carrying only the last value — `A → B` coalesces.
|
||||
#[test]
|
||||
fn set_text_short_circuits_on_same_value_else_fires_textset_then_textchanged() {
|
||||
let s = script();
|
||||
fn set_text_fires_ontextset_at_once_and_ontextchanged_at_the_drain() {
|
||||
let mut s = script();
|
||||
s.run(
|
||||
r#"
|
||||
log = {}
|
||||
E = CreateFrame("EditBox", "E")
|
||||
E:SetScript("OnTextSet", function() table.insert(log, "set") end)
|
||||
E:SetScript("OnTextChanged", function() table.insert(log, "changed") end)
|
||||
E:SetText("hi") -- fires set then changed
|
||||
E:SetText("hi") -- unchanged → short-circuit, no events
|
||||
E:SetText("bye") -- fires set then changed
|
||||
E:SetText("hi") -- fires set NOW, marks changed for the drain
|
||||
E:SetText("hi") -- unchanged → short-circuit, no events at all
|
||||
E:SetText("bye") -- fires set NOW; the pending change simply becomes "bye"
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
let log: Vec<String> = s.eval("return log").unwrap();
|
||||
assert_eq!(log, vec!["set", "changed", "set", "changed"]);
|
||||
assert_eq!(log, vec!["set", "set"], "no drain has run yet");
|
||||
|
||||
s.tick(0.0);
|
||||
let log: Vec<String> = s.eval("return log").unwrap();
|
||||
assert_eq!(log, vec!["set", "set", "changed"]);
|
||||
assert_eq!(s.eval::<String>("return E:GetText()").unwrap(), "bye");
|
||||
}
|
||||
|
||||
@@ -328,8 +357,10 @@ fn paste_inserts_at_the_cursor_and_replaces_the_selection() {
|
||||
s.run("E:HighlightText(0, -1)").unwrap();
|
||||
assert!(s.paste("xyz"));
|
||||
assert_eq!(s.eval::<String>("return E:GetText()").unwrap(), "xyz");
|
||||
// Each paste that changed the text fired OnTextChanged once.
|
||||
assert_eq!(s.eval::<i64>("return changed").unwrap(), 2);
|
||||
// Both pastes marked the same box, so the drain owes exactly one fire — not one per paste.
|
||||
assert_eq!(s.eval::<i64>("return changed").unwrap(), 0);
|
||||
s.tick(0.0);
|
||||
assert_eq!(s.eval::<i64>("return changed").unwrap(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1450,3 +1481,73 @@ fn an_editbox_is_born_with_the_ctors_five_regions_ahead_of_its_authored_ones() {
|
||||
"the selection trio and the caret are blank quads"
|
||||
);
|
||||
}
|
||||
|
||||
/// `SetNumber` is `SetText` with Lua's own `%.14g` in front of it — the two bindings are
|
||||
/// byte-identical in the reference (`0x798690` / `0x7984c0`), so the numeric verb does no numeric
|
||||
/// work of its own. Decision 1831; the expected strings are the reference's executed output.
|
||||
#[test]
|
||||
fn set_number_formats_like_the_references_printf() {
|
||||
let s = UiScript::new().unwrap();
|
||||
s.run(r#"box = CreateFrame("EditBox", "NumBox", UIParent)"#)
|
||||
.unwrap();
|
||||
|
||||
for (input, want) in [
|
||||
("3", "3"),
|
||||
("0.8", "0.8"),
|
||||
("-0.5", "-0.5"),
|
||||
("0.1 + 0.2", "0.3"),
|
||||
("1/3", "0.33333333333333"),
|
||||
("1e13", "10000000000000"),
|
||||
// The three-digit exponent, which is MSVC's `%g` and not C99's. `1e+020`, never `1e+20`.
|
||||
("1e14", "1e+014"),
|
||||
("1e20", "1e+020"),
|
||||
("1e-5", "1e-005"),
|
||||
// Just inside the fixed-notation window.
|
||||
("1e-4", "0.0001"),
|
||||
// `%g` drops the sign on negative zero.
|
||||
("-0.0", "0"),
|
||||
] {
|
||||
s.run(&format!("box:SetNumber({input})")).unwrap();
|
||||
assert_eq!(
|
||||
s.eval::<String>("return box:GetText()").unwrap(),
|
||||
want,
|
||||
"SetNumber({input})"
|
||||
);
|
||||
}
|
||||
|
||||
// A STRING is accepted and passed through verbatim — the gate is `lua_isstring`, so the
|
||||
// argument is never parsed as a number.
|
||||
s.run(r#"box:SetNumber("abc")"#).unwrap();
|
||||
assert_eq!(s.eval::<String>("return box:GetText()").unwrap(), "abc");
|
||||
|
||||
// Everything else raises, including an ABSENT argument. `luaL_error` does not return.
|
||||
for bad in [
|
||||
"box:SetNumber()",
|
||||
"box:SetNumber(nil)",
|
||||
"box:SetNumber(true)",
|
||||
] {
|
||||
assert!(s.run(bad).is_err(), "{bad} must raise");
|
||||
}
|
||||
}
|
||||
|
||||
/// A `numeric` box takes the reference's wholesale abort: the clear-all has already run when the
|
||||
/// insert fails the digit test, so the box is left EMPTY rather than partly filled. The money
|
||||
/// boxes (`MoneyInputFrame.xml`) are numeric, which is why this matters — though the money path
|
||||
/// itself only ever passes non-negative integers.
|
||||
#[test]
|
||||
fn set_number_on_a_numeric_box_empties_it_when_the_text_is_not_all_digits() {
|
||||
let s = UiScript::new().unwrap();
|
||||
s.run(r#"box = CreateFrame("EditBox", "NumOnly", UIParent) box:SetNumeric(true)"#)
|
||||
.unwrap();
|
||||
|
||||
s.run("box:SetNumber(1234)").unwrap();
|
||||
assert_eq!(s.eval::<String>("return box:GetText()").unwrap(), "1234");
|
||||
|
||||
// The minus sign fails the digit test, and the abort is wholesale.
|
||||
s.run("box:SetNumber(-5)").unwrap();
|
||||
assert_eq!(
|
||||
s.eval::<String>("return box:GetText()").unwrap(),
|
||||
"",
|
||||
"a sign empties a numeric box rather than partly filling it"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -146,12 +146,6 @@ impl super::UiScript {
|
||||
pub fn take_inspect_clear(&mut self) -> bool {
|
||||
std::mem::take(&mut self.model_mut().inspect_clear)
|
||||
}
|
||||
|
||||
/// The inspect model pane's bake yaw in radians — the twin of
|
||||
/// [`Self::paperdoll_yaw`], read by the app onto the `"inspect"` booth slot each frame.
|
||||
pub fn inspect_yaw(&self) -> f32 {
|
||||
self.model_ref().inspect_yaw
|
||||
}
|
||||
}
|
||||
|
||||
/// What the app resolved for `token`, or `None` when it resolved to no live unit at all — the
|
||||
@@ -271,19 +265,5 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
})?,
|
||||
)?;
|
||||
|
||||
// BenillaInspectModel_SetFacing(radians) — the inspect pane's own bake yaw, the exact twin of
|
||||
// `BenillaPaperDollModel_SetFacing` (decision 0208 §5's rotate-adjusts-the-bake). Two scalars
|
||||
// rather than one shared: the two windows can be open at different facings, and the ref's own
|
||||
// `InspectModelFrame` carries its own `rotation` independent of the character pane's.
|
||||
g.set(
|
||||
"BenillaInspectModel_SetFacing",
|
||||
lua.create_function(|lua, radians: f32| {
|
||||
lua.app_data_mut::<Model>()
|
||||
.expect("model app_data")
|
||||
.inspect_yaw = radians;
|
||||
Ok(())
|
||||
})?,
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -288,6 +288,18 @@ pub(crate) struct Model {
|
||||
/// resolve at a 3,988-frame roster, which made it the biggest phase left in the preamble
|
||||
/// (decision 1634's `[layout-pre] watched=`).
|
||||
pub(crate) on_size_changed_frames: Vec<FrameHandle>,
|
||||
/// Edit boxes whose text changed and whose `OnTextChanged` has **not fired yet** — the
|
||||
/// reference's `textChanged` dirty bit (`[E+0x31c]` bit 0), which `SetText`/`Insert` raise and
|
||||
/// only the dirty-word drain `0x77d3e0` clears (decision 1831).
|
||||
///
|
||||
/// The fire is deferred, not immediate: `SetText` returns to Lua before any handler runs, and N
|
||||
/// changes to one box between two drains produce exactly ONE fire carrying the final text. The
|
||||
/// reference's own FrameXML depends on both halves — `MoneyInputFrame_SetCopper` sets three
|
||||
/// boxes while counting the fires it expects back, and errors if one arrives mid-loop.
|
||||
///
|
||||
/// A hidden box stays on this list: `Hide` splices it out of the chain the update walk follows,
|
||||
/// so its pending fire waits until it is shown again (or a key/mouse-down reaches it).
|
||||
pub(crate) dirty_editboxes: Vec<FrameHandle>,
|
||||
/// `event name → frames registered for it` (RegisterEvent), in **registration order** — an
|
||||
/// ordered Vec, never a set: the client's `SignalEvent 0x703e50` walks a per-event listener
|
||||
/// LIST, so cross-frame dispatch order is a law, not an accident (the abbey territory-line
|
||||
@@ -1330,8 +1342,6 @@ pub(crate) struct Model {
|
||||
/// The inspect model pane's bake yaw — the last of the benilla-named pane scalars beside
|
||||
/// [`Self::dressup_yaw`]. A migrated window's pane carries its own facing in `ModelState`
|
||||
/// (`UiScript::model_pane_facing`, decision 1751); these two remain because their windows are
|
||||
/// still ours, and each retires with its file.
|
||||
pub(crate) inspect_yaw: f32,
|
||||
/// The dressing room's queued intents (decision 1060) — `BenillaDressUpModel_Dress/TryOn/Close`,
|
||||
/// drained by the app in order (see [`super::dressup`] on why order matters).
|
||||
pub(crate) dressup_intents: Vec<super::dressup::DressUpIntent>,
|
||||
@@ -1574,6 +1584,7 @@ impl Model {
|
||||
scripts: HashMap::new(),
|
||||
on_update_frames: Vec::new(),
|
||||
on_size_changed_frames: Vec::new(),
|
||||
dirty_editboxes: Vec::new(),
|
||||
event_to_frames: HashMap::new(),
|
||||
frame_events: HashMap::new(),
|
||||
focused_editbox: None,
|
||||
@@ -1865,7 +1876,6 @@ impl Model {
|
||||
inspect: None,
|
||||
inspect_notifies: Vec::new(),
|
||||
inspect_clear: false,
|
||||
inspect_yaw: 0.0,
|
||||
dressup_intents: Vec::new(),
|
||||
dressup_yaw: 0.0,
|
||||
unit_reach: HashMap::new(),
|
||||
|
||||
@@ -117,7 +117,18 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
// the bake replaces them. A later `SetTexture`/`SetPortraitToTexture` clears the binding.
|
||||
lua.globals().set(
|
||||
"SetPortraitTexture",
|
||||
lua.create_function(|lua, (region, unit): (Table, String)| {
|
||||
lua.create_function(|lua, (region, unit): (Table, Value)| {
|
||||
// `0x519ef0` gates its unit argument at `0x519fb4` with `lua_isstring` and fails into
|
||||
// `luaL_error` with this exact string (`0x8513b8`); `luaL_error` does not return, so
|
||||
// nil/absent/boolean/table abandon the caller's statement. Stock
|
||||
// `Blizzard_InspectUI.lua:49` reaches it on a re-open, which is how it was found.
|
||||
// A token that RESOLVES to nothing is a different, quiet case — it blanks the portrait
|
||||
// and returns no values. Decision 1834.
|
||||
let unit = crate::script::binding_abi::string_arg(
|
||||
lua,
|
||||
unit,
|
||||
r#"Usage: SetPortraitTexture(texture, "unit")"#,
|
||||
)?;
|
||||
let rh = region_handle_of(lua, ®ion)?;
|
||||
let mut model = lua.app_data_mut::<Model>().expect("model");
|
||||
let data = model.region_data.entry(rh).or_default();
|
||||
|
||||
@@ -107,6 +107,11 @@ impl UiScript {
|
||||
}
|
||||
// The focused edit box's caret blink (`0x77a790` runs on the client's frame tick).
|
||||
editbox::tick_blink(&self.lua, elapsed);
|
||||
// …and, from inside that same `0x77a790` (`0x77a7a1`), the dirty-word drain that fires the
|
||||
// `OnTextChanged`s an edit only marked (decision 1831). It runs BEFORE this tick's
|
||||
// OnUpdate sweep, matching the reference's order: the box's own OnUpdate override drains
|
||||
// before the FrameXML handlers that read the box get their turn.
|
||||
editbox::drain_text_changed(&self.lua);
|
||||
// Events queued by Lua bindings last tick (`Model::pending_events` — e.g. `SetMapZoom` →
|
||||
// `WORLD_MAP_UPDATE`; the cursor arc's `CURSOR_UPDATE`/`ITEM_LOCK_CHANGED`/
|
||||
// `DELETE_ITEM_CONFIRM`, decision 0216) fire first, so handlers see them before this
|
||||
|
||||
@@ -72,8 +72,11 @@ pub(in crate::script) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
// `lua_isstring` gate admits tag 3 and hands the resolver `"5"`). `check_unit_token` inside
|
||||
// `with_unit` is the second of those; the first is the `?` on the argument type below.
|
||||
// The gate is the BINDING's, not `with_unit`'s: `check_unit_token` lets a nil through by
|
||||
// design, because that is right for `UnitExists`, `UnitIsVisible` and most of this family. It
|
||||
// is wrong for exactly these two, so the shape-A test happens here, before the shared path.
|
||||
// design, because that is right for `UnitExists`, `UnitIsVisible` and eleven others. **"and
|
||||
// most of this family" is what this comment used to say, and it was the wrong way round** —
|
||||
// wow-re has since censused all 83 entries of the table at `0x850438` and 53 of them gate and
|
||||
// raise, with only 13 unit-token bindings quiet (decision 1834). These two were never the
|
||||
// exception; they were an early instance of the rule.
|
||||
for (name, usage, by_player) in [
|
||||
("UnitIsTapped", r#"Usage: UnitIsTapped("unit")"#, false),
|
||||
(
|
||||
@@ -151,7 +154,12 @@ pub(in crate::script) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
|
||||
g.set(
|
||||
"UnitName",
|
||||
lua.create_function(|lua, token: Option<String>| {
|
||||
lua.create_function(|lua, token: Value| {
|
||||
let token = Some(crate::script::binding_abi::string_arg(
|
||||
lua,
|
||||
token,
|
||||
r#"Usage: UnitName("unit")"#,
|
||||
)?);
|
||||
let name = with_unit(lua, &token, None, |u| u.name.clone())?;
|
||||
match name {
|
||||
Some(n) => Ok(Value::String(lua.create_string(&n)?)),
|
||||
@@ -162,14 +170,24 @@ pub(in crate::script) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
|
||||
g.set(
|
||||
"UnitHealth",
|
||||
lua.create_function(|lua, token: Option<String>| {
|
||||
lua.create_function(|lua, token: Value| {
|
||||
let token = Some(crate::script::binding_abi::string_arg(
|
||||
lua,
|
||||
token,
|
||||
r#"Usage: UnitHealth("unit")"#,
|
||||
)?);
|
||||
with_unit(lua, &token, 0i64, |u| i64::from(u.health))
|
||||
})?,
|
||||
)?;
|
||||
|
||||
g.set(
|
||||
"UnitHealthMax",
|
||||
lua.create_function(|lua, token: Option<String>| {
|
||||
lua.create_function(|lua, token: Value| {
|
||||
let token = Some(crate::script::binding_abi::string_arg(
|
||||
lua,
|
||||
token,
|
||||
r#"Usage: UnitHealthMax("unit")"#,
|
||||
)?);
|
||||
with_unit(lua, &token, 0i64, |u| i64::from(u.max_health))
|
||||
})?,
|
||||
)?;
|
||||
@@ -183,7 +201,12 @@ pub(in crate::script) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
// `b` INFERRED and 0 in normal play — it can never drive the value ≤ 0 anyway).
|
||||
g.set(
|
||||
"UnitLevel",
|
||||
lua.create_function(|lua, token: Option<String>| {
|
||||
lua.create_function(|lua, token: Value| {
|
||||
let token = Some(crate::script::binding_abi::string_arg(
|
||||
lua,
|
||||
token,
|
||||
r#"Usage: UnitLevel("unit")"#,
|
||||
)?);
|
||||
check_unit_token(&token)?;
|
||||
let model = lua.app_data_ref::<Model>().expect("model app_data");
|
||||
let Some(u) = token.as_ref().and_then(|t| model.unit(t)) else {
|
||||
@@ -223,7 +246,19 @@ pub(in crate::script) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
// (`UnitCanAttack("player", "target")`).
|
||||
g.set(
|
||||
"UnitCanAttack",
|
||||
lua.create_function(|lua, (a, b): (Option<String>, Option<String>)| {
|
||||
lua.create_function(|lua, (a, b): (Value, Value)| {
|
||||
// BOTH positions carry an `lua_isstring` gate — two sites in the body —
|
||||
// so either argument being nil raises (decision 1836).
|
||||
let a = Some(crate::script::binding_abi::string_arg(
|
||||
lua,
|
||||
a,
|
||||
r#"Usage: UnitCanAttack("unit", "otherUnit")"#,
|
||||
)?);
|
||||
let b = Some(crate::script::binding_abi::string_arg(
|
||||
lua,
|
||||
b,
|
||||
r#"Usage: UnitCanAttack("unit", "otherUnit")"#,
|
||||
)?);
|
||||
let token = pick_unit_token(&a, &b);
|
||||
Ok(if with_unit(lua, &token, false, |u| u.can_attack)? {
|
||||
Value::Integer(1)
|
||||
@@ -247,7 +282,12 @@ pub(in crate::script) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
|
||||
g.set(
|
||||
"UnitIsDead",
|
||||
lua.create_function(|lua, token: Option<String>| {
|
||||
lua.create_function(|lua, token: Value| {
|
||||
let token = Some(crate::script::binding_abi::string_arg(
|
||||
lua,
|
||||
token,
|
||||
r#"Usage: UnitIsDead("unit")"#,
|
||||
)?);
|
||||
with_unit(lua, &token, false, |u| u.dead)
|
||||
})?,
|
||||
)?;
|
||||
@@ -256,13 +296,23 @@ pub(in crate::script) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
// so IsDead is false for it and the popup flow branches on all three.
|
||||
g.set(
|
||||
"UnitIsGhost",
|
||||
lua.create_function(|lua, token: Option<String>| {
|
||||
lua.create_function(|lua, token: Value| {
|
||||
let token = Some(crate::script::binding_abi::string_arg(
|
||||
lua,
|
||||
token,
|
||||
r#"Usage: UnitIsGhost("unit")"#,
|
||||
)?);
|
||||
with_unit(lua, &token, false, |u| u.ghost)
|
||||
})?,
|
||||
)?;
|
||||
g.set(
|
||||
"UnitIsDeadOrGhost",
|
||||
lua.create_function(|lua, token: Option<String>| {
|
||||
lua.create_function(|lua, token: Value| {
|
||||
let token = Some(crate::script::binding_abi::string_arg(
|
||||
lua,
|
||||
token,
|
||||
r#"Usage: UnitIsDeadOrGhost("unit")"#,
|
||||
)?);
|
||||
with_unit(lua, &token, false, |u| u.dead || u.ghost)
|
||||
})?,
|
||||
)?;
|
||||
@@ -274,7 +324,19 @@ pub(in crate::script) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
// frame paints its name plate blue then, exactly like the reference.
|
||||
g.set(
|
||||
"UnitReaction",
|
||||
lua.create_function(|lua, (token, _other): (Option<String>, Option<String>)| {
|
||||
lua.create_function(|lua, (token, _other): (Value, Value)| {
|
||||
// BOTH positions carry an `lua_isstring` gate — two sites in the body —
|
||||
// so either argument being nil raises (decision 1836).
|
||||
let token = Some(crate::script::binding_abi::string_arg(
|
||||
lua,
|
||||
token,
|
||||
r#"Usage: UnitReaction("unit", "otherUnit")"#,
|
||||
)?);
|
||||
let _other = Some(crate::script::binding_abi::string_arg(
|
||||
lua,
|
||||
_other,
|
||||
r#"Usage: UnitReaction("unit", "otherUnit")"#,
|
||||
)?);
|
||||
let r = with_unit(lua, &token, 0u8, |u| u.reaction)?;
|
||||
Ok(if r == 0 {
|
||||
Value::Nil
|
||||
@@ -294,7 +356,19 @@ pub(in crate::script) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
// reaction (0) → nil for both, the API's "can't tell". `1`/nil returns, era-style.
|
||||
g.set(
|
||||
"UnitIsEnemy",
|
||||
lua.create_function(|lua, (a, b): (Option<String>, Option<String>)| {
|
||||
lua.create_function(|lua, (a, b): (Value, Value)| {
|
||||
// BOTH positions carry an `lua_isstring` gate — two sites in the body —
|
||||
// so either argument being nil raises (decision 1836).
|
||||
let a = Some(crate::script::binding_abi::string_arg(
|
||||
lua,
|
||||
a,
|
||||
r#"Usage: UnitIsEnemy("unit", "otherUnit")"#,
|
||||
)?);
|
||||
let b = Some(crate::script::binding_abi::string_arg(
|
||||
lua,
|
||||
b,
|
||||
r#"Usage: UnitIsEnemy("unit", "otherUnit")"#,
|
||||
)?);
|
||||
let token = pick_unit_token(&a, &b);
|
||||
let r = with_unit(lua, &token, 0u8, |u| u.reaction)?;
|
||||
Ok(if (1..=2).contains(&r) {
|
||||
@@ -306,7 +380,19 @@ pub(in crate::script) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
)?;
|
||||
g.set(
|
||||
"UnitIsFriend",
|
||||
lua.create_function(|lua, (a, b): (Option<String>, Option<String>)| {
|
||||
lua.create_function(|lua, (a, b): (Value, Value)| {
|
||||
// BOTH positions carry an `lua_isstring` gate — two sites in the body —
|
||||
// so either argument being nil raises (decision 1836).
|
||||
let a = Some(crate::script::binding_abi::string_arg(
|
||||
lua,
|
||||
a,
|
||||
r#"Usage: UnitIsFriend("unit", "otherUnit")"#,
|
||||
)?);
|
||||
let b = Some(crate::script::binding_abi::string_arg(
|
||||
lua,
|
||||
b,
|
||||
r#"Usage: UnitIsFriend("unit", "otherUnit")"#,
|
||||
)?);
|
||||
let token = pick_unit_token(&a, &b);
|
||||
let r = with_unit(lua, &token, 0u8, |u| u.reaction)?;
|
||||
Ok(if r >= 5 {
|
||||
@@ -335,7 +421,12 @@ pub(in crate::script) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
// target plate disagree about the same mob.
|
||||
g.set(
|
||||
"UnitIsCivilian",
|
||||
lua.create_function(|lua, unit: Option<String>| {
|
||||
lua.create_function(|lua, unit: Value| {
|
||||
let unit = Some(crate::script::binding_abi::string_arg(
|
||||
lua,
|
||||
unit,
|
||||
r#"Usage: UnitIsCivilian("unit")"#,
|
||||
)?);
|
||||
let model = lua.app_data_ref::<Model>().expect("model app_data");
|
||||
let player_level = model.player_req.level;
|
||||
Ok(match unit.and_then(|u| model.unit(&u)) {
|
||||
@@ -374,7 +465,19 @@ pub(in crate::script) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
// is trivially the same unit; otherwise both snapshots must carry a real (nonzero) guid.
|
||||
g.set(
|
||||
"UnitIsUnit",
|
||||
lua.create_function(|lua, (a, b): (Option<String>, Option<String>)| {
|
||||
lua.create_function(|lua, (a, b): (Value, Value)| {
|
||||
// BOTH positions carry an `lua_isstring` gate — two sites in the body —
|
||||
// so either argument being nil raises (decision 1836).
|
||||
let a = Some(crate::script::binding_abi::string_arg(
|
||||
lua,
|
||||
a,
|
||||
r#"Usage: UnitIsUnit("unit", "otherUnit")"#,
|
||||
)?);
|
||||
let b = Some(crate::script::binding_abi::string_arg(
|
||||
lua,
|
||||
b,
|
||||
r#"Usage: UnitIsUnit("unit", "otherUnit")"#,
|
||||
)?);
|
||||
// BOTH arguments go through the resolver, so either being unrecognised raises.
|
||||
check_unit_token(&a)?;
|
||||
check_unit_token(&b)?;
|
||||
@@ -494,7 +597,19 @@ pub(in crate::script) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
// whisper a same-faction player), without the faction machinery the engine doesn't carry.
|
||||
g.set(
|
||||
"UnitCanCooperate",
|
||||
lua.create_function(|lua, (a, b): (Option<String>, Option<String>)| {
|
||||
lua.create_function(|lua, (a, b): (Value, Value)| {
|
||||
// BOTH positions carry an `lua_isstring` gate — two sites in the body —
|
||||
// so either argument being nil raises (decision 1836).
|
||||
let a = Some(crate::script::binding_abi::string_arg(
|
||||
lua,
|
||||
a,
|
||||
r#"Usage: UnitCanCooperate("unit", "otherUnit")"#,
|
||||
)?);
|
||||
let b = Some(crate::script::binding_abi::string_arg(
|
||||
lua,
|
||||
b,
|
||||
r#"Usage: UnitCanCooperate("unit", "otherUnit")"#,
|
||||
)?);
|
||||
let token = pick_unit_token(&a, &b);
|
||||
let ok = with_unit(lua, &token, false, |u| {
|
||||
u.exists && u.is_player && u.reaction >= 5
|
||||
@@ -507,7 +622,14 @@ pub(in crate::script) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
// fed per token).
|
||||
g.set(
|
||||
"GetRaidTargetIndex",
|
||||
lua.create_function(|lua, token: Option<String>| {
|
||||
lua.create_function(|lua, token: Value| {
|
||||
// Gated (`0x4bb4be` → `0x4bb4cd`), and the usage string names the
|
||||
// argument unquoted — the reference's own spelling. Decision 1836.
|
||||
let token = Some(crate::script::binding_abi::string_arg(
|
||||
lua,
|
||||
token,
|
||||
"Usage: GetRaidTargetIndex(unit)",
|
||||
)?);
|
||||
let idx = with_unit(lua, &token, 0u8, |u| u.raid_target)?;
|
||||
Ok(if idx > 0 {
|
||||
Value::Integer(i64::from(idx))
|
||||
@@ -522,7 +644,12 @@ pub(in crate::script) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
// stated v1 shortcut, module doc), these are new and follow the era shape from the start.
|
||||
g.set(
|
||||
"UnitIsConnected",
|
||||
lua.create_function(|lua, token: Option<String>| {
|
||||
lua.create_function(|lua, token: Value| {
|
||||
let token = Some(crate::script::binding_abi::string_arg(
|
||||
lua,
|
||||
token,
|
||||
r#"Usage: UnitIsConnected("unit")"#,
|
||||
)?);
|
||||
Ok(if with_unit(lua, &token, false, |u| u.is_connected)? {
|
||||
Value::Integer(1)
|
||||
} else {
|
||||
@@ -554,7 +681,12 @@ pub(in crate::script) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
// two callers) — see the field doc.
|
||||
g.set(
|
||||
"UnitIsPVP",
|
||||
lua.create_function(|lua, token: Option<String>| {
|
||||
lua.create_function(|lua, token: Value| {
|
||||
let token = Some(crate::script::binding_abi::string_arg(
|
||||
lua,
|
||||
token,
|
||||
r#"Usage: UnitIsPVP("unit")"#,
|
||||
)?);
|
||||
Ok(if with_unit(lua, &token, false, |u| u.pvp)? {
|
||||
Value::Integer(1)
|
||||
} else {
|
||||
@@ -564,7 +696,12 @@ pub(in crate::script) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
)?;
|
||||
g.set(
|
||||
"UnitIsPVPFreeForAll",
|
||||
lua.create_function(|lua, token: Option<String>| {
|
||||
lua.create_function(|lua, token: Value| {
|
||||
let token = Some(crate::script::binding_abi::string_arg(
|
||||
lua,
|
||||
token,
|
||||
r#"Usage: UnitIsPVPFreeForAll("unit")"#,
|
||||
)?);
|
||||
Ok(if with_unit(lua, &token, false, |u| u.is_pvp_ffa)? {
|
||||
Value::Integer(1)
|
||||
} else {
|
||||
@@ -588,7 +725,12 @@ pub(in crate::script) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
// files are on our chain.
|
||||
g.set(
|
||||
"UnitFactionGroup",
|
||||
lua.create_function(|lua, token: Option<String>| {
|
||||
lua.create_function(|lua, token: Value| {
|
||||
let token = Some(crate::script::binding_abi::string_arg(
|
||||
lua,
|
||||
token,
|
||||
r#"Usage: UnitFactionGroup("unit")"#,
|
||||
)?);
|
||||
let pair = with_unit(lua, &token, (None, None), |u| {
|
||||
(u.faction_group.clone(), u.faction_group_localized.clone())
|
||||
})?;
|
||||
@@ -615,7 +757,12 @@ pub(in crate::script) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
// reports nil, nil — the live API's shape for an absent unit.
|
||||
g.set(
|
||||
"UnitRace",
|
||||
lua.create_function(|lua, token: Option<String>| {
|
||||
lua.create_function(|lua, token: Value| {
|
||||
let token = Some(crate::script::binding_abi::string_arg(
|
||||
lua,
|
||||
token,
|
||||
r#"Usage: UnitRace("unit")"#,
|
||||
)?);
|
||||
let pair = with_unit(lua, &token, None, |u| {
|
||||
u.race.clone().zip(u.race_file.clone())
|
||||
})?;
|
||||
@@ -630,7 +777,12 @@ pub(in crate::script) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
)?;
|
||||
g.set(
|
||||
"UnitClass",
|
||||
lua.create_function(|lua, token: Option<String>| {
|
||||
lua.create_function(|lua, token: Value| {
|
||||
let token = Some(crate::script::binding_abi::string_arg(
|
||||
lua,
|
||||
token,
|
||||
r#"Usage: UnitClass("unit")"#,
|
||||
)?);
|
||||
let pair = with_unit(lua, &token, None, |u| {
|
||||
u.class.clone().zip(u.class_file.clone())
|
||||
})?;
|
||||
@@ -667,7 +819,12 @@ pub(in crate::script) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
// The truthy leg is the NUMBER 1, never a boolean; the false leg is nil, never `false`.
|
||||
g.set(
|
||||
"UnitHasRelicSlot",
|
||||
lua.create_function(|lua, token: Option<String>| {
|
||||
lua.create_function(|lua, token: Value| {
|
||||
let token = Some(crate::script::binding_abi::string_arg(
|
||||
lua,
|
||||
token,
|
||||
r#"Usage: UnitHasRelicSlot("unit")"#,
|
||||
)?);
|
||||
let has = with_unit(lua, &token, false, |u| u.has_relic_slot)?;
|
||||
Ok(if has { Value::Integer(1) } else { Value::Nil })
|
||||
})?,
|
||||
@@ -677,7 +834,12 @@ pub(in crate::script) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
// unit is absent or the sex hasn't streamed (`0`), the API's "can't tell".
|
||||
g.set(
|
||||
"UnitSex",
|
||||
lua.create_function(|lua, token: Option<String>| {
|
||||
lua.create_function(|lua, token: Value| {
|
||||
let token = Some(crate::script::binding_abi::string_arg(
|
||||
lua,
|
||||
token,
|
||||
r#"Usage: UnitSex("unit")"#,
|
||||
)?);
|
||||
let sex = with_unit(lua, &token, 0u8, |u| u.sex)?;
|
||||
Ok(if sex == 0 {
|
||||
Value::Nil
|
||||
@@ -763,7 +925,12 @@ pub(in crate::script) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
// alt-power color components; addons that read those handle nil).
|
||||
g.set(
|
||||
"UnitPowerType",
|
||||
lua.create_function(|lua, token: Option<String>| {
|
||||
lua.create_function(|lua, token: Value| {
|
||||
let token = Some(crate::script::binding_abi::string_arg(
|
||||
lua,
|
||||
token,
|
||||
r#"Usage: UnitPowerType("unit")"#,
|
||||
)?);
|
||||
let ty = with_unit(lua, &token, 0u8, |u| u.power_type)?;
|
||||
Ok((i64::from(ty), power_token(ty).to_string()))
|
||||
})?,
|
||||
@@ -782,13 +949,23 @@ pub(in crate::script) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
// have" is spelled `UnitPowerType(unit)` first, which we already provide and which IS 1.12.
|
||||
g.set(
|
||||
"UnitMana",
|
||||
lua.create_function(|lua, token: Option<String>| {
|
||||
lua.create_function(|lua, token: Value| {
|
||||
let token = Some(crate::script::binding_abi::string_arg(
|
||||
lua,
|
||||
token,
|
||||
r#"Usage: UnitMana("unit")"#,
|
||||
)?);
|
||||
with_unit(lua, &token, 0i64, |u| i64::from(u.power))
|
||||
})?,
|
||||
)?;
|
||||
g.set(
|
||||
"UnitManaMax",
|
||||
lua.create_function(|lua, token: Option<String>| {
|
||||
lua.create_function(|lua, token: Value| {
|
||||
let token = Some(crate::script::binding_abi::string_arg(
|
||||
lua,
|
||||
token,
|
||||
r#"Usage: UnitManaMax("unit")"#,
|
||||
)?);
|
||||
with_unit(lua, &token, 0i64, |u| i64::from(u.max_power))
|
||||
})?,
|
||||
)?;
|
||||
@@ -800,7 +977,12 @@ pub(in crate::script) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
let is_player = |token: &Option<String>| token.as_deref() == Some("player");
|
||||
g.set(
|
||||
"UnitXP",
|
||||
lua.create_function(move |lua, token: Option<String>| {
|
||||
lua.create_function(move |lua, token: Value| {
|
||||
let token = Some(crate::script::binding_abi::string_arg(
|
||||
lua,
|
||||
token,
|
||||
r#"Usage: UnitXP("unit")"#,
|
||||
)?);
|
||||
let model = lua.app_data_ref::<Model>().expect("model app_data");
|
||||
Ok(if is_player(&token) {
|
||||
i64::from(model.player_xp)
|
||||
@@ -811,7 +993,12 @@ pub(in crate::script) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
)?;
|
||||
g.set(
|
||||
"UnitXPMax",
|
||||
lua.create_function(move |lua, token: Option<String>| {
|
||||
lua.create_function(move |lua, token: Value| {
|
||||
let token = Some(crate::script::binding_abi::string_arg(
|
||||
lua,
|
||||
token,
|
||||
r#"Usage: UnitXPMax("unit")"#,
|
||||
)?);
|
||||
let model = lua.app_data_ref::<Model>().expect("model app_data");
|
||||
Ok(if is_player(&token) {
|
||||
i64::from(model.player_next_level_xp)
|
||||
|
||||
@@ -644,9 +644,14 @@ pub(crate) fn token_recognised(token: &str) -> bool {
|
||||
/// 1203's shape pointed the other way: a failure the client reports, silently swallowed.
|
||||
///
|
||||
/// The split is THREE-way, not two, and the two quiet legs are as carved:
|
||||
/// * **absent argument** — quiet nil (the per-binding argument gates are NOT uniform; `UnitName`
|
||||
/// has its own `lua_isstring` gate and `UnitExists` has none at all, and only those two poles
|
||||
/// are verified, so this does not invent a gate for the other ~102);
|
||||
/// * **absent argument** — quiet nil *here*, because this helper is only the resolver's half.
|
||||
/// Whether a nil ever reaches it is the BINDING's question, and it is settled per binding:
|
||||
/// wow-re's census of all 83 entries at `0x850438` found 53 that gate the token position with
|
||||
/// `lua_isstring` and raise `Usage:`, against 13 unit-token bindings with no gate at all
|
||||
/// (decision 1834). The gated ones call `binding_abi::string_arg` before they get here, so a
|
||||
/// nil never arrives; the quiet 13 pass it straight through. This comment used to say the
|
||||
/// gates were "NOT uniform … only those two poles are verified" and decline to guess, which
|
||||
/// was the right call at the time — the table now exists, so it is applied rather than feared;
|
||||
/// * **`""`** — quiet nil;
|
||||
/// * a **recognised** token naming nothing (`"party5"` solo, `"playerfoo"`) — quiet nil.
|
||||
pub(crate) fn check_unit_token(token: &Option<String>) -> mlua::Result<()> {
|
||||
|
||||
@@ -984,11 +984,32 @@ fn an_unrecognised_unit_token_raises_and_a_recognised_empty_one_does_not() {
|
||||
implementation that raises on \"did not resolve\" gets wrong."
|
||||
);
|
||||
}
|
||||
// QUIET NIL, leg 3: the empty string and an absent argument. The per-binding argument gates are
|
||||
// NOT uniform in the client and only two poles are verified, so this asserts the resolver's
|
||||
// behaviour and does not invent a gate.
|
||||
// QUIET NIL, leg 3: the empty string — which passes the `lua_isstring` gate (it IS a string)
|
||||
// and dies quietly in the resolver's own NULL/empty guard.
|
||||
assert!(s.run(r#"UnitName("")"#).is_ok());
|
||||
assert!(s.run("UnitName()").is_ok());
|
||||
|
||||
// …but an ABSENT or nil argument is NOT leg 3, and this line used to assert that it was.
|
||||
// `UnitName 0x517020` gates its token position at `0x517048` and raises
|
||||
// `Usage: UnitName("unit")` (`0x850ee0`); `luaL_error` does not return. The comment this
|
||||
// replaces said the per-binding gates were "not uniform … only two poles verified", which was
|
||||
// true when it was written — wow-re has since censused all 83 entries of the table at
|
||||
// `0x850438`: 53 gate and raise, and only 13 unit-token bindings are quiet. Decision 1834.
|
||||
assert!(s.run("UnitName()").is_err(), "absent argument raises");
|
||||
assert!(s.run("UnitName(nil)").is_err(), "nil argument raises");
|
||||
// A NUMBER passes the gate — `lua_isstring` admits tag 3 — and is handed to the resolver as
|
||||
// "5", which then raises the family's OTHER message rather than the `Usage:` one.
|
||||
assert!(
|
||||
s.run("UnitName(5)").is_err(),
|
||||
"a number reaches the resolver"
|
||||
);
|
||||
|
||||
// The thirteen that stay quiet, three of them here: no gate at all, so nil is fine.
|
||||
for quiet in ["UnitExists", "UnitIsVisible", "UnitClassification"] {
|
||||
assert!(
|
||||
s.run(&format!("{quiet}()")).is_ok(),
|
||||
"{quiet} is one of the 13 with no gate — nil must stay quiet"
|
||||
);
|
||||
}
|
||||
|
||||
// A two-unit call gates BOTH arguments.
|
||||
assert!(s.run(r#"UnitIsUnit("player", "bogus")"#).is_err());
|
||||
@@ -1371,3 +1392,58 @@ fn unit_has_relic_slot_answers_one_or_nil() {
|
||||
"function"
|
||||
);
|
||||
}
|
||||
|
||||
/// **The two-token predicates gate BOTH positions** — decision 1836, closing the half 1834 left
|
||||
/// open on purpose.
|
||||
///
|
||||
/// 1834 applied the `lua_isstring` gate only where wow-re's partial list named a single-token
|
||||
/// binding, because a census that finds "a gate somewhere in this body" cannot say *which*
|
||||
/// argument carries it. The complete 83-row table (`nil-unit-token-arg-law.md` §10) resolves it:
|
||||
/// these seven each carry **two** `lua_isstring` sites, so either argument being nil raises.
|
||||
#[test]
|
||||
fn a_two_token_predicate_raises_on_either_nil_argument() {
|
||||
let mut s = UiScript::new().unwrap();
|
||||
s.set_unit("player", Some(player()));
|
||||
s.set_unit("target", Some(player()));
|
||||
|
||||
for verb in [
|
||||
"UnitIsUnit",
|
||||
"UnitIsEnemy",
|
||||
"UnitIsFriend",
|
||||
"UnitCanCooperate",
|
||||
"UnitCanAttack",
|
||||
"UnitReaction",
|
||||
] {
|
||||
assert!(
|
||||
s.run(&format!(r#"{verb}("player", "target")"#)).is_ok(),
|
||||
"{verb} with both tokens is fine"
|
||||
);
|
||||
assert!(
|
||||
s.run(&format!(r#"{verb}("player")"#)).is_err(),
|
||||
"{verb} with the SECOND argument absent must raise"
|
||||
);
|
||||
assert!(
|
||||
s.run(&format!(r#"{verb}(nil, "target")"#)).is_err(),
|
||||
"{verb} with the FIRST argument nil must raise"
|
||||
);
|
||||
assert!(s.run(&format!("{verb}()")).is_err(), "{verb} with neither");
|
||||
}
|
||||
|
||||
// `GetRaidTargetIndex` is the one of six previously-unclassified names that takes a token, and
|
||||
// it is gated. Its usage string names the argument UNQUOTED — the reference's own spelling.
|
||||
assert!(s.run(r#"GetRaidTargetIndex("player")"#).is_ok());
|
||||
assert!(s.run("GetRaidTargetIndex()").is_err());
|
||||
|
||||
// …while the other five take no argument at all and have no gate. `GetTimeToWellRested` is a
|
||||
// three-instruction stub in the reference that always pushes nil; computing a value there
|
||||
// would be divergence, not a feature.
|
||||
for none in [
|
||||
"GetQuestGreenRange",
|
||||
"GetRestState",
|
||||
"GetXPExhaustion",
|
||||
"GetTimeToWellRested",
|
||||
"GetBillingTimeRested",
|
||||
] {
|
||||
assert!(s.run(&format!("{none}()")).is_ok(), "{none} takes no token");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -714,13 +714,32 @@ impl EditBoxState {
|
||||
EditOutcome::changed(self.insert(&cleaned).text_changed)
|
||||
}
|
||||
|
||||
/// `SetText` (`0x77be00`): short-circuits when unchanged (the caller then fires nothing); else
|
||||
/// replaces, caret to the end, caps enforced.
|
||||
/// `SetText` (`0x77be00`) is a clear-all followed by `Insert`, not a plain assignment, and the
|
||||
/// order of its three parts is load-bearing (decision 1831):
|
||||
///
|
||||
/// 1. the selection collapses **unconditionally**, before anything is compared — an identical
|
||||
/// `SetText` still drops a highlight;
|
||||
/// 2. a case-sensitive equality test then short-circuits (`SStrCmp` at `0x77be4b`), skipping the
|
||||
/// clear-all, the insert and both fires — so the caller fires nothing;
|
||||
/// 3. the changing leg is `0x77c500` (delete the whole buffer) then `0x77bee0` (`Insert`), which
|
||||
/// means `SetText` **inherits `Insert`'s gates**. The one that shows is `numeric`: the gate
|
||||
/// abandons the insert wholesale on any non-digit, and the clear-all has already run, so the
|
||||
/// box is left **empty** rather than unchanged. `SetNumber(-5)` and `SetNumber(0.8)` both
|
||||
/// empty a `numeric` box, because `-` and `.` are not digits.
|
||||
///
|
||||
/// The clear-all raises the `textChanged` dirty bit on every path it reaches, so the aborted
|
||||
/// insert still reports a change and still fires — reporting `""`.
|
||||
pub fn set_text(&mut self, s: &str) -> bool {
|
||||
self.collapse(); // (1) unconditional, ahead of the comparison
|
||||
if self.text == s {
|
||||
return false;
|
||||
return false; // (2)
|
||||
}
|
||||
self.text = s.to_string();
|
||||
// (3) the clear-all, then Insert — whose `numeric` gate refuses the whole string.
|
||||
self.text = if self.numeric && !s.chars().all(|c| c.is_ascii_digit()) {
|
||||
String::new()
|
||||
} else {
|
||||
s.to_string()
|
||||
};
|
||||
self.cursor = self.text.len();
|
||||
self.collapse();
|
||||
self.enforce_caps();
|
||||
|
||||
Reference in New Issue
Block a user