the numerals kit goes stock, and a chat link is its own mouse target
- the status-bar numerals kit (`TextStatusBar.xml` + `.lua`) is the reference's own, and the attempt found a class of silent failure: `TextStatusBar_Initialize()` takes no argument in 1.12 and acts on `this`; ours took an optional bar, our unit frames passed one, and the reference's version ignores it — so the numerals would simply never appear, no error, nothing missing; seventeen of our signatures differ from the reference's that way (Lua drops a surplus argument in silence), written down before their files migrate, and the three "ours narrower" ones — `StaticPopup_Hide(which, data)`, `StaticPopup_FindVisible`, `WorldMapButton_OnClick(button, frame)` — closed now, because that direction bites when a STOCK file arrives and calls our version
- a chat link needs no mouse on its frame: wow-re read the client's ctors as mouse-enabling Button, CheckButton, Slider, ColorSelect and EditBox and NOT the scroll frames; the objection ("something makes a 1.12 chat link clickable") resolved as the engine synthesising one mouse-enabled `CSimpleHyperlinkButton` per link span (`0x7a3240`) — so a link span is a hit-test candidate on its own, the chat window is inert everywhere else, and the wheel is its own predicate rather than a borrowed mouse bit; seven `*ScrollFrame` known divergences retire
- the relic fit rule: `IsValidForSlot`'s slot-17 leg is an EQUALITY — `(InventoryType == RELIC) == hasRelicSlot` — so a relic lights the ranged slot for a druid and a bow does not, on the bag bar and the paper doll alike; closing it half-right would have lit the highlight for a druid dragging a bow
- a chain `.xml` whose `.lua` never loaded: `MoneyInputFrame.xml` had been on the chain without its `.lua`, so the template loaded and all ten `MoneyInputFrame_*` verbs read nil, silently — a gate now fails any chain `.xml` whose `.lua` sibling is neither sourced nor listed; the gap report's LOAD column carries the probe's collision caveat, its fx column stops crying wolf (it follows `<Script file=>`, stops reading `.Field(` as a global, subtracts a file's own locals), and a window is its xml PLUS the lua it sources
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
@@ -76,7 +76,7 @@
|
||||
forced-show path). A real forced-highlight affordance is a `benilla-ui` follow-up, not
|
||||
blocking this slice — `BenillaPaperDollSlot_LockHighlight`/`_UnlockHighlight` below
|
||||
tint the slot's Quickslot2 ring instead, flagged at their own definition.
|
||||
- The relic slot: **restored, and this note used to be wrong** (decision 1785). It read
|
||||
- The relic slot: **restored, and this note used to be wrong** (decision 1796). It read
|
||||
"`UnitHasRelicSlot` is vanilla-always-false (there is no relic slot in 1.12), so the
|
||||
ref's branches are DEAD CODE" — they are not. `ChrClasses.dbc` field 16 is set for
|
||||
Paladin, Shaman and Druid, and INVSLOT 17 doubles as their relic slot. All four of the
|
||||
@@ -527,7 +527,7 @@
|
||||
-- ref PaperDollFrame_OnShow (l.568-585): SetGuild OMITTED (named deferral, this file's
|
||||
-- header). The relic branch (l.580-584) is transcribed verbatim — a relic class has no
|
||||
-- ranged weapon and therefore no ammo, so its ammo slot hides. This used to be dropped on
|
||||
-- the belief that UnitHasRelicSlot is always false; decision 1785.
|
||||
-- the belief that UnitHasRelicSlot is always false; decision 1796.
|
||||
function PaperDollFrame_OnShow()
|
||||
paintRows(paperDollRows)
|
||||
if UnitHasRelicSlot("player") then
|
||||
@@ -823,7 +823,7 @@
|
||||
end
|
||||
|
||||
-- ref PaperDollFrame_SetRangedAttack (l.419-466). `hasRelic` is transcribed (it used to
|
||||
-- be dropped — decision 1785). It is the whole reason a paladin's ranged block reads
|
||||
-- be dropped — decision 1796). It is the whole reason a paladin's ranged block reads
|
||||
-- "n/a" rather than the stats of the libram sitting in that slot: `noRanged` is set when
|
||||
-- there is no ranged texture OR the class has a relic slot, and `SetRangedDamage` /
|
||||
-- `SetRangedAttackPower` both read it (ref l.482 / l.552), so one flag blanks all three
|
||||
@@ -1020,7 +1020,7 @@
|
||||
button.emptyTexture = texture
|
||||
-- `checkRelic` is the number 1 for the ranged slot alone (a property of the SLOT); the
|
||||
-- class half is `UnitHasRelicSlot`, and the ref reads the two as a conjunction. Both
|
||||
-- halves are live — decision 1785.
|
||||
-- halves are live — decision 1796.
|
||||
button.checkRelic = checkRelic
|
||||
button.icon = getglobal(button:GetName() .. "IconTexture")
|
||||
button.count = getglobal(button:GetName() .. "Count")
|
||||
@@ -1105,7 +1105,7 @@
|
||||
else
|
||||
-- ref l.678-681: the empty ranged slot of a relic class shows the relic plate,
|
||||
-- not the bow plate. The art ships (`UI-PaperDoll-Slot-Relic.blp`, 3916 bytes,
|
||||
-- patch.MPQ). Restored — decision 1785.
|
||||
-- patch.MPQ). Restored — decision 1796.
|
||||
local emptyTexture = button.emptyTexture
|
||||
if button.checkRelic and UnitHasRelicSlot("player") then
|
||||
emptyTexture = "Interface\\Paperdoll\\UI-PaperDoll-Slot-Relic.blp"
|
||||
@@ -1145,7 +1145,7 @@
|
||||
-- the doll popped a "Currently Equipped" 0/XX beside a template-full main tooltip).
|
||||
-- The ref's repair-mode branch (l.757-762) is INTERIM-omitted with the binding's
|
||||
-- repairCost. checkRelic (l.744-746) is transcribed — it was called TBC-flagged dead
|
||||
-- weight here, which was wrong (decision 1785): a paladin hovering their empty ranged
|
||||
-- weight here, which was wrong (decision 1796): a paladin hovering their empty ranged
|
||||
-- slot reads "Relic", not "Ranged".
|
||||
function BenillaPaperDollSlot_OnEnter(button)
|
||||
GameTooltip:SetOwner(button, "ANCHOR_RIGHT")
|
||||
|
||||
@@ -48,7 +48,7 @@
|
||||
1059/1060): ctrl previews the inspected item on your own body, shift posts its link.
|
||||
Nothing else happens on a click, which is still the reference's own shape.
|
||||
- `UnitHasRelicSlot` (ref l.123, l.22) — this note used to say "vanilla-always-false, so
|
||||
the relic branch can never fire". **That is wrong** (decision 1785): the flag is set
|
||||
the relic branch can never fire". **That is wrong** (decision 1796): the flag is set
|
||||
for Paladin, Shaman and Druid, and the binding now exists and answers for any token,
|
||||
with no `"player"` fast path — inspecting an enemy druid answers 1. The branch is
|
||||
still not transcribed here, but for a different and honest reason: 1.12's inspect pane
|
||||
|
||||
@@ -1,183 +0,0 @@
|
||||
<!--
|
||||
The shared status-bar readout machinery — the contract 1.12's TextStatusBar.lua/.xml defines
|
||||
(decision 1082), written here in our own terms. A bar opts in by calling
|
||||
TextStatusBar_Initialize() in its OnLoad (setting `textLockable`), wiring a FontString via
|
||||
SetTextStatusBarText(bar, text), and routing its OnEnter/OnLeave/OnValueChanged through the
|
||||
Show/Hide/Update functions here. The text is "value / max", shown while hovered (a lockShow
|
||||
refcount) or always when the `statusBarText` cvar is "1" ("Status Text" in the options; our
|
||||
GetCVar returns nil for a cvar the engine doesn't carry, which reads as OFF — the 1.12
|
||||
default).
|
||||
|
||||
Customers: the XP bar (ActionBar.xml) and, since decision 1143, the player and pet frames'
|
||||
health/power bars (UnitFrames.xml). `textLockable` is what the statusBarText cvar pins, and
|
||||
which bars carry it is the REFERENCE's own split, not an omission of ours: 1.12 sets it on the
|
||||
player's two bars, the pet's two and the XP bar, and on nothing else. The target frame's bars
|
||||
are deliberately not lockable there — its numerals are hover-only — so ours have the machinery
|
||||
wired and no text regions yet; the hover half needs mouse-enabled bars we do not have.
|
||||
-->
|
||||
<Ui xmlns="http://www.blizzard.com/wow/ui/">
|
||||
<Script><![CDATA[
|
||||
-- The readout has exactly three reasons to be on screen, and every function below is some
|
||||
-- combination of them: the Status Text option pins it (only on a bar that opted in with
|
||||
-- `textLockable`), the pointer is on the bar (a refcount, so nested hovers balance), or the
|
||||
-- bar is showing its zero text — a phrase rather than a number, which stands on its own.
|
||||
--
|
||||
-- The option test is the one of the three that reads from outside the bar, and it is asked
|
||||
-- from three places, so it lives here once. It takes the option's VALUE rather than calling
|
||||
-- GetCVar itself: the CVAR_UPDATE handler is handed the new value along with the event and
|
||||
-- must not go asking the engine for the one it is being told about.
|
||||
local function optionPins(bar, setting)
|
||||
return setting == "1" and bar.textLockable
|
||||
end
|
||||
|
||||
-- `bar` is optional and falls back to `this`, the same shape TextStatusBar_UpdateTextString
|
||||
-- below already has (and for the same reason): the ref calls this from a bar's own OnLoad,
|
||||
-- where `this` is the bar, but our unit frames wire their children from ONE shared
|
||||
-- BenillaUnitFrame_OnLoad that holds each bar as a value, not as `this` (decision 1143).
|
||||
function TextStatusBar_Initialize(bar)
|
||||
if not bar then
|
||||
bar = this
|
||||
end
|
||||
bar:RegisterEvent("CVAR_UPDATE")
|
||||
bar.lockShow = 0
|
||||
end
|
||||
|
||||
-- Hand a bar the FontString its readout lives in. A bar without one is never an error
|
||||
-- below — it simply has nothing to say, and every entry point here checks for that.
|
||||
function SetTextStatusBarText(bar, text)
|
||||
if not bar or not text then
|
||||
return
|
||||
end
|
||||
bar.TextString = text
|
||||
end
|
||||
|
||||
function TextStatusBar_OnEvent(cvar, value)
|
||||
if event ~= "CVAR_UPDATE" or cvar ~= "STATUS_BAR_TEXT" or not this.TextString then
|
||||
return
|
||||
end
|
||||
-- Switching the option ON pins the readout up at once. Switching it off only puts the
|
||||
-- readout away when nothing else is holding it there: a hover in progress keeps its own,
|
||||
-- and gets to hide it when it ends.
|
||||
if optionPins(this, value) then
|
||||
this.TextString:Show()
|
||||
elseif this.lockShow == 0 then
|
||||
this.TextString:Hide()
|
||||
end
|
||||
end
|
||||
|
||||
-- Repaint one bar's readout from the value it is currently carrying. `textStatusBar` is
|
||||
-- optional and falls back to `this` for the same reason TextStatusBar_Initialize's is.
|
||||
function TextStatusBar_UpdateTextString(textStatusBar)
|
||||
if not textStatusBar then
|
||||
textStatusBar = this
|
||||
end
|
||||
local readout = textStatusBar.TextString
|
||||
if not readout then
|
||||
return
|
||||
end
|
||||
local value = textStatusBar:GetValue()
|
||||
local _, valueMax = textStatusBar:GetMinMaxValues()
|
||||
-- No ceiling is not the same thing as a bar at zero (below): it means the bar does not
|
||||
-- apply to this unit at all, so the BAR leaves the screen rather than reading out a
|
||||
-- number. Its readout is left exactly as it stands and comes back with it.
|
||||
if valueMax <= 0 then
|
||||
textStatusBar:Hide()
|
||||
return
|
||||
end
|
||||
textStatusBar:Show()
|
||||
if value == 0 and textStatusBar.zeroText then
|
||||
-- Empty, and this bar has a phrase for empty. The phrase displaces the numbers and
|
||||
-- is shown outright: it is a message, so neither the option nor a hover gates it,
|
||||
-- and `isZero` is the flag that tells the hover not to take it away again.
|
||||
textStatusBar.isZero = 1
|
||||
readout:SetText(textStatusBar.zeroText)
|
||||
readout:Show()
|
||||
return
|
||||
end
|
||||
textStatusBar.isZero = nil
|
||||
if textStatusBar.prefix then
|
||||
readout:SetText(textStatusBar.prefix .. " " .. value .. " / " .. valueMax)
|
||||
else
|
||||
readout:SetText(value .. " / " .. valueMax)
|
||||
end
|
||||
if optionPins(textStatusBar, GetCVar("statusBarText")) or textStatusBar.lockShow > 0 then
|
||||
readout:Show()
|
||||
else
|
||||
readout:Hide()
|
||||
end
|
||||
end
|
||||
|
||||
function TextStatusBar_OnValueChanged()
|
||||
TextStatusBar_UpdateTextString()
|
||||
end
|
||||
|
||||
function SetTextStatusBarTextPrefix(bar, prefix)
|
||||
if bar and bar.TextString then
|
||||
bar.prefix = prefix
|
||||
end
|
||||
end
|
||||
|
||||
function SetTextStatusBarTextZeroText(bar, zeroText)
|
||||
if bar and bar.TextString then
|
||||
bar.zeroText = zeroText
|
||||
end
|
||||
end
|
||||
|
||||
-- One more hold on the readout. `lockShow` counts rather than flags because two things can
|
||||
-- be holding it open at once: the character sheet takes a hold for as long as it is open
|
||||
-- (CharacterFrame_OnShow, decision 1146) and a hover on the same bar takes a second one on
|
||||
-- top. Only the last release puts the readout away, so the hover still balances.
|
||||
function ShowTextStatusBarText(bar)
|
||||
if bar and bar.TextString then
|
||||
bar.lockShow = (bar.lockShow or 0) + 1
|
||||
bar.TextString:Show()
|
||||
end
|
||||
end
|
||||
|
||||
-- One hold released. The count floors at none rather than going negative, so an unbalanced
|
||||
-- release — a frame hidden out from under a hover — cannot strand the readout on screen.
|
||||
function HideTextStatusBarText(bar)
|
||||
if bar and bar.TextString then
|
||||
bar.lockShow = math.max((bar.lockShow or 0) - 1, 0)
|
||||
-- Releasing a hold only puts the readout away if none of the three reasons is left.
|
||||
-- The ref reads `this.isZero` for the middle one — a leak of the handler's own frame
|
||||
-- into a helper that otherwise takes `bar`. Equivalent at every call site (each
|
||||
-- caller passes its handler's own frame), so it is written as `bar`, which also
|
||||
-- keeps the function callable outside a handler, where `this` is nil.
|
||||
if bar.lockShow > 0 or bar.isZero == 1
|
||||
or optionPins(bar, GetCVar("statusBarText")) then
|
||||
bar.TextString:Show()
|
||||
else
|
||||
bar.TextString:Hide()
|
||||
end
|
||||
end
|
||||
end
|
||||
]]></Script>
|
||||
|
||||
<!--
|
||||
The opt-in template (ref-TextStatusBar.xml): every script routed through the functions
|
||||
above. One transcription cut, named: the ref OnEnter's TargetFrame arm (re-anchor the unit
|
||||
tooltip under the target frame's bars) references a TextStatusBar-inheriting TargetFrame
|
||||
bar we don't build yet — it is dead until that slice, so it is left out rather than
|
||||
shipped unreachable. A frame that overrides all five scripts (the XP bar does) takes only
|
||||
the StatusBar widget type from inheriting this.
|
||||
-->
|
||||
<StatusBar name="TextStatusBar" virtual="true">
|
||||
<Scripts>
|
||||
<OnLoad>TextStatusBar_Initialize()</OnLoad>
|
||||
<OnEvent>TextStatusBar_OnEvent(arg1, arg2)</OnEvent>
|
||||
<OnEnter>
|
||||
TextStatusBar_UpdateTextString()
|
||||
ShowTextStatusBarText(this)
|
||||
if this.tooltipTitle and GameTooltip_AddNewbieTip then
|
||||
BenillaTooltip_AddNewbieTipFor(this, this.tooltipTitle, 1.0, 1.0, 1.0, this.tooltipText, 1)
|
||||
end
|
||||
</OnEnter>
|
||||
<OnLeave>
|
||||
HideTextStatusBarText(this)
|
||||
if GameTooltip then GameTooltip:Hide() end
|
||||
</OnLeave>
|
||||
<OnValueChanged>TextStatusBar_OnValueChanged()</OnValueChanged>
|
||||
</Scripts>
|
||||
</StatusBar>
|
||||
</Ui>
|
||||
@@ -1025,10 +1025,24 @@
|
||||
return "seconds"
|
||||
end
|
||||
|
||||
function StaticPopup_FindVisible(which)
|
||||
-- ref StaticPopup.lua l.1414-1426, `data` included. Ours took `which` alone until 1793
|
||||
-- found the arity drift; the reference's second argument is what makes a `multiple` dialog
|
||||
-- addressable, and dropping it is silent in both directions (Lua discards a surplus
|
||||
-- argument without complaint).
|
||||
--
|
||||
-- The `info.multiple` gate is the reference's own and is why `data` is not simply an
|
||||
-- equality: a dialog kind declared `multiple` can have several instances up at once, so it
|
||||
-- is matched by data; every other kind has at most one, so its data is not consulted at
|
||||
-- all. An unknown `which` answers nil rather than scanning — the reference's own early out.
|
||||
function StaticPopup_FindVisible(which, data)
|
||||
local info = StaticPopupDialogs[which]
|
||||
if not info then
|
||||
return nil
|
||||
end
|
||||
for index = 1, STATICPOPUP_NUMDIALOGS do
|
||||
local frame = getglobal("StaticPopup" .. index)
|
||||
if frame:IsShown() and frame.which == which then
|
||||
if frame:IsShown() and frame.which == which
|
||||
and (not info.multiple or frame.data == data) then
|
||||
return frame
|
||||
end
|
||||
end
|
||||
@@ -1044,11 +1058,19 @@
|
||||
return nil
|
||||
end
|
||||
|
||||
function StaticPopup_Hide(which)
|
||||
-- ref StaticPopup.lua l.1703-1710, `data` included (1793). With a `data` the reference
|
||||
-- hides only the instance carrying it; without one it hides every instance of the kind,
|
||||
-- which is what every one-argument caller means and gets.
|
||||
--
|
||||
-- Note the reference does NOT test `IsShown()` here, where `FindVisible` does — hiding an
|
||||
-- already-hidden dialog is a no-op, and the missing test is not an oversight to correct:
|
||||
-- `Hide()` on a hidden frame still runs nothing, and adding the guard would change which
|
||||
-- frames the loop touches for no visible difference. Transcribed as it is.
|
||||
function StaticPopup_Hide(which, data)
|
||||
for index = 1, STATICPOPUP_NUMDIALOGS do
|
||||
local frame = getglobal("StaticPopup" .. index)
|
||||
if frame:IsShown() and frame.which == which then
|
||||
frame:Hide()
|
||||
local dialog = getglobal("StaticPopup" .. index)
|
||||
if dialog.which == which and (not data or data == dialog.data) then
|
||||
dialog:Hide()
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -277,9 +277,9 @@
|
||||
-- and the only conditional here: 1.12 sets it on the player's two bars, the pet's two
|
||||
-- and the XP bar, and on nothing else, which is exactly why Status Bar Text pins your
|
||||
-- own numbers and your pet's but never the target's (decision 1143).
|
||||
if TextStatusBar_Initialize then -- engine tests load file subsets
|
||||
TextStatusBar_Initialize(frame.healthbar)
|
||||
TextStatusBar_Initialize(frame.manabar)
|
||||
-- `TextStatusBar_Initialize` is NOT called here any more — each bar calls it from its
|
||||
-- own OnLoad, the reference's way (see the bar declarations below).
|
||||
if SetTextStatusBarText then -- engine tests load file subsets
|
||||
SetTextStatusBarText(frame.healthbar, getglobal(name .. "HealthBarText"))
|
||||
SetTextStatusBarText(frame.manabar, getglobal(name .. "ManaBarText"))
|
||||
if unit == "player" then
|
||||
@@ -1265,6 +1265,13 @@
|
||||
<BarTexture file="Interface\TargetingFrame\UI-StatusBar"/>
|
||||
<BarColor r="0.0" g="1.0" b="0.0"/>
|
||||
<Scripts>
|
||||
<!-- The reference initialises a numerals bar from the BAR'S OWN OnLoad
|
||||
(PlayerFrame.xml l.300-303, PetFrame.xml l.138/163), never from the parent
|
||||
with the bar as an argument — `TextStatusBar_Initialize()` takes no
|
||||
argument in 1.12 and acts on `this`. Ours called it from the parent's
|
||||
OnLoad, which needed a widened signature and would have broken silently
|
||||
the day TextStatusBar.xml came off the player's chain (decisions 1793). -->
|
||||
<OnLoad>if TextStatusBar_Initialize then TextStatusBar_Initialize() end</OnLoad>
|
||||
<OnEvent>if TextStatusBar_OnEvent then TextStatusBar_OnEvent(arg1, arg2) end</OnEvent>
|
||||
<OnValueChanged>if TextStatusBar_OnValueChanged then TextStatusBar_OnValueChanged() end</OnValueChanged>
|
||||
<OnEnter>BenillaUnitFrameBar_OnEnter(self)</OnEnter>
|
||||
@@ -1281,6 +1288,13 @@
|
||||
<BarTexture file="Interface\TargetingFrame\UI-StatusBar"/>
|
||||
<BarColor r="0.0" g="0.0" b="1.0"/>
|
||||
<Scripts>
|
||||
<!-- The reference initialises a numerals bar from the BAR'S OWN OnLoad
|
||||
(PlayerFrame.xml l.300-303, PetFrame.xml l.138/163), never from the parent
|
||||
with the bar as an argument — `TextStatusBar_Initialize()` takes no
|
||||
argument in 1.12 and acts on `this`. Ours called it from the parent's
|
||||
OnLoad, which needed a widened signature and would have broken silently
|
||||
the day TextStatusBar.xml came off the player's chain (decisions 1793). -->
|
||||
<OnLoad>if TextStatusBar_Initialize then TextStatusBar_Initialize() end</OnLoad>
|
||||
<OnEvent>if TextStatusBar_OnEvent then TextStatusBar_OnEvent(arg1, arg2) end</OnEvent>
|
||||
<OnValueChanged>if TextStatusBar_OnValueChanged then TextStatusBar_OnValueChanged() end</OnValueChanged>
|
||||
<OnEnter>BenillaUnitFrameBar_OnEnter(self)</OnEnter>
|
||||
@@ -1609,6 +1623,13 @@
|
||||
<BarTexture file="Interface\TargetingFrame\UI-StatusBar"/>
|
||||
<BarColor r="0.0" g="1.0" b="0.0"/>
|
||||
<Scripts>
|
||||
<!-- The reference initialises a numerals bar from the BAR'S OWN OnLoad
|
||||
(PlayerFrame.xml l.300-303, PetFrame.xml l.138/163), never from the parent
|
||||
with the bar as an argument — `TextStatusBar_Initialize()` takes no
|
||||
argument in 1.12 and acts on `this`. Ours called it from the parent's
|
||||
OnLoad, which needed a widened signature and would have broken silently
|
||||
the day TextStatusBar.xml came off the player's chain (decisions 1793). -->
|
||||
<OnLoad>if TextStatusBar_Initialize then TextStatusBar_Initialize() end</OnLoad>
|
||||
<OnEvent>if TextStatusBar_OnEvent then TextStatusBar_OnEvent(arg1, arg2) end</OnEvent>
|
||||
<OnValueChanged>if TextStatusBar_OnValueChanged then TextStatusBar_OnValueChanged() end</OnValueChanged>
|
||||
<OnEnter>BenillaUnitFrameBar_OnEnter(self)</OnEnter>
|
||||
@@ -1625,6 +1646,13 @@
|
||||
<BarTexture file="Interface\TargetingFrame\UI-StatusBar"/>
|
||||
<BarColor r="0.0" g="0.0" b="1.0"/>
|
||||
<Scripts>
|
||||
<!-- The reference initialises a numerals bar from the BAR'S OWN OnLoad
|
||||
(PlayerFrame.xml l.300-303, PetFrame.xml l.138/163), never from the parent
|
||||
with the bar as an argument — `TextStatusBar_Initialize()` takes no
|
||||
argument in 1.12 and acts on `this`. Ours called it from the parent's
|
||||
OnLoad, which needed a widened signature and would have broken silently
|
||||
the day TextStatusBar.xml came off the player's chain (decisions 1793). -->
|
||||
<OnLoad>if TextStatusBar_Initialize then TextStatusBar_Initialize() end</OnLoad>
|
||||
<OnEvent>if TextStatusBar_OnEvent then TextStatusBar_OnEvent(arg1, arg2) end</OnEvent>
|
||||
<OnValueChanged>if TextStatusBar_OnValueChanged then TextStatusBar_OnValueChanged() end</OnValueChanged>
|
||||
<OnEnter>BenillaUnitFrameBar_OnEnter(self)</OnEnter>
|
||||
@@ -2265,9 +2293,9 @@
|
||||
frame.healthbar:SetFrameLevel(level - 1)
|
||||
frame.manabar:SetFrameLevel(level - 1)
|
||||
-- The pet's numerals, textLockable like the ref's (PetFrame.xml l.139/164; 1143).
|
||||
if TextStatusBar_Initialize then -- engine tests load file subsets
|
||||
TextStatusBar_Initialize(frame.healthbar)
|
||||
TextStatusBar_Initialize(frame.manabar)
|
||||
-- `TextStatusBar_Initialize` is NOT called here any more — each bar calls it from its
|
||||
-- own OnLoad, the reference's way (PetFrame.xml l.138/163).
|
||||
if SetTextStatusBarText then -- engine tests load file subsets
|
||||
SetTextStatusBarText(frame.healthbar, PetFrameHealthBarText)
|
||||
SetTextStatusBarText(frame.manabar, PetFrameManaBarText)
|
||||
frame.healthbar.textLockable = 1
|
||||
@@ -2574,6 +2602,13 @@
|
||||
<BarTexture file="Interface\TargetingFrame\UI-StatusBar"/>
|
||||
<BarColor r="0" g="1.0" b="0"/>
|
||||
<Scripts>
|
||||
<!-- The reference initialises a numerals bar from the BAR'S OWN OnLoad
|
||||
(PlayerFrame.xml l.300-303, PetFrame.xml l.138/163), never from the parent
|
||||
with the bar as an argument — `TextStatusBar_Initialize()` takes no
|
||||
argument in 1.12 and acts on `this`. Ours called it from the parent's
|
||||
OnLoad, which needed a widened signature and would have broken silently
|
||||
the day TextStatusBar.xml came off the player's chain (decisions 1793). -->
|
||||
<OnLoad>if TextStatusBar_Initialize then TextStatusBar_Initialize() end</OnLoad>
|
||||
<OnEvent>if TextStatusBar_OnEvent then TextStatusBar_OnEvent(arg1, arg2) end</OnEvent>
|
||||
<OnValueChanged>if TextStatusBar_OnValueChanged then TextStatusBar_OnValueChanged() end</OnValueChanged>
|
||||
<OnEnter>BenillaUnitFrameBar_OnEnter(self)</OnEnter>
|
||||
@@ -2587,6 +2622,13 @@
|
||||
<BarTexture file="Interface\TargetingFrame\UI-StatusBar"/>
|
||||
<BarColor r="0" g="0" b="1.0"/>
|
||||
<Scripts>
|
||||
<!-- The reference initialises a numerals bar from the BAR'S OWN OnLoad
|
||||
(PlayerFrame.xml l.300-303, PetFrame.xml l.138/163), never from the parent
|
||||
with the bar as an argument — `TextStatusBar_Initialize()` takes no
|
||||
argument in 1.12 and acts on `this`. Ours called it from the parent's
|
||||
OnLoad, which needed a widened signature and would have broken silently
|
||||
the day TextStatusBar.xml came off the player's chain (decisions 1793). -->
|
||||
<OnLoad>if TextStatusBar_Initialize then TextStatusBar_Initialize() end</OnLoad>
|
||||
<OnEvent>if TextStatusBar_OnEvent then TextStatusBar_OnEvent(arg1, arg2) end</OnEvent>
|
||||
<OnValueChanged>if TextStatusBar_OnValueChanged then TextStatusBar_OnValueChanged() end</OnValueChanged>
|
||||
<OnEnter>BenillaUnitFrameBar_OnEnter(self)</OnEnter>
|
||||
|
||||
@@ -389,12 +389,22 @@
|
||||
end
|
||||
|
||||
-- Left click drills into whatever the host finds at that map position; every other button
|
||||
-- is the zoom-out shortcut (ref WorldMapButton_OnClick — ours takes no button argument,
|
||||
-- since nothing but the map button itself routes here).
|
||||
function WorldMapButton_OnClick(mouseButton)
|
||||
-- is the zoom-out shortcut. ref WorldMapFrame.lua l.277-296. The SECOND argument is the button the click landed on,
|
||||
-- defaulting to `this` — restored in 1793's sweep, having been dropped in favour of naming
|
||||
-- `WorldMapButton` directly.
|
||||
--
|
||||
-- Equivalent for 1.12's own caller, which passes exactly that button
|
||||
-- (`WorldMapFrame.lua:174`) — and not equivalent for anyone else, which is the whole
|
||||
-- reason the reference takes it: a caller aiming the click at a different map surface was
|
||||
-- silently redirected to the main one, because Lua discards a surplus argument without
|
||||
-- complaint.
|
||||
function WorldMapButton_OnClick(mouseButton, button)
|
||||
CloseDropDownMenus();
|
||||
if ( mouseButton == "LeftButton" ) then
|
||||
ProcessMapClick(cursorMapPosition(WorldMapButton));
|
||||
if ( not button ) then
|
||||
button = this
|
||||
end
|
||||
ProcessMapClick(cursorMapPosition(button));
|
||||
else
|
||||
WorldMapZoomOutButton_OnClick();
|
||||
end
|
||||
|
||||
@@ -149,6 +149,14 @@ MoneyFrame.xml
|
||||
# `_SetCopper`/`_ClearFocus`/`_SetTextColor`/`_SetOnvalueChangedFunc` — six of the names below —
|
||||
# and neither can be sourced until they exist. (Trade still needs four ENGINE verbs beyond this:
|
||||
# `AddTradeMoney`, `GetCursorMoney`, `GetTradePlayerItemLink`, `GetTradeTargetItemLink`.)
|
||||
#
|
||||
# **TWO entries, and the `.lua` was missing.** This `.xml` declares only the template and does not
|
||||
# source its `.lua`; the reference's toc lists both (l.11-12). Listing the `.xml` alone brought the
|
||||
# template and none of the ten verbs — the header above has claimed otherwise since the day it was
|
||||
# written, and a probe of a full manifest load answered `nil` for every one of them. Same shape as
|
||||
# `TextStatusBar`, found by sweeping every chain entry for it once that one bit;
|
||||
# `every_chain_xml_brings_its_own_lua` is now a gate so neither can recur.
|
||||
Interface\FrameXML\MoneyInputFrame.lua
|
||||
Interface\FrameXML\MoneyInputFrame.xml
|
||||
# The UIPanel slot manager (decision 0084 §2): script-only, no frames of its own — must load
|
||||
# before any panel frame (Gossip/Merchant today) so ShowUIPanel/HideUIPanel already exist
|
||||
@@ -210,10 +218,24 @@ UnitPopup.xml
|
||||
# (`script/object/movable.rs`), so that note was stale too.
|
||||
Interface\FrameXML\ItemRef.xml
|
||||
# The shared status-bar numerals machinery (decision 1082): TextStatusBar_* + the opt-in
|
||||
# template — before its customers (the XP bar in ActionBar.xml today; the unit-frame
|
||||
# health/mana numerals are the ref's other users, when that slice lands). The ref's own TOC
|
||||
# loads TextStatusBar right before its unit frames, same seat as here.
|
||||
TextStatusBar.xml
|
||||
# template. The ref's own TOC loads TextStatusBar right before its unit frames, same seat.
|
||||
#
|
||||
# ══ THE STATUS-BAR NUMERALS — the reference's own, off the player's chain (1751) ═══════════
|
||||
# `TextStatusBar_*` + the `TextStatusBar` template. The reference's own toc loads it right
|
||||
# before its unit frames; this is the same seat.
|
||||
#
|
||||
# Nothing of ours inherits the template — it exists for the reference's windows, whose rows
|
||||
# carry `inherits="TextStatusBar"` and would have found ours. What blocked the swap was a
|
||||
# signature WE widened (`TextStatusBar_Initialize`), fixed by giving our own bars the OnLoad
|
||||
# the reference's have.
|
||||
#
|
||||
# **TWO entries, because the reference has two** (its toc l.32-33). This `.xml` declares only the
|
||||
# template and does NOT source its `.lua` — unlike most stock windows, whose `<Script file=>`
|
||||
# brings their code in. Listing the `.xml` alone loads a template whose OnLoad calls a function
|
||||
# nothing defined: every `TextStatusBar_*` name reads nil, every guarded call is a silent no-op,
|
||||
# and the numerals simply never appear. The `.lua` comes first, as it does there.
|
||||
Interface\FrameXML\TextStatusBar.lua
|
||||
Interface\FrameXML\TextStatusBar.xml
|
||||
# ══ THE UNIT-FRAME KIT — the reference's own, off the player's chain (1751, window 22) ══
|
||||
# `UnitFrame.xml` declares NO frames — it is a `<Script file="UnitFrame.lua"/>` and nothing else —
|
||||
# so this is a pure addition of thirteen globals: `UnitFrame_Initialize`, `_Update`, `_OnEvent`,
|
||||
@@ -674,10 +696,10 @@ RaidFrame.xml
|
||||
# label, our measurement being a frame late; that is what the swap tests.
|
||||
Interface\FrameXML\GossipFrame.xml
|
||||
# ══ THE VENDOR WINDOW — the reference's own, off the player's chain (1751) ══════════════════
|
||||
# It took three things, in this order: `PickupMerchantItem` (the vendor cursor, 1786), the
|
||||
# It took three things, in this order: `PickupMerchantItem` (the vendor cursor, 1797), the
|
||||
# `BenillaMoney_*` coin kit moving out to MoneyFrame.xml where eight other windows can reach it
|
||||
# without loading this one, and `SetMerchantCompareItem` (1791) — which the gap report could not
|
||||
# see at all until 1787 taught it to read `:Method(` sites, and which would have raised on the
|
||||
# without loading this one, and `SetMerchantCompareItem` (1802) — which the gap report could not
|
||||
# see at all until 1798 taught it to read `:Method(` sites, and which would have raised on the
|
||||
# first vendor-row hover with every check green.
|
||||
#
|
||||
# That is why the notes below say "grouping" rather than "reuses that file's helpers": the
|
||||
@@ -779,11 +801,11 @@ TradeSkillFrame.xml
|
||||
CraftFrame.xml
|
||||
# ══ THE LOOT WINDOW — the reference's own, off the player's chain (1751) ════════════════════
|
||||
# It never needed an engine VERB. It needed a widget TYPE: `<LootButton>` is a real registered
|
||||
# `CreateFrame` type (decision 1788), and registering it as a plain Button would have loaded the
|
||||
# `CreateFrame` type (decision 1799), and registering it as a plain Button would have loaded the
|
||||
# file and left every row dead — 1.12 has no Lua verb for "take slot N".
|
||||
#
|
||||
# Its ONE method, `SetSlot`, is also what the gap report's new `method=` column found from the
|
||||
# call site while wow-re was deriving it from the vtable (1787). Both columns read 0 now, and
|
||||
# call site while wow-re was deriving it from the vtable (1798). Both columns read 0 now, and
|
||||
# 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
|
||||
|
||||
@@ -1149,7 +1149,7 @@ pub(crate) enum ClientCommand {
|
||||
entry: u32,
|
||||
count: u8,
|
||||
},
|
||||
/// Buy into a **named** container slot (`CMSG_BUY_ITEM_IN_SLOT`, decision 1786) — what a
|
||||
/// Buy into a **named** container slot (`CMSG_BUY_ITEM_IN_SLOT`, decision 1797) — what a
|
||||
/// vendor row dragged off the merchant window and dropped into a bag or an equipment slot
|
||||
/// sends, as against [`ClientCommand::BuyItem`]'s auto-place from a plain row click.
|
||||
/// `bag_guid` is the container object's guid, or the player's own for the backpack and the
|
||||
|
||||
@@ -565,6 +565,9 @@ fn slot_view(
|
||||
// `ItemSubClass.dbc` — the count gate below reads its `DisplayFlags` bit 2. `None` (the DBC
|
||||
// failed to load) leaves every bag at 0, which is the plain-bag answer and the safe one.
|
||||
sub_classes: Option<&crate::ui_items::ItemSubClasses>,
|
||||
// The player's own `ChrClasses.dbc` relic flag — `find_equip_slot`'s second half (1803). It
|
||||
// decides INVSLOT 17 both ways, so it reaches every fit-rule read rather than one of them.
|
||||
has_relic_slot: bool,
|
||||
guid: u64,
|
||||
live_id: u32,
|
||||
) -> Option<InvSlotView> {
|
||||
@@ -630,7 +633,7 @@ fn slot_view(
|
||||
Some(crate::ui_items::item_link_full(
|
||||
entry, 0, roll, 0, &name, t.quality,
|
||||
)),
|
||||
find_equip_slot(t.inventory_type),
|
||||
find_equip_slot(t.inventory_type, has_relic_slot),
|
||||
(t.class, t.subclass),
|
||||
t.placeable_on_action_bar(),
|
||||
)
|
||||
@@ -725,7 +728,14 @@ fn inventory_slots(
|
||||
pending: &PendingItemOps,
|
||||
names: &mut crate::names::NameCache,
|
||||
sub_classes: Option<&crate::ui_items::ItemSubClasses>,
|
||||
classes: Option<&benilla_formats::ChrClasses>,
|
||||
) -> InventorySlots {
|
||||
// Resolved once from the player's own class byte, right here — the store is already in scope,
|
||||
// so nothing new is threaded through the callers for the class half.
|
||||
let has_relic_slot = store
|
||||
.0
|
||||
.unit_class()
|
||||
.is_some_and(|c| classes.is_some_and(|t| t.has_relic_slot(u32::from(c))));
|
||||
let mut inv: InventorySlots = Default::default();
|
||||
let ammo_id = store.0.player_ammo_id().unwrap_or(0);
|
||||
if ammo_id != 0 {
|
||||
@@ -786,6 +796,7 @@ fn inventory_slots(
|
||||
pending,
|
||||
names,
|
||||
sub_classes,
|
||||
has_relic_slot,
|
||||
guid,
|
||||
live,
|
||||
);
|
||||
@@ -825,6 +836,10 @@ fn bank_bag_slots(
|
||||
pending,
|
||||
names,
|
||||
sub_classes,
|
||||
// A bank bag slot holds a BAG, and `find_equip_slot(INVTYPE_BAG)` answers the four
|
||||
// bag slots whatever this says — the flag decides INVSLOT 17 alone, which no item in
|
||||
// this band can reach. `false` here is provable, not a default.
|
||||
false,
|
||||
guid,
|
||||
BANK_BAG_LIVE_FIRST + i as u32,
|
||||
);
|
||||
@@ -941,6 +956,9 @@ pub(crate) fn fire_stat_transitions(
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub(crate) fn feed_char(
|
||||
// `ChrClasses.dbc` field 16, for the fit rule's relic half — `ui_items::find_equip_slot`
|
||||
// (1803). Absent client data reads every class as an ordinary ranged wielder.
|
||||
classes: Option<Res<crate::chr_classes::ChrClassTable>>,
|
||||
script: Option<NonSendMut<UiScript>>,
|
||||
self_q: Query<&ObjectStore, With<SelfPlayer>>,
|
||||
changed_self: Query<(), (With<SelfPlayer>, Changed<ObjectStore>)>,
|
||||
@@ -1075,6 +1093,7 @@ pub(crate) fn feed_char(
|
||||
&pending,
|
||||
&mut names,
|
||||
sub_classes.as_deref(),
|
||||
classes.as_deref().map(|t| &t.0),
|
||||
);
|
||||
let bank_bags = bank_bag_slots(
|
||||
store,
|
||||
|
||||
@@ -553,6 +553,10 @@ fn resolve_slot(
|
||||
petitions: &mut crate::ui_petition::PetitionState,
|
||||
now: std::time::Instant,
|
||||
ui_now: f64,
|
||||
// The player's own `ChrClasses.dbc` relic flag — `find_equip_slot`'s second half. It decides
|
||||
// ONE slot (INVSLOT 17) and it decides it both ways, so it has to reach every fit-rule read
|
||||
// rather than being applied at one of them (1803).
|
||||
has_relic_slot: bool,
|
||||
) -> Option<ContainerSlot> {
|
||||
if guid == 0 {
|
||||
return None;
|
||||
@@ -674,7 +678,7 @@ fn resolve_slot(
|
||||
flags,
|
||||
already_bound,
|
||||
enchants: enchant_lines,
|
||||
equip_slots: find_equip_slot(t.inventory_type),
|
||||
equip_slots: find_equip_slot(t.inventory_type, has_relic_slot),
|
||||
bar_placeable: t.placeable_on_action_bar(),
|
||||
cooldown: t.use_spell.and_then(|u| {
|
||||
let sd = spells.and_then(|s| s.get(u.spell_id));
|
||||
@@ -765,6 +769,8 @@ pub(crate) fn resolve_item_locks(
|
||||
#[allow(clippy::too_many_arguments, clippy::type_complexity)] // the param list IS the input set
|
||||
pub(crate) fn feed_containers(
|
||||
script: Option<NonSendMut<UiScript>>,
|
||||
// `ChrClasses.dbc` field 16, for the fit rule's relic half — see `find_equip_slot` (1803).
|
||||
classes: Option<Res<crate::chr_classes::ChrClassTable>>,
|
||||
mut items: ResMut<Items>,
|
||||
icons: Option<Res<ItemDisplays>>,
|
||||
// The two item-DBC catalogs, as one param (the 16-SystemParam ceiling): `SpellItemEnchantment`'s
|
||||
@@ -961,6 +967,16 @@ pub(crate) fn feed_containers(
|
||||
let transitioned: Vec<(i64, u32)> = std::mem::take(&mut lock_cleared.0);
|
||||
|
||||
let mut fresh: HashMap<i64, ContainerState> = HashMap::new();
|
||||
// The player's own relic flag, resolved once. `find_equip_slot` reads it for INVSLOT 17 and
|
||||
// reads it BOTH ways, so an absent table (no client data) answers false — every class then
|
||||
// reads as an ordinary ranged wielder, which is the safe degradation.
|
||||
let has_relic_slot = player.is_some_and(|p| {
|
||||
p.0.unit_class().is_some_and(|c| {
|
||||
classes
|
||||
.as_deref()
|
||||
.is_some_and(|t| t.0.has_relic_slot(u32::from(c)))
|
||||
})
|
||||
});
|
||||
if let Some(store) = player {
|
||||
// Bag 0: the backpack — its slots live directly in the player descriptor.
|
||||
let mut slots = HashMap::new();
|
||||
@@ -978,6 +994,7 @@ pub(crate) fn feed_containers(
|
||||
&mut petitions,
|
||||
now,
|
||||
ui_now,
|
||||
has_relic_slot,
|
||||
) {
|
||||
slot.locked = pending.contains(0, u32::from(i) + 1);
|
||||
slots.insert(u32::from(i) + 1, slot);
|
||||
@@ -1032,6 +1049,7 @@ pub(crate) fn feed_containers(
|
||||
&mut petitions,
|
||||
now,
|
||||
ui_now,
|
||||
has_relic_slot,
|
||||
) {
|
||||
slot.locked = pending.contains(i64::from(bag), j as u32 + 1);
|
||||
slots.insert(j as u32 + 1, slot);
|
||||
@@ -1066,6 +1084,7 @@ pub(crate) fn feed_containers(
|
||||
&mut petitions,
|
||||
now,
|
||||
ui_now,
|
||||
has_relic_slot,
|
||||
) {
|
||||
slot.locked = pending.contains(BANK_CONTAINER, u32::from(i) + 1);
|
||||
slots.insert(u32::from(i) + 1, slot);
|
||||
@@ -1116,6 +1135,7 @@ pub(crate) fn feed_containers(
|
||||
&mut petitions,
|
||||
now,
|
||||
ui_now,
|
||||
has_relic_slot,
|
||||
) {
|
||||
slot.locked = pending.contains(bag_id, j as u32 + 1);
|
||||
slots.insert(j as u32 + 1, slot);
|
||||
@@ -1154,6 +1174,7 @@ pub(crate) fn feed_containers(
|
||||
&mut petitions,
|
||||
now,
|
||||
ui_now,
|
||||
has_relic_slot,
|
||||
) {
|
||||
slot.locked = pending.contains(KEYRING_CONTAINER, u32::from(i) + 1);
|
||||
slots.insert(u32::from(i) + 1, slot);
|
||||
|
||||
@@ -928,21 +928,17 @@ pub(super) const INVTYPE_AMMO: u32 = 24;
|
||||
/// actual equip still round-trips through `SMSG_INVENTORY_CHANGE_FAILURE`
|
||||
/// (`EQUIP_ERR_CANT_DUAL_WIELD`) if the class can't. Simpler than threading class into every
|
||||
/// caller for a highlight-only consequence.
|
||||
/// - **`INVTYPE_RELIC` answers no slots, and this one is a real gap** (decision 1785). It used to
|
||||
/// be excused on the grounds that the relic slot is vanilla-UI-invisible — that
|
||||
/// `UnitHasRelicSlot` is always false and no relic slot ever shows on the 1.12 paper doll.
|
||||
/// **That is false.** `ChrClasses.dbc` field 16 is set for Paladin, Shaman and Druid, and
|
||||
/// `IsValidForSlot 0x5da1d0`'s `slot == 0x11` leg requires `(InventoryType == 28) ==
|
||||
/// hasRelicSlot` — so for those three classes INVSLOT 17 takes a relic and refuses a ranged
|
||||
/// weapon, and for the other six the reverse.
|
||||
/// - **`INVTYPE_RELIC` is CLOSED** (decision 1803, over the gap 1796 named). `has_relic_slot` is
|
||||
/// the player's own, read off `ChrClasses.dbc` field 16, and `IsValidForSlot 0x5da1d0`'s
|
||||
/// `slot == 0x11` leg is an EQUALITY — it takes the slot iff
|
||||
/// `(InventoryType == 28 RELIC) == hasRelicSlot`. So RELIC and the three ranged types are exact
|
||||
/// complements on that one slot rather than a union: a druid dragging a bow is offered nothing,
|
||||
/// a warrior dragging an idol likewise, and a druid dragging an idol is offered INVSLOT 17.
|
||||
///
|
||||
/// What remains true is the *consequence*: this function drives the `CURSOR_UPDATE` highlight
|
||||
/// only, and the equip still round-trips through `SMSG_INVENTORY_CHANGE_FAILURE`. So the gap
|
||||
/// costs a relic-class player the highlight on their ranged slot when dragging a libram, and
|
||||
/// over-offers it when dragging a bow. Closing it needs the class threaded through
|
||||
/// [`crate::ui_char::slot_view`], which is the same threading the dual-wield bullet above
|
||||
/// declines — one change would close both.
|
||||
pub(super) fn find_equip_slot(inventory_type: u32) -> Vec<u8> {
|
||||
/// The dual-wield bullet above is NOT closed by the same threading, which is why it is still
|
||||
/// here: "knows dual wield" is a SPELL the player has learned, not a fact about their class, so
|
||||
/// it needs the known-spell set rather than the class byte.
|
||||
pub(super) fn find_equip_slot(inventory_type: u32, has_relic_slot: bool) -> Vec<u8> {
|
||||
// Live-API ids (`char_stats::SLOT_INFO`'s own numbering): wire `EQUIPMENT_SLOT_*` + 1. The
|
||||
// ammo slot is the client's own `GetInventorySlotInfo("AmmoSlot")` == 0 (not a real equip slot;
|
||||
// ammo loads by entry via `CMSG_SET_AMMO`, decision 0526) — it just names the fit-rule target.
|
||||
@@ -972,31 +968,36 @@ pub(super) fn find_equip_slot(inventory_type: u32) -> Vec<u8> {
|
||||
const BAG3: u8 = 23;
|
||||
|
||||
match inventory_type {
|
||||
1 => vec![HEAD], // INVTYPE_HEAD
|
||||
2 => vec![NECK], // INVTYPE_NECK
|
||||
3 => vec![SHOULDERS], // INVTYPE_SHOULDERS
|
||||
4 => vec![BODY], // INVTYPE_BODY (the shirt)
|
||||
5 | 20 => vec![CHEST], // INVTYPE_CHEST / INVTYPE_ROBE (same slot)
|
||||
6 => vec![WAIST], // INVTYPE_WAIST
|
||||
7 => vec![LEGS], // INVTYPE_LEGS
|
||||
8 => vec![FEET], // INVTYPE_FEET
|
||||
9 => vec![WRISTS], // INVTYPE_WRISTS
|
||||
10 => vec![HANDS], // INVTYPE_HANDS
|
||||
11 => vec![FINGER1, FINGER2], // INVTYPE_FINGER
|
||||
12 => vec![TRINKET1, TRINKET2], // INVTYPE_TRINKET
|
||||
13 => vec![MAINHAND, OFFHAND], // INVTYPE_WEAPON (dual-wield simplified, see doc)
|
||||
14 => vec![OFFHAND], // INVTYPE_SHIELD
|
||||
15 => vec![RANGED], // INVTYPE_RANGED
|
||||
16 => vec![BACK], // INVTYPE_CLOAK
|
||||
17 => vec![MAINHAND], // INVTYPE_2HWEAPON
|
||||
18 => vec![BAG0, BAG1, BAG2, BAG3], // INVTYPE_BAG
|
||||
19 => vec![TABARD], // INVTYPE_TABARD
|
||||
21 => vec![MAINHAND], // INVTYPE_WEAPONMAINHAND
|
||||
22 => vec![OFFHAND], // INVTYPE_WEAPONOFFHAND
|
||||
23 => vec![OFFHAND], // INVTYPE_HOLDABLE
|
||||
24 => vec![AMMO], // INVTYPE_AMMO → the ammo slot (loaded via SET_AMMO)
|
||||
25 => vec![RANGED], // INVTYPE_THROWN
|
||||
26 => vec![RANGED], // INVTYPE_RANGEDRIGHT
|
||||
1 => vec![HEAD], // INVTYPE_HEAD
|
||||
2 => vec![NECK], // INVTYPE_NECK
|
||||
3 => vec![SHOULDERS], // INVTYPE_SHOULDERS
|
||||
4 => vec![BODY], // INVTYPE_BODY (the shirt)
|
||||
5 | 20 => vec![CHEST], // INVTYPE_CHEST / INVTYPE_ROBE (same slot)
|
||||
6 => vec![WAIST], // INVTYPE_WAIST
|
||||
7 => vec![LEGS], // INVTYPE_LEGS
|
||||
8 => vec![FEET], // INVTYPE_FEET
|
||||
9 => vec![WRISTS], // INVTYPE_WRISTS
|
||||
10 => vec![HANDS], // INVTYPE_HANDS
|
||||
11 => vec![FINGER1, FINGER2], // INVTYPE_FINGER
|
||||
12 => vec![TRINKET1, TRINKET2], // INVTYPE_TRINKET
|
||||
13 => vec![MAINHAND, OFFHAND], // INVTYPE_WEAPON (dual-wield simplified, see doc)
|
||||
14 => vec![OFFHAND], // INVTYPE_SHIELD
|
||||
// The RANGED slot is the relic slot for Paladin/Shaman/Druid, and `IsValidForSlot
|
||||
// 0x5da1d0`'s `slot == 0x11` leg is an EQUALITY: it takes the slot iff
|
||||
// `(InventoryType == 28 RELIC) == hasRelicSlot`. So the three ranged types and RELIC are
|
||||
// exact complements here, not a union — a druid dragging a bow is offered nothing, and a
|
||||
// warrior dragging an idol likewise (decisions 1796, 1803).
|
||||
15 if !has_relic_slot => vec![RANGED], // INVTYPE_RANGED
|
||||
16 => vec![BACK], // INVTYPE_CLOAK
|
||||
17 => vec![MAINHAND], // INVTYPE_2HWEAPON
|
||||
18 => vec![BAG0, BAG1, BAG2, BAG3], // INVTYPE_BAG
|
||||
19 => vec![TABARD], // INVTYPE_TABARD
|
||||
21 => vec![MAINHAND], // INVTYPE_WEAPONMAINHAND
|
||||
22 => vec![OFFHAND], // INVTYPE_WEAPONOFFHAND
|
||||
23 => vec![OFFHAND], // INVTYPE_HOLDABLE
|
||||
24 => vec![AMMO], // INVTYPE_AMMO → the ammo slot (loaded via SET_AMMO)
|
||||
25 | 26 if !has_relic_slot => vec![RANGED], // INVTYPE_THROWN / INVTYPE_RANGEDRIGHT
|
||||
28 if has_relic_slot => vec![RANGED], // INVTYPE_RELIC — the same slot, the other way
|
||||
// INVTYPE_NON_EQUIP(0), INVTYPE_QUIVER(27), INVTYPE_RELIC(28, see doc), and anything past
|
||||
// MAX_INVTYPE(29): not equippable.
|
||||
_ => Vec::new(),
|
||||
@@ -1326,30 +1327,75 @@ mod tests {
|
||||
/// `ItemPrototype::GetAllowedEquipSlots` (`Objects/Item.cpp:577-696`).
|
||||
#[test]
|
||||
fn find_equip_slot_matches_the_vmangos_table() {
|
||||
assert_eq!(find_equip_slot(1), vec![1], "HEAD");
|
||||
assert_eq!(find_equip_slot(2), vec![2], "NECK");
|
||||
assert_eq!(find_equip_slot(4), vec![4], "BODY (shirt)");
|
||||
assert_eq!(find_equip_slot(5), vec![5], "CHEST");
|
||||
assert_eq!(find_equip_slot(20), vec![5], "ROBE aliases CHEST");
|
||||
assert_eq!(find_equip_slot(11), vec![11, 12], "FINGER, two slots");
|
||||
assert_eq!(find_equip_slot(12), vec![13, 14], "TRINKET, two slots");
|
||||
assert_eq!(find_equip_slot(1, false), vec![1], "HEAD");
|
||||
assert_eq!(find_equip_slot(2, false), vec![2], "NECK");
|
||||
assert_eq!(find_equip_slot(4, false), vec![4], "BODY (shirt)");
|
||||
assert_eq!(find_equip_slot(5, false), vec![5], "CHEST");
|
||||
assert_eq!(find_equip_slot(20, false), vec![5], "ROBE aliases CHEST");
|
||||
assert_eq!(
|
||||
find_equip_slot(13),
|
||||
find_equip_slot(11, false),
|
||||
vec![11, 12],
|
||||
"FINGER, two slots"
|
||||
);
|
||||
assert_eq!(
|
||||
find_equip_slot(12, false),
|
||||
vec![13, 14],
|
||||
"TRINKET, two slots"
|
||||
);
|
||||
assert_eq!(
|
||||
find_equip_slot(13, false),
|
||||
vec![16, 17],
|
||||
"WEAPON offers both hands (dual-wield simplified)"
|
||||
);
|
||||
assert_eq!(find_equip_slot(14), vec![17], "SHIELD -> off hand");
|
||||
assert_eq!(find_equip_slot(15), vec![18], "RANGED");
|
||||
assert_eq!(find_equip_slot(16), vec![15], "CLOAK -> back");
|
||||
assert_eq!(find_equip_slot(17), vec![16], "2HWEAPON -> main hand only");
|
||||
assert_eq!(find_equip_slot(19), vec![19], "TABARD");
|
||||
assert_eq!(find_equip_slot(21), vec![16], "WEAPONMAINHAND");
|
||||
assert_eq!(find_equip_slot(22), vec![17], "WEAPONOFFHAND");
|
||||
assert_eq!(find_equip_slot(18), vec![20, 21, 22, 23], "BAG");
|
||||
assert_eq!(find_equip_slot(24), vec![0], "AMMO -> the ammo slot (id 0)");
|
||||
// Not equippable: no vanilla paper-doll slot, or a named deferral (quiver/relic).
|
||||
for t in [0u32, 27, 28, 100] {
|
||||
assert!(find_equip_slot(t).is_empty(), "inventory type {t}");
|
||||
assert_eq!(find_equip_slot(14, false), vec![17], "SHIELD -> off hand");
|
||||
assert_eq!(find_equip_slot(15, false), vec![18], "RANGED");
|
||||
assert_eq!(find_equip_slot(16, false), vec![15], "CLOAK -> back");
|
||||
assert_eq!(
|
||||
find_equip_slot(17, false),
|
||||
vec![16],
|
||||
"2HWEAPON -> main hand only"
|
||||
);
|
||||
assert_eq!(find_equip_slot(19, false), vec![19], "TABARD");
|
||||
assert_eq!(find_equip_slot(21, false), vec![16], "WEAPONMAINHAND");
|
||||
assert_eq!(find_equip_slot(22, false), vec![17], "WEAPONOFFHAND");
|
||||
assert_eq!(find_equip_slot(18, false), vec![20, 21, 22, 23], "BAG");
|
||||
assert_eq!(
|
||||
find_equip_slot(24, false),
|
||||
vec![0],
|
||||
"AMMO -> the ammo slot (id 0)"
|
||||
);
|
||||
// Not equippable for anyone: no vanilla paper-doll slot, or a named deferral (quiver).
|
||||
for t in [0u32, 27, 100] {
|
||||
for relic in [false, true] {
|
||||
assert!(find_equip_slot(t, relic).is_empty(), "inventory type {t}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// **INVSLOT 17 is one slot with two meanings, and the flag decides it BOTH ways** —
|
||||
/// `IsValidForSlot 0x5da1d0`'s `slot == 0x11` leg is an equality, not a union
|
||||
/// (decisions 1796/1803). A relic class is offered the slot for a libram and refused it for a
|
||||
/// bow; everyone else the reverse. Getting this half-right — offering relics without
|
||||
/// withdrawing bows — would light the highlight for a druid dragging a bow, which is the one
|
||||
/// case the reference is unambiguous about.
|
||||
#[test]
|
||||
fn the_ranged_slot_takes_a_relic_or_a_weapon_never_both() {
|
||||
const RANGED: u8 = 18;
|
||||
// Paladin / Shaman / Druid.
|
||||
assert_eq!(find_equip_slot(28, true), vec![RANGED], "RELIC");
|
||||
for t in [15u32, 25, 26] {
|
||||
assert!(
|
||||
find_equip_slot(t, true).is_empty(),
|
||||
"a relic class is offered no ranged slot for inventory type {t}"
|
||||
);
|
||||
}
|
||||
// Everyone else.
|
||||
assert!(
|
||||
find_equip_slot(28, false).is_empty(),
|
||||
"RELIC, no relic slot"
|
||||
);
|
||||
for (t, who) in [(15u32, "RANGED"), (25, "THROWN"), (26, "RANGEDRIGHT")] {
|
||||
assert_eq!(find_equip_slot(t, false), vec![RANGED], "{who}");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -11,27 +11,19 @@ use benilla_ui::script::{QuadContent, ScriptValue, UiScript, UnitState};
|
||||
fn exp_bar_harness() -> UiScript {
|
||||
let mut s = UiScript::new().unwrap();
|
||||
s.set_screen_size(1024.0, 768.0);
|
||||
// Through the shared both-stores loader, not a disk read off `assets/ui`: `TextStatusBar.xml`
|
||||
// is the reference's own since 1751, and a hand-rolled reader here would look for it in the
|
||||
// wrong store — and would leave its `<Script file="TextStatusBar.lua"/>` unresolved besides.
|
||||
for file in [
|
||||
"Fonts.xml",
|
||||
"UIParent.xml",
|
||||
"GameTooltip.xml",
|
||||
"TextStatusBar.xml",
|
||||
"Interface\\FrameXML\\TextStatusBar.lua",
|
||||
"Interface\\FrameXML\\TextStatusBar.xml",
|
||||
"Cooldown.xml",
|
||||
"ActionBar.xml",
|
||||
] {
|
||||
let text = std::fs::read_to_string(
|
||||
std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
|
||||
.join("assets/ui")
|
||||
.join(file),
|
||||
)
|
||||
.unwrap();
|
||||
let doc = benilla_ui::framexml::parse(&text).unwrap();
|
||||
let report = benilla_ui::loader::load(&s, &doc, &|_| None);
|
||||
assert!(
|
||||
report.errors.is_empty(),
|
||||
"{file}: loader errors: {:?}",
|
||||
report.errors
|
||||
);
|
||||
super::test_ui::load_ui(&s, file);
|
||||
}
|
||||
s
|
||||
}
|
||||
@@ -309,7 +301,12 @@ fn the_xp_bar_numerals_show_on_hover() {
|
||||
.eval::<bool>("return MainMenuBarExpText:IsShown()")
|
||||
.unwrap());
|
||||
|
||||
s.run("BenillaExpBar_OnLeave(MainMenuExpBar)").unwrap();
|
||||
// `this` bound, because the engine binds it: the stock `HideTextStatusBarText` reads
|
||||
// `this.isZero` on one line where every other line reads `bar` (TextStatusBar.lua:95 — the
|
||||
// reference's own slip, benign there because `this` is always SOME frame during dispatch, and
|
||||
// a nil index only from bare Lua). Called from the bar's own `<OnLeave>` in play.
|
||||
s.run("this = MainMenuExpBar BenillaExpBar_OnLeave(MainMenuExpBar) this = nil")
|
||||
.unwrap();
|
||||
assert!(
|
||||
!s.eval::<bool>("return MainMenuBarExpText:IsShown()")
|
||||
.unwrap(),
|
||||
|
||||
@@ -115,28 +115,19 @@ const MOUSE_HANDLERS: &[&str] = &[
|
||||
"OnDragStart",
|
||||
];
|
||||
|
||||
/// Widget kinds whose **constructor** enables the mouse, so an element of that tag needs no
|
||||
/// Whether an element's TAG is a widget kind the constructor mouse-enables — so it needs no
|
||||
/// `enableMouse` and no handler to be clickable.
|
||||
///
|
||||
/// This is deliberately the SAME list [`benilla_ui::widget::WidgetArena::create`] uses, and it has
|
||||
/// to be: both are one claim about the client's ctors (`CSimpleButton`/`CSimpleEditBox` enable
|
||||
/// input in theirs; a `Slider` thumb must be draggable; a scroll frame takes the wheel — each
|
||||
/// carries its own citation there). Reading the engine's list here means a wrong entry is wrong in
|
||||
/// one place rather than two, and the sweep never reports a divergence that is only the two models
|
||||
/// disagreeing with each other.
|
||||
///
|
||||
/// It is also what answers the chat window: `ChatFrame1` is a `ScrollingMessageFrame`, so the
|
||||
/// reference's takes the mouse from its ctor even though `FloatingChatFrameTemplate` writes
|
||||
/// `enableMouse="false"` — which is a no-op, the XML path only ever ENABLES
|
||||
/// (`scripts-auto-enable.md` §1.3). That is how a 1.12 chat link is clickable at all.
|
||||
const MOUSE_BY_CTOR: &[&str] = &[
|
||||
"Button",
|
||||
"CheckButton",
|
||||
"EditBox",
|
||||
"ScrollingMessageFrame",
|
||||
"ScrollFrame",
|
||||
"Slider",
|
||||
];
|
||||
/// **Answered by the engine, not by a copy of its list.** This used to be a `MOUSE_BY_CTOR` array
|
||||
/// here whose own comment said it was "deliberately the SAME list `WidgetArena::create` uses… so a
|
||||
/// wrong entry is wrong in one place rather than two". It was a second list, and it drifted
|
||||
/// silently the first time the engine's was edited — this sweep then reported six scroll frames as
|
||||
/// divergences that were only the two models disagreeing with each other, which is precisely what
|
||||
/// the comment claimed could not happen.
|
||||
fn tag_mouse_enabled_by_ctor(tag: &str) -> bool {
|
||||
benilla_ui::script::frame_kind_from_tag(tag)
|
||||
.is_some_and(benilla_ui::widget::mouse_enabled_by_ctor)
|
||||
}
|
||||
|
||||
/// One accepted difference between benilla and the reference, with the reason it is accepted.
|
||||
///
|
||||
@@ -185,6 +176,102 @@ const KNOWN: &[Known] = &[
|
||||
"the reference bar inherits TextStatusBar, whose OnEnter/OnLeave show the value text; \
|
||||
ours is a plain StatusBar. Wants the status-text arc, not the flag.",
|
||||
},
|
||||
// Seven `*ScrollFrame` mouse entries RETIRED here (1795). They read "our faux scroll pane is a
|
||||
// Frame with an explicit `<OnMouseWheel>`, not a ScrollFrame" — a divergence that existed only
|
||||
// because OUR `ScrollFrame` ctor took the mouse and the reference's does not. Correcting the
|
||||
// ctor list to the client's made both sides agree, and an accepted divergence that has been
|
||||
// fixed is documentation claiming a defect we do not have.
|
||||
Known {
|
||||
frame: "ChatFrame1",
|
||||
flag: Flag::Mouse,
|
||||
why: "our chat window takes the mouse and the reference's does not. It was believed a \
|
||||
`<ScrollingMessageFrame>` takes it BY CONSTRUCTION; wow-re's follow-up round \
|
||||
(`68987021`) proved that ctor leaves the bit clear, and that a 1.12 chat link is \
|
||||
clickable through one synthesised `CSimpleHyperlinkButton` child per link span. Our \
|
||||
hit test now carries that law as a span disjunct, so the LINKS no longer need the \
|
||||
flag — but ChatFrame.xml still sets it, and a documented chain of choices hangs off \
|
||||
the old premise (no `SetFrameLevel-1` OnLoad because it would sink the resize grips \
|
||||
under a mouse-enabled parent; `OnClick` dismisses a held spell, 0843). Unwinding \
|
||||
that is the chat window's own piece of work, not this flag's.",
|
||||
},
|
||||
Known {
|
||||
frame: "ChatFrame2",
|
||||
flag: Flag::Mouse,
|
||||
why: "our chat window takes the mouse and the reference's does not. It was believed a \
|
||||
`<ScrollingMessageFrame>` takes it BY CONSTRUCTION; wow-re's follow-up round \
|
||||
(`68987021`) proved that ctor leaves the bit clear, and that a 1.12 chat link is \
|
||||
clickable through one synthesised `CSimpleHyperlinkButton` child per link span. Our \
|
||||
hit test now carries that law as a span disjunct, so the LINKS no longer need the \
|
||||
flag — but ChatFrame.xml still sets it, and a documented chain of choices hangs off \
|
||||
the old premise (no `SetFrameLevel-1` OnLoad because it would sink the resize grips \
|
||||
under a mouse-enabled parent; `OnClick` dismisses a held spell, 0843). Unwinding \
|
||||
that is the chat window's own piece of work, not this flag's.",
|
||||
},
|
||||
Known {
|
||||
frame: "ChatFrame3",
|
||||
flag: Flag::Mouse,
|
||||
why: "our chat window takes the mouse and the reference's does not. It was believed a \
|
||||
`<ScrollingMessageFrame>` takes it BY CONSTRUCTION; wow-re's follow-up round \
|
||||
(`68987021`) proved that ctor leaves the bit clear, and that a 1.12 chat link is \
|
||||
clickable through one synthesised `CSimpleHyperlinkButton` child per link span. Our \
|
||||
hit test now carries that law as a span disjunct, so the LINKS no longer need the \
|
||||
flag — but ChatFrame.xml still sets it, and a documented chain of choices hangs off \
|
||||
the old premise (no `SetFrameLevel-1` OnLoad because it would sink the resize grips \
|
||||
under a mouse-enabled parent; `OnClick` dismisses a held spell, 0843). Unwinding \
|
||||
that is the chat window's own piece of work, not this flag's.",
|
||||
},
|
||||
Known {
|
||||
frame: "ChatFrame4",
|
||||
flag: Flag::Mouse,
|
||||
why: "our chat window takes the mouse and the reference's does not. It was believed a \
|
||||
`<ScrollingMessageFrame>` takes it BY CONSTRUCTION; wow-re's follow-up round \
|
||||
(`68987021`) proved that ctor leaves the bit clear, and that a 1.12 chat link is \
|
||||
clickable through one synthesised `CSimpleHyperlinkButton` child per link span. Our \
|
||||
hit test now carries that law as a span disjunct, so the LINKS no longer need the \
|
||||
flag — but ChatFrame.xml still sets it, and a documented chain of choices hangs off \
|
||||
the old premise (no `SetFrameLevel-1` OnLoad because it would sink the resize grips \
|
||||
under a mouse-enabled parent; `OnClick` dismisses a held spell, 0843). Unwinding \
|
||||
that is the chat window's own piece of work, not this flag's.",
|
||||
},
|
||||
Known {
|
||||
frame: "ChatFrame5",
|
||||
flag: Flag::Mouse,
|
||||
why: "our chat window takes the mouse and the reference's does not. It was believed a \
|
||||
`<ScrollingMessageFrame>` takes it BY CONSTRUCTION; wow-re's follow-up round \
|
||||
(`68987021`) proved that ctor leaves the bit clear, and that a 1.12 chat link is \
|
||||
clickable through one synthesised `CSimpleHyperlinkButton` child per link span. Our \
|
||||
hit test now carries that law as a span disjunct, so the LINKS no longer need the \
|
||||
flag — but ChatFrame.xml still sets it, and a documented chain of choices hangs off \
|
||||
the old premise (no `SetFrameLevel-1` OnLoad because it would sink the resize grips \
|
||||
under a mouse-enabled parent; `OnClick` dismisses a held spell, 0843). Unwinding \
|
||||
that is the chat window's own piece of work, not this flag's.",
|
||||
},
|
||||
Known {
|
||||
frame: "ChatFrame6",
|
||||
flag: Flag::Mouse,
|
||||
why: "our chat window takes the mouse and the reference's does not. It was believed a \
|
||||
`<ScrollingMessageFrame>` takes it BY CONSTRUCTION; wow-re's follow-up round \
|
||||
(`68987021`) proved that ctor leaves the bit clear, and that a 1.12 chat link is \
|
||||
clickable through one synthesised `CSimpleHyperlinkButton` child per link span. Our \
|
||||
hit test now carries that law as a span disjunct, so the LINKS no longer need the \
|
||||
flag — but ChatFrame.xml still sets it, and a documented chain of choices hangs off \
|
||||
the old premise (no `SetFrameLevel-1` OnLoad because it would sink the resize grips \
|
||||
under a mouse-enabled parent; `OnClick` dismisses a held spell, 0843). Unwinding \
|
||||
that is the chat window's own piece of work, not this flag's.",
|
||||
},
|
||||
Known {
|
||||
frame: "ChatFrame7",
|
||||
flag: Flag::Mouse,
|
||||
why: "our chat window takes the mouse and the reference's does not. It was believed a \
|
||||
`<ScrollingMessageFrame>` takes it BY CONSTRUCTION; wow-re's follow-up round \
|
||||
(`68987021`) proved that ctor leaves the bit clear, and that a 1.12 chat link is \
|
||||
clickable through one synthesised `CSimpleHyperlinkButton` child per link span. Our \
|
||||
hit test now carries that law as a span disjunct, so the LINKS no longer need the \
|
||||
flag — but ChatFrame.xml still sets it, and a documented chain of choices hangs off \
|
||||
the old premise (no `SetFrameLevel-1` OnLoad because it would sink the resize grips \
|
||||
under a mouse-enabled parent; `OnClick` dismisses a held spell, 0843). Unwinding \
|
||||
that is the chat window's own piece of work, not this flag's.",
|
||||
},
|
||||
Known {
|
||||
frame: "TargetofTargetHealthBar",
|
||||
flag: Flag::Mouse,
|
||||
@@ -242,41 +329,6 @@ const KNOWN: &[Known] = &[
|
||||
//
|
||||
// The two mail panes are a further step out: ours are flat art with a FontString body, a
|
||||
// render approximation MailFrame.xml names at the site, so neither side scrolls them.
|
||||
Known {
|
||||
frame: "FriendsFrameFriendsScrollFrame",
|
||||
flag: Flag::Mouse,
|
||||
why: "our faux scroll pane is a Frame with an explicit <OnMouseWheel>, not a ScrollFrame",
|
||||
},
|
||||
Known {
|
||||
frame: "FriendsFrameIgnoreScrollFrame",
|
||||
flag: Flag::Mouse,
|
||||
why: "as FriendsFrameFriendsScrollFrame",
|
||||
},
|
||||
Known {
|
||||
frame: "GuildListScrollFrame",
|
||||
flag: Flag::Mouse,
|
||||
why: "as FriendsFrameFriendsScrollFrame",
|
||||
},
|
||||
Known {
|
||||
frame: "WhoListScrollFrame",
|
||||
flag: Flag::Mouse,
|
||||
why: "as FriendsFrameFriendsScrollFrame",
|
||||
},
|
||||
Known {
|
||||
frame: "SkillListScrollFrame",
|
||||
flag: Flag::Mouse,
|
||||
why: "as FriendsFrameFriendsScrollFrame",
|
||||
},
|
||||
Known {
|
||||
frame: "ReputationListScrollFrame",
|
||||
flag: Flag::Mouse,
|
||||
why: "as FriendsFrameFriendsScrollFrame",
|
||||
},
|
||||
Known {
|
||||
frame: "QuestLogListScrollFrame",
|
||||
flag: Flag::Mouse,
|
||||
why: "as FriendsFrameFriendsScrollFrame",
|
||||
},
|
||||
// ── id ─────────────────────────────────────────────────────────────────────────────────────
|
||||
//
|
||||
// Empty, and it is worth saying why rather than leaving a bare header. The reference side of
|
||||
@@ -482,10 +534,7 @@ fn reference_takes_mouse(name: &str, frames: &HashMap<String, Element>) -> bool
|
||||
let Some(el) = frames.get(name) else {
|
||||
return false;
|
||||
};
|
||||
if MOUSE_BY_CTOR
|
||||
.iter()
|
||||
.any(|t| t.eq_ignore_ascii_case(&el.tag))
|
||||
{
|
||||
if tag_mouse_enabled_by_ctor(&el.tag) {
|
||||
return true;
|
||||
}
|
||||
if resolved_attr(name, frames, "enableMouse", 0).is_some_and(|v| v.eq_ignore_ascii_case("true"))
|
||||
|
||||
@@ -71,7 +71,8 @@ fn load_page(s: &UiScript) {
|
||||
"UiPanels.xml",
|
||||
"UIParent.xml",
|
||||
"GameTooltip.xml",
|
||||
"TextStatusBar.xml",
|
||||
"Interface\\FrameXML\\TextStatusBar.lua",
|
||||
"Interface\\FrameXML\\TextStatusBar.xml",
|
||||
"ScrollTemplates.xml",
|
||||
"UIPanelTemplates.xml",
|
||||
"OptionsFrameTemplates.xml",
|
||||
|
||||
@@ -728,7 +728,8 @@ fn interface_harness() -> UiScript {
|
||||
"GameTooltip.xml",
|
||||
"UIDropDownMenu.xml",
|
||||
"UnitPopup.xml",
|
||||
"TextStatusBar.xml",
|
||||
"Interface\\FrameXML\\TextStatusBar.lua",
|
||||
"Interface\\FrameXML\\TextStatusBar.xml",
|
||||
"UnitFrames.xml",
|
||||
"Cooldown.xml",
|
||||
"ActionBar.xml",
|
||||
|
||||
@@ -395,13 +395,13 @@ mod tests {
|
||||
/// manifest in a fresh VM — the same pass [`chain_readiness_report`] makes, run here so the
|
||||
/// answer is in one table.
|
||||
///
|
||||
/// That last one is 1790, and it is the same mistake as the third column one step later.
|
||||
/// That last one is 1801, and it is the same mistake as the third column one step later.
|
||||
/// `<LootButton>` and `<TaxiRouteFrame>` are element TAGS: no census of `Name(` or `:Name(`
|
||||
/// can reach them, so `LootFrame.xml` sat in this report's "needs NO engine work" list while
|
||||
/// the readiness probe was printing `4 issue(s)` for it in a different table. Both tables were
|
||||
/// right. Joining them was left to whoever read them, and I read it wrong.
|
||||
///
|
||||
/// Until 1787 the method sites were **silently dropped**: a name in neither the engine nor the
|
||||
/// Until 1798 the method sites were **silently dropped**: a name in neither the engine nor the
|
||||
/// FrameXML half of `1.12-globals.tsv` was assumed to be a widget method and skipped, on the
|
||||
/// reasoning that widget methods are not in `_G`. True, and it meant the report could not see
|
||||
/// a widget method we had *not built*. `MerchantFrame.xml` read `0 engine` and
|
||||
@@ -410,10 +410,16 @@ mod tests {
|
||||
/// file loaded, every check passed, and hovering a vendor row would have raised in play. Both
|
||||
/// instruments were right about what they measure. Neither measured the window.
|
||||
///
|
||||
/// **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.
|
||||
///
|
||||
/// 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.
|
||||
///
|
||||
/// They no longer over-report on comments. Both counted commented-out calls until 1789 — 1.12
|
||||
/// They no longer over-report on comments. Both counted commented-out calls until 1800 — 1.12
|
||||
/// comments out whole blocks, and `PaperDollFrame.lua:754-756`'s `ShowInventorySellCursor` is
|
||||
/// three commented lines that this report named as that window's last engine gap. A real
|
||||
/// binding, never called, blocking a window that was not blocked. [`strip_comments`] takes
|
||||
@@ -457,7 +463,7 @@ mod tests {
|
||||
.collect();
|
||||
|
||||
// Strip what is not code before either scan. Both `Name(` and `:Name(` counted calls
|
||||
// inside comments until 1789: the round that built `PickupMerchantItem` also asked wow-re
|
||||
// inside comments until 1800: the round that built `PickupMerchantItem` also asked wow-re
|
||||
// for `ShowInventorySellCursor`, which this report had named as `PaperDollFrame.xml`'s
|
||||
// last engine gap — and the answer was that stock `PaperDollFrame.lua:754-756` has the
|
||||
// call **commented out**, all three lines. A real binding, never called, blocking a window
|
||||
@@ -604,7 +610,7 @@ mod tests {
|
||||
.collect()
|
||||
};
|
||||
// The instrument's own tripwire: if the probe stops working, the `method=` column goes
|
||||
// silently empty — which is precisely the failure mode 1787 exists to end.
|
||||
// silently empty — which is precisely the failure mode 1798 exists to end.
|
||||
let control: Vec<String> = ["SetPoint", "SetMerchantItem", "BenillaNotAMethod"]
|
||||
.iter()
|
||||
.map(|s| (*s).to_string())
|
||||
@@ -636,10 +642,48 @@ mod tests {
|
||||
.map(str::to_string)
|
||||
.collect();
|
||||
|
||||
// The `.lua` files a stock `.xml` SOURCES through `<Script file=>`. A window's own sourced
|
||||
// code is part of the window: its functions are not gaps, and they are where most of its
|
||||
// `fx=` names would otherwise be looked for.
|
||||
let sourced_luas = |xml_leaf: &str| -> Vec<String> {
|
||||
let Some(b) = super::read(&format!("Interface\\FrameXML\\{xml_leaf}")) else {
|
||||
return Vec::new();
|
||||
};
|
||||
let text = String::from_utf8_lossy(&b).into_owned();
|
||||
let mut out = Vec::new();
|
||||
for (i, _) in text.match_indices("<Script") {
|
||||
let rest = &text[i..];
|
||||
let Some(end) = rest.find("/>").or_else(|| rest.find('>')) else {
|
||||
continue;
|
||||
};
|
||||
let tag = &rest[..end];
|
||||
if let Some(fi) = tag.find("file=\"") {
|
||||
let after = &tag[fi + 6..];
|
||||
if let Some(q) = after.find('"') {
|
||||
let leaf = after[..q].rsplit(['\\', '/']).next().unwrap_or("");
|
||||
if leaf.ends_with(".lua") {
|
||||
out.push(leaf.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
out
|
||||
};
|
||||
|
||||
// `function Name(` across the whole corpus, so an fx gap can name the file that holds it.
|
||||
//
|
||||
// Each toc `.xml` is scanned together with **every `.lua` it SOURCES**, not just the
|
||||
// `X.lua` its own name suggests. Guessing missed the common case: `ActionBarFrame.xml`
|
||||
// sources `ActionButton.lua`, there is no `ActionBarFrame.lua`, and `ActionButton.lua` is
|
||||
// not a toc line of its own — so the whole `ActionButton_*` family read `<?>`, which says
|
||||
// "nothing defines this" about five functions a stock file defines and brings with it.
|
||||
// The difference matters for how the column is read: a name with a home ARRIVES when that
|
||||
// file does; a `<?>` is something to build.
|
||||
let mut home: std::collections::HashMap<String, String> = std::collections::HashMap::new();
|
||||
for f in &stock {
|
||||
for cand in [f.clone(), format!("{}.lua", &f[..f.len() - 4])] {
|
||||
let mut cands = vec![f.clone(), format!("{}.lua", &f[..f.len() - 4])];
|
||||
cands.extend(sourced_luas(f));
|
||||
for cand in cands {
|
||||
let Some(bytes) = super::read(&format!("Interface\\FrameXML\\{cand}")) else {
|
||||
continue;
|
||||
};
|
||||
@@ -662,7 +706,16 @@ mod tests {
|
||||
let mut out = std::collections::HashSet::new();
|
||||
let mut i = 0;
|
||||
while i < b.len() {
|
||||
if b[i].is_ascii_uppercase() && (i == 0 || !super::is_word(b[i - 1])) {
|
||||
// A GLOBAL call: uppercase, not inside a word, and **not preceded by `.` or
|
||||
// `:`**. Without that last test `info.UpdateFunc(` and `dialog.OnAccept(` read as
|
||||
// globals — `.` is not a word character — and a dozen `StaticPopupDialogs` /
|
||||
// `MoneyTypeInfo` FIELD names showed up as `<?>` gaps. `:` belongs to the method
|
||||
// scan below; `.` belongs to nobody, since a field call arrives with its table.
|
||||
let after_field = i > 0 && (b[i - 1] == '.' || b[i - 1] == ':');
|
||||
if b[i].is_ascii_uppercase()
|
||||
&& !after_field
|
||||
&& (i == 0 || !super::is_word(b[i - 1]))
|
||||
{
|
||||
let mut j = i;
|
||||
while j < b.len() && super::is_word(b[j]) {
|
||||
j += 1;
|
||||
@@ -696,13 +749,39 @@ mod tests {
|
||||
if migrated.contains(f) {
|
||||
continue;
|
||||
}
|
||||
// The window IS its xml plus every `.lua` it sources — `ActionBarFrame.xml` sources
|
||||
// `ActionButton.lua`, and the whole `ActionButton_*` family is that window's own code,
|
||||
// not a dependency on somebody else's. Gathering only `X.xml` + `X.lua` reported five
|
||||
// of its own functions as gaps.
|
||||
let mut text = String::new();
|
||||
for cand in [f.clone(), format!("{}.lua", &f[..f.len() - 4])] {
|
||||
let mut parts = vec![f.clone(), format!("{}.lua", &f[..f.len() - 4])];
|
||||
parts.extend(sourced_luas(f));
|
||||
for cand in parts {
|
||||
if let Some(b) = super::read(&format!("Interface\\FrameXML\\{cand}")) {
|
||||
text.push_str(&String::from_utf8_lossy(&b));
|
||||
}
|
||||
}
|
||||
let text = strip_comments(&text);
|
||||
// …and the names the file declares as LOCALS, which a bare call resolves to. The
|
||||
// idiom that needs it is the reference's own dispatch shape:
|
||||
//
|
||||
// local OnAccept = StaticPopupDialogs[dialog.which].OnAccept
|
||||
// if ( OnAccept ) then dontHide = OnAccept(dialog.data, dialog.data2) end
|
||||
//
|
||||
// The call is bare, so the global scan sees `OnAccept(` and reports a gap for a name
|
||||
// that is a table field one line up. `OnAccept`, `OnCancel`, `OnShow`, `OnHide` and
|
||||
// the three `EditBoxOn*` all arrived that way. A `local` declaration in the same file
|
||||
// is proof enough: nothing else could be meant.
|
||||
let locals: std::collections::HashSet<String> = text
|
||||
.lines()
|
||||
.filter_map(|l| l.trim_start().strip_prefix("local "))
|
||||
.map(|r| {
|
||||
r.chars()
|
||||
.take_while(|c| c.is_alphanumeric() || *c == '_')
|
||||
.collect::<String>()
|
||||
})
|
||||
.filter(|n| !n.is_empty())
|
||||
.collect();
|
||||
let own: std::collections::HashSet<String> = text
|
||||
.lines()
|
||||
.filter_map(|l| l.trim_start().strip_prefix("function "))
|
||||
@@ -715,7 +794,7 @@ mod tests {
|
||||
let (mut eng, mut fx) = (Vec::new(), Vec::new());
|
||||
let mut names: Vec<String> = called(&text)
|
||||
.into_iter()
|
||||
.filter(|c| !own.contains(c) && !have.contains(c))
|
||||
.filter(|c| !own.contains(c) && !locals.contains(c) && !have.contains(c))
|
||||
.collect();
|
||||
names.sort();
|
||||
for c in names {
|
||||
@@ -737,8 +816,30 @@ mod tests {
|
||||
// while `chain_readiness_report` was printing `4 issue(s)` for it in another table.
|
||||
//
|
||||
// Both tables were right. Joining them was left to whoever read them, and I got it
|
||||
// wrong (1790). So this one runs the load itself — the same fresh-VM-plus-manifest
|
||||
// wrong (1801). So this one runs the load itself — the same fresh-VM-plus-manifest
|
||||
// pass the readiness probe does — and reports it in the same row.
|
||||
// **A `LOAD:` line for a window we ALSO ship is suspect**, and the reason is the same
|
||||
// one `chain_readiness_report` carries: `publish_global` is non-overwriting (RF-0023),
|
||||
// so loading the stock file on top of our identically-named one leaves every colliding
|
||||
// frame's global pointing at OUR frame while the stock file's handlers run against
|
||||
// THEIRS. A field set on `this` in an OnLoad is then invisible to `getglobal(name)`,
|
||||
// and the failure looks like a bug in whatever read it back.
|
||||
//
|
||||
// Worked example, because it cost an hour: stock `TradeFrame.xml` reported
|
||||
// `MoneyFrame.xml:525: attempt to index local 'info'`. Nothing was wrong with our
|
||||
// MoneyFrame — `MoneyFrame_SetType` had set `this.info` correctly, and
|
||||
// `getglobal(this:GetName())` answered a DIFFERENT table, because our own
|
||||
// `TradeFrame.xml` already owned `TradeRecipientMoneyFrame`.
|
||||
//
|
||||
// So the column is marked, not trusted. A row whose file we do not ship is a real
|
||||
// load failure; a row whose file we do ship needs the swap attempted to know.
|
||||
let ours_too = !migrated.contains(f)
|
||||
&& std::fs::metadata(
|
||||
std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
|
||||
.join("assets/ui")
|
||||
.join(f),
|
||||
)
|
||||
.is_ok();
|
||||
let loads = {
|
||||
let mut probe = UiScript::new().expect("VM");
|
||||
probe.set_screen_size(1024.0, 768.0);
|
||||
@@ -759,8 +860,22 @@ mod tests {
|
||||
let known = answers(&s, &asked);
|
||||
let mut meth: Vec<String> = asked.into_iter().filter(|m| !known.contains(m)).collect();
|
||||
meth.sort();
|
||||
// A suspect LOAD line does not count as a blocker — it is a question, not an answer.
|
||||
let load_blockers = if ours_too { 0 } else { loads.len() };
|
||||
let loads: Vec<String> = loads
|
||||
.into_iter()
|
||||
.map(|e| {
|
||||
let one = e.replace('\n', " ");
|
||||
let mark = if ours_too {
|
||||
" (ours too — suspect)"
|
||||
} else {
|
||||
""
|
||||
};
|
||||
format!("{mark} {}", &one[..one.len().min(150)])
|
||||
})
|
||||
.collect();
|
||||
rows.push((
|
||||
eng.len() + meth.len() + loads.len(),
|
||||
eng.len() + meth.len() + load_blockers,
|
||||
f.clone(),
|
||||
eng,
|
||||
fx,
|
||||
@@ -778,8 +893,7 @@ mod tests {
|
||||
println!(" method: {}", meth.join(" "));
|
||||
}
|
||||
for e in loads {
|
||||
let one = e.replace('\n', " ");
|
||||
println!(" LOAD: {}", &one[..one.len().min(150)]);
|
||||
println!(" LOAD:{e}");
|
||||
}
|
||||
if !fx.is_empty() {
|
||||
println!(" fx: {}", fx.join(" "));
|
||||
@@ -816,12 +930,103 @@ mod tests {
|
||||
/// load, which is what this reports: a name ours defines that a chain entry in our own
|
||||
/// manifest also defines.
|
||||
///
|
||||
/// **Two halves, both exact, because both sides DECLARE rather than mention.** The function
|
||||
/// half is above. The frame half asks the other question a swap has to answer: *can this stock
|
||||
/// window be added at all* — which is different from "does it load", because a stock window we
|
||||
/// do not ship under its own name usually has a counterpart of ours under a different one, and
|
||||
/// both would declare the same frames.
|
||||
///
|
||||
/// That half doubles as the map nothing else holds: `ActionBarFrame.xml` is our
|
||||
/// `ActionBar.xml`, `FloatingChatFrame.xml` is our `ChatFrame.xml`, `PlayerFrame.xml` and
|
||||
/// `TargetFrame.xml` and `PetFrame.xml` are all our one `UnitFrames.xml`,
|
||||
/// `MainMenuBarMicroButtons.xml` is our `MicroMenu.xml`, `StaticPopup.xml` is our
|
||||
/// `UiPanels.xml`. `chain_gap_report` calls several of those unblocked, and they are —
|
||||
/// individually. They just cannot load beside the file of ours already holding their names.
|
||||
///
|
||||
/// Run it before attempting a swap. It predicts which ones will fail without attempting them.
|
||||
#[test]
|
||||
#[ignore = "instrument: run by hand before attempting a window swap"]
|
||||
fn shadowed_reference_functions() {
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
|
||||
// The FRAME NAMES a document declares — `name="X"` on a widget element, minus the
|
||||
// `$parent`-relative and `virtual` template forms, which name nothing globally.
|
||||
//
|
||||
// The sibling of the function check below and exactly as exact, because both sides
|
||||
// DECLARE rather than mention. This is the version of the "frame names" idea that works:
|
||||
// an earlier attempt scanned names a file *referenced* and was pure noise (it found `UI`
|
||||
// and missed the case it was built for), because a reference can live in any file. A
|
||||
// declaration cannot.
|
||||
//
|
||||
// What it catches: our `UnitFrames.xml` declares `PetFrame`, and so does the stock
|
||||
// `PetFrame.xml`. Adding that stock file alongside ours would declare the name twice.
|
||||
// Several stock windows we do not ship under their own name have an equivalent of ours
|
||||
// under a different one, and `chain_gap_report` calls every one of them unblocked —
|
||||
// truthfully, because the stock file WOULD load; it just cannot load *beside* ours.
|
||||
let declares = |text: &str| -> Vec<String> {
|
||||
let mut out = Vec::new();
|
||||
for (i, _) in text.match_indices("name=\"") {
|
||||
let rest = &text[i + 6..];
|
||||
let Some(end) = rest.find('"') else { continue };
|
||||
let name = &rest[..end];
|
||||
if name.starts_with('$') || name.is_empty() {
|
||||
continue;
|
||||
}
|
||||
// A `virtual="true"` element is a template: its name is a registry key, not a
|
||||
// frame, and two files may legitimately hold the same template name only if one
|
||||
// replaces the other — which is the same question, so they are reported too.
|
||||
out.push(name.to_string());
|
||||
}
|
||||
out
|
||||
};
|
||||
|
||||
// `function Name(a, b, c)` → the name and its PARAMETER LIST. Arity is the third way our
|
||||
// files and the reference's can disagree about a name, and the one that fails most
|
||||
// quietly: `TextStatusBar_Initialize()` takes no argument in 1.12 and acts on `this`, ours
|
||||
// took an optional bar, and `UnitFrames.xml` calls it with one. Swapping that file stopped
|
||||
// initialising the unit-frame bars with no error and no missing global — the numerals
|
||||
// simply never appeared (decision 1793).
|
||||
//
|
||||
// **Both directions are silent, and that is the point.** Lua drops extra arguments without
|
||||
// complaint, so neither an over- nor an under-supplied call raises; they differ only in
|
||||
// WHO loses information:
|
||||
//
|
||||
// * ours WIDER — our callers pass the extra argument and the reference's version drops
|
||||
// it. Breaks when OUR file is swapped out (the `TextStatusBar` case).
|
||||
// * ours NARROWER — the reference's callers pass more than ours takes and ours drops it.
|
||||
// Breaks when a STOCK file is added beside ours and calls our version.
|
||||
//
|
||||
// The report names the direction because it says which swap the difference is waiting for,
|
||||
// not because one of them is safe.
|
||||
let params_in = |text: &str| -> Vec<(String, usize)> {
|
||||
let mut out = Vec::new();
|
||||
for line in text.lines() {
|
||||
let Some(rest) = line.trim_start().strip_prefix("function ") else {
|
||||
continue;
|
||||
};
|
||||
let name: String = rest
|
||||
.chars()
|
||||
.take_while(|c| c.is_alphanumeric() || *c == '_')
|
||||
.collect();
|
||||
if name.is_empty() {
|
||||
continue;
|
||||
}
|
||||
let Some(open) = rest.find('(') else { continue };
|
||||
let Some(close) = rest[open..].find(')') else {
|
||||
continue;
|
||||
};
|
||||
let args = rest[open + 1..open + close].trim();
|
||||
// `...` is a vararg, which has no fixed arity to compare.
|
||||
let n = if args.is_empty() || args == "..." {
|
||||
0
|
||||
} else {
|
||||
args.split(',').count()
|
||||
};
|
||||
out.push((name, n));
|
||||
}
|
||||
out
|
||||
};
|
||||
|
||||
let defined_in = |text: &str| -> Vec<String> {
|
||||
text.lines()
|
||||
.filter_map(|l| l.trim_start().strip_prefix("function "))
|
||||
@@ -841,6 +1046,8 @@ mod tests {
|
||||
toc.iter().enumerate().map(|(i, f)| (f, i)).collect();
|
||||
let mut chain_home: std::collections::HashMap<String, (String, usize)> =
|
||||
std::collections::HashMap::new();
|
||||
let mut chain_frames: std::collections::HashMap<String, String> =
|
||||
std::collections::HashMap::new();
|
||||
for entry in toc.iter().filter(|f| super::is_chain_entry(f)) {
|
||||
let leaf = entry.rsplit(['\\', '/']).next().unwrap_or(entry);
|
||||
let mut cands = vec![entry.clone()];
|
||||
@@ -878,6 +1085,67 @@ mod tests {
|
||||
}
|
||||
hits.sort();
|
||||
hits.dedup();
|
||||
// The frame names every STOCK window declares — read off the reference's own toc, not off
|
||||
// our manifest's chain entries. Scanning only what we already load was the first attempt
|
||||
// and it answered 0 by construction: a stock file we do not load is exactly the one whose
|
||||
// names could collide, and it was never opened.
|
||||
let ref_toc = String::from_utf8_lossy(
|
||||
&super::read("Interface\\FrameXML\\FrameXML.toc").expect("the reference's own toc"),
|
||||
)
|
||||
.into_owned();
|
||||
let mut ref_arity: std::collections::HashMap<String, (usize, String)> =
|
||||
std::collections::HashMap::new();
|
||||
for line in ref_toc.lines().map(str::trim) {
|
||||
if line.is_empty() || line.starts_with('#') || !line.ends_with(".xml") {
|
||||
continue;
|
||||
}
|
||||
for cand in [line.to_string(), format!("{}.lua", &line[..line.len() - 4])] {
|
||||
let Some(b) = super::read(&format!("Interface/FrameXML/{cand}")) else {
|
||||
continue;
|
||||
};
|
||||
let text = String::from_utf8_lossy(&b).into_owned();
|
||||
if cand.ends_with(".xml") {
|
||||
for name in declares(&text) {
|
||||
chain_frames.entry(name).or_insert_with(|| line.to_string());
|
||||
}
|
||||
}
|
||||
for (name, n) in params_in(&text) {
|
||||
ref_arity.entry(name).or_insert((n, cand.clone()));
|
||||
}
|
||||
}
|
||||
}
|
||||
assert!(
|
||||
chain_frames.contains_key("GameTooltip") && chain_frames.contains_key("PetFrame"),
|
||||
"the stock frame-name scan found nothing recognisable ({} names) — an empty answer \
|
||||
here reads as \"no collisions\", which is what the first version of this reported \
|
||||
for the wrong reason",
|
||||
chain_frames.len()
|
||||
);
|
||||
|
||||
// …and the frame-name half, over the stock files the manifest does NOT already take off
|
||||
// the chain. A file of ours whose stock counterpart we already load is a swap that has
|
||||
// happened; this is about the ones that have not.
|
||||
let mut frame_hits: Vec<(String, String, String)> = Vec::new();
|
||||
for entry in toc.iter().filter(|f| !super::is_chain_entry(f)) {
|
||||
let Ok(text) = std::fs::read_to_string(dir.join(entry)) else {
|
||||
continue;
|
||||
};
|
||||
for name in declares(&text) {
|
||||
if let Some(home) = chain_frames.get(&name) {
|
||||
let already = toc
|
||||
.iter()
|
||||
.filter(|f| super::is_chain_entry(f))
|
||||
.any(|f| f.ends_with(home.as_str()));
|
||||
// A template's name is a registry key rather than a frame, but two files
|
||||
// holding one is the same question, so it is reported the same way.
|
||||
if !already {
|
||||
frame_hits.push((name, entry.clone(), home.clone()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
frame_hits.sort();
|
||||
frame_hits.dedup();
|
||||
|
||||
println!("\n=== names ours redefines that a CHAIN entry already defines ===");
|
||||
println!("{:<36} {:<28} {:<34} winner", "name", "ours", "chain");
|
||||
@@ -901,6 +1169,114 @@ mod tests {
|
||||
for (f, n) in &by_file {
|
||||
println!(" {n:>3} {f}");
|
||||
}
|
||||
|
||||
// The ARITY half: a name we define that the reference also defines, with a different
|
||||
// parameter count. Not a collision — the two need never both load for this to bite — so it
|
||||
// is its own section rather than a column.
|
||||
let mut arity: Vec<(String, String, usize, usize, String)> = Vec::new();
|
||||
for entry in toc.iter().filter(|f| !super::is_chain_entry(f)) {
|
||||
let Ok(text) = std::fs::read_to_string(dir.join(entry)) else {
|
||||
continue;
|
||||
};
|
||||
for (name, ours_n) in params_in(&text) {
|
||||
if let Some((ref_n, home)) = ref_arity.get(&name) {
|
||||
if *ref_n != ours_n {
|
||||
arity.push((name, entry.clone(), ours_n, *ref_n, home.clone()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
arity.sort();
|
||||
arity.dedup();
|
||||
println!(
|
||||
"\n=== {} signatures of ours differ in ARITY from the reference's ===",
|
||||
arity.len()
|
||||
);
|
||||
println!("{:<34} {:<26} ours ref direction", "name", "ours");
|
||||
for (name, ours, a, b, home) in &arity {
|
||||
// Which swap this one is waiting for — see the note above; both are silent.
|
||||
let dir = if a > b {
|
||||
"ours WIDER — bites when OUR file goes"
|
||||
} else {
|
||||
"ours NARROWER — bites when THEIRS arrives"
|
||||
};
|
||||
println!("{name:<34} {ours:<26} {a:>4} {b:>3} {dir} — ref in {home}");
|
||||
}
|
||||
|
||||
// The frame half, grouped the other way — by the STOCK file, because that is the unit of
|
||||
// the question it answers: "can this stock window be added?"
|
||||
let mut by_stock: std::collections::BTreeMap<&String, std::collections::BTreeSet<&String>> =
|
||||
std::collections::BTreeMap::new();
|
||||
for (_, ours, home) in &frame_hits {
|
||||
by_stock.entry(home).or_default().insert(ours);
|
||||
}
|
||||
println!(
|
||||
"\n=== {} FRAME-NAME collisions: {} stock windows we do not load already have their \
|
||||
names declared by a file of ours ===",
|
||||
frame_hits.len(),
|
||||
by_stock.len()
|
||||
);
|
||||
for (stock, ours) in &by_stock {
|
||||
let n = frame_hits.iter().filter(|(_, _, h)| h == *stock).count();
|
||||
let mine: Vec<&str> = ours.iter().map(|s| s.as_str()).collect();
|
||||
println!(" {n:>3} {stock:<34} vs {}", mine.join(", "));
|
||||
}
|
||||
}
|
||||
|
||||
/// **A chain `.xml` that does not source its own `.lua` needs TWO manifest lines**, and
|
||||
/// getting it wrong is silent.
|
||||
///
|
||||
/// Most stock windows pull their code in with `<Script file="X.lua"/>`, so naming the `.xml`
|
||||
/// brings both. A few do not — `TextStatusBar.xml` and `MoneyInputFrame.xml` declare only a
|
||||
/// template, and the reference's own toc lists their `.lua` on the preceding line (l.32-33,
|
||||
/// l.11-12). Name the `.xml` alone and the template loads against nothing: every global that
|
||||
/// file was supposed to define reads nil, every guarded call becomes a no-op, and there is no
|
||||
/// error anywhere.
|
||||
///
|
||||
/// Both were live. `TextStatusBar` was caught by three tests that happened to assert on the
|
||||
/// numerals; `MoneyInputFrame` was caught only by sweeping for the shape afterwards, and its
|
||||
/// manifest header had claimed for months to bring "the ten `MoneyInputFrame_*` verbs" while a
|
||||
/// full-manifest probe answered nil for all of them.
|
||||
///
|
||||
/// A gate rather than an instrument, because the answer should always be zero and the failure
|
||||
/// mode is invisible.
|
||||
#[test]
|
||||
fn every_chain_xml_brings_its_own_lua() {
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
let toc = &super::super::addons::Addon::builtin().toc.files;
|
||||
let listed: std::collections::HashSet<&str> = toc
|
||||
.iter()
|
||||
.map(|f| f.rsplit(['\\', '/']).next().unwrap_or(f))
|
||||
.collect();
|
||||
|
||||
let mut orphans = Vec::new();
|
||||
for entry in toc.iter().filter(|f| super::is_chain_entry(f)) {
|
||||
let leaf = entry.rsplit(['\\', '/']).next().unwrap_or(entry);
|
||||
let Some(stem) = leaf.strip_suffix(".xml") else {
|
||||
continue;
|
||||
};
|
||||
let lua = format!("{stem}.lua");
|
||||
// No sibling in the archive means there is nothing to miss.
|
||||
if super::read(&format!("Interface/FrameXML/{lua}")).is_none() {
|
||||
continue;
|
||||
}
|
||||
let xml = super::read(&entry.replace('\\', "/"))
|
||||
.unwrap_or_else(|| panic!("{entry}: not in the chain"));
|
||||
let text = String::from_utf8_lossy(&xml);
|
||||
let sourced = text
|
||||
.to_ascii_lowercase()
|
||||
.contains(&format!("file=\"{}\"", lua.to_ascii_lowercase()));
|
||||
if !sourced && !listed.contains(lua.as_str()) {
|
||||
orphans.push(format!(
|
||||
"{leaf} does not source {lua}, and {lua} is not a manifest entry"
|
||||
));
|
||||
}
|
||||
}
|
||||
assert!(
|
||||
orphans.is_empty(),
|
||||
"a chain window whose code never loads — silent, every global it defines reads nil:\n {}",
|
||||
orphans.join("\n ")
|
||||
);
|
||||
}
|
||||
|
||||
/// A path is a chain entry; a bare name is ours. The one-line rule the manifest rests on.
|
||||
|
||||
@@ -87,7 +87,8 @@ fn load_page(s: &UiScript) {
|
||||
"UiPanels.xml",
|
||||
"UIParent.xml",
|
||||
"GameTooltip.xml",
|
||||
"TextStatusBar.xml",
|
||||
"Interface\\FrameXML\\TextStatusBar.lua",
|
||||
"Interface\\FrameXML\\TextStatusBar.xml",
|
||||
"Cooldown.xml",
|
||||
"ActionBar.xml",
|
||||
"ScrollTemplates.xml",
|
||||
|
||||
@@ -191,3 +191,68 @@ fn the_death_countdown_text_rerenders_each_tick() {
|
||||
);
|
||||
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
|
||||
}
|
||||
|
||||
/// **`StaticPopup_Hide`'s and `StaticPopup_FindVisible`'s second argument** (decisions 1793).
|
||||
///
|
||||
/// Both took `which` alone until the arity scan found the drift. The reference takes `(which,
|
||||
/// data)`, and the difference is invisible without a test: Lua discards a surplus argument without
|
||||
/// complaint, so a stock caller's `StaticPopup_Hide("CONFIRM_BATTLEFIELD_ENTRY", i)` — the shape
|
||||
/// `BattlefieldFrame.lua:120` uses — would have hidden every instance of that dialog instead of
|
||||
/// the one it named, and said nothing.
|
||||
///
|
||||
/// The `multiple` gate is the reference's own and is why `data` is not a plain equality: a kind
|
||||
/// declared `multiple` can have several instances up at once and is matched by data; every other
|
||||
/// kind has at most one, so its data is never consulted.
|
||||
#[test]
|
||||
fn hide_and_find_address_one_instance_by_data_only_for_a_multiple_dialog() {
|
||||
let s = setup();
|
||||
s.run(
|
||||
r#"
|
||||
StaticPopupDialogs["T_MULTI"] = { text = "multi", button1 = "Ok", multiple = 1, timeout = 0 }
|
||||
StaticPopupDialogs["T_ONE"] = { text = "one", button1 = "Ok", timeout = 0 }
|
||||
a = StaticPopup_Show("T_MULTI") a.data = "alpha"
|
||||
b = StaticPopup_Show("T_MULTI") b.data = "beta"
|
||||
c = StaticPopup_Show("T_ONE") c.data = "gamma"
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
assert!(s.errors().is_empty(), "{:?}", s.errors());
|
||||
|
||||
// A `multiple` kind is addressed by data — both ways round, so this cannot pass by finding
|
||||
// whichever instance happens to be first.
|
||||
for (data, want) in [("alpha", "a"), ("beta", "b")] {
|
||||
assert!(
|
||||
s.eval::<bool>(&format!(
|
||||
"return StaticPopup_FindVisible(\"T_MULTI\", {data:?}) == {want}"
|
||||
))
|
||||
.unwrap(),
|
||||
"FindVisible picks the instance carrying {data:?}"
|
||||
);
|
||||
}
|
||||
// …and a data nothing carries finds nothing, rather than the first instance.
|
||||
assert!(s
|
||||
.eval::<bool>(r#"return StaticPopup_FindVisible("T_MULTI", "nobody") == nil"#)
|
||||
.unwrap());
|
||||
|
||||
// Hiding by data takes ONE instance and leaves its sibling up.
|
||||
s.run(r#"StaticPopup_Hide("T_MULTI", "alpha")"#).unwrap();
|
||||
assert!(s
|
||||
.eval::<bool>("return not a:IsShown() and b:IsShown()")
|
||||
.unwrap());
|
||||
|
||||
// No data hides every instance of the kind — which is what every one-argument caller means.
|
||||
s.run(r#"StaticPopup_Hide("T_MULTI")"#).unwrap();
|
||||
assert!(s.eval::<bool>("return not b:IsShown()").unwrap());
|
||||
|
||||
// A NON-multiple kind ignores data entirely: the reference gates the comparison on
|
||||
// `info.multiple`, so a wrong data still finds it.
|
||||
assert!(s
|
||||
.eval::<bool>(r#"return StaticPopup_FindVisible("T_ONE", "wrong") == c"#)
|
||||
.unwrap());
|
||||
|
||||
// An unknown kind answers nil rather than scanning — the reference's own early out.
|
||||
assert!(s
|
||||
.eval::<bool>(r#"return StaticPopup_FindVisible("T_NOPE") == nil"#)
|
||||
.unwrap());
|
||||
assert!(s.errors().is_empty(), "{:?}", s.errors());
|
||||
}
|
||||
|
||||
@@ -17,7 +17,8 @@ fn load_unit_frames(s: &UiScript) {
|
||||
load_xml(s, "UIParent.xml");
|
||||
// The bars' numerals machinery (decision 1082), which the manifest loads immediately ahead of
|
||||
// UnitFrames.xml and which every bar's OnLoad wires into since 1143.
|
||||
load_xml(s, "TextStatusBar.xml");
|
||||
load_xml(s, "Interface\\FrameXML\\TextStatusBar.lua");
|
||||
load_xml(s, "Interface\\FrameXML\\TextStatusBar.xml");
|
||||
load_xml(s, "GameTooltip.xml");
|
||||
load_xml(s, "UIDropDownMenu.xml");
|
||||
load_xml(s, "UnitPopup.xml");
|
||||
@@ -1516,8 +1517,13 @@ fn status_bar_text_paints_the_player_numerals_but_not_the_targets() {
|
||||
.unwrap();
|
||||
assert!(shown(&s, "TargetFrameHealthBarText"));
|
||||
assert_eq!(text(&s, "TargetFrameHealthBarText"), "50 / 100");
|
||||
s.run("BenillaUnitFrameBar_OnLeave(TargetFrameHealthBar)")
|
||||
.unwrap();
|
||||
// `this` bound, as the engine binds it for a bar's own `<OnLeave>`: the stock
|
||||
// `HideTextStatusBarText` reads `this.isZero` on one line where every other reads `bar`
|
||||
// (TextStatusBar.lua:95 — the reference's own slip; a nil index only from bare Lua).
|
||||
s.run(
|
||||
"this = TargetFrameHealthBar BenillaUnitFrameBar_OnLeave(TargetFrameHealthBar) this = nil",
|
||||
)
|
||||
.unwrap();
|
||||
assert!(
|
||||
!shown(&s, "TargetFrameHealthBarText"),
|
||||
"and go away again — the lockShow refcount balances"
|
||||
@@ -1535,8 +1541,10 @@ fn status_bar_text_paints_the_player_numerals_but_not_the_targets() {
|
||||
shown(&s, "PlayerFrameHealthBarText"),
|
||||
"hover shows them even with the option off"
|
||||
);
|
||||
s.run("BenillaUnitFrameBar_OnLeave(PlayerFrameHealthBar)")
|
||||
.unwrap();
|
||||
s.run(
|
||||
"this = PlayerFrameHealthBar BenillaUnitFrameBar_OnLeave(PlayerFrameHealthBar) this = nil",
|
||||
)
|
||||
.unwrap();
|
||||
assert!(!shown(&s, "PlayerFrameHealthBarText"));
|
||||
s.run("SetCVar(\"statusBarText\", \"1\", \"STATUS_BAR_TEXT\")")
|
||||
.unwrap();
|
||||
|
||||
@@ -49,7 +49,7 @@
|
||||
//! (`0x54240e`/`0x542446`), so the patch copy is the only one the client can read. Read the base
|
||||
//! archive alone — or an early-vanilla 16-column struct — and the flag simply is not there, which
|
||||
//! is how "the relic slot post-dates 1.12" became a settled belief in this codebase and stayed one
|
||||
//! across two decision records. It is false; see decision 1785. [`CHR_CLASSES_FIELDS`] is what
|
||||
//! across two decision records. It is false; see decision 1796. [`CHR_CLASSES_FIELDS`] is what
|
||||
//! keeps us on the patch copy, and it is load-bearing, not defensive.
|
||||
//!
|
||||
//! The flag is engine-enforced, not a UI conceit: `IsValidForSlot 0x5da1d0`'s `slot == 0x11` leg
|
||||
|
||||
@@ -782,7 +782,7 @@ pub const SMSG_SELL_ITEM: u16 = 0x01A1; // 417
|
||||
pub const CMSG_BUY_ITEM: u16 = 0x01A2; // 418
|
||||
/// Buy into a **specific** bag slot. This is what a vendor row dropped out of the merchant
|
||||
/// cursor sends (`PickupMerchantItem` mode 5 → the container/doll drop), as against
|
||||
/// `CMSG_BUY_ITEM`'s auto-place, which is what clicking the row sends. Both ship (decision 1786).
|
||||
/// `CMSG_BUY_ITEM`'s auto-place, which is what clicking the row sends. Both ship (decision 1797).
|
||||
pub const CMSG_BUY_ITEM_IN_SLOT: u16 = 0x01A3; // 419
|
||||
pub const SMSG_BUY_ITEM: u16 = 0x01A4; // 420
|
||||
pub const SMSG_BUY_FAILED: u16 = 0x01A5; // 421
|
||||
|
||||
@@ -1115,7 +1115,7 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
// a property of the SLOT, not of the player. It pairs with `UnitHasRelicSlot("player")`,
|
||||
// which is the class half, and stock reads the two as a conjunction
|
||||
// (`PaperDollFrame.lua:680`/`:744`). This comment used to say `UnitHasRelicSlot` is a
|
||||
// later-era concept, always false in vanilla; it is not — decision 1785.
|
||||
// later-era concept, always false in vanilla; it is not — decision 1796.
|
||||
g.set(
|
||||
"GetInventorySlotInfo",
|
||||
lua.create_function(|lua, name: String| {
|
||||
|
||||
@@ -101,6 +101,7 @@ mod model;
|
||||
mod modelframe;
|
||||
mod net_stats;
|
||||
mod object;
|
||||
pub use object::frame_kind_from_tag;
|
||||
mod party;
|
||||
mod pet;
|
||||
mod petition;
|
||||
|
||||
@@ -192,12 +192,20 @@ pub(super) fn draw_layer_name(l: DrawLayer) -> &'static str {
|
||||
}
|
||||
}
|
||||
|
||||
/// A widget tag or `CreateFrame` type string → its [`FrameKind`], case- and separator-insensitive
|
||||
/// ([`enum_token`]). Public because it is the ONE mapping: `benilla-app`'s `frame_flag_gate` sweep
|
||||
/// needs it to ask the engine whether a tag is mouse-enabled by construction, and a second copy
|
||||
/// there is exactly what drifted.
|
||||
pub fn frame_kind_from_tag(s: &str) -> Option<FrameKind> {
|
||||
frame_kind_from_str(s)
|
||||
}
|
||||
|
||||
fn frame_kind_from_str(s: &str) -> Option<FrameKind> {
|
||||
Some(match enum_token(s).as_str() {
|
||||
"FRAME" => FrameKind::Frame,
|
||||
"BUTTON" => FrameKind::Button,
|
||||
"CHECKBUTTON" => FrameKind::CheckButton,
|
||||
// A real registered type (`0x4959a6`), not an alias for Button — decision 1788. Registering
|
||||
// A real registered type (`0x4959a6`), not an alias for Button — decision 1799. Registering
|
||||
// it AS a Button would load `LootFrame.xml` and leave every row dead, because 1.12 has no
|
||||
// Lua verb for "take slot N": `LootSlot` is the bind-confirm continuation, hard-wired to
|
||||
// `flag = 1`.
|
||||
|
||||
@@ -50,7 +50,7 @@ use crate::order;
|
||||
use crate::widget::FrameHandle;
|
||||
|
||||
use super::clip::{effective_clip, scroll_clip_sources};
|
||||
use super::{button, cursor, editbox, event, UiScript};
|
||||
use super::{button, cursor, editbox, event, Model, UiScript};
|
||||
|
||||
/// The double-click interval — **300 ms, VERIFIED off the bytes**, a hardcoded instruction
|
||||
/// immediate: `0x77937b 81 f9 2c 01 00 00 cmp ecx, 0x12c`, comparing `now − [this+0x334]` inside
|
||||
@@ -127,6 +127,15 @@ pub(super) fn install(lua: &mlua::Lua) -> mlua::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Whether `(x, y)` falls inside one of this frame's hyperlink spans — the engine half of the
|
||||
/// reference's per-span `CSimpleHyperlinkButton` children.
|
||||
fn link_span_hit(model: &Model, fh: FrameHandle, x: f32, y: f32) -> bool {
|
||||
model
|
||||
.link_spans
|
||||
.get(&fh)
|
||||
.is_some_and(|spans| spans.iter().any(|(r, _, _)| point_in_rect(*r, x, y)))
|
||||
}
|
||||
|
||||
impl UiScript {
|
||||
/// Hit-test the cursor at `(x, y)` and return the **handle** of the captured frame (the
|
||||
/// topmost-drawn mouse-enabled, effective-visible frame whose rect contains the point **and**
|
||||
@@ -146,7 +155,22 @@ impl UiScript {
|
||||
let sorted = order::traversal(&model.arena);
|
||||
let scroll_sources = scroll_clip_sources(&model);
|
||||
order::hit_test(&sorted, |fh| {
|
||||
model.arena.is_mouse_enabled(fh)
|
||||
// Mouse-enabled, **or inside one of this frame's hyperlink spans**.
|
||||
//
|
||||
// That second half is how the reference makes a chat link clickable on a frame that
|
||||
// takes no mouse at all. `CSimpleMessageScrollFrame`'s ctor leaves `[+0xcc] = 0`, so
|
||||
// `ChatFrame1` is never in the hit-test index — instead the engine synthesises one
|
||||
// mouse-enabled `CSimpleHyperlinkButton` child PER SPAN (`0x7a3240`, born through the
|
||||
// `CSimpleButton` ctor) and routes the three hyperlink scripts back to the parent
|
||||
// (wow-re `mouse-enable-law.md` + the follow-up round `68987021`).
|
||||
//
|
||||
// We hold the spans as rects on the parent rather than as child frames, so the same
|
||||
// law expresses as a disjunct here. What it buys is everything the reference gets from
|
||||
// the split: the chat window is a hit candidate only over a LINK, so `GetMouseFocus()`
|
||||
// answers what is behind it elsewhere, its `movable="true"` +
|
||||
// `<TitleRegion setAllPoints="true"/>` stay inert so it drags by its tab and not its
|
||||
// body, and a hover over plain text reassigns to whatever is underneath.
|
||||
(model.arena.is_mouse_enabled(fh) || link_span_hit(&model, fh, x, y))
|
||||
&& model.resolved.get(&fh).is_some_and(|r| {
|
||||
point_in_rect(inset_rect(*r, model.arena.hit_rect_insets(fh)), x, y)
|
||||
})
|
||||
@@ -155,6 +179,26 @@ impl UiScript {
|
||||
})
|
||||
}
|
||||
|
||||
/// [`Self::hit_test`] over the frames that take the WHEEL — either bit, since a mouse-enabled
|
||||
/// frame with an `<OnMouseWheel>` is a candidate for both. Same rect and clip rules; only the
|
||||
/// enable predicate differs.
|
||||
fn hit_test_wheel(&self, x: f32, y: f32) -> Option<u32> {
|
||||
let model = self.model_ref();
|
||||
let sorted = order::traversal(&model.arena);
|
||||
let scroll_sources = scroll_clip_sources(&model);
|
||||
let fh = order::hit_test(&sorted, |fh| {
|
||||
(model.arena.is_mouse_wheel_enabled(fh)
|
||||
|| model.arena.is_mouse_enabled(fh)
|
||||
|| link_span_hit(&model, fh, x, y))
|
||||
&& model.resolved.get(&fh).is_some_and(|r| {
|
||||
point_in_rect(inset_rect(*r, model.arena.hit_rect_insets(fh)), x, y)
|
||||
})
|
||||
&& effective_clip(&model, &scroll_sources, fh)
|
||||
.is_none_or(|c| point_in_rect(c, x, y))
|
||||
})?;
|
||||
model.frame_to_id.get(&fh).copied()
|
||||
}
|
||||
|
||||
/// Hit-test the cursor at `(x, y)` and return the **id** of the captured frame, or `None` —
|
||||
/// [`Self::hit_test_frame`] with the id lookup every caller in the input path wants. The id is
|
||||
/// the same one used across this host (e.g. the layout [`crate::layout::Handle`]); the app can
|
||||
@@ -749,7 +793,13 @@ impl UiScript {
|
||||
/// [`UiScript::errors`].
|
||||
pub fn mouse_wheel(&mut self, x: f32, y: f32, delta: f32) {
|
||||
let target: Option<u32> = {
|
||||
let hit = self.hit_test(x, y);
|
||||
// **Wheel-enabled OR mouse-enabled**, not mouse-enabled alone. The reference's wheel
|
||||
// dispatcher `0x7664f0` walks its OWN index and calls the hit probe directly, ignoring
|
||||
// hover, capture and the mouse bit — `EnableMouse` is not a prerequisite
|
||||
// (wow-re `mouse-enable-law.md` §6). Ours went through the mouse hit test, which
|
||||
// coupled the two: correcting the ctor list to the reference's (ScrollFrame is inert
|
||||
// on the mouse bit) silently stopped every scroll pane from taking the wheel.
|
||||
let hit = self.hit_test_wheel(x, y);
|
||||
let model = self.model_ref();
|
||||
hit.and_then(|id| {
|
||||
let mut h = *model.id_to_frame.get(&id)?;
|
||||
|
||||
@@ -599,7 +599,7 @@ pub(in crate::script) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
// **This shipped absent for a long time, on a claim that was simply false** — that the relic
|
||||
// slot post-dates 1.12 and `UnitHasRelicSlot` is a later-era verb. The base `dbc.MPQ` copy of
|
||||
// `ChrClasses.dbc` really does have no field 16; the `patch.MPQ` copy the client actually
|
||||
// reads has it, set for Paladin, Shaman and Druid. Decision 1785. Stock `PaperDollFrame.lua`
|
||||
// reads has it, set for Paladin, Shaman and Druid. Decision 1796. Stock `PaperDollFrame.lua`
|
||||
// calls this **unconditionally** at l.429 and l.580 — so while it was missing, the character
|
||||
// sheet raised `attempt to call global 'UnitHasRelicSlot' (a nil value)` for *every* class,
|
||||
// not just the three.
|
||||
|
||||
@@ -173,7 +173,7 @@ pub struct UnitState {
|
||||
/// which is the nil leg.
|
||||
///
|
||||
/// True for exactly Paladin, Shaman and Druid in 1.12 (Libram, Totem, Idol). It was believed
|
||||
/// here for a long time that no 1.12 class had one; see decision 1785 for how that survived.
|
||||
/// here for a long time that no 1.12 class had one; see decision 1796 for how that survived.
|
||||
pub has_relic_slot: bool,
|
||||
/// The unit's sex on the `UnitSex` scale: `2` male, `3` female (`1` = neuter/unknown). `0`
|
||||
/// (the unfilled default) reports as nil — the API's "can't tell", like [`Self::reaction`].
|
||||
|
||||
@@ -1295,7 +1295,7 @@ fn unit_is_party_leader_ors_two_legs_and_answers_one_when_solo() {
|
||||
/// `UnitHasRelicSlot` — the number 1 or nil, per token, never a boolean.
|
||||
///
|
||||
/// This shipped **absent** for months on the belief that the relic slot post-dates 1.12, which is
|
||||
/// false (decision 1785). Stock `PaperDollFrame.lua` calls it unconditionally at l.429 and l.580,
|
||||
/// false (decision 1796). Stock `PaperDollFrame.lua` calls it unconditionally at l.429 and l.580,
|
||||
/// so while it was missing the character sheet raised `attempt to call global` for every class —
|
||||
/// which is why the nil-global case is asserted here too, not just the answer.
|
||||
#[test]
|
||||
|
||||
@@ -48,7 +48,7 @@ pub enum FrameKind {
|
||||
///
|
||||
/// Its whole Lua surface of its own is **one method, `SetSlot(index)`** (table `0x847ce4`,
|
||||
/// count 1 read off the registrar's own `mov edx,1`) — 1-based in, 0-based stored, no returns.
|
||||
/// `LootFrame.lua:94` is its only caller. Decision 1788; wow-re `e5338caf`.
|
||||
/// `LootFrame.lua:94` is its only caller. Decision 1799; wow-re `e5338caf`.
|
||||
LootButton,
|
||||
CheckButton,
|
||||
EditBox,
|
||||
|
||||
@@ -447,6 +447,47 @@ impl Default for WidgetArena {
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether a widget of this kind is **mouse-enabled by its constructor**, needing neither an
|
||||
/// `enableMouse` attribute nor an auto-enabling `<Scripts>` handler to be clickable.
|
||||
///
|
||||
/// One list, because there are two readers: this arena, and `benilla-app`'s `frame_flag_gate`
|
||||
/// sweep, which compares our frames against a model of the reference's XML. That sweep used to
|
||||
/// carry its own copy with a comment claiming it was "deliberately the SAME list" — it was not,
|
||||
/// and it drifted silently the first time this one was edited. A wrong entry should be wrong in
|
||||
/// one place.
|
||||
///
|
||||
/// **Read off the client's ctors** (wow-re `ui/scratch/mouse-enable-law.md` `774f7eb6`, and the
|
||||
/// follow-up `68987021`). `[frame+0xcc]` is a four-bit input mask — char, keyboard, MOUSE, wheel —
|
||||
/// zeroed by the base `CSimpleFrame` ctor `0x769090` and set by exactly five subclass ctors:
|
||||
/// Button `0x778771`, CheckButton (through Button), Slider `0x789467`, ColorSelect `0x78b2b5`,
|
||||
/// and EditBox `0x779ced`, which takes char+keyboard+mouse together because it must take a click
|
||||
/// to focus. `Minimap` is an engine frame born `0x4` (`0x4edc38`), and ours is a `FrameKind`.
|
||||
///
|
||||
/// **`ScrollFrame` and `ScrollingMessageFrame` are NOT here, and that was the hard one.** They
|
||||
/// were, on the reasoning that a scroll frame "takes the wheel in its ctor" — the wheel is a
|
||||
/// different bit, and no ctor writes it at all. Against that stood a recorded observation:
|
||||
/// `ChatFrame1` is a `ScrollingMessageFrame`, and something has to make a 1.12 chat link
|
||||
/// clickable. The follow-up round settled it: nothing about the frame does. The engine synthesises
|
||||
/// one mouse-enabled `CSimpleHyperlinkButton` child PER LINK SPAN (`0x7a3240`, born through the
|
||||
/// `CSimpleButton` ctor) and routes the hyperlink scripts back to the parent. We hold spans as
|
||||
/// rects rather than child frames, so `script::pointer`'s hit test carries the same law as a
|
||||
/// disjunct: the chat window is a candidate over a link and inert everywhere else.
|
||||
pub fn mouse_enabled_by_ctor(kind: FrameKind) -> bool {
|
||||
matches!(
|
||||
kind,
|
||||
FrameKind::Button
|
||||
| FrameKind::CheckButton
|
||||
| FrameKind::EditBox
|
||||
// A Slider's thumb must be draggable: every scrollbar is a UIPanelScrollBarTemplate
|
||||
// Slider that declares no `enableMouse` yet is draggable in-game (decision 0250).
|
||||
| FrameKind::Slider
|
||||
| FrameKind::ColorSelect
|
||||
| FrameKind::Minimap
|
||||
// `LootButton` extends `CSimpleButton` and inherits its ctor's bit (decision 1799).
|
||||
| FrameKind::LootButton
|
||||
)
|
||||
}
|
||||
|
||||
impl WidgetArena {
|
||||
/// A new, empty arena in **1.12 mode** (alpha does not propagate).
|
||||
pub fn new() -> WidgetArena {
|
||||
@@ -602,27 +643,7 @@ impl WidgetArena {
|
||||
effective_alpha,
|
||||
shown,
|
||||
effective_visible: shown && parent_visible,
|
||||
// A button/editbox is mouse-enabled by construction (the client's CSimpleButton and
|
||||
// CSimpleEditBox enable input in their ctors — an XML button needs no enableMouse attr
|
||||
// to be clickable, and an editbox must take clicks to focus, RF-0082 §1).
|
||||
mouse_enabled: matches!(
|
||||
kind,
|
||||
FrameKind::Button
|
||||
| FrameKind::CheckButton
|
||||
| FrameKind::EditBox
|
||||
// The scroll frame takes the wheel in its ctor (msgframe-runtime.md) — an XML
|
||||
// ScrollingMessageFrame is wheel-interactive with no enableMouse attr, exactly
|
||||
// like a Button is clickable without one.
|
||||
| FrameKind::ScrollingMessageFrame
|
||||
// A plain ScrollFrame likewise takes the wheel/drag by construction (decision
|
||||
// 0112) — an XML ScrollFrame is scroll-interactive with no enableMouse attr,
|
||||
// the same rationale as the ScrollingMessageFrame above.
|
||||
| FrameKind::ScrollFrame
|
||||
// A Slider takes the mouse in its ctor too (the thumb must be draggable): every
|
||||
// scrollbar is a UIPanelScrollBarTemplate Slider that declares no enableMouse yet
|
||||
// is draggable in-game (decision 0250), so the enablement is by construction.
|
||||
| FrameKind::Slider
|
||||
),
|
||||
mouse_enabled: mouse_enabled_by_ctor(kind),
|
||||
// The two kinds whose ctor takes the WHEEL rather than the mouse generally — the same
|
||||
// by-construction argument as above, narrowed to the flag it is actually about
|
||||
// (decision 1198): a ScrollingMessageFrame and a ScrollFrame are wheel-interactive
|
||||
|
||||
Reference in New Issue
Block a user