the main action bar pages, and <Include> runs Lua like the client
- the main bar's page arrows page for real, with the reference's own rule that a page a shown multibar displays is not also reachable by paging - `<Include file="X.lua">` executes the file the way the client does — dispatch on the extension — so an addon pulling its libraries through XML stops losing them - UIParentLoadAddOn lands, the on-demand loader behind every "open the panel" path - nine constants take the reference's names back (NUM_GROUP_LOOT_FRAMES and friends — an addon reads the real name, not our prefix), and UnitCreatureType answers through the client's own three-stage resolver Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -67,12 +67,12 @@
|
||||
--
|
||||
-- Found by the use-probe, which is the only column that touches anything.
|
||||
--
|
||||
-- `CURRENT_ACTIONBAR_PAGE` is deliberately NOT among them. It is the reference's mutable
|
||||
-- page cursor (ActionButton.lua:64/74 walk it), and benilla does not page the main bar that
|
||||
-- way — the bonus offset re-pages it instead. Defining it as a frozen 1 would hand every
|
||||
-- addon a number that is silently wrong forever; absent, a reader fails loudly and we find
|
||||
-- out. That is this arc's own rule (1203/1205/1230: a silent wrong answer costs more than
|
||||
-- a missing one), applied to ourselves.
|
||||
-- `CURRENT_ACTIONBAR_PAGE` used to be withheld from this list on the grounds that it is the
|
||||
-- reference's MUTABLE page cursor and we did not page the main bar — "defining it as a
|
||||
-- frozen 1 would hand every addon a number that is silently wrong forever". That reasoning
|
||||
-- was right and is now discharged rather than overruled: the bar pages, the global is live
|
||||
-- state the paged-id formula reads, and it is declared with the paging verbs below rather
|
||||
-- than here among the true constants. A frozen 1 would still be a lie; this one moves.
|
||||
NUM_ACTIONBAR_PAGES = 6
|
||||
NUM_ACTIONBAR_BUTTONS = 12
|
||||
BOTTOMLEFT_ACTIONBAR_PAGE = 6
|
||||
@@ -134,6 +134,80 @@
|
||||
-- feedback events per-button; our bar-level fan needs the full set in one place.
|
||||
BENILLA_ACTION_BUTTONS = {}
|
||||
|
||||
-- ── Main-bar paging (ref ActionButton.lua l.1-12 and l.63-101) ───────────────────────
|
||||
-- The main bar has six pages, and we already hold every slot they address: the app owns
|
||||
-- the authoritative 1..120 action table, so the DATA for pages 2-6 was always there and
|
||||
-- only the page SELECTOR was missing. Its absence was on screen, not just in the API —
|
||||
-- the page arrows below shipped as art with no OnClick, which this file's own comment
|
||||
-- admitted ("ART ONLY - we have no bar paging, so the clicks are no-ops").
|
||||
--
|
||||
-- `Bartender2.lua:686` is the corpus tell: three bare `ChangeActionBarPage()` calls, and
|
||||
-- the addon died on the nil global at session start.
|
||||
CURRENT_ACTIONBAR_PAGE = 1
|
||||
-- "Table of actionbar pages and whether they're viewable or not" (the reference's own
|
||||
-- comment). The page walk below honours it rather than counting, which is what makes the
|
||||
-- next two lines work at all.
|
||||
--
|
||||
-- **Pages 5 and 6 are deliberately NOT viewable, and that is the reference's own rule
|
||||
-- applied to our configuration rather than a deviation from it.**
|
||||
-- `MultiActionBar_Update` (ref MultiActionBars.lua l.49-60) blanks
|
||||
-- `VIEWABLE_ACTION_BAR_PAGES[BOTTOMLEFT_ACTIONBAR_PAGE]` whenever MultiBarBottomLeft is
|
||||
-- SHOWN — a page a multibar already displays must not also be reachable on the main bar,
|
||||
-- or paging up shows you the twelve actions already on screen below. benilla's two bottom
|
||||
-- bars are always-on with no `SHOW_MULTI_ACTIONBAR_*` state (0270, the director's call),
|
||||
-- and they own pages 6 and 5 (MultiBars.xml: BottomLeft = actions 61..72, BottomRight =
|
||||
-- 49..60), so the two rows are blanked at declaration instead of by a toggle.
|
||||
--
|
||||
-- MultiBarLeft/Right (pages 4 and 3) stay viewable because ours ship HIDDEN (1219), which
|
||||
-- is the same state the reference is in with SHOW_MULTI_ACTIONBAR_3/4 unset.
|
||||
VIEWABLE_ACTION_BAR_PAGES = {1, 1, 1, 1}
|
||||
VIEWABLE_ACTION_BAR_PAGES[BOTTOMRIGHT_ACTIONBAR_PAGE] = nil
|
||||
VIEWABLE_ACTION_BAR_PAGES[BOTTOMLEFT_ACTIONBAR_PAGE] = nil
|
||||
|
||||
-- `ChangeActionBarPage()` takes NO arguments — every reference caller sets
|
||||
-- CURRENT_ACTIONBAR_PAGE first and then calls this to apply it (l.79, l.100), and
|
||||
-- Bartender2 calls it bare.
|
||||
--
|
||||
-- Upstream it is a client binding; here it is FrameXML, deliberately. Our paged action id
|
||||
-- is computed in Lua by `BenillaActionBar_ActionFor`, so the page IS this global and the
|
||||
-- only work left for the verb is the repaint the reference gets by firing
|
||||
-- ACTIONBAR_PAGE_CHANGED. Addons CALL this, they never replace it, so the divergence is
|
||||
-- unobservable from Lua — unlike a name we shadow, which is 1222's class.
|
||||
function ChangeActionBarPage()
|
||||
for i = 1, table.getn(BENILLA_ACTION_BUTTONS) do
|
||||
BenillaActionButton_Update(BENILLA_ACTION_BUTTONS[i])
|
||||
end
|
||||
end
|
||||
|
||||
function ActionBar_PageUp()
|
||||
local page = CURRENT_ACTIONBAR_PAGE + 1
|
||||
local nextPage
|
||||
for i = page, NUM_ACTIONBAR_PAGES do
|
||||
if VIEWABLE_ACTION_BAR_PAGES[i] then nextPage = i break end
|
||||
end
|
||||
-- Past the last viewable page, wrap to 1 — the reference wraps to the LITERAL 1 here
|
||||
-- rather than to the first viewable page, which is its own asymmetry with PageDown
|
||||
-- (which rescans). Kept, because it is observable: with page 1 blanked, paging up off
|
||||
-- the end lands on a page the down-walk would have skipped.
|
||||
CURRENT_ACTIONBAR_PAGE = nextPage or 1
|
||||
ChangeActionBarPage()
|
||||
end
|
||||
|
||||
function ActionBar_PageDown()
|
||||
local page = CURRENT_ACTIONBAR_PAGE - 1
|
||||
local prevPage
|
||||
for i = page, 1, -1 do
|
||||
if VIEWABLE_ACTION_BAR_PAGES[i] then prevPage = i break end
|
||||
end
|
||||
if not prevPage then
|
||||
for i = NUM_ACTIONBAR_PAGES, 1, -1 do
|
||||
if VIEWABLE_ACTION_BAR_PAGES[i] then prevPage = i break end
|
||||
end
|
||||
end
|
||||
CURRENT_ACTIONBAR_PAGE = prevPage
|
||||
ChangeActionBarPage()
|
||||
end
|
||||
|
||||
-- Seat a TOP-LEVEL frame that anchors INTO the bar art (the bag bar + backpack toggle,
|
||||
-- BagFrame.xml; the micro-button row, MicroMenu.xml) one frame level above the art, so its
|
||||
-- icons draw over the bar plate instead of under it.
|
||||
@@ -224,6 +298,11 @@
|
||||
local base = 0
|
||||
if offset > 0 then
|
||||
base = (6 + offset - 1) * 12
|
||||
else
|
||||
-- The paged main bar. A bonus page (stance/form/stealth) OUTRANKS it, which is
|
||||
-- why this is the else arm and not an addition: the reference's own
|
||||
-- `ActionButton_GetPagedID` takes the bonus branch first.
|
||||
base = (CURRENT_ACTIONBAR_PAGE - 1) * 12
|
||||
end
|
||||
return base + index
|
||||
end
|
||||
@@ -1485,8 +1564,8 @@
|
||||
</CheckButton>
|
||||
|
||||
<!-- Page up/down: 32x32 at CENTER of the art frame's TOPLEFT +(522,-22)/(522,-42)
|
||||
(ref-ActionBarFrame.xml l.159-217). ART ONLY — we have no bar paging, so the
|
||||
clicks are no-ops (no OnClick handler). -->
|
||||
(ref-ActionBarFrame.xml l.159-217). Live now that the bar pages — they were
|
||||
art with no OnClick for as long as `CURRENT_ACTIONBAR_PAGE` did not exist. -->
|
||||
<Button name="ActionBarUpButton">
|
||||
<Size><AbsDimension x="32" y="32"/></Size>
|
||||
<Anchors>
|
||||
@@ -1495,6 +1574,7 @@
|
||||
<NormalTexture file="Interface\MainMenuBar\UI-MainMenu-ScrollUpButton-Up"/>
|
||||
<PushedTexture file="Interface\MainMenuBar\UI-MainMenu-ScrollUpButton-Down"/>
|
||||
<HighlightTexture file="Interface\MainMenuBar\UI-MainMenu-ScrollUpButton-Highlight" alphaMode="ADD"/>
|
||||
<Scripts><OnClick>ActionBar_PageUp()</OnClick></Scripts>
|
||||
</Button>
|
||||
<Button name="ActionBarDownButton">
|
||||
<Size><AbsDimension x="32" y="32"/></Size>
|
||||
@@ -1504,6 +1584,7 @@
|
||||
<NormalTexture file="Interface\MainMenuBar\UI-MainMenu-ScrollDownButton-Up"/>
|
||||
<PushedTexture file="Interface\MainMenuBar\UI-MainMenu-ScrollDownButton-Down"/>
|
||||
<HighlightTexture file="Interface\MainMenuBar\UI-MainMenu-ScrollDownButton-Highlight" alphaMode="ADD"/>
|
||||
<Scripts><OnClick>ActionBar_PageDown()</OnClick></Scripts>
|
||||
</Button>
|
||||
</Frames>
|
||||
</Frame>
|
||||
|
||||
@@ -56,8 +56,8 @@
|
||||
established cross-window reuse `TrainerFrame.xml`/`BagFrame.xml` already lean on).
|
||||
-->
|
||||
<Script><![CDATA[
|
||||
BENILLA_BANK_ITEM_SLOTS = 24 -- NUM_BANKGENERIC_SLOTS (ref BankFrame.lua:3)
|
||||
BENILLA_BANK_BAG_SLOTS = 6 -- NUM_BANKBAGSLOTS (ref BankFrame.lua:4)
|
||||
NUM_BANKGENERIC_SLOTS = 24 -- (ref BankFrame.lua:3)
|
||||
NUM_BANKBAGSLOTS = 6 -- (ref BankFrame.lua:4)
|
||||
|
||||
function BankFrame_OnLoad()
|
||||
local frame = this
|
||||
@@ -95,7 +95,7 @@
|
||||
-- Repaint every one of the 24 generic slots (PLAYERBANKSLOTS_CHANGED names ONE slot; the
|
||||
-- spec allows repainting all 24 — cheap, and spares tracking a slot->button map).
|
||||
function BenillaBankFrame_RepaintItems()
|
||||
for i = 1, BENILLA_BANK_ITEM_SLOTS do
|
||||
for i = 1, NUM_BANKGENERIC_SLOTS do
|
||||
local button = getglobal("BankItem" .. i)
|
||||
if button then BenillaBagSlot_Update(button) end
|
||||
end
|
||||
@@ -107,7 +107,7 @@
|
||||
-- afford color; the whole row hidden once GetNumBankSlots() reports `full`.
|
||||
function BenillaBankFrame_UpdateBagRow()
|
||||
local numSlots, full = GetNumBankSlots()
|
||||
for i = 1, BENILLA_BANK_BAG_SLOTS do
|
||||
for i = 1, NUM_BANKBAGSLOTS do
|
||||
local button = getglobal("BankBagButton" .. i)
|
||||
if button then
|
||||
local texture = BenillaGetBankBagTexture(i)
|
||||
@@ -188,7 +188,7 @@
|
||||
|
||||
-- CloseBankBagFrames (ref BankFrame.lua:119-123), over our popout naming.
|
||||
function CloseBankBagFrames()
|
||||
for j = 1, BENILLA_BANK_BAG_SLOTS do
|
||||
for j = 1, NUM_BANKBAGSLOTS do
|
||||
local frame = getglobal("BenillaBankBagFrame" .. j)
|
||||
if frame and frame:IsShown() then frame:Hide() end
|
||||
local button = getglobal("BankBagButton" .. j)
|
||||
|
||||
@@ -70,6 +70,39 @@
|
||||
function TEXT(text)
|
||||
return text
|
||||
end
|
||||
|
||||
-- ── UIParentLoadAddOn (ref UIParent.lua l.638-647) ────────────────────────────────────
|
||||
-- The on-demand loader every "open the auction house / tradeskill / talent panel" path
|
||||
-- goes through, and the one 5 corpus addons name — FuBar_MCPFu, Fubar_RareTrackerFu, MCP,
|
||||
-- MikScrollingBattleText, SpecialTalent: five separate addons, not one library replicated
|
||||
-- (1207/1240).
|
||||
--
|
||||
-- Every piece was already here and only the join was missing: `LoadAddOn` answers the
|
||||
-- reference's own `(1, nil)` / `(nil, reason)` pair, `message` and `TEXT` are directly
|
||||
-- above, and the `ADDON_*` reason words come from the real `GlobalStrings.lua` we RUN off
|
||||
-- the patch chain rather than copy (1234).
|
||||
--
|
||||
-- **`FailedAddOnLoad` is the reference's own latch, and it is not decoration.** Without it
|
||||
-- an addon that retries the load from a handler raises a modal on every attempt; with it
|
||||
-- the failure shows once per addon name per session. It is exactly the detail this would
|
||||
-- have lacked had it been reconstructed from the name instead of read.
|
||||
--
|
||||
-- Every reason our `LoadAddOn` can answer resolves to a string: MISSING, DISABLED,
|
||||
-- NOT_DEMAND_LOADED, DEP_MISSING and DEP_DISABLED all have an `ADDON_..reason` word
|
||||
-- (GlobalStrings l.16-31), the same reason set wow-re's `ui.md` l.2396 carves off
|
||||
-- `0x51e780`. So the `getglobal` below cannot hand `format` a nil.
|
||||
FailedAddOnLoad = {};
|
||||
|
||||
function UIParentLoadAddOn(name)
|
||||
local loaded, reason = LoadAddOn(name)
|
||||
if not loaded then
|
||||
if not FailedAddOnLoad[name] then
|
||||
message(format(TEXT(ADDON_LOAD_FAILED), name, TEXT(getglobal("ADDON_" .. reason))))
|
||||
FailedAddOnLoad[name] = true
|
||||
end
|
||||
end
|
||||
return loaded
|
||||
end
|
||||
]]></Script>
|
||||
|
||||
<!-- The dialog button faces (ref l.28-37). Same three-piece art as UIPanelButtonTemplate's,
|
||||
|
||||
@@ -233,7 +233,7 @@
|
||||
-- ── This window's own constants (ref-PaperDollFrame.lua:1-4, Benilla-prefixed) ────────────
|
||||
BENILLA_NUM_STATS = 5 -- NUM_STATS
|
||||
BENILLA_NUM_RESISTANCE_TYPES = 5 -- NUM_RESISTANCE_TYPES
|
||||
BENILLA_ATTACK_POWER_MAGIC_NUMBER = 14 -- ATTACK_POWER_MAGIC_NUMBER
|
||||
ATTACK_POWER_MAGIC_NUMBER = 14 -- ATTACK_POWER_MAGIC_NUMBER
|
||||
-- MagicResFrame1..5's ref `id=` attrs (ref-PaperDollFrame.xml:654-759) — arcane/fire/nature/
|
||||
-- frost/shadow; Holy(1) never rides the paper doll (only 5 of the 6 schools show here).
|
||||
BENILLA_MAGICRES_IDS = { 6, 2, 3, 4, 5 }
|
||||
@@ -689,7 +689,7 @@
|
||||
local text = getglobal(prefix .. "AttackPowerFrameStatText")
|
||||
PaperDollFormatStat(MELEE_ATTACK_POWER, base, posBuff, negBuff, frame, text)
|
||||
frame.tooltipSubtext = format(MELEE_ATTACK_POWER_TOOLTIP,
|
||||
math.max(base + posBuff + negBuff, 0) / BENILLA_ATTACK_POWER_MAGIC_NUMBER)
|
||||
math.max(base + posBuff + negBuff, 0) / ATTACK_POWER_MAGIC_NUMBER)
|
||||
end
|
||||
|
||||
-- ref PaperDollFrame_SetAttackBothHands (l.391-417) — the weapon-skill line (decision 0208's
|
||||
@@ -837,7 +837,7 @@
|
||||
local base, posBuff, negBuff = UnitRangedAttackPower(unit)
|
||||
PaperDollFormatStat(RANGED_ATTACK_POWER, base, posBuff, negBuff, frame, text)
|
||||
frame.tooltipSubtext = format(RANGED_ATTACK_POWER_TOOLTIP,
|
||||
math.max(base + posBuff + negBuff, 0) / BENILLA_ATTACK_POWER_MAGIC_NUMBER)
|
||||
math.max(base + posBuff + negBuff, 0) / ATTACK_POWER_MAGIC_NUMBER)
|
||||
end
|
||||
|
||||
-- ref PaperDollFormatStat (l.804-833).
|
||||
|
||||
@@ -99,9 +99,16 @@
|
||||
hides the one that owns it (`crates/benilla/src/ui_loot_roll.rs` fires both).
|
||||
-->
|
||||
<Script><![CDATA[
|
||||
-- LootFrame.lua l.2's NUM_GROUP_LOOT_FRAMES, renamed to the house BENILLA_ prefix (the same
|
||||
-- treatment LootFrame.xml gave LOOTFRAME_NUMBUTTONS -> BENILLA_LOOT_NUMBUTTONS).
|
||||
BENILLA_NUM_GROUP_LOOT_FRAMES = 4
|
||||
-- LootFrame.lua l.2's NUM_GROUP_LOOT_FRAMES, under the REFERENCE's own name.
|
||||
--
|
||||
-- It carried the house `BENILLA_` prefix until an addon died on the bare one:
|
||||
-- `Bongos_RollBar/bar.lua:85` is `for i=1, NUM_GROUP_LOOT_FRAMES do` and got a nil limit.
|
||||
-- That is 1222's shadowed-global class again — the fifth instance — and the rule it
|
||||
-- settled applies to a CONSTANT an addon reads exactly as it does to a frame: our name for
|
||||
-- a global is as load-bearing as our name for a function. The prefix is right only where
|
||||
-- the reference has no counterpart; here it has one, with the same value (4) counting the
|
||||
-- same four frames, which already carry the reference's bare names two paragraphs down.
|
||||
NUM_GROUP_LOOT_FRAMES = 4
|
||||
|
||||
-- The bind-on-pickup confirm (ref StaticPopup.lua l.1371-1382, verbatim bar the TEXT()
|
||||
-- indirection this house dialect drops). Need/Greed on a BoP roll never reaches the wire
|
||||
@@ -138,7 +145,7 @@
|
||||
-- Timer StatusBar's max before showing it (fields set BEFORE Show(), so OnShow below can
|
||||
-- already read frame.rollID).
|
||||
function GroupLootFrame_OpenNewFrame(rollId, rollTime)
|
||||
for i = 1, BENILLA_NUM_GROUP_LOOT_FRAMES do
|
||||
for i = 1, NUM_GROUP_LOOT_FRAMES do
|
||||
local frame = getglobal("GroupLootFrame" .. i)
|
||||
if frame and not frame:IsVisible() then
|
||||
frame.rollID = rollId
|
||||
|
||||
@@ -59,7 +59,7 @@
|
||||
AUTOCAST_* .
|
||||
-->
|
||||
<Script><![CDATA[
|
||||
BENILLA_NUM_PET_ACTION_SLOTS = 10
|
||||
NUM_PET_ACTION_SLOTS = 10
|
||||
|
||||
-- The token textures, quoted verbatim from ref PetActionBarFrame.lua:6-12. They are
|
||||
-- GLOBALS on purpose: `GetPetActionInfo` returns the NAME of one for a token slot, and the
|
||||
@@ -325,7 +325,7 @@
|
||||
-- the filled UI-Quickslot2 to the empty UI-Quickslot.
|
||||
function PetActionBar_Update()
|
||||
local usable = GetPetActionsUsable()
|
||||
for i = 1, BENILLA_NUM_PET_ACTION_SLOTS do
|
||||
for i = 1, NUM_PET_ACTION_SLOTS do
|
||||
local button = getglobal("PetActionButton" .. i)
|
||||
local name, subtext, texture, isToken, isActive, autoCastAllowed, autoCastEnabled = GetPetActionInfo(i)
|
||||
button.isToken = isToken
|
||||
@@ -411,7 +411,7 @@
|
||||
if frame.clock > BENILLA_PET_AUTOCAST_PERIOD then
|
||||
frame.clock = frame.clock - BENILLA_PET_AUTOCAST_PERIOD
|
||||
end
|
||||
for i = 1, BENILLA_NUM_PET_ACTION_SLOTS do
|
||||
for i = 1, NUM_PET_ACTION_SLOTS do
|
||||
BenillaPetAutocast_Update(getglobal("PetActionButton" .. i), frame.clock)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -81,7 +81,7 @@
|
||||
UNIT_LEVEL_TEMPLATE = UNIT_LEVEL_TEMPLATE or "Level %d" -- :4436
|
||||
PET_DIET_TEMPLATE = PET_DIET_TEMPLATE or "Diet: %s" -- :3037
|
||||
|
||||
BENILLA_NUM_PET_STATS = 5 -- NUM_PET_STATS (ref l.2)
|
||||
NUM_PET_STATS = 5 -- (ref l.2)
|
||||
BENILLA_NUM_PET_RESISTANCE_TYPES = 5 -- NUM_PET_RESISTANCE_TYPES (ref l.1)
|
||||
|
||||
-- ── The page ────────────────────────────────────────────────────────────────────────────
|
||||
@@ -244,7 +244,7 @@
|
||||
-- duplication; unifying them would mean inventing a prefix the reference's own
|
||||
-- `PetStatFrame`/`CharacterStatFrame` naming doesn't share).
|
||||
function PetPaperDollFrame_SetStats()
|
||||
for i = 1, BENILLA_NUM_PET_STATS do
|
||||
for i = 1, NUM_PET_STATS do
|
||||
local label = getglobal("PetStatFrame" .. i .. "Label")
|
||||
local text = getglobal("PetStatFrame" .. i .. "StatText")
|
||||
local frame = getglobal("PetStatFrame" .. i)
|
||||
|
||||
@@ -67,9 +67,9 @@
|
||||
to be the real widget method rather than a benilla global.
|
||||
-->
|
||||
<Script><![CDATA[
|
||||
BENILLA_QUESTLOG_ROWS = 6 -- QUESTS_DISPLAYED (pin §2, QuestLogFrame.lua:1)
|
||||
BENILLA_QUESTLOG_MAX_DISPLAY = 20 -- MAX_QUESTLOG_QUESTS (pin §2, :6) — display cap only
|
||||
BENILLA_QUESTLOG_MAX_OBJECTIVES = 10 -- MAX_OBJECTIVES (pin §3, QuestLogFrame.lua:3)
|
||||
QUESTS_DISPLAYED = 6 -- QUESTS_DISPLAYED (pin §2, QuestLogFrame.lua:1)
|
||||
MAX_QUESTLOG_QUESTS = 20 -- MAX_QUESTLOG_QUESTS (pin §2, :6) — display cap only
|
||||
MAX_OBJECTIVES = 10 -- MAX_OBJECTIVES (pin §3, QuestLogFrame.lua:3)
|
||||
-- Reward/choice row cap: the SAME v1 cap `QuestFrame.xml`'s BENILLA_QUEST_MAX_ITEMS uses (the
|
||||
-- ref's own MAX_NUM_ITEMS is 10, QuestFrame.lua:2 — we halve it there too).
|
||||
BENILLA_QUESTLOG_MAX_ITEMS = 6
|
||||
@@ -270,7 +270,7 @@
|
||||
-- pill's middle piece then shrinks to the text (ref sizes it every update). The >10
|
||||
-- guard skips the first-ever paint, where the host hasn't measured the string yet
|
||||
-- (GetWidth 0 would collapse the pill; the next update — and every one after — sizes it).
|
||||
getglobal("QuestLogCount"):SetText(string.format("Quests: |cffffffff%d/%d|r", numQuests, BENILLA_QUESTLOG_MAX_DISPLAY))
|
||||
getglobal("QuestLogCount"):SetText(string.format("Quests: |cffffffff%d/%d|r", numQuests, MAX_QUESTLOG_QUESTS))
|
||||
local countWidth = getglobal("QuestLogCount"):GetWidth()
|
||||
if countWidth and countWidth > 10 then
|
||||
getglobal("QuestLogCountMiddle"):SetWidth(countWidth)
|
||||
@@ -293,14 +293,14 @@
|
||||
end
|
||||
|
||||
-- Clamp the faux-scroll offset (pin §2's FauxScrollFrame_Update clamp).
|
||||
local maxOffset = numEntries - BENILLA_QUESTLOG_ROWS
|
||||
local maxOffset = numEntries - QUESTS_DISPLAYED
|
||||
if maxOffset < 0 then maxOffset = 0 end
|
||||
if questLogOffset > maxOffset then questLogOffset = maxOffset end
|
||||
if questLogOffset < 0 then questLogOffset = 0 end
|
||||
|
||||
local selection = GetQuestLogSelection()
|
||||
|
||||
for i = 1, BENILLA_QUESTLOG_ROWS do
|
||||
for i = 1, QUESTS_DISPLAYED do
|
||||
local button = getglobal("QuestLogTitle" .. i)
|
||||
local index = i + questLogOffset
|
||||
if index <= numEntries then
|
||||
@@ -362,7 +362,7 @@
|
||||
-- vertex-colored to the row's own difficulty color (that green bar in the ref shot IS
|
||||
-- this vertex color on the white ADD gradient).
|
||||
local hl = getglobal("QuestLogSelectHighlight")
|
||||
if selection > questLogOffset and selection <= questLogOffset + BENILLA_QUESTLOG_ROWS
|
||||
if selection > questLogOffset and selection <= questLogOffset + QUESTS_DISPLAYED
|
||||
and selection <= numEntries then
|
||||
local selButton = getglobal("QuestLogTitle" .. (selection - questLogOffset))
|
||||
local _, selLevel, _, selIsHeader = GetQuestLogTitle(selection)
|
||||
@@ -563,7 +563,7 @@
|
||||
getglobal("QuestLogTimerText"):SetText("")
|
||||
getglobal("QuestLogDescriptionTitle"):SetText("")
|
||||
getglobal("QuestLogDescription"):SetText("")
|
||||
for i = 1, BENILLA_QUESTLOG_MAX_OBJECTIVES do
|
||||
for i = 1, MAX_OBJECTIVES do
|
||||
getglobal("QuestLogObjective" .. i):SetText("")
|
||||
end
|
||||
getglobal("QuestLogRewardTitleText"):SetText("")
|
||||
@@ -819,7 +819,7 @@
|
||||
getglobal("QuestLogDescriptionTitle"):SetText("Description")
|
||||
|
||||
local numObjectives = GetNumQuestLeaderBoards()
|
||||
for i = 1, BENILLA_QUESTLOG_MAX_OBJECTIVES do
|
||||
for i = 1, MAX_OBJECTIVES do
|
||||
local line = getglobal("QuestLogObjective" .. i)
|
||||
if i <= numObjectives then
|
||||
local text, kind, finished = GetQuestLogLeaderBoard(i)
|
||||
@@ -847,7 +847,7 @@
|
||||
-- widget backs GetQuestLogRequiredMoney yet) — that ref branch collapses into the rest.
|
||||
-- The objective-less TIMED branch (ref :432-437) is transcribed: a timed quest with no
|
||||
-- leaderboard lines hangs the header off the countdown row, not off ObjectivesText.
|
||||
local objAnchorIndex = math.min(numObjectives, BENILLA_QUESTLOG_MAX_OBJECTIVES)
|
||||
local objAnchorIndex = math.min(numObjectives, MAX_OBJECTIVES)
|
||||
local descTitle = getglobal("QuestLogDescriptionTitle")
|
||||
if objAnchorIndex > 0 then
|
||||
descTitle:SetPoint("TOPLEFT", getglobal("QuestLogObjective" .. objAnchorIndex), "BOTTOMLEFT", 0, -10)
|
||||
|
||||
@@ -1068,10 +1068,18 @@ fn the_reference_action_bar_constants_are_defined() {
|
||||
"zBar.lua:40's numeric for must have a limit"
|
||||
);
|
||||
|
||||
assert!(
|
||||
s.eval::<bool>("return CURRENT_ACTIONBAR_PAGE == nil").unwrap(),
|
||||
"CURRENT_ACTIONBAR_PAGE is mutable page state we do not keep — nil fails loudly, a frozen 1 lies"
|
||||
// `CURRENT_ACTIONBAR_PAGE` was asserted ABSENT here, on the grounds that a frozen 1 lies where
|
||||
// nil fails loudly. That objection is discharged, not overruled: the bar pages now, so the
|
||||
// global is live state the paged-id formula reads rather than a frozen number. It is therefore
|
||||
// asserted as state — present, and MOVING — instead of as one of the constants above.
|
||||
assert_eq!(s.eval::<i64>("return CURRENT_ACTIONBAR_PAGE").unwrap(), 1);
|
||||
s.run("ActionBar_PageUp()").unwrap();
|
||||
assert_eq!(
|
||||
s.eval::<i64>("return CURRENT_ACTIONBAR_PAGE").unwrap(),
|
||||
2,
|
||||
"a frozen 1 would still be a lie — this one has to move"
|
||||
);
|
||||
s.run("ActionBar_PageDown()").unwrap();
|
||||
}
|
||||
|
||||
/// **The reference's two-level action-button split, both halves inheritable by name.**
|
||||
@@ -1155,3 +1163,102 @@ fn both_reference_action_button_templates_are_inheritable() {
|
||||
"BenillaActionButtonTemplate's 48 inherits= sites must be untouched by the split"
|
||||
);
|
||||
}
|
||||
|
||||
/// Main-bar paging — `CURRENT_ACTIONBAR_PAGE` and the three verbs around it.
|
||||
///
|
||||
/// The data was always there (the app owns all 120 action slots); only the selector was missing,
|
||||
/// and its absence was visible on screen as page arrows with no `OnClick`. `Bartender2.lua:686`
|
||||
/// died on the nil `ChangeActionBarPage` at session start.
|
||||
///
|
||||
/// Two things are asserted that a reconstruction would get wrong. **A bonus page outranks the
|
||||
/// paged one** — the reference's own `ActionButton_GetPagedID` takes the bonus branch first, so
|
||||
/// paging must be the `else` arm and not an addition. And **page-up wraps to the literal page 1**
|
||||
/// while page-down rescans for the last viewable page: that asymmetry is the reference's, and it
|
||||
/// is observable the moment a page is blanked from `VIEWABLE_ACTION_BAR_PAGES`.
|
||||
#[test]
|
||||
fn the_main_bar_pages_and_a_bonus_page_still_outranks_it() {
|
||||
let mut s = UiScript::new().unwrap();
|
||||
s.set_screen_size(1024.0, 768.0);
|
||||
for file in ["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}: {:?}", report.errors);
|
||||
}
|
||||
|
||||
assert_eq!(s.eval::<i64>("return CURRENT_ACTIONBAR_PAGE").unwrap(), 1);
|
||||
assert_eq!(
|
||||
s.eval::<i64>("return ActionButton_GetPagedID(ActionButton1)")
|
||||
.unwrap(),
|
||||
1,
|
||||
"page 1 button 1 is action 1"
|
||||
);
|
||||
|
||||
// Page up walks to 2, so button 1 shows action 13.
|
||||
s.run("ActionBar_PageUp()").unwrap();
|
||||
assert_eq!(s.eval::<i64>("return CURRENT_ACTIONBAR_PAGE").unwrap(), 2);
|
||||
assert_eq!(
|
||||
s.eval::<i64>("return ActionButton_GetPagedID(ActionButton1)")
|
||||
.unwrap(),
|
||||
13
|
||||
);
|
||||
assert_eq!(
|
||||
s.eval::<i64>("return ActionButton_GetPagedID(ActionButton12)")
|
||||
.unwrap(),
|
||||
24
|
||||
);
|
||||
|
||||
// Down again, and below page 1 it rescans to the LAST viewable page.
|
||||
s.run("ActionBar_PageDown()").unwrap();
|
||||
assert_eq!(s.eval::<i64>("return CURRENT_ACTIONBAR_PAGE").unwrap(), 1);
|
||||
s.run("ActionBar_PageDown()").unwrap();
|
||||
assert_eq!(
|
||||
s.eval::<i64>("return CURRENT_ACTIONBAR_PAGE").unwrap(),
|
||||
4,
|
||||
"page-down off the bottom rescans for the last VIEWABLE page — 4, not 6, because the \
|
||||
always-on bottom multibars own pages 5 and 6"
|
||||
);
|
||||
assert_eq!(
|
||||
s.eval::<i64>("return ActionButton_GetPagedID(ActionButton1)")
|
||||
.unwrap(),
|
||||
37
|
||||
);
|
||||
// The pages the bottom bars already display are unreachable from the main bar, which is what
|
||||
// MultiActionBar_Update does on the reference the moment those bars are shown. Without this,
|
||||
// paging up lands on a duplicate of the twelve actions already on screen below.
|
||||
assert!(s
|
||||
.eval::<bool>(
|
||||
"return VIEWABLE_ACTION_BAR_PAGES[5] == nil and VIEWABLE_ACTION_BAR_PAGES[6] == nil"
|
||||
)
|
||||
.unwrap());
|
||||
s.run("CURRENT_ACTIONBAR_PAGE = 4 ActionBar_PageUp()")
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
s.eval::<i64>("return CURRENT_ACTIONBAR_PAGE").unwrap(),
|
||||
1,
|
||||
"walking up from the last viewable page skips 5 and 6 and wraps to the LITERAL 1 — the \
|
||||
reference's own asymmetry with page-down, which rescans instead"
|
||||
);
|
||||
|
||||
// A bonus page outranks the paged one entirely: with an offset up, the page is ignored.
|
||||
s.run("CURRENT_ACTIONBAR_PAGE = 3").unwrap();
|
||||
s.set_bonus_bar_offset(1);
|
||||
assert_eq!(
|
||||
s.eval::<i64>("return ActionButton_GetPagedID(ActionButton1)")
|
||||
.unwrap(),
|
||||
73,
|
||||
"bonus offset 1 is action 73, whatever page the main bar is on"
|
||||
);
|
||||
s.set_bonus_bar_offset(0);
|
||||
assert_eq!(
|
||||
s.eval::<i64>("return ActionButton_GetPagedID(ActionButton1)")
|
||||
.unwrap(),
|
||||
25,
|
||||
"and the page comes back when the form drops"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -415,3 +415,66 @@ fn the_list_reports_an_out_of_date_interface_like_the_char_select_twin() {
|
||||
"no `## Interface` at all is silent, not out of date"
|
||||
);
|
||||
}
|
||||
|
||||
/// `UIParentLoadAddOn(name)` — the on-demand loader 5 corpus addons name (FuBar_MCPFu,
|
||||
/// Fubar_RareTrackerFu, MCP, MikScrollingBattleText, SpecialTalent), and which raised as a nil
|
||||
/// global until every piece it needs was joined up in `BasicControls.xml`.
|
||||
///
|
||||
/// Two things are asserted, and the second is the one a reconstruction from the name would miss:
|
||||
/// it answers `LoadAddOn`'s own falsey result so a caller can branch, and `FailedAddOnLoad` shows
|
||||
/// the failure **once per addon name**, not once per attempt (ref `UIParent.lua` l.641-643).
|
||||
#[test]
|
||||
fn ui_parent_load_addon_reports_once_and_returns_the_load_result() {
|
||||
let s = UiScript::new().unwrap();
|
||||
let text = std::fs::read_to_string(
|
||||
std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("assets/ui/BasicControls.xml"),
|
||||
)
|
||||
.unwrap();
|
||||
let doc = benilla_ui::framexml::parse(&text).unwrap();
|
||||
let report = benilla_ui::loader::load(&s, &doc, &|_| None);
|
||||
assert!(
|
||||
report.errors.is_empty(),
|
||||
"loader errors: {:?}",
|
||||
report.errors
|
||||
);
|
||||
|
||||
// The two GlobalStrings this leans on. The real client has them from FrameXML's first file and
|
||||
// the corpus survey runs the real `GlobalStrings.lua`; a bare VM does not, so they are seated
|
||||
// here rather than left to make `format` raise on a nil.
|
||||
s.run(
|
||||
r#"
|
||||
ADDON_LOAD_FAILED = "Couldn't load %s: %s"
|
||||
ADDON_MISSING = "Missing"
|
||||
shown = {}
|
||||
message = function(t) table.insert(shown, t) end
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// A name no registry has: LoadAddOn answers (nil, "MISSING"), so this answers falsey.
|
||||
assert!(
|
||||
s.eval::<bool>(r#"return UIParentLoadAddOn("NoSuchAddonAnywhere") == nil"#)
|
||||
.unwrap(),
|
||||
"must hand back LoadAddOn's own falsey result for a caller to branch on"
|
||||
);
|
||||
assert_eq!(
|
||||
s.eval::<String>("return shown[1]").unwrap(),
|
||||
"Couldn't load NoSuchAddonAnywhere: Missing",
|
||||
"the reason word resolves through ADDON_..reason"
|
||||
);
|
||||
|
||||
// The latch: a retrying addon gets one modal, not one per attempt.
|
||||
for _ in 0..3 {
|
||||
s.run(r#"UIParentLoadAddOn("NoSuchAddonAnywhere")"#)
|
||||
.unwrap();
|
||||
}
|
||||
assert_eq!(
|
||||
s.eval::<i64>("return table.getn(shown)").unwrap(),
|
||||
1,
|
||||
"FailedAddOnLoad must suppress every repeat for the same name"
|
||||
);
|
||||
|
||||
// A DIFFERENT name is a different latch entry, so it still reports.
|
||||
s.run(r#"UIParentLoadAddOn("AlsoNotThere")"#).unwrap();
|
||||
assert_eq!(s.eval::<i64>("return table.getn(shown)").unwrap(), 2);
|
||||
}
|
||||
|
||||
@@ -664,7 +664,15 @@ fn creature_type_word(t: u32) -> Option<&'static str> {
|
||||
7 => "Humanoid",
|
||||
8 => "Critter",
|
||||
9 => "Mechanical",
|
||||
_ => return None, // 10 "Not specified" shows nothing
|
||||
// The shipped `CreatureType.dbc` is **1..11 dense**, not 1..9 — this table stopped two
|
||||
// rows early. 11 is reachable and wow-re's own nameplate filter tests for it
|
||||
// (`0x605570 == 0xb`).
|
||||
11 => "Totem",
|
||||
// 10 is "Not specified" in the DBC. Deliberately still None: this word is the tooltip's
|
||||
// level-line class slot and a literal "Not specified" there is noise. The cost is named
|
||||
// rather than hidden — `UnitCreatureType` shares this field, so it answers nil for a
|
||||
// type-10 unit where the reference answers the DBC word.
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -288,6 +288,17 @@ pub fn join_ref(base: &str, path: &str) -> String {
|
||||
out.join("/")
|
||||
}
|
||||
|
||||
/// The `.lua` test `0x6ede10` opens with: `strrchr(path, '.')` on the **whole resolved path**,
|
||||
/// then a **case-insensitive** compare against `".lua"` (`0x8710c8`, through `0x64a4c0`).
|
||||
///
|
||||
/// Deliberately the last dot anywhere in the path rather than the basename's, because that is what
|
||||
/// `strrchr` does — a directory carrying a dot and a file carrying none would take the same branch
|
||||
/// in the client as it does here.
|
||||
fn has_lua_suffix(path: &str) -> bool {
|
||||
path.rsplit_once('.')
|
||||
.is_some_and(|(_, ext)| ext.eq_ignore_ascii_case("lua"))
|
||||
}
|
||||
|
||||
/// The directory part of a provider path — `""` for a bare name.
|
||||
fn dir_of(path: &str) -> &str {
|
||||
match path.rfind('/') {
|
||||
@@ -414,6 +425,29 @@ impl Loader<'_> {
|
||||
for item in &doc.items {
|
||||
match item {
|
||||
TopLevel::Include(path) => self.do_include(path),
|
||||
// **A DELIBERATE DIVERGENCE, and a strict superset of the reference.**
|
||||
//
|
||||
// `<Script file=X>` does NOT share `<Include>`'s path law. `0x6ee070`-`0x6ee079`
|
||||
// tests X itself for a separator and, when it has one, uses X **verbatim** — no
|
||||
// `dirname(referrer)` prefix, and no `..` collapse either, since that arm never
|
||||
// enters `0x6ede10`. Only a *bare* name gets `dirname(referrer) + name`.
|
||||
//
|
||||
// A verbatim value cannot open on 1.12. Resolution runs through `0x647e60` against
|
||||
// a hash index whose key is each file's path **relative to the scan root**, and the
|
||||
// root is the process CWD (Storm's base-path global `0xc52418` has no writer
|
||||
// image-wide, so `0x646ebc` substitutes `"."`). The basename retry at `0x647ed3` is
|
||||
// real code but off — it needs a flag bit that `0x648be0(0)` clears at startup. So
|
||||
// `Libs\Ace\Ace.lua` misses every leg: full-string, base-prefixed and the MPQ
|
||||
// chain. There is no per-addon or per-document current directory anywhere in the
|
||||
// client. (wow-re `scratch/include-lua-dispatch.md` §4.1.)
|
||||
//
|
||||
// We resolve it against the including document's directory anyway. That converts a
|
||||
// guaranteed failure into a success and **cannot break anything that worked on the
|
||||
// reference**, because under 1.12 there is no install-root `Libs\` for the verbatim
|
||||
// value to have found. The corpus addons carrying the form are TBC-era anyway —
|
||||
// all three report `iface=20400` and ship LibStub/CallbackHandler-1.0, none of
|
||||
// which existed in 1.12 — so reproducing the failure would buy nothing and cost
|
||||
// `FuBar_AtlasFu`, which works today.
|
||||
TopLevel::Script(ScriptRef::File(path)) => {
|
||||
let joined = join_ref(self.base(), path);
|
||||
match (self.files)(&joined) {
|
||||
@@ -470,6 +504,33 @@ impl Loader<'_> {
|
||||
));
|
||||
return;
|
||||
};
|
||||
// **`<Include>` dispatches on the EXTENSION, and a `.lua` target is RUN, not parsed.**
|
||||
//
|
||||
// `0x6ede10` is not "the XML loader" — it is *load one file by path*, and its first act
|
||||
// after the `..` collapse is a case-insensitive `.lua` suffix test on the **resolved** path
|
||||
// (`0x6edee6`-`0x6edf0f`: `strrchr(path,'.')`, then a case-insensitive compare against
|
||||
// `".lua"` @`0x8710c8`). Extension, never a content sniff — no byte of the file has been
|
||||
// read at that point. A match runs the chunk (`0x6edf0f call 0x704bc0`, chunk name
|
||||
// `"@<resolved path>"` via `0x8716e0`); anything else is opened and parsed as XML.
|
||||
//
|
||||
// `<Include file=X>` is a **recursion into that same routine** (`0x6ee00d`), so it inherits
|
||||
// the dispatch for free: there is no `<Include>`-specific Lua code in the client at all,
|
||||
// and it is the same mechanism that makes a `.lua` line in a `.toc` work. Which is also the
|
||||
// internal evidence that this was ours and not the corpus's — our own `.toc` loader has
|
||||
// always split on extension (`ui_script::addons`, 1185), and only this arm never did.
|
||||
//
|
||||
// Three addons ship the form — FonzAppraiser (67 sites), AckisRecipeList (24), FonzSummon
|
||||
// (22): an authoring habit rather than a convention, but each lost its ENTIRE library set
|
||||
// here, because `embeds.xml` is nothing but these lines. (wow-re
|
||||
// `scratch/include-lua-dispatch.md`, §5-verified.)
|
||||
if has_lua_suffix(&joined) {
|
||||
if let Err(e) = self.run(crate::source::chunk(&bytes), &joined) {
|
||||
self.report
|
||||
.errors
|
||||
.push(format!("<Include file=\"{path}\">: {e}"));
|
||||
}
|
||||
return;
|
||||
}
|
||||
// An included document is the one place text is forced: roxmltree parses `&str` (1193).
|
||||
match framexml::parse(&crate::source::decode(&bytes)) {
|
||||
Ok(sub) => {
|
||||
|
||||
@@ -247,6 +247,86 @@ mod loader_tests {
|
||||
assert_eq!(s.eval::<f32>("return PinChild:GetHeight()").unwrap(), 200.0);
|
||||
}
|
||||
|
||||
/// **`<Include file="X.lua">` RUNS the file** — the client's `<Include>` is a recursion into
|
||||
/// its load-one-file routine `0x6ede10`, whose first act is a case-insensitive `.lua` suffix
|
||||
/// test on the resolved path. It never sniffs content; it never parses a `.lua` as XML.
|
||||
///
|
||||
/// We parsed every target as FrameXML, so a `.lua` target died `unknown token at 1:1` and took
|
||||
/// the whole library set with it. Three corpus addons load nothing but these lines —
|
||||
/// FonzAppraiser, AckisRecipeList, FonzSummon — and `embeds.xml` is the whole of their Ace
|
||||
/// dependency chain.
|
||||
///
|
||||
/// The mixed document is the point: one `<Include>` of each kind, in one file, both landing.
|
||||
#[test]
|
||||
fn include_runs_a_lua_target_and_still_parses_an_xml_one() {
|
||||
let s = UiScript::new().unwrap();
|
||||
let provider = |path: &str| -> Option<Vec<u8>> {
|
||||
match path {
|
||||
"libs/Lib.lua" => Some(b"IncludedLua = 41 + 1".to_vec()),
|
||||
// Case-insensitive, exactly as `0x64a4c0` compares it.
|
||||
"libs/Shouty.LUA" => Some(b"ShoutyLua = true".to_vec()),
|
||||
"Sub.xml" => Some(br#"<Ui><Frame name="FromXmlInclude"/></Ui>"#.to_vec()),
|
||||
_ => None,
|
||||
}
|
||||
};
|
||||
let doc = parse(
|
||||
r#"<Ui>
|
||||
<Include file="libs\Lib.lua"/>
|
||||
<Include file="libs\Shouty.LUA"/>
|
||||
<Include file="Sub.xml"/>
|
||||
<Frame name="FromMain"/>
|
||||
</Ui>"#,
|
||||
);
|
||||
let report = load(&s, &doc, &provider);
|
||||
assert!(report.errors.is_empty(), "{:?}", report.errors);
|
||||
assert_eq!(
|
||||
s.eval::<i64>("return IncludedLua").unwrap(),
|
||||
42,
|
||||
"a .lua Include must be executed as a chunk, not parsed as a document"
|
||||
);
|
||||
assert!(
|
||||
s.eval::<bool>("return ShoutyLua").unwrap(),
|
||||
"case-insensitive suffix"
|
||||
);
|
||||
// ...and the XML arm is untouched: both frames still exist.
|
||||
assert_eq!(report.frames, 2);
|
||||
assert!(s
|
||||
.eval::<bool>("return FromXmlInclude ~= nil and FromMain ~= nil")
|
||||
.unwrap());
|
||||
}
|
||||
|
||||
/// A raise inside an `.lua` Include is reported and the enclosing document CONTINUES — the
|
||||
/// reference logs at severity 2 and falls through (`0x6ee00d` -> `0x6ee012` is unconditional,
|
||||
/// `eax` never read), so one bad library must not cost the frames declared after it.
|
||||
#[test]
|
||||
fn a_raising_lua_include_does_not_take_the_rest_of_the_document() {
|
||||
let s = UiScript::new().unwrap();
|
||||
let provider = |path: &str| -> Option<Vec<u8>> {
|
||||
match path {
|
||||
"Bad.lua" => Some(b"error('library exploded')".to_vec()),
|
||||
_ => None,
|
||||
}
|
||||
};
|
||||
let doc = parse(
|
||||
r#"<Ui>
|
||||
<Include file="Bad.lua"/>
|
||||
<Frame name="AfterTheBadInclude"/>
|
||||
</Ui>"#,
|
||||
);
|
||||
let report = load(&s, &doc, &provider);
|
||||
assert_eq!(
|
||||
report.errors.len(),
|
||||
1,
|
||||
"the raise is reported: {:?}",
|
||||
report.errors
|
||||
);
|
||||
assert!(report.errors[0].contains("Bad.lua"));
|
||||
assert!(
|
||||
s.eval::<bool>("return AfterTheBadInclude ~= nil").unwrap(),
|
||||
"the element after a failed Include must still be built"
|
||||
);
|
||||
}
|
||||
|
||||
/// `<Include>` resolved through an in-memory provider closure.
|
||||
#[test]
|
||||
fn include_resolves_through_provider() {
|
||||
|
||||
@@ -968,3 +968,57 @@ fn set_desaturated_reports_no_shader_support_and_does_not_raise() {
|
||||
s.run("DsTex:SetDesaturated(nil) DsTex:SetDesaturated(false)")
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
/// `UnitCreatureType(unit)` — `0x51a280`'s three-stage resolver, of which we model stages 2 and 3.
|
||||
///
|
||||
/// The load-bearing assertion is the **player** one. `0x605570` falls through the (never populated)
|
||||
/// creature record to a `ChrRaces.dbc` col-9 lookup that is **7 for all nine shipped races**, and
|
||||
/// `CreatureType[7]` is `"Humanoid"` — so a player answers a word, not nil. Reading only the
|
||||
/// creature record, which is what our data alone suggested, would have been wrong for every
|
||||
/// `UnitCreatureType("player")` call in the corpus.
|
||||
#[test]
|
||||
fn unit_creature_type_answers_the_record_then_falls_back_to_humanoid() {
|
||||
let mut s = script();
|
||||
s.set_unit(
|
||||
"target",
|
||||
Some(crate::script::UnitState {
|
||||
exists: true,
|
||||
creature_type_name: Some("Beast".into()),
|
||||
..Default::default()
|
||||
}),
|
||||
);
|
||||
s.set_unit(
|
||||
"player",
|
||||
Some(crate::script::UnitState {
|
||||
exists: true,
|
||||
is_player: true,
|
||||
..Default::default()
|
||||
}),
|
||||
);
|
||||
|
||||
// Stage 2: a creature's cached record wins.
|
||||
assert_eq!(
|
||||
s.eval::<String>(r#"return UnitCreatureType("target")"#)
|
||||
.unwrap(),
|
||||
"Beast"
|
||||
);
|
||||
// Stage 3: a player has no record and falls through to race -> Humanoid.
|
||||
assert_eq!(
|
||||
s.eval::<String>(r#"return UnitCreatureType("player")"#)
|
||||
.unwrap(),
|
||||
"Humanoid"
|
||||
);
|
||||
// An unresolved TOKEN is nil — not a raise.
|
||||
assert!(s
|
||||
.eval::<bool>(r#"return UnitCreatureType("party4") == nil"#)
|
||||
.unwrap());
|
||||
// A non-string ARGUMENT is a raise, which is a different failure from the nil above:
|
||||
// `lua_isstring` gates it and `luaL_error` longjmps, so a missing arg abandons the statement.
|
||||
let err = s
|
||||
.run("UnitCreatureType()")
|
||||
.expect_err("a missing arg must raise");
|
||||
assert!(
|
||||
format!("{err}").contains("Usage: UnitCreatureType"),
|
||||
"got {err}"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -477,6 +477,65 @@ pub(in crate::script) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
})?,
|
||||
)?;
|
||||
|
||||
// UnitCreatureType(unit) → **1 string, or nil** (`0x51a280`), the sibling of
|
||||
// `UnitCreatureFamily 0x51a310`. Adjacent addresses and the same shape — but a DIFFERENT
|
||||
// column: this reads `CreatureType.dbc` col `1 + locale`, the family reads col `8 + locale`.
|
||||
// "Adjacent so probably identical" was right about the shape and wrong about the map.
|
||||
//
|
||||
// **A three-stage resolver** (`0x605570`), with no class or typemask gate anywhere — its only
|
||||
// three tests are bounds checks:
|
||||
// 1. **shapeshift override** — `SpellShapeshiftForm.dbc` col 12, keyed by field 138
|
||||
// `UNIT_FIELD_BYTES_1` byte 2, taken only if signed `> 0` (`0x60559a jg`), so `0` **and
|
||||
// the `-1` sentinel** fall through;
|
||||
// 2. **the cached creature record** `[unit+0xb30]+0x18`, returned **unvalidated** — this is
|
||||
// what [`UnitState::creature_type_name`] carries;
|
||||
// 3. **race fallback** — `ChrRaces.dbc` col 9, keyed by field 36 `UNIT_FIELD_BYTES_0` byte 0.
|
||||
//
|
||||
// Stage 3 is why **a PLAYER answers `"Humanoid"`, not nil**: `ChrRaces` col 9 is **7 for all
|
||||
// nine shipped race rows** and `CreatureType[7]` is `"Humanoid"`, while `[player+0xb30]` is
|
||||
// never populated (§5-verified by a 4-hit writer census — the ctor zeroes it, two writers
|
||||
// early-out on `key==0`, and the third is hard-gated on `OBJECT_FIELD_TYPE == 0x9`, an
|
||||
// equality a player's `0x19` fails structurally). A creature-record-only reading answers nil
|
||||
// there, which would have been wrong for every `UnitCreatureType("player")` and every
|
||||
// `("target")` aimed at a player.
|
||||
//
|
||||
// **Stage 1 is NOT modelled, and it is the one divergence to know about.** A druid in Cat,
|
||||
// Bear, Dire Bear, Travel or Aquatic form — and a shaman in Ghost Wolf — answers `"Beast"` on
|
||||
// the reference and `"Humanoid"` here. Tree Form, Moonkin, Battle Stance, Shadowform and
|
||||
// Spirit of Redemption carry col 12 = `-1` and fall through to race anyway, so those already
|
||||
// agree. We hold the active form's spell id and name ([`super::super::ShapeshiftFormView`])
|
||||
// but **not the form INDEX the DBC is keyed by**, and matching on a localised form name would
|
||||
// be inventing a mechanism the client does not use.
|
||||
//
|
||||
// The argument gate is real, and differs from the two boolean siblings: arg1 goes through
|
||||
// `lua_isstring` (`0x6f3510`) and a miss calls `luaL_error 0x6f4940`, which **longjmps** — so a
|
||||
// non-string, including a MISSING argument, abandons the caller's statement. An unresolved
|
||||
// *token* is a different thing entirely and answers nil (`0x51a2b8`).
|
||||
//
|
||||
// Demand: 6 distinct corpus addons over 11 files.
|
||||
g.set(
|
||||
"UnitCreatureType",
|
||||
lua.create_function(|lua, token: Value| {
|
||||
let token = match &token {
|
||||
Value::String(s) => Some(s.to_str()?.to_string()),
|
||||
// `lua_isstring` coerces a number, so a numeric token is accepted and simply
|
||||
// resolves to nothing.
|
||||
Value::Number(_) | Value::Integer(_) => Some(String::new()),
|
||||
_ => return Err(mlua::Error::runtime("Usage: UnitCreatureType(\"unit\")")),
|
||||
};
|
||||
let word = with_unit(lua, &token, None, |u| {
|
||||
u.creature_type_name
|
||||
.clone()
|
||||
// Stage 3, collapsed: every player race maps to CreatureType 7.
|
||||
.or_else(|| u.is_player.then(|| "Humanoid".to_string()))
|
||||
});
|
||||
match word {
|
||||
Some(w) => Ok(Value::String(lua.create_string(&w)?)),
|
||||
None => Ok(Value::Nil),
|
||||
}
|
||||
})?,
|
||||
)?;
|
||||
|
||||
// UnitClassification(unit) → "normal" | "elite" | "rareelite" | "worldboss" | "rare" (decision
|
||||
// 0782, byte-verified `0x516d90`): a plain table index by the gated rank, and it answers a
|
||||
// STRING for every input — never nil. An absent snapshot deliberately reports "normal" rather
|
||||
|
||||
Reference in New Issue
Block a user