the action bars are the reference's own, and a gift can be wrapped
- the action-bar cluster is the reference's own: `MainMenuBar.xml`, `ActionBarFrame.xml`, `MultiActionBars.xml` and `BonusActionBarFrame.xml` run off the player's own chain, and our `ActionBar.xml` (2047 lines), `MultiBars.xml` and `StanceBar.xml` are deleted — three, since the stock bonus-bar file declares the shapeshift bar too; one engine verb, `ChangeActionBarPage`, fires `ACTIONBAR_PAGE_CHANGED` and nothing else; one load blocker: stock `MultiActionBars.lua` writes the options window's table from its OnLoad, the reference's own load-order hack, so our options window moves to the reference's toc slot; and ten things the reference does that ours did not: an empty button is HIDDEN while showgrid is 0, the bonus overlay sits one pixel right of the main buttons, every slide takes one frame more than its time, and `ActionButton_ShowGrid` ghosts EVERY ring, occupied included (1938) - a gift can be wrapped: right-clicking wrapping paper fell through to plain use and sent `CMSG_USE_ITEM`, so nothing happened — you could receive a gift and never give one; the wrap is a displayed-cursor MODE plus a stashed slot, never a payload, so `CursorHasItem()` stays false and every stock gate on it stays shut: right-click arms it, a left-click on a bag slot sends `CMSG_WRAP_ITEM`, and the client applies no eligibility test — the server's `ERR_CANT_WRAP_*` reasons are the error line (1934); a duration-limited item counts down: `SMSG_ITEM_TIME_UPDATE` parks a deadline and the tooltip's `ITEM_DURATION` line runs the enchant countdown's bucket ladder, so "Duration: 1 days" is what the reference renders; an update for an item we do not hold is DROPPED, no queue (1933); and a renamed crowd-control refusal names its mechanic — "Can't do that while stunned" — off `SpellMechanic.dbc` (1943) - a creature paths on a boat: `SMSG_MONSTER_MOVE_TRANSPORT` carries the transport's guid and deck-local coordinates, and is the attach/detach edge; a deck sample lands on the rider or nowhere, never as a world write a few yards from the map origin; and "there is no terrain under a boat" turned out wrong: the ground probe admits GameObject meshes — a boat's `.wmo` is a GameObject, so the probe hits the deck (1936); `MSG_MOVE_TIME_SKIPPED` is read, not dropped — the reference adds the lag to the observed unit's last-wire stamp, so dropping it scheduled their next packet `lag` ms late, a hitch for every player who stalled zoning in near us; and our own settle hold reports once (1935); and the tradeskill tool order was backwards — focus FIRST, so every two-tool recipe had read "Blacksmith Hammer, Anvil" for the reference's "Anvil, Blacksmith Hammer"; the focus's `hasTool` is 1.0 in the reference too (1942) - the crowd's second sitting, instruments first: the probe line carries the tail — cpu p95/p99 and the six worst frames — because a mean under budget hides the one frame in twenty over it (raid legs railed at 16.67 with p95 at 18–20 ms) and whether that frame was work or a wait is the first question about a stutter, and the visible meshes are counted by lane; then two cuts — a terrain tile furnishes as 4×4 cells of 4×4 chunks, one mesh, one entity, one draw per 133-yd cell instead of per 33-yd chunk (a city horizon is over a thousand chunks a frame, and a cell changes nothing a pixel can see; Stormwind 10.55 → 10.32 `cpu_ms`), and an authored point light is our own `WorldPointLight`, not Bevy's `PointLight`, which was only a data carrier while Bevy ran its whole light lane over every lamp, ~2 % of the alone frame; the sitting's record is 1945, and the code says so (1945) Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -85,6 +85,13 @@
|
||||
</Button>
|
||||
|
||||
<Script><![CDATA[
|
||||
-- Seat a micro button above the bar's art plate (was ActionBar.xml's; moved here with 1938 —
|
||||
-- this file is its one caller, and it goes when this window migrates).
|
||||
function BenillaActionBarArt_SeatAbove(frame)
|
||||
local art = getglobal("MainMenuBarArtFrame")
|
||||
if art then frame:SetFrameLevel(art:GetFrameLevel() + 1) end
|
||||
end
|
||||
|
||||
-- ref GlobalStrings.lua — the eight labels (enUS), in the `X = X or` FALLBACK form: the
|
||||
-- app runs the real localized GlobalStrings.lua off the patch chain BEFORE this file
|
||||
-- (ui_script/mod.rs load_global_strings), so the installed locale wins and these copies
|
||||
|
||||
@@ -1,532 +0,0 @@
|
||||
<Ui>
|
||||
<!--
|
||||
**The four extra action bars — MultiBarBottomLeft/BottomRight and the vertical
|
||||
MultiBarRight/MultiBarLeft — are PLAYER OPTIONS, and all four ship OFF (decision 1500,
|
||||
which reverses 0270 and the half of 1219 that made "hidden" unconditional).**
|
||||
|
||||
0270 made the two bottom bars unconditionally on and the toggles unshippable; 1219 added
|
||||
the two vertical ones as unconditionally hidden frames for the addons that index them. Both
|
||||
were the director's call at the time, and 1500 is the director's call now: the four bars
|
||||
become rows on the Options window's Action Bars page, and a fresh character sees none of
|
||||
them.
|
||||
|
||||
THE STORE IS THE SERVER'S, not a CVar and not a saved variable — verified against the real
|
||||
1.12.1 binary by a wow-re §5 cross-check (three independent derivations, arbitrated;
|
||||
wow-5875-re commit 417c2d31, node system/ui/scratch/action-bar-toggles.md):
|
||||
- The four bits live in **PLAYER_FIELD_BYTES byte 2**, written with
|
||||
**CMSG_SET_ACTIONBAR_TOGGLES (opcode 703)**. Per-character and server-persisted, which is
|
||||
what makes "off by default" fall out for free: a fresh character's byte is 0.
|
||||
- `SetActionBarToggles` takes exactly **FOUR** arguments (`cmp esi,4` at 0x4e770e) and packs
|
||||
them as arg1→0x01, arg2→0x02, arg3→0x04, arg4→0x08. **A FIFTH argument is silently
|
||||
DROPPED by the binding** — it never reaches the byte or the wire — even though the
|
||||
reference's own options panel passes ALWAYS_SHOW_MULTIBARS as one. That is why the grid
|
||||
option below is a saved variable and the four bars are not: the split is a verified
|
||||
consequence of the binding's arity, not an authoring choice.
|
||||
- `GetActionBarToggles` returns exactly FOUR values, each the Lua number `1` or `nil` —
|
||||
never booleans, and four nils when not in world.
|
||||
- The client NEVER writes that field locally and NOTHING fires on a change to it. So the
|
||||
LIVE state is the four Lua globals below; `GetActionBarToggles()` is read ONCE, on
|
||||
PLAYER_ENTERING_WORLD (UIParent.xml), purely as their seed. Nothing re-reads it.
|
||||
|
||||
The bar↔bit mapping is a FrameXML-layer rule — the binary is bar-agnostic: 0x01 →
|
||||
MultiBarBottomLeft, 0x02 → MultiBarBottomRight, 0x04 → MultiBarRight, 0x08 → MultiBarLeft,
|
||||
and **MultiBarLeft shows only when bar 3 is ALSO on** (confirmed by reading the install's
|
||||
own MultiActionBars.lua l.73, `if ( SHOW_MULTI_ACTIONBAR_3 and SHOW_MULTI_ACTIONBAR_4 )`).
|
||||
That conjunction is not arbitrary: MultiBarLeft's TOPRIGHT hangs off MultiBarRight's
|
||||
TOPLEFT, so on its own it would be a column of buttons anchored to a bar that is not there.
|
||||
|
||||
Geometry + slot ranges QUOTED from the real 1.12 FrameXML (extracted from patch.MPQ):
|
||||
- ref-MultiActionBars.xml l.36-39/496-513: each bar a 500x38 frame; BottomLeft's
|
||||
BOTTOMLEFT anchors to ActionButton1's TOPLEFT +(0,17); BottomRight's LEFT anchors to
|
||||
BottomLeft's RIGHT +(10,0); 12 buttons chained BOTTOMLEFT then LEFT-of-prev-RIGHT +6.
|
||||
- ref-ActionButton.lua l.6-9/446-467 (ActionButton_GetPagedID): BottomLeft = page 6 ⇒
|
||||
actions 61..72; BottomRight = page 5 ⇒ actions 49..60. The bases below feed
|
||||
BenillaActionButton_Action (ActionBar.xml), which carries the ref's parent-name fork
|
||||
as per-button data.
|
||||
- Buttons reuse BenillaActionButtonTemplate + the whole ActionBar.xml handler set: same
|
||||
click/drag/keyless-use, same feedback layer (state/usable/cooldown/flash), fanned from
|
||||
MainMenuBar's bar-level events via the BENILLA_ACTION_BUTTONS roster each button
|
||||
joins at OnLoad.
|
||||
|
||||
Ref divergences (deliberate, mirroring ActionBar.xml's SCOPE notes):
|
||||
- MULTIACTIONBAR1/2BUTTON1-12 are BINDABLE since 1008 (the ref's own commands, registered
|
||||
in crate::bindings::commands with the ref's runOnUp pair below); they ship UNBOUND like
|
||||
the ref (no MULTIACTIONBAR* line in any of the install's bindings-cache.wtf files), so
|
||||
out of the box the hotkey corner still carries the ref's range dot — a bound slot wears
|
||||
its abbreviated label exactly like the main bar (ActionBar.xml's PaintHotkey law).
|
||||
- Empty wells hide (the ref's own default) and appear as drop-target rings only while the
|
||||
grid is asked for — a held cursor payload, or ALWAYS_SHOW_MULTIBARS below — unlike the
|
||||
main bar's always-visible wells (its art has sockets; floating rings over bare world
|
||||
would not read as a bar).
|
||||
- MULTIACTIONBAR3/4BUTTON (the vertical bars' commands) still stay OUT of
|
||||
`bindings::commands`. 1008's honest tree held while those bars could never be shown, and
|
||||
it holds differently now that they can: the keybind screen lists what the player can
|
||||
reach, and a bar behind an option is reachable. Registering them is a separate slice with
|
||||
its own default-bindings question; until then painting a hotkey from an unregistered
|
||||
command is a no-op (GetBindingKey answers nil) and the label is simply blank.
|
||||
-->
|
||||
<Script><![CDATA[
|
||||
-- ref ActionButton.lua:6-7 — BOTTOMLEFT_ACTIONBAR_PAGE = 6, BOTTOMRIGHT_ACTIONBAR_PAGE = 5;
|
||||
-- base = (page - 1) * 12.
|
||||
BENILLA_MULTIBAR_BOTTOMLEFT_BASE = 60
|
||||
BENILLA_MULTIBAR_BOTTOMRIGHT_BASE = 48
|
||||
-- ref ActionButton.lua:8-9 — LEFT_ACTIONBAR_PAGE = 4, RIGHT_ACTIONBAR_PAGE = 3.
|
||||
BENILLA_MULTIBAR_LEFT_BASE = 36
|
||||
BENILLA_MULTIBAR_RIGHT_BASE = 24
|
||||
|
||||
-- ref MultiActionBars.lua:1 — every extra bar is twelve wells wide.
|
||||
NUM_MULTIBAR_BUTTONS = 12
|
||||
|
||||
-- ── The four toggles: LIVE state, seeded once from the server byte ──────────────────────
|
||||
-- These names are the REFERENCE's and they are a published contract, not a private cache —
|
||||
-- the addon corpus reads and writes them directly. Bartender2.lua:45-48 sets all four to 1
|
||||
-- at load (it takes the bars over), and CT_BarMod reads each one by name in three separate
|
||||
-- places to decide whether a button of ours is its business (CT_BarMod.lua:232-235,
|
||||
-- 1573-1576, 1624-1627). So the names are fixed and the VALUES have to be what the corpus
|
||||
-- expects: 1 or nil, exactly what GetActionBarToggles returns, never true/false.
|
||||
--
|
||||
-- nil here = OFF, which is also the whole of "off by default": a fresh character's server
|
||||
-- byte is 0, and the seed on PLAYER_ENTERING_WORLD (UIParent.xml) writes these four back
|
||||
-- to nil. Declared explicitly anyway rather than left absent — an undeclared global reads
|
||||
-- nil too, but then nothing in this file says the state exists.
|
||||
SHOW_MULTI_ACTIONBAR_1 = nil
|
||||
SHOW_MULTI_ACTIONBAR_2 = nil
|
||||
SHOW_MULTI_ACTIONBAR_3 = nil
|
||||
SHOW_MULTI_ACTIONBAR_4 = nil
|
||||
|
||||
-- The FIFTH switch, and the one with a different store — see the header: the engine
|
||||
-- binding drops a fifth argument, so this one has no server side and is an ordinary
|
||||
-- saved-variable global, exactly as the reference makes it (UIOptionsFrame.lua l.106-107
|
||||
-- assigns "0" and RegisterForSave's it). It holds the four bars' EMPTY wells open so the
|
||||
-- player can see the shape of a bar they have not filled yet. String "1"/"0", because that
|
||||
-- is what the options row's uvar store writes; zBar reads it as either
|
||||
-- (zActionButton.lua:61 tests both spellings) and so does the reader below.
|
||||
--
|
||||
-- This assignment is also the row's registered DEFAULT: OptionsGlobalRow_OnLoad captures
|
||||
-- the file-scope value at load, so this line is the single source of it.
|
||||
ALWAYS_SHOW_MULTIBARS = "0"
|
||||
|
||||
-- The four bars in toggle (and bit) order, each paired with the main-bar page it takes OUT
|
||||
-- of the paging cycle while it is up — 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
|
||||
-- beside it. ActionBar.xml declares VIEWABLE_ACTION_BAR_PAGES with all six viewable (all
|
||||
-- four bars start hidden) and this file does the arithmetic from there.
|
||||
BENILLA_MULTIBARS = {
|
||||
{frame = "MultiBarBottomLeft", page = BOTTOMLEFT_ACTIONBAR_PAGE},
|
||||
{frame = "MultiBarBottomRight", page = BOTTOMRIGHT_ACTIONBAR_PAGE},
|
||||
{frame = "MultiBarRight", page = RIGHT_ACTIONBAR_PAGE},
|
||||
{frame = "MultiBarLeft", page = LEFT_ACTIONBAR_PAGE},
|
||||
}
|
||||
|
||||
-- One bar: on screen or not, and the main-bar page it owns settled to match. Split out so
|
||||
-- MultiActionBar_Update below reads as the four-line list it actually is — which is where
|
||||
-- the one thing a reader must not miss lives (bar 4's compound condition).
|
||||
function BenillaMultiBar_Settle(bar, up)
|
||||
local frame = getglobal(bar.frame)
|
||||
if up then
|
||||
frame:Show()
|
||||
VIEWABLE_ACTION_BAR_PAGES[bar.page] = nil
|
||||
else
|
||||
frame:Hide()
|
||||
VIEWABLE_ACTION_BAR_PAGES[bar.page] = 1
|
||||
end
|
||||
end
|
||||
|
||||
-- Re-derive all four bars from the four globals. Every writer of that state calls this —
|
||||
-- the options rows, the PLAYER_ENTERING_WORLD seed, and any addon that sets a global and
|
||||
-- asks for the bars back (the reference's own contract, and Bartender2 relies on it).
|
||||
--
|
||||
-- **The manage pass runs from HERE, not from per-bar OnShow/OnHide**, and that is a choice
|
||||
-- worth naming because both work. A bar coming up or going down moves the cast bar, the
|
||||
-- chat frames, the stance/pet row and the bag stack's corner
|
||||
-- (UIPARENT_MANAGED_FRAME_POSITIONS), and the pass reads the bars' own :IsShown() rather
|
||||
-- than these globals — so it must run AFTER the last Show/Hide, not four times in the
|
||||
-- middle of them. One call at the end of the one function that moves them is that; four
|
||||
-- OnShow/OnHide hooks would fire the pass up to four times per click, and three of those
|
||||
-- times against a half-applied state. The reference reaches the same place from the other
|
||||
-- side: its options panel calls MultiActionBar_Update() and UIParent_ManageFramePositions()
|
||||
-- back to back at every one of its five click sites (UIOptionsFrame.xml l.665-666 et al).
|
||||
function MultiActionBar_Update()
|
||||
local bars = BENILLA_MULTIBARS
|
||||
BenillaMultiBar_Settle(bars[1], SHOW_MULTI_ACTIONBAR_1)
|
||||
BenillaMultiBar_Settle(bars[2], SHOW_MULTI_ACTIONBAR_2)
|
||||
BenillaMultiBar_Settle(bars[3], SHOW_MULTI_ACTIONBAR_3)
|
||||
-- Bar 4 is the one bar with a COMPOUND condition (ref MultiActionBars.lua l.73) — see
|
||||
-- the header for why MultiBarLeft cannot stand without MultiBarRight under it.
|
||||
BenillaMultiBar_Settle(bars[4], SHOW_MULTI_ACTIONBAR_3 and SHOW_MULTI_ACTIONBAR_4)
|
||||
UIParent_ManageFramePositions()
|
||||
end
|
||||
|
||||
-- ── The empty-well grid over the four bars ──────────────────────────────────────────────
|
||||
-- The three verbs are the reference's own published names and signatures, and the corpus
|
||||
-- is why they are worth shipping as such: CT_BarMod REPLACES MultiActionBar_UpdateGrid
|
||||
-- wholesale (CT_BarMod.lua:1649) and zBar WRAPS ShowAllGrids/HideAllGrids around its own
|
||||
-- bars (zActionButton.lua:83-94), both by name and both expecting this shape. The
|
||||
-- per-button half lives in ActionBar.xml (ActionButton_ShowGrid/_HideGrid over
|
||||
-- `button.showgrid`), which is where the reference keeps it too.
|
||||
function MultiActionBar_UpdateGrid(barName, show)
|
||||
for i = 1, NUM_MULTIBAR_BUTTONS do
|
||||
local button = getglobal(barName .. "Button" .. i)
|
||||
if show then
|
||||
ActionButton_ShowGrid(button)
|
||||
else
|
||||
ActionButton_HideGrid(button)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function MultiActionBar_ShowAllGrids()
|
||||
for _, bar in ipairs(BENILLA_MULTIBARS) do
|
||||
MultiActionBar_UpdateGrid(bar.frame, 1)
|
||||
end
|
||||
end
|
||||
|
||||
function MultiActionBar_HideAllGrids()
|
||||
for _, bar in ipairs(BENILLA_MULTIBARS) do
|
||||
MultiActionBar_UpdateGrid(bar.frame)
|
||||
end
|
||||
end
|
||||
|
||||
-- The option's apply: re-derive the grid from ALWAYS_SHOW_MULTIBARS. Run on every write of
|
||||
-- that global (the row's applyFunc) and once at startup over the restored saved value —
|
||||
-- the reference does the same restore by hand in its VARIABLES_LOADED ladder
|
||||
-- (UIOptionsFrame.lua l.217-220); ours is OptionsFrame_ApplySavedSettings' walk.
|
||||
--
|
||||
-- Both spellings tested because the global is published and the two stores disagree: our
|
||||
-- options row writes the STRING "1", while an addon setting it writes the number (zBar
|
||||
-- tests both for exactly this reason, zActionButton.lua:61).
|
||||
--
|
||||
-- The `held` latch is ours and it is what makes this option a WELL-BEHAVED holder of the
|
||||
-- count: one asker may hold at most one. Without it, re-applying an "off" that was already
|
||||
-- off decrements every button again — so applying defaults while a payload is in hand would
|
||||
-- close the wells that payload is holding open. The reference has exactly that flaw; the
|
||||
-- latch also means the startup apply of the default "0" is a no-op instead of a decrement.
|
||||
BENILLA_MULTIBAR_GRID_HELD = nil
|
||||
|
||||
function MultiActionBar_UpdateGridVisibility()
|
||||
local want
|
||||
if ALWAYS_SHOW_MULTIBARS == "1" or ALWAYS_SHOW_MULTIBARS == 1 then want = 1 end
|
||||
if want == BENILLA_MULTIBAR_GRID_HELD then return end
|
||||
BENILLA_MULTIBAR_GRID_HELD = want
|
||||
if want then
|
||||
MultiActionBar_ShowAllGrids()
|
||||
return
|
||||
end
|
||||
MultiActionBar_HideAllGrids()
|
||||
end
|
||||
|
||||
-- ── The write half of a bar toggle (the Options rows' setter) ───────────────────────────
|
||||
-- The reference has no counterpart: its panel edits SHOW_MULTI_ACTIONBAR_n inline in each
|
||||
-- checkbox's OnClick and batches ONE SetActionBarToggles into UIOptionsFrame_Save. Our
|
||||
-- window commits on the click (0961's grammar, no Save button), so the whole edit — the
|
||||
-- global, the bars, the packet — is this one function and the rows pass it a bar index.
|
||||
--
|
||||
-- `value` is the row's "1"/"0" STRING (OptionsRow_Set hands an API row its own string), and
|
||||
-- the number/boolean spellings are tolerated because this is reachable from addon code too.
|
||||
-- What is stored is 1 or nil and nothing else: that is what GetActionBarToggles returns and
|
||||
-- what the corpus expects to read back (see the globals' own comment above).
|
||||
--
|
||||
-- **FOUR arguments.** Not five. The reference passes ALWAYS_SHOW_MULTIBARS as a fifth and
|
||||
-- the binding drops it on the floor (`cmp esi,4` at 0x4e770e) — see the header. The whole
|
||||
-- byte is re-sent every time because the packet carries the byte, not a delta.
|
||||
function BenillaMultiBar_SetShown(bar, value)
|
||||
local on
|
||||
if value and value ~= "0" and value ~= 0 then on = 1 end
|
||||
setglobal("SHOW_MULTI_ACTIONBAR_" .. bar, on)
|
||||
MultiActionBar_Update()
|
||||
SetActionBarToggles(SHOW_MULTI_ACTIONBAR_1, SHOW_MULTI_ACTIONBAR_2,
|
||||
SHOW_MULTI_ACTIONBAR_3, SHOW_MULTI_ACTIONBAR_4)
|
||||
end
|
||||
|
||||
-- The MULTIACTIONBAR keybind pair — ref MultiActionBars.lua:24-45 MultiActionButtonDown/Up,
|
||||
-- the same NORMAL→PUSHED debounce dance as the main bar's ActionButtonDown/Up
|
||||
-- (ActionBar.xml — the ref's MacroFrame_SaveMacro hook is not modeled there either).
|
||||
-- Driven by the binding dispatch (crate::bindings, Kind::EdgeUpDown — decision 1008).
|
||||
--
|
||||
-- `bar` IS THE WHOLE FRAME-NAME PREFIX, not a suffix (decision 1199). The reference resolves
|
||||
-- `getglobal(bar.."Button"..id)` and its own Bindings.xml passes "MultiBarBottomLeft"; ours
|
||||
-- used to pass "BottomLeft" and splice "MultiBar" on in here. Same arity, different MEANING
|
||||
-- — the silent kind of wrong shape: zBar calls `MultiActionButtonDown("zBar"..barid, id)`
|
||||
-- and under the old splice built the nonexistent frame name "MultiBarzBar1Button1". The
|
||||
-- caller now names the bar in full, exactly as the reference's binding does, so a third-
|
||||
-- party bar can address these at all. `onSelf` rides to UseAction like ActionButtonUp's.
|
||||
function MultiActionButtonDown(bar, id)
|
||||
local button = getglobal(bar .. "Button" .. id)
|
||||
if ( button:GetButtonState() == "NORMAL" ) then
|
||||
button:SetButtonState("PUSHED")
|
||||
end
|
||||
end
|
||||
|
||||
function MultiActionButtonUp(bar, id, onSelf)
|
||||
local button = getglobal(bar .. "Button" .. id)
|
||||
if ( button:GetButtonState() == "PUSHED" ) then
|
||||
button:SetButtonState("NORMAL")
|
||||
UseAction(BenillaActionButton_Action(button), 0, onSelf)
|
||||
-- Ref MultiActionButtonUp:39-43: the checked ring reflects the use immediately.
|
||||
BenillaActionButton_UpdateState(button)
|
||||
end
|
||||
end
|
||||
]]></Script>
|
||||
|
||||
<!-- frameStrata HIGH on both bottom bars is the ref's own (`MultiActionBars.xml` l.36 and l.151,
|
||||
HorizontalMultiBar1/2). They share the bottom band with the bag windows, which are MEDIUM on
|
||||
both sides by design — and both sides carry deep nested button trees, so co-tenanting one
|
||||
stratum is exactly the level-outranks-insertion collision decision 0597 fixed for the party
|
||||
frame. HIGH is what keeps the bars above an open bag.
|
||||
|
||||
`hidden="true"` on ALL FOUR bars since 1500 — the reference's own posture for every one of
|
||||
them (its HorizontalMultiBar1/2 templates carry it too, l.36/l.151), and the shipped state
|
||||
a character with a zero toggle byte lands in. MultiActionBar_Update is the only thing that
|
||||
opens one. -->
|
||||
<Frame name="MultiBarBottomLeft" toplevel="true" parent="UIParent" frameStrata="HIGH" hidden="true">
|
||||
<Size><AbsDimension x="500" y="38"/></Size>
|
||||
<Anchors>
|
||||
<Anchor point="BOTTOMLEFT" relativeTo="ActionButton1" relativePoint="TOPLEFT"><Offset><AbsDimension x="0" y="17"/></Offset></Anchor>
|
||||
</Anchors>
|
||||
<Frames>
|
||||
<CheckButton name="MultiBarBottomLeftButton1" inherits="BenillaActionButtonTemplate">
|
||||
<Anchors><Anchor point="BOTTOMLEFT" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="0"/></Offset></Anchor></Anchors>
|
||||
<Scripts><OnLoad>BenillaActionButton_OnLoad(self, 1, BENILLA_MULTIBAR_BOTTOMLEFT_BASE, "MULTIACTIONBAR1BUTTON")</OnLoad></Scripts>
|
||||
</CheckButton>
|
||||
<CheckButton name="MultiBarBottomLeftButton2" inherits="BenillaActionButtonTemplate">
|
||||
<Anchors><Anchor point="LEFT" relativeTo="MultiBarBottomLeftButton1" relativePoint="RIGHT"><Offset><AbsDimension x="6" y="0"/></Offset></Anchor></Anchors>
|
||||
<Scripts><OnLoad>BenillaActionButton_OnLoad(self, 2, BENILLA_MULTIBAR_BOTTOMLEFT_BASE, "MULTIACTIONBAR1BUTTON")</OnLoad></Scripts>
|
||||
</CheckButton>
|
||||
<CheckButton name="MultiBarBottomLeftButton3" inherits="BenillaActionButtonTemplate">
|
||||
<Anchors><Anchor point="LEFT" relativeTo="MultiBarBottomLeftButton2" relativePoint="RIGHT"><Offset><AbsDimension x="6" y="0"/></Offset></Anchor></Anchors>
|
||||
<Scripts><OnLoad>BenillaActionButton_OnLoad(self, 3, BENILLA_MULTIBAR_BOTTOMLEFT_BASE, "MULTIACTIONBAR1BUTTON")</OnLoad></Scripts>
|
||||
</CheckButton>
|
||||
<CheckButton name="MultiBarBottomLeftButton4" inherits="BenillaActionButtonTemplate">
|
||||
<Anchors><Anchor point="LEFT" relativeTo="MultiBarBottomLeftButton3" relativePoint="RIGHT"><Offset><AbsDimension x="6" y="0"/></Offset></Anchor></Anchors>
|
||||
<Scripts><OnLoad>BenillaActionButton_OnLoad(self, 4, BENILLA_MULTIBAR_BOTTOMLEFT_BASE, "MULTIACTIONBAR1BUTTON")</OnLoad></Scripts>
|
||||
</CheckButton>
|
||||
<CheckButton name="MultiBarBottomLeftButton5" inherits="BenillaActionButtonTemplate">
|
||||
<Anchors><Anchor point="LEFT" relativeTo="MultiBarBottomLeftButton4" relativePoint="RIGHT"><Offset><AbsDimension x="6" y="0"/></Offset></Anchor></Anchors>
|
||||
<Scripts><OnLoad>BenillaActionButton_OnLoad(self, 5, BENILLA_MULTIBAR_BOTTOMLEFT_BASE, "MULTIACTIONBAR1BUTTON")</OnLoad></Scripts>
|
||||
</CheckButton>
|
||||
<CheckButton name="MultiBarBottomLeftButton6" inherits="BenillaActionButtonTemplate">
|
||||
<Anchors><Anchor point="LEFT" relativeTo="MultiBarBottomLeftButton5" relativePoint="RIGHT"><Offset><AbsDimension x="6" y="0"/></Offset></Anchor></Anchors>
|
||||
<Scripts><OnLoad>BenillaActionButton_OnLoad(self, 6, BENILLA_MULTIBAR_BOTTOMLEFT_BASE, "MULTIACTIONBAR1BUTTON")</OnLoad></Scripts>
|
||||
</CheckButton>
|
||||
<CheckButton name="MultiBarBottomLeftButton7" inherits="BenillaActionButtonTemplate">
|
||||
<Anchors><Anchor point="LEFT" relativeTo="MultiBarBottomLeftButton6" relativePoint="RIGHT"><Offset><AbsDimension x="6" y="0"/></Offset></Anchor></Anchors>
|
||||
<Scripts><OnLoad>BenillaActionButton_OnLoad(self, 7, BENILLA_MULTIBAR_BOTTOMLEFT_BASE, "MULTIACTIONBAR1BUTTON")</OnLoad></Scripts>
|
||||
</CheckButton>
|
||||
<CheckButton name="MultiBarBottomLeftButton8" inherits="BenillaActionButtonTemplate">
|
||||
<Anchors><Anchor point="LEFT" relativeTo="MultiBarBottomLeftButton7" relativePoint="RIGHT"><Offset><AbsDimension x="6" y="0"/></Offset></Anchor></Anchors>
|
||||
<Scripts><OnLoad>BenillaActionButton_OnLoad(self, 8, BENILLA_MULTIBAR_BOTTOMLEFT_BASE, "MULTIACTIONBAR1BUTTON")</OnLoad></Scripts>
|
||||
</CheckButton>
|
||||
<CheckButton name="MultiBarBottomLeftButton9" inherits="BenillaActionButtonTemplate">
|
||||
<Anchors><Anchor point="LEFT" relativeTo="MultiBarBottomLeftButton8" relativePoint="RIGHT"><Offset><AbsDimension x="6" y="0"/></Offset></Anchor></Anchors>
|
||||
<Scripts><OnLoad>BenillaActionButton_OnLoad(self, 9, BENILLA_MULTIBAR_BOTTOMLEFT_BASE, "MULTIACTIONBAR1BUTTON")</OnLoad></Scripts>
|
||||
</CheckButton>
|
||||
<CheckButton name="MultiBarBottomLeftButton10" inherits="BenillaActionButtonTemplate">
|
||||
<Anchors><Anchor point="LEFT" relativeTo="MultiBarBottomLeftButton9" relativePoint="RIGHT"><Offset><AbsDimension x="6" y="0"/></Offset></Anchor></Anchors>
|
||||
<Scripts><OnLoad>BenillaActionButton_OnLoad(self, 10, BENILLA_MULTIBAR_BOTTOMLEFT_BASE, "MULTIACTIONBAR1BUTTON")</OnLoad></Scripts>
|
||||
</CheckButton>
|
||||
<CheckButton name="MultiBarBottomLeftButton11" inherits="BenillaActionButtonTemplate">
|
||||
<Anchors><Anchor point="LEFT" relativeTo="MultiBarBottomLeftButton10" relativePoint="RIGHT"><Offset><AbsDimension x="6" y="0"/></Offset></Anchor></Anchors>
|
||||
<Scripts><OnLoad>BenillaActionButton_OnLoad(self, 11, BENILLA_MULTIBAR_BOTTOMLEFT_BASE, "MULTIACTIONBAR1BUTTON")</OnLoad></Scripts>
|
||||
</CheckButton>
|
||||
<CheckButton name="MultiBarBottomLeftButton12" inherits="BenillaActionButtonTemplate">
|
||||
<Anchors><Anchor point="LEFT" relativeTo="MultiBarBottomLeftButton11" relativePoint="RIGHT"><Offset><AbsDimension x="6" y="0"/></Offset></Anchor></Anchors>
|
||||
<Scripts><OnLoad>BenillaActionButton_OnLoad(self, 12, BENILLA_MULTIBAR_BOTTOMLEFT_BASE, "MULTIACTIONBAR1BUTTON")</OnLoad></Scripts>
|
||||
</CheckButton>
|
||||
</Frames>
|
||||
</Frame>
|
||||
|
||||
<!-- HIGH and hidden, same as BottomLeft above (ref `MultiActionBars.xml` l.151). -->
|
||||
<Frame name="MultiBarBottomRight" toplevel="true" parent="UIParent" frameStrata="HIGH" hidden="true">
|
||||
<Size><AbsDimension x="500" y="38"/></Size>
|
||||
<Anchors>
|
||||
<Anchor point="LEFT" relativeTo="MultiBarBottomLeft" relativePoint="RIGHT"><Offset><AbsDimension x="10" y="0"/></Offset></Anchor>
|
||||
</Anchors>
|
||||
<Frames>
|
||||
<CheckButton name="MultiBarBottomRightButton1" inherits="BenillaActionButtonTemplate">
|
||||
<Anchors><Anchor point="BOTTOMLEFT" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="0"/></Offset></Anchor></Anchors>
|
||||
<Scripts><OnLoad>BenillaActionButton_OnLoad(self, 1, BENILLA_MULTIBAR_BOTTOMRIGHT_BASE, "MULTIACTIONBAR2BUTTON")</OnLoad></Scripts>
|
||||
</CheckButton>
|
||||
<CheckButton name="MultiBarBottomRightButton2" inherits="BenillaActionButtonTemplate">
|
||||
<Anchors><Anchor point="LEFT" relativeTo="MultiBarBottomRightButton1" relativePoint="RIGHT"><Offset><AbsDimension x="6" y="0"/></Offset></Anchor></Anchors>
|
||||
<Scripts><OnLoad>BenillaActionButton_OnLoad(self, 2, BENILLA_MULTIBAR_BOTTOMRIGHT_BASE, "MULTIACTIONBAR2BUTTON")</OnLoad></Scripts>
|
||||
</CheckButton>
|
||||
<CheckButton name="MultiBarBottomRightButton3" inherits="BenillaActionButtonTemplate">
|
||||
<Anchors><Anchor point="LEFT" relativeTo="MultiBarBottomRightButton2" relativePoint="RIGHT"><Offset><AbsDimension x="6" y="0"/></Offset></Anchor></Anchors>
|
||||
<Scripts><OnLoad>BenillaActionButton_OnLoad(self, 3, BENILLA_MULTIBAR_BOTTOMRIGHT_BASE, "MULTIACTIONBAR2BUTTON")</OnLoad></Scripts>
|
||||
</CheckButton>
|
||||
<CheckButton name="MultiBarBottomRightButton4" inherits="BenillaActionButtonTemplate">
|
||||
<Anchors><Anchor point="LEFT" relativeTo="MultiBarBottomRightButton3" relativePoint="RIGHT"><Offset><AbsDimension x="6" y="0"/></Offset></Anchor></Anchors>
|
||||
<Scripts><OnLoad>BenillaActionButton_OnLoad(self, 4, BENILLA_MULTIBAR_BOTTOMRIGHT_BASE, "MULTIACTIONBAR2BUTTON")</OnLoad></Scripts>
|
||||
</CheckButton>
|
||||
<CheckButton name="MultiBarBottomRightButton5" inherits="BenillaActionButtonTemplate">
|
||||
<Anchors><Anchor point="LEFT" relativeTo="MultiBarBottomRightButton4" relativePoint="RIGHT"><Offset><AbsDimension x="6" y="0"/></Offset></Anchor></Anchors>
|
||||
<Scripts><OnLoad>BenillaActionButton_OnLoad(self, 5, BENILLA_MULTIBAR_BOTTOMRIGHT_BASE, "MULTIACTIONBAR2BUTTON")</OnLoad></Scripts>
|
||||
</CheckButton>
|
||||
<CheckButton name="MultiBarBottomRightButton6" inherits="BenillaActionButtonTemplate">
|
||||
<Anchors><Anchor point="LEFT" relativeTo="MultiBarBottomRightButton5" relativePoint="RIGHT"><Offset><AbsDimension x="6" y="0"/></Offset></Anchor></Anchors>
|
||||
<Scripts><OnLoad>BenillaActionButton_OnLoad(self, 6, BENILLA_MULTIBAR_BOTTOMRIGHT_BASE, "MULTIACTIONBAR2BUTTON")</OnLoad></Scripts>
|
||||
</CheckButton>
|
||||
<CheckButton name="MultiBarBottomRightButton7" inherits="BenillaActionButtonTemplate">
|
||||
<Anchors><Anchor point="LEFT" relativeTo="MultiBarBottomRightButton6" relativePoint="RIGHT"><Offset><AbsDimension x="6" y="0"/></Offset></Anchor></Anchors>
|
||||
<Scripts><OnLoad>BenillaActionButton_OnLoad(self, 7, BENILLA_MULTIBAR_BOTTOMRIGHT_BASE, "MULTIACTIONBAR2BUTTON")</OnLoad></Scripts>
|
||||
</CheckButton>
|
||||
<CheckButton name="MultiBarBottomRightButton8" inherits="BenillaActionButtonTemplate">
|
||||
<Anchors><Anchor point="LEFT" relativeTo="MultiBarBottomRightButton7" relativePoint="RIGHT"><Offset><AbsDimension x="6" y="0"/></Offset></Anchor></Anchors>
|
||||
<Scripts><OnLoad>BenillaActionButton_OnLoad(self, 8, BENILLA_MULTIBAR_BOTTOMRIGHT_BASE, "MULTIACTIONBAR2BUTTON")</OnLoad></Scripts>
|
||||
</CheckButton>
|
||||
<CheckButton name="MultiBarBottomRightButton9" inherits="BenillaActionButtonTemplate">
|
||||
<Anchors><Anchor point="LEFT" relativeTo="MultiBarBottomRightButton8" relativePoint="RIGHT"><Offset><AbsDimension x="6" y="0"/></Offset></Anchor></Anchors>
|
||||
<Scripts><OnLoad>BenillaActionButton_OnLoad(self, 9, BENILLA_MULTIBAR_BOTTOMRIGHT_BASE, "MULTIACTIONBAR2BUTTON")</OnLoad></Scripts>
|
||||
</CheckButton>
|
||||
<CheckButton name="MultiBarBottomRightButton10" inherits="BenillaActionButtonTemplate">
|
||||
<Anchors><Anchor point="LEFT" relativeTo="MultiBarBottomRightButton9" relativePoint="RIGHT"><Offset><AbsDimension x="6" y="0"/></Offset></Anchor></Anchors>
|
||||
<Scripts><OnLoad>BenillaActionButton_OnLoad(self, 10, BENILLA_MULTIBAR_BOTTOMRIGHT_BASE, "MULTIACTIONBAR2BUTTON")</OnLoad></Scripts>
|
||||
</CheckButton>
|
||||
<CheckButton name="MultiBarBottomRightButton11" inherits="BenillaActionButtonTemplate">
|
||||
<Anchors><Anchor point="LEFT" relativeTo="MultiBarBottomRightButton10" relativePoint="RIGHT"><Offset><AbsDimension x="6" y="0"/></Offset></Anchor></Anchors>
|
||||
<Scripts><OnLoad>BenillaActionButton_OnLoad(self, 11, BENILLA_MULTIBAR_BOTTOMRIGHT_BASE, "MULTIACTIONBAR2BUTTON")</OnLoad></Scripts>
|
||||
</CheckButton>
|
||||
<CheckButton name="MultiBarBottomRightButton12" inherits="BenillaActionButtonTemplate">
|
||||
<Anchors><Anchor point="LEFT" relativeTo="MultiBarBottomRightButton11" relativePoint="RIGHT"><Offset><AbsDimension x="6" y="0"/></Offset></Anchor></Anchors>
|
||||
<Scripts><OnLoad>BenillaActionButton_OnLoad(self, 12, BENILLA_MULTIBAR_BOTTOMRIGHT_BASE, "MULTIACTIONBAR2BUTTON")</OnLoad></Scripts>
|
||||
</CheckButton>
|
||||
</Frames>
|
||||
</Frame>
|
||||
|
||||
<!-- **The two VERTICAL bars — now the same kind of thing as the two above them.**
|
||||
|
||||
They were scoped out first (a bar nobody asked for should not exist), then 1219 built them
|
||||
as real frames that shipped unconditionally hidden — because the reference does not choose
|
||||
between "shown" and "absent": `MultiActionBars.xml` l.515/523 instantiates BOTH as real
|
||||
`parent="UIParent"` frames whose templates (VerticalMultiBar3/4, l.266/381) carry
|
||||
`hidden="true"`, and they stay hidden until SHOW_MULTI_ACTIONBAR_3/4 says otherwise.
|
||||
|
||||
What 1219 bought, and still buys, is that Atlas, Bartender2 and FuBartender2 stopped DYING
|
||||
at session start on `MultiBarLeft` — Atlas at `Atlas.lua:387`
|
||||
(`MultiBarLeft:SetFrameStrata("MEDIUM")`), Bartender2 at `Bartender2.lua:74`
|
||||
(`MultiBarLeft:ClearAllPoints()`). Neither shows the bar; both only need it to be there.
|
||||
Five more addons index `MultiBarRight` (decision 1218's corrected ranking).
|
||||
|
||||
What 1500 changed is only the last sentence of that story: "hidden, and nothing can show
|
||||
it" has become "hidden, until the player ticks its row" — SHOW_MULTI_ACTIONBAR_3/4 and the
|
||||
Options page's two Right ActionBar rows. `MultiBarLeft` still needs `MultiBarRight` under
|
||||
it (the header's conjunction), which is why its row is a dependent child of bar 3's.
|
||||
|
||||
Pages from ref-ActionButton.lua l.8-9: LEFT_ACTIONBAR_PAGE = 4 (actions 37..48),
|
||||
RIGHT_ACTIONBAR_PAGE = 3 (actions 25..36). Geometry from ref-MultiActionBars.xml: a 38x500
|
||||
frame, button 1 at TOPRIGHT, each next TOP-to-BOTTOM of the previous at -6. -->
|
||||
|
||||
<Frame name="MultiBarRight" toplevel="true" parent="UIParent" frameStrata="HIGH" hidden="true">
|
||||
<Size><AbsDimension x="38" y="500"/></Size>
|
||||
<Anchors>
|
||||
<Anchor point="BOTTOMRIGHT"><Offset><AbsDimension x="-7" y="98"/></Offset></Anchor>
|
||||
</Anchors>
|
||||
<Frames>
|
||||
<CheckButton name="MultiBarRightButton1" inherits="BenillaActionButtonTemplate">
|
||||
<Anchors><Anchor point="TOPRIGHT" relativePoint="TOPRIGHT"><Offset><AbsDimension x="0" y="0"/></Offset></Anchor></Anchors>
|
||||
<Scripts><OnLoad>BenillaActionButton_OnLoad(self, 1, BENILLA_MULTIBAR_RIGHT_BASE, "MULTIACTIONBAR3BUTTON")</OnLoad></Scripts>
|
||||
</CheckButton>
|
||||
<CheckButton name="MultiBarRightButton2" inherits="BenillaActionButtonTemplate">
|
||||
<Anchors><Anchor point="TOP" relativeTo="MultiBarRightButton1" relativePoint="BOTTOM"><Offset><AbsDimension x="0" y="-6"/></Offset></Anchor></Anchors>
|
||||
<Scripts><OnLoad>BenillaActionButton_OnLoad(self, 2, BENILLA_MULTIBAR_RIGHT_BASE, "MULTIACTIONBAR3BUTTON")</OnLoad></Scripts>
|
||||
</CheckButton>
|
||||
<CheckButton name="MultiBarRightButton3" inherits="BenillaActionButtonTemplate">
|
||||
<Anchors><Anchor point="TOP" relativeTo="MultiBarRightButton2" relativePoint="BOTTOM"><Offset><AbsDimension x="0" y="-6"/></Offset></Anchor></Anchors>
|
||||
<Scripts><OnLoad>BenillaActionButton_OnLoad(self, 3, BENILLA_MULTIBAR_RIGHT_BASE, "MULTIACTIONBAR3BUTTON")</OnLoad></Scripts>
|
||||
</CheckButton>
|
||||
<CheckButton name="MultiBarRightButton4" inherits="BenillaActionButtonTemplate">
|
||||
<Anchors><Anchor point="TOP" relativeTo="MultiBarRightButton3" relativePoint="BOTTOM"><Offset><AbsDimension x="0" y="-6"/></Offset></Anchor></Anchors>
|
||||
<Scripts><OnLoad>BenillaActionButton_OnLoad(self, 4, BENILLA_MULTIBAR_RIGHT_BASE, "MULTIACTIONBAR3BUTTON")</OnLoad></Scripts>
|
||||
</CheckButton>
|
||||
<CheckButton name="MultiBarRightButton5" inherits="BenillaActionButtonTemplate">
|
||||
<Anchors><Anchor point="TOP" relativeTo="MultiBarRightButton4" relativePoint="BOTTOM"><Offset><AbsDimension x="0" y="-6"/></Offset></Anchor></Anchors>
|
||||
<Scripts><OnLoad>BenillaActionButton_OnLoad(self, 5, BENILLA_MULTIBAR_RIGHT_BASE, "MULTIACTIONBAR3BUTTON")</OnLoad></Scripts>
|
||||
</CheckButton>
|
||||
<CheckButton name="MultiBarRightButton6" inherits="BenillaActionButtonTemplate">
|
||||
<Anchors><Anchor point="TOP" relativeTo="MultiBarRightButton5" relativePoint="BOTTOM"><Offset><AbsDimension x="0" y="-6"/></Offset></Anchor></Anchors>
|
||||
<Scripts><OnLoad>BenillaActionButton_OnLoad(self, 6, BENILLA_MULTIBAR_RIGHT_BASE, "MULTIACTIONBAR3BUTTON")</OnLoad></Scripts>
|
||||
</CheckButton>
|
||||
<CheckButton name="MultiBarRightButton7" inherits="BenillaActionButtonTemplate">
|
||||
<Anchors><Anchor point="TOP" relativeTo="MultiBarRightButton6" relativePoint="BOTTOM"><Offset><AbsDimension x="0" y="-6"/></Offset></Anchor></Anchors>
|
||||
<Scripts><OnLoad>BenillaActionButton_OnLoad(self, 7, BENILLA_MULTIBAR_RIGHT_BASE, "MULTIACTIONBAR3BUTTON")</OnLoad></Scripts>
|
||||
</CheckButton>
|
||||
<CheckButton name="MultiBarRightButton8" inherits="BenillaActionButtonTemplate">
|
||||
<Anchors><Anchor point="TOP" relativeTo="MultiBarRightButton7" relativePoint="BOTTOM"><Offset><AbsDimension x="0" y="-6"/></Offset></Anchor></Anchors>
|
||||
<Scripts><OnLoad>BenillaActionButton_OnLoad(self, 8, BENILLA_MULTIBAR_RIGHT_BASE, "MULTIACTIONBAR3BUTTON")</OnLoad></Scripts>
|
||||
</CheckButton>
|
||||
<CheckButton name="MultiBarRightButton9" inherits="BenillaActionButtonTemplate">
|
||||
<Anchors><Anchor point="TOP" relativeTo="MultiBarRightButton8" relativePoint="BOTTOM"><Offset><AbsDimension x="0" y="-6"/></Offset></Anchor></Anchors>
|
||||
<Scripts><OnLoad>BenillaActionButton_OnLoad(self, 9, BENILLA_MULTIBAR_RIGHT_BASE, "MULTIACTIONBAR3BUTTON")</OnLoad></Scripts>
|
||||
</CheckButton>
|
||||
<CheckButton name="MultiBarRightButton10" inherits="BenillaActionButtonTemplate">
|
||||
<Anchors><Anchor point="TOP" relativeTo="MultiBarRightButton9" relativePoint="BOTTOM"><Offset><AbsDimension x="0" y="-6"/></Offset></Anchor></Anchors>
|
||||
<Scripts><OnLoad>BenillaActionButton_OnLoad(self, 10, BENILLA_MULTIBAR_RIGHT_BASE, "MULTIACTIONBAR3BUTTON")</OnLoad></Scripts>
|
||||
</CheckButton>
|
||||
<CheckButton name="MultiBarRightButton11" inherits="BenillaActionButtonTemplate">
|
||||
<Anchors><Anchor point="TOP" relativeTo="MultiBarRightButton10" relativePoint="BOTTOM"><Offset><AbsDimension x="0" y="-6"/></Offset></Anchor></Anchors>
|
||||
<Scripts><OnLoad>BenillaActionButton_OnLoad(self, 11, BENILLA_MULTIBAR_RIGHT_BASE, "MULTIACTIONBAR3BUTTON")</OnLoad></Scripts>
|
||||
</CheckButton>
|
||||
<CheckButton name="MultiBarRightButton12" inherits="BenillaActionButtonTemplate">
|
||||
<Anchors><Anchor point="TOP" relativeTo="MultiBarRightButton11" relativePoint="BOTTOM"><Offset><AbsDimension x="0" y="-6"/></Offset></Anchor></Anchors>
|
||||
<Scripts><OnLoad>BenillaActionButton_OnLoad(self, 12, BENILLA_MULTIBAR_RIGHT_BASE, "MULTIACTIONBAR3BUTTON")</OnLoad></Scripts>
|
||||
</CheckButton>
|
||||
</Frames>
|
||||
</Frame>
|
||||
|
||||
<Frame name="MultiBarLeft" toplevel="true" parent="UIParent" frameStrata="HIGH" hidden="true">
|
||||
<Size><AbsDimension x="38" y="500"/></Size>
|
||||
<Anchors>
|
||||
<Anchor point="TOPRIGHT" relativeTo="MultiBarRight" relativePoint="TOPLEFT"><Offset><AbsDimension x="-5" y="0"/></Offset></Anchor>
|
||||
</Anchors>
|
||||
<Frames>
|
||||
<CheckButton name="MultiBarLeftButton1" inherits="BenillaActionButtonTemplate">
|
||||
<Anchors><Anchor point="TOPRIGHT" relativePoint="TOPRIGHT"><Offset><AbsDimension x="0" y="0"/></Offset></Anchor></Anchors>
|
||||
<Scripts><OnLoad>BenillaActionButton_OnLoad(self, 1, BENILLA_MULTIBAR_LEFT_BASE, "MULTIACTIONBAR4BUTTON")</OnLoad></Scripts>
|
||||
</CheckButton>
|
||||
<CheckButton name="MultiBarLeftButton2" inherits="BenillaActionButtonTemplate">
|
||||
<Anchors><Anchor point="TOP" relativeTo="MultiBarLeftButton1" relativePoint="BOTTOM"><Offset><AbsDimension x="0" y="-6"/></Offset></Anchor></Anchors>
|
||||
<Scripts><OnLoad>BenillaActionButton_OnLoad(self, 2, BENILLA_MULTIBAR_LEFT_BASE, "MULTIACTIONBAR4BUTTON")</OnLoad></Scripts>
|
||||
</CheckButton>
|
||||
<CheckButton name="MultiBarLeftButton3" inherits="BenillaActionButtonTemplate">
|
||||
<Anchors><Anchor point="TOP" relativeTo="MultiBarLeftButton2" relativePoint="BOTTOM"><Offset><AbsDimension x="0" y="-6"/></Offset></Anchor></Anchors>
|
||||
<Scripts><OnLoad>BenillaActionButton_OnLoad(self, 3, BENILLA_MULTIBAR_LEFT_BASE, "MULTIACTIONBAR4BUTTON")</OnLoad></Scripts>
|
||||
</CheckButton>
|
||||
<CheckButton name="MultiBarLeftButton4" inherits="BenillaActionButtonTemplate">
|
||||
<Anchors><Anchor point="TOP" relativeTo="MultiBarLeftButton3" relativePoint="BOTTOM"><Offset><AbsDimension x="0" y="-6"/></Offset></Anchor></Anchors>
|
||||
<Scripts><OnLoad>BenillaActionButton_OnLoad(self, 4, BENILLA_MULTIBAR_LEFT_BASE, "MULTIACTIONBAR4BUTTON")</OnLoad></Scripts>
|
||||
</CheckButton>
|
||||
<CheckButton name="MultiBarLeftButton5" inherits="BenillaActionButtonTemplate">
|
||||
<Anchors><Anchor point="TOP" relativeTo="MultiBarLeftButton4" relativePoint="BOTTOM"><Offset><AbsDimension x="0" y="-6"/></Offset></Anchor></Anchors>
|
||||
<Scripts><OnLoad>BenillaActionButton_OnLoad(self, 5, BENILLA_MULTIBAR_LEFT_BASE, "MULTIACTIONBAR4BUTTON")</OnLoad></Scripts>
|
||||
</CheckButton>
|
||||
<CheckButton name="MultiBarLeftButton6" inherits="BenillaActionButtonTemplate">
|
||||
<Anchors><Anchor point="TOP" relativeTo="MultiBarLeftButton5" relativePoint="BOTTOM"><Offset><AbsDimension x="0" y="-6"/></Offset></Anchor></Anchors>
|
||||
<Scripts><OnLoad>BenillaActionButton_OnLoad(self, 6, BENILLA_MULTIBAR_LEFT_BASE, "MULTIACTIONBAR4BUTTON")</OnLoad></Scripts>
|
||||
</CheckButton>
|
||||
<CheckButton name="MultiBarLeftButton7" inherits="BenillaActionButtonTemplate">
|
||||
<Anchors><Anchor point="TOP" relativeTo="MultiBarLeftButton6" relativePoint="BOTTOM"><Offset><AbsDimension x="0" y="-6"/></Offset></Anchor></Anchors>
|
||||
<Scripts><OnLoad>BenillaActionButton_OnLoad(self, 7, BENILLA_MULTIBAR_LEFT_BASE, "MULTIACTIONBAR4BUTTON")</OnLoad></Scripts>
|
||||
</CheckButton>
|
||||
<CheckButton name="MultiBarLeftButton8" inherits="BenillaActionButtonTemplate">
|
||||
<Anchors><Anchor point="TOP" relativeTo="MultiBarLeftButton7" relativePoint="BOTTOM"><Offset><AbsDimension x="0" y="-6"/></Offset></Anchor></Anchors>
|
||||
<Scripts><OnLoad>BenillaActionButton_OnLoad(self, 8, BENILLA_MULTIBAR_LEFT_BASE, "MULTIACTIONBAR4BUTTON")</OnLoad></Scripts>
|
||||
</CheckButton>
|
||||
<CheckButton name="MultiBarLeftButton9" inherits="BenillaActionButtonTemplate">
|
||||
<Anchors><Anchor point="TOP" relativeTo="MultiBarLeftButton8" relativePoint="BOTTOM"><Offset><AbsDimension x="0" y="-6"/></Offset></Anchor></Anchors>
|
||||
<Scripts><OnLoad>BenillaActionButton_OnLoad(self, 9, BENILLA_MULTIBAR_LEFT_BASE, "MULTIACTIONBAR4BUTTON")</OnLoad></Scripts>
|
||||
</CheckButton>
|
||||
<CheckButton name="MultiBarLeftButton10" inherits="BenillaActionButtonTemplate">
|
||||
<Anchors><Anchor point="TOP" relativeTo="MultiBarLeftButton9" relativePoint="BOTTOM"><Offset><AbsDimension x="0" y="-6"/></Offset></Anchor></Anchors>
|
||||
<Scripts><OnLoad>BenillaActionButton_OnLoad(self, 10, BENILLA_MULTIBAR_LEFT_BASE, "MULTIACTIONBAR4BUTTON")</OnLoad></Scripts>
|
||||
</CheckButton>
|
||||
<CheckButton name="MultiBarLeftButton11" inherits="BenillaActionButtonTemplate">
|
||||
<Anchors><Anchor point="TOP" relativeTo="MultiBarLeftButton10" relativePoint="BOTTOM"><Offset><AbsDimension x="0" y="-6"/></Offset></Anchor></Anchors>
|
||||
<Scripts><OnLoad>BenillaActionButton_OnLoad(self, 11, BENILLA_MULTIBAR_LEFT_BASE, "MULTIACTIONBAR4BUTTON")</OnLoad></Scripts>
|
||||
</CheckButton>
|
||||
<CheckButton name="MultiBarLeftButton12" inherits="BenillaActionButtonTemplate">
|
||||
<Anchors><Anchor point="TOP" relativeTo="MultiBarLeftButton11" relativePoint="BOTTOM"><Offset><AbsDimension x="0" y="-6"/></Offset></Anchor></Anchors>
|
||||
<Scripts><OnLoad>BenillaActionButton_OnLoad(self, 12, BENILLA_MULTIBAR_LEFT_BASE, "MULTIACTIONBAR4BUTTON")</OnLoad></Scripts>
|
||||
</CheckButton>
|
||||
</Frames>
|
||||
</Frame>
|
||||
</Ui>
|
||||
@@ -190,6 +190,111 @@
|
||||
</Font>
|
||||
|
||||
<Script><![CDATA[
|
||||
-- ── The reference's own option table, verbatim (stock UIOptionsFrame.lua l.1-84; 1938) ──
|
||||
-- Stock MultiActionBars.lua:10 writes five rows of this table from MultiActionBarFrame_OnLoad
|
||||
-- ("Hack to get around load order dependencies": the reference's toc loads UIOptionsFrame.xml
|
||||
-- at l.21, before MultiActionBars.xml at l.39, so the MultiBar*_IsVisible funcs named at
|
||||
-- l.55-59 are still nil when the table is declared, exactly as they are here). Our rows
|
||||
-- below keep their own store (OptionsRow_Get's three kinds) and do not read this table; it
|
||||
-- is published because the reference's bar file indexes it at LOAD, and this file is the
|
||||
-- options window — the table's home — now seated at the reference's position in the
|
||||
-- manifest. It goes with the window when UIOptionsFrame.xml migrates.
|
||||
-- The two Action Bars uvars the reference's own options window inits in UIOptionsFrame_Init
|
||||
-- (stock UIOptionsFrame.lua l.102-107), at file scope here because the rows below capture the
|
||||
-- value at their OnLoad as the registered default. LOCK_ACTIONBAR is read by stock
|
||||
-- ActionBarFrame.xml's drag guards and PetActionBarFrame.lua:270/278 (ours PetActionBar.xml);
|
||||
-- ALWAYS_SHOW_MULTIBARS by stock MultiActionBar_UpdateGridVisibility. Both lived in the bar
|
||||
-- files until 1938 took those stock.
|
||||
LOCK_ACTIONBAR = "0"
|
||||
RegisterForSave("LOCK_ACTIONBAR")
|
||||
ALWAYS_SHOW_MULTIBARS = "0"
|
||||
RegisterForSave("ALWAYS_SHOW_MULTIBARS")
|
||||
|
||||
UIOptionsFrameCheckButtons = { };
|
||||
------------------- Basic Options -------------------
|
||||
-- Controls
|
||||
UIOptionsFrameCheckButtons["INVERT_MOUSE"] = { index = 1, cvar = "mouseInvertPitch" };
|
||||
|
||||
-- General
|
||||
UIOptionsFrameCheckButtons["CLICK_TO_MOVE"] = { index = 6, cvar = "autointeract" };
|
||||
UIOptionsFrameCheckButtons["GAMEFIELD_DESELECT_TEXT"] = { index = 45, cvar = "deselectOnClick"};
|
||||
UIOptionsFrameCheckButtons["ASSIST_ATTACK"] = { index = 3, cvar = "assistAttack" };
|
||||
UIOptionsFrameCheckButtons["CLEAR_AFK"] = { index = 16, cvar = "autoClearAFK" };
|
||||
UIOptionsFrameCheckButtons["BLOCK_TRADES"] = { index = 14, cvar = "BlockTrades" };
|
||||
UIOptionsFrameCheckButtons["AUTO_SELF_CAST_TEXT"] = { index = 61, cvar = "autoSelfCast"};
|
||||
|
||||
-- Display
|
||||
UIOptionsFrameCheckButtons["USE_UBERTOOLTIPS"] = { index = 12, cvar = "UberTooltips" };
|
||||
UIOptionsFrameCheckButtons["STATUS_BAR_TEXT"] = { index = 2, cvar = "statusBarText" };
|
||||
UIOptionsFrameCheckButtons["PROFANITY_FILTER"] = { index = 5, cvar = "profanityFilter" };
|
||||
UIOptionsFrameCheckButtons["SHOW_CLOAK"] = { index = 31, func = ShowingCloak, setFunc = ShowCloak};
|
||||
UIOptionsFrameCheckButtons["SHOW_HELM"] = { index = 20, func = ShowingHelm , setFunc = ShowHelm };
|
||||
UIOptionsFrameCheckButtons["SHOW_QUEST_FADING_TEXT"] = { index = 42, uvar = "QUEST_FADING_DISABLE", default = "0"};
|
||||
UIOptionsFrameCheckButtons["SHOW_BUFF_DURATION_TEXT"] = { index = 39, uvar = "SHOW_BUFF_DURATIONS", default = "0"};
|
||||
UIOptionsFrameCheckButtons["SHOW_PLAYER_NAMES"] = { index = 21, cvar = "UnitNamePlayer"};
|
||||
UIOptionsFrameCheckButtons["SHOW_GUILD_NAMES"] = { index = 22, cvar = "UnitNamePlayerGuild"};
|
||||
UIOptionsFrameCheckButtons["SHOW_PLAYER_TITLES"] = { index = 23, cvar = "UnitNamePlayerPVPTitle"};
|
||||
UIOptionsFrameCheckButtons["SHOW_NPC_NAMES"] = { index = 30, cvar = "UnitNameNPC"};
|
||||
UIOptionsFrameCheckButtons["SHOW_OWN_NAME"] = { index = 67, cvar = "UnitNameOwn"};
|
||||
UIOptionsFrameCheckButtons["SHOW_PARTY_BACKGROUND_TEXT"] = { index = 43, uvar = "SHOW_PARTY_BACKGROUND"};
|
||||
UIOptionsFrameCheckButtons["HIDE_OUTDOOR_WORLD_STATE_TEXT"] = { index = 62, uvar = "HIDE_OUTDOOR_WORLD_STATE", default = "0"};
|
||||
UIOptionsFrameCheckButtons["AUTO_QUEST_WATCH_TEXT"] = { index = 66, uvar = "AUTO_QUEST_WATCH", default = "1"};
|
||||
|
||||
-- Camera Controls
|
||||
UIOptionsFrameCheckButtons["FOLLOW_TERRAIN"] = { index = 24, cvar = "cameraTerrainTilt"};
|
||||
UIOptionsFrameCheckButtons["HEAD_BOB"] = { index = 26, cvar = "cameraBobbing"};
|
||||
UIOptionsFrameCheckButtons["WATER_COLLISION"] = { index = 27, cvar = "cameraWaterCollision"};
|
||||
UIOptionsFrameCheckButtons["SMART_PIVOT"] = { index = 15, cvar = "cameraPivot" };
|
||||
-- Help
|
||||
UIOptionsFrameCheckButtons["SHOW_TUTORIALS"] = { index = 28};
|
||||
UIOptionsFrameCheckButtons["SHOW_NEWBIE_TIPS_TEXT"] = { index = 29, uvar = "SHOW_NEWBIE_TIPS"};
|
||||
UIOptionsFrameCheckButtons["SHOW_TIPOFTHEDAY_TEXT"] = { index = 44, cvar = "showGameTips"};
|
||||
|
||||
------------------- Advanced Options -------------------
|
||||
-- Chat
|
||||
UIOptionsFrameCheckButtons["SIMPLE_CHAT_TEXT"] = { index = 7, uvar = "SIMPLE_CHAT" , default = "1"};
|
||||
UIOptionsFrameCheckButtons["CHAT_LOCKED_TEXT"] = { index = 8, uvar = "CHAT_LOCKED" , default = "1"};
|
||||
UIOptionsFrameCheckButtons["GUILDMEMBER_ALERT"] = { index = 13, cvar = "guildMemberNotify" };
|
||||
UIOptionsFrameCheckButtons["REMOVE_CHAT_DELAY_TEXT"] = { index = 18, uvar = "REMOVE_CHAT_DELAY", default = "0"};
|
||||
UIOptionsFrameCheckButtons["CHAT_BUBBLES_TEXT"] = { index = 37, cvar = "ChatBubbles"};
|
||||
UIOptionsFrameCheckButtons["PARTY_CHAT_BUBBLES_TEXT"] = { index = 38, cvar = "ChatBubblesParty"};
|
||||
UIOptionsFrameCheckButtons["SHOW_LOOT_SPAM"] = { index = 46, cvar = "showLootSpam"};
|
||||
UIOptionsFrameCheckButtons["AUTO_JOIN_GUILD_CHANNEL"] = { index = 51};
|
||||
UIOptionsFrameCheckButtons["DISABLE_SPAM_FILTER"] = { index = 68, cvar = "spamFilter"};
|
||||
|
||||
-- Action Bars
|
||||
UIOptionsFrameCheckButtons["LOCK_ACTIONBAR_TEXT"] = { index = 32, uvar = "LOCK_ACTIONBAR", default = "0"};
|
||||
UIOptionsFrameCheckButtons["SHOW_MULTIBAR1_TEXT"] = { index = 33, func = MultiBar1_IsVisible, setFunc = Multibar_EmptyFunc};
|
||||
UIOptionsFrameCheckButtons["SHOW_MULTIBAR2_TEXT"] = { index = 34, func = MultiBar2_IsVisible, setFunc = Multibar_EmptyFunc};
|
||||
UIOptionsFrameCheckButtons["SHOW_MULTIBAR3_TEXT"] = { index = 35, func = MultiBar3_IsVisible, setFunc = Multibar_EmptyFunc};
|
||||
UIOptionsFrameCheckButtons["SHOW_MULTIBAR4_TEXT"] = { index = 36, func = MultiBar4_IsVisible, setFunc = Multibar_EmptyFunc};
|
||||
UIOptionsFrameCheckButtons["ALWAYS_SHOW_MULTIBARS_TEXT"] = { index = 40, func = MultibarGrid_IsVisible, setFunc = Multibar_EmptyFunc};
|
||||
|
||||
-- Raid & Party Options
|
||||
UIOptionsFrameCheckButtons["HIDE_PARTY_INTERFACE_TEXT"] = { index = 47, uvar = "HIDE_PARTY_INTERFACE" , default = "0"};
|
||||
UIOptionsFrameCheckButtons["SHOW_DISPELLABLE_DEBUFFS_TEXT"] = { index = 48, uvar = "SHOW_DISPELLABLE_DEBUFFS" , default = "1"};
|
||||
UIOptionsFrameCheckButtons["SHOW_CASTABLE_BUFFS_TEXT"] = { index = 49, uvar = "SHOW_CASTABLE_BUFFS" , default = "1"};
|
||||
UIOptionsFrameCheckButtons["SHOW_TARGET_OF_TARGET_TEXT"] = { index = 50, uvar = "SHOW_TARGET_OF_TARGET" , default = "0"};
|
||||
UIOptionsFrameCheckButtons["SHOW_PARTY_PETS_TEXT"] = { index = 41, uvar = "SHOW_PARTY_PETS", default = "1"};
|
||||
|
||||
-- Combat Text
|
||||
UIOptionsFrameCheckButtons["SHOW_COMBAT_TEXT_TEXT"] = { index = 52, uvar = "SHOW_COMBAT_TEXT" , default = "0"};
|
||||
UIOptionsFrameCheckButtons["COMBAT_TEXT_SHOW_LOW_HEALTH_MANA_TEXT"] = { index = 53, uvar = "COMBAT_TEXT_SHOW_LOW_HEALTH_MANA" , default = "1"};
|
||||
UIOptionsFrameCheckButtons["COMBAT_TEXT_SHOW_AURAS_TEXT"] = { index = 54, uvar = "COMBAT_TEXT_SHOW_AURAS" , default = "1"};
|
||||
UIOptionsFrameCheckButtons["COMBAT_TEXT_SHOW_AURA_FADE_TEXT"] = { index = 55, uvar = "COMBAT_TEXT_SHOW_AURA_FADE" , default = "0"};
|
||||
UIOptionsFrameCheckButtons["COMBAT_TEXT_SHOW_COMBAT_STATE_TEXT"] = { index = 56, uvar = "COMBAT_TEXT_SHOW_COMBAT_STATE" , default = "1"};
|
||||
UIOptionsFrameCheckButtons["COMBAT_TEXT_SHOW_DODGE_PARRY_MISS_TEXT"] = { index = 57, uvar = "COMBAT_TEXT_SHOW_DODGE_PARRY_MISS", default = "0"};
|
||||
UIOptionsFrameCheckButtons["COMBAT_TEXT_SHOW_RESISTANCES_TEXT"] = { index = 58, uvar = "COMBAT_TEXT_SHOW_RESISTANCES", default = "0"};
|
||||
UIOptionsFrameCheckButtons["COMBAT_TEXT_SHOW_REPUTATION_TEXT"] = { index = 60, uvar = "COMBAT_TEXT_SHOW_REPUTATION", default = "0"};
|
||||
UIOptionsFrameCheckButtons["SHOW_DAMAGE_TEXT"] = { index = 19, cvar = "CombatDamage"};
|
||||
UIOptionsFrameCheckButtons["LOG_PERIODIC_EFFECTS"] = { index = 11, cvar = "CombatLogPeriodicSpells" };
|
||||
UIOptionsFrameCheckButtons["SHOW_PET_MELEE_DAMAGE"] = { index = 9, cvar = "PetMeleeDamage" };
|
||||
UIOptionsFrameCheckButtons["COMBAT_TEXT_SHOW_REACTIVES_TEXT"] = { index = 63, uvar = "COMBAT_TEXT_SHOW_REACTIVES", default = "0"};
|
||||
UIOptionsFrameCheckButtons["COMBAT_TEXT_SHOW_FRIENDLY_NAMES_TEXT"] = { index = 64, uvar = "COMBAT_TEXT_SHOW_FRIENDLY_NAMES", default = "0"};
|
||||
UIOptionsFrameCheckButtons["COMBAT_TEXT_SHOW_COMBO_POINTS_TEXT"] = { index = 65, uvar = "COMBAT_TEXT_SHOW_COMBO_POINTS", default = "0"};
|
||||
UIOptionsFrameCheckButtons["COMBAT_TEXT_SHOW_MANA_TEXT"] = { index = 59, uvar = "COMBAT_TEXT_SHOW_MANA", default = "0"};
|
||||
UIOptionsFrameCheckButtons["COMBAT_TEXT_SHOW_HONOR_GAINED_TEXT"] = { index = 69, uvar = "COMBAT_TEXT_SHOW_HONOR_GAINED", default = "1"};
|
||||
|
||||
-- ── SHOW_BUFF_DURATIONS: the buff bar's timer switch (decision 1139) ──────────────────
|
||||
--
|
||||
-- The reference declares this in `UIOptionsFrame.lua` (l.21/104), whose `default` is
|
||||
@@ -787,8 +892,8 @@
|
||||
-- four bar rows are `func` rows over the server's own PLAYER_FIELD_BYTES byte 2 (no
|
||||
-- local store exists to save), while Always Show ActionBars is a `uvar` row — the
|
||||
-- engine binding takes exactly four arguments and silently drops the fifth the
|
||||
-- reference passes it, so that switch has no server side at all. MultiBars.xml's
|
||||
-- header carries the byte-level evidence.
|
||||
-- reference passes it, so that switch has no server side at all. Decision 1500 and
|
||||
-- wow-re action-bar-toggles.md carry the byte-level evidence.
|
||||
ActionBars = {
|
||||
"RowMultiBar1", "RowMultiBar2", "RowMultiBar3", "RowMultiBar4",
|
||||
"RowAlwaysShowMultibars", "RowLockActionBar",
|
||||
@@ -864,7 +969,14 @@
|
||||
function OptionsRow_Set(row, value)
|
||||
if row.uvar then
|
||||
setglobal(row.uvar, value)
|
||||
if row.applyFunc then row.applyFunc() end
|
||||
-- `applyFunc` is a global's NAME: the rows are read at this file's load, which since
|
||||
-- 1938 sits at the reference's slot — before every file an apply lives in — so the
|
||||
-- function is looked up when the row is applied, not when it is declared. A name
|
||||
-- with no global behind it (a page harness without that definer) applies nothing,
|
||||
-- which is what a nil capture always did.
|
||||
local apply = row.applyFunc
|
||||
if type(apply) == "string" then apply = getglobal(apply) end
|
||||
if apply then apply() end
|
||||
return
|
||||
end
|
||||
-- An API row hands the setter the row's own "1"/"0" STRING, which is what 1.12 does
|
||||
@@ -925,14 +1037,27 @@
|
||||
-- no particular order (they are independent by construction — each one re-derives one
|
||||
-- feature's state from one global). A row whose defining file was not loaded has a nil
|
||||
-- applyFunc and is skipped, which is what keeps a partial test harness honest.
|
||||
-- The multibar grid switch's load arm — stock UIOptionsFrame.lua l.218-220 verbatim.
|
||||
function OptionsFrame_MultibarGrid_Load()
|
||||
if ( ALWAYS_SHOW_MULTIBARS == "1" or ALWAYS_SHOW_MULTIBARS == 1 ) then
|
||||
MultiActionBar_ShowAllGrids()
|
||||
end
|
||||
end
|
||||
|
||||
function OptionsFrame_ApplySavedSettings()
|
||||
local ran = {}
|
||||
for key, rows in pairs(OPTIONS_PAGE_ROWS) do
|
||||
for _, rkey in ipairs(rows) do
|
||||
local row = getglobal("OptionsFrameContainerBody" .. key .. rkey)
|
||||
local apply = row and row.applyFunc
|
||||
if apply and not ran[apply] then
|
||||
ran[apply] = 1
|
||||
-- `applyFunc` is a NAME (see OptionsPage_Apply): resolved here, when the
|
||||
-- definer has long since loaded; a name with nothing behind it is skipped. A
|
||||
-- row whose LOAD-time side effect differs from its click's names it as
|
||||
-- `loadFunc` (the multibar grid switch, below).
|
||||
local name = row and (row.loadFunc or row.applyFunc)
|
||||
local apply = name
|
||||
if type(name) == "string" then apply = getglobal(name) end
|
||||
if apply and not ran[name] then
|
||||
ran[name] = 1
|
||||
apply()
|
||||
end
|
||||
end
|
||||
@@ -1299,8 +1424,8 @@
|
||||
if key == "ActionBars" then
|
||||
-- 1.12's own rule for this pair, verbatim in effect (UIOptionsFrame.lua l.722-726):
|
||||
-- Show Right ActionBar 2 is dead while Show Right ActionBar is unticked, because
|
||||
-- MultiBarLeft does not appear without MultiBarRight under it (MultiBars.xml's
|
||||
-- conjunction). It is the only cross-row dependency on this page — the two bottom
|
||||
-- MultiBarLeft does not appear without MultiBarRight under it (stock
|
||||
-- MultiActionBar_Update's conjunction). It is the only cross-row dependency on this page — the two bottom
|
||||
-- bars, the grid switch and the lock are all independent.
|
||||
OptionsRow_SetEnabled(OptionsFrameContainerBodyActionBarsRowMultiBar4,
|
||||
SHOW_MULTI_ACTIONBAR_3)
|
||||
@@ -2639,7 +2764,7 @@
|
||||
<Scripts><OnLoad>
|
||||
OptionsGlobalRow_OnLoad(self, "SHOW_COMBAT_TEXT",
|
||||
"Enable Floating Combat Text", "OPTION_TOOLTIP_SHOW_COMBAT_TEXT")
|
||||
self.applyFunc = CombatText_UpdateDisplayedMessages
|
||||
self.applyFunc = "CombatText_UpdateDisplayedMessages" -- by NAME, resolved when applied: this file loads before its definer now (1938)
|
||||
</OnLoad></Scripts>
|
||||
</Frame>
|
||||
<!-- The scroll mode. 1.12's own dropdown, values and all
|
||||
@@ -2659,7 +2784,7 @@
|
||||
{ text = "Arc", value = "3" },
|
||||
})
|
||||
self.parentRow = "RowCombatText"
|
||||
self.applyFunc = CombatText_UpdateDisplayedMessages
|
||||
self.applyFunc = "CombatText_UpdateDisplayedMessages" -- by NAME, resolved when applied: this file loads before its definer now (1938)
|
||||
</OnLoad></Scripts>
|
||||
</Frame>
|
||||
<Frame name="$parentRowLowHealthMana" inherits="OptionsCheckboxRowTemplate">
|
||||
@@ -2672,7 +2797,7 @@
|
||||
"Show Low Mana and Health",
|
||||
"OPTION_TOOLTIP_COMBAT_TEXT_SHOW_LOW_HEALTH_MANA", 1)
|
||||
self.parentRow = "RowCombatText"
|
||||
self.applyFunc = CombatText_UpdateDisplayedMessages
|
||||
self.applyFunc = "CombatText_UpdateDisplayedMessages" -- by NAME, resolved when applied: this file loads before its definer now (1938)
|
||||
</OnLoad></Scripts>
|
||||
</Frame>
|
||||
<Frame name="$parentRowAuras" inherits="OptionsCheckboxRowTemplate">
|
||||
@@ -2684,7 +2809,7 @@
|
||||
OptionsGlobalRow_OnLoad(self, "COMBAT_TEXT_SHOW_AURAS",
|
||||
"Show Auras", "OPTION_TOOLTIP_COMBAT_TEXT_SHOW_AURAS", 1)
|
||||
self.parentRow = "RowCombatText"
|
||||
self.applyFunc = CombatText_UpdateDisplayedMessages
|
||||
self.applyFunc = "CombatText_UpdateDisplayedMessages" -- by NAME, resolved when applied: this file loads before its definer now (1938)
|
||||
</OnLoad></Scripts>
|
||||
</Frame>
|
||||
<Frame name="$parentRowAuraFade" inherits="OptionsCheckboxRowTemplate">
|
||||
@@ -2696,7 +2821,7 @@
|
||||
OptionsGlobalRow_OnLoad(self, "COMBAT_TEXT_SHOW_AURA_FADE",
|
||||
"Show Fading Auras", "OPTION_TOOLTIP_COMBAT_TEXT_SHOW_AURA_FADE", 1)
|
||||
self.parentRow = "RowCombatText"
|
||||
self.applyFunc = CombatText_UpdateDisplayedMessages
|
||||
self.applyFunc = "CombatText_UpdateDisplayedMessages" -- by NAME, resolved when applied: this file loads before its definer now (1938)
|
||||
</OnLoad></Scripts>
|
||||
</Frame>
|
||||
<Frame name="$parentRowCombatState" inherits="OptionsCheckboxRowTemplate">
|
||||
@@ -2709,7 +2834,7 @@
|
||||
"Show Combat State",
|
||||
"OPTION_TOOLTIP_COMBAT_TEXT_SHOW_COMBAT_STATE", 1)
|
||||
self.parentRow = "RowCombatText"
|
||||
self.applyFunc = CombatText_UpdateDisplayedMessages
|
||||
self.applyFunc = "CombatText_UpdateDisplayedMessages" -- by NAME, resolved when applied: this file loads before its definer now (1938)
|
||||
</OnLoad></Scripts>
|
||||
</Frame>
|
||||
<Frame name="$parentRowDodgeParryMiss" inherits="OptionsCheckboxRowTemplate">
|
||||
@@ -2722,7 +2847,7 @@
|
||||
"Show Dodge/Parries/Misses",
|
||||
"OPTION_TOOLTIP_COMBAT_TEXT_SHOW_DODGE_PARRY_MISS", 1)
|
||||
self.parentRow = "RowCombatText"
|
||||
self.applyFunc = CombatText_UpdateDisplayedMessages
|
||||
self.applyFunc = "CombatText_UpdateDisplayedMessages" -- by NAME, resolved when applied: this file loads before its definer now (1938)
|
||||
</OnLoad></Scripts>
|
||||
</Frame>
|
||||
<Frame name="$parentRowResistances" inherits="OptionsCheckboxRowTemplate">
|
||||
@@ -2735,7 +2860,7 @@
|
||||
"Show Damage Reduction",
|
||||
"OPTION_TOOLTIP_COMBAT_TEXT_SHOW_RESISTANCES", 1)
|
||||
self.parentRow = "RowCombatText"
|
||||
self.applyFunc = CombatText_UpdateDisplayedMessages
|
||||
self.applyFunc = "CombatText_UpdateDisplayedMessages" -- by NAME, resolved when applied: this file loads before its definer now (1938)
|
||||
</OnLoad></Scripts>
|
||||
</Frame>
|
||||
<Frame name="$parentRowReputation" inherits="OptionsCheckboxRowTemplate">
|
||||
@@ -2748,7 +2873,7 @@
|
||||
"Show Reputation",
|
||||
"OPTION_TOOLTIP_COMBAT_TEXT_SHOW_REPUTATION", 1)
|
||||
self.parentRow = "RowCombatText"
|
||||
self.applyFunc = CombatText_UpdateDisplayedMessages
|
||||
self.applyFunc = "CombatText_UpdateDisplayedMessages" -- by NAME, resolved when applied: this file loads before its definer now (1938)
|
||||
</OnLoad></Scripts>
|
||||
</Frame>
|
||||
<Frame name="$parentRowReactives" inherits="OptionsCheckboxRowTemplate">
|
||||
@@ -2761,7 +2886,7 @@
|
||||
"Show Reactive Spells and Abilities",
|
||||
"OPTION_TOOLTIP_COMBAT_TEXT_SHOW_REACTIVES", 1)
|
||||
self.parentRow = "RowCombatText"
|
||||
self.applyFunc = CombatText_UpdateDisplayedMessages
|
||||
self.applyFunc = "CombatText_UpdateDisplayedMessages" -- by NAME, resolved when applied: this file loads before its definer now (1938)
|
||||
</OnLoad></Scripts>
|
||||
</Frame>
|
||||
<Frame name="$parentRowFriendlyNames" inherits="OptionsCheckboxRowTemplate">
|
||||
@@ -2774,7 +2899,7 @@
|
||||
"Show Friendly Healer Names",
|
||||
"OPTION_TOOLTIP_COMBAT_TEXT_SHOW_FRIENDLY_NAMES", 1)
|
||||
self.parentRow = "RowCombatText"
|
||||
self.applyFunc = CombatText_UpdateDisplayedMessages
|
||||
self.applyFunc = "CombatText_UpdateDisplayedMessages" -- by NAME, resolved when applied: this file loads before its definer now (1938)
|
||||
</OnLoad></Scripts>
|
||||
</Frame>
|
||||
<!-- Combo Points carries the reference's SECOND gate: greyed
|
||||
@@ -2790,7 +2915,7 @@
|
||||
"Show Combo Points",
|
||||
"OPTION_TOOLTIP_COMBAT_TEXT_SHOW_COMBO_POINTS", 1)
|
||||
self.parentRow = "RowCombatText"
|
||||
self.applyFunc = CombatText_UpdateDisplayedMessages
|
||||
self.applyFunc = "CombatText_UpdateDisplayedMessages" -- by NAME, resolved when applied: this file loads before its definer now (1938)
|
||||
</OnLoad></Scripts>
|
||||
</Frame>
|
||||
<Frame name="$parentRowEnergyGains" inherits="OptionsCheckboxRowTemplate">
|
||||
@@ -2803,7 +2928,7 @@
|
||||
"Show Energy Gains",
|
||||
"OPTION_TOOLTIP_COMBAT_TEXT_SHOW_MANA", 1)
|
||||
self.parentRow = "RowCombatText"
|
||||
self.applyFunc = CombatText_UpdateDisplayedMessages
|
||||
self.applyFunc = "CombatText_UpdateDisplayedMessages" -- by NAME, resolved when applied: this file loads before its definer now (1938)
|
||||
</OnLoad></Scripts>
|
||||
</Frame>
|
||||
<Frame name="$parentRowHonorGained" inherits="OptionsCheckboxRowTemplate">
|
||||
@@ -2816,7 +2941,7 @@
|
||||
"Show Honor Gained",
|
||||
"OPTION_TOOLTIP_COMBAT_TEXT_SHOW_HONOR_GAINED", 1)
|
||||
self.parentRow = "RowCombatText"
|
||||
self.applyFunc = CombatText_UpdateDisplayedMessages
|
||||
self.applyFunc = "CombatText_UpdateDisplayedMessages" -- by NAME, resolved when applied: this file loads before its definer now (1938)
|
||||
</OnLoad></Scripts>
|
||||
</Frame>
|
||||
</Frames>
|
||||
@@ -2841,11 +2966,11 @@
|
||||
four arguments and silently DROPS the fifth that the reference
|
||||
passes it (`cmp esi,4` at 0x4e770e), so that switch has no
|
||||
server store to write and the reference RegisterForSave's it
|
||||
locally too (UIOptionsFrame.lua l.106-107). MultiBars.xml's
|
||||
header carries the whole byte-level finding.
|
||||
locally too (UIOptionsFrame.lua l.106-107). Decision 1500 and
|
||||
wow-re action-bar-toggles.md carry the whole byte-level finding.
|
||||
|
||||
The getters and setters are written here as closures rather
|
||||
than captured from MultiBars.xml by name, deliberately: a row's
|
||||
than captured from the bar file by name, deliberately: a row's
|
||||
OnLoad captures a function VALUE, so a named import would make
|
||||
this page depend on load ORDER — and a harness (or a manifest
|
||||
reordering) that loaded the window without the bars would give
|
||||
@@ -2874,7 +2999,15 @@
|
||||
<Scripts><OnLoad>
|
||||
OptionsFuncRow_OnLoad(self,
|
||||
function() return SHOW_MULTI_ACTIONBAR_1 end,
|
||||
function(value) BenillaMultiBar_SetShown(1, value) end,
|
||||
function(value)
|
||||
local on
|
||||
if value and value ~= "0" and value ~= 0 then on = 1 end
|
||||
SHOW_MULTI_ACTIONBAR_1 = on
|
||||
MultiActionBar_Update()
|
||||
SetActionBarToggles(SHOW_MULTI_ACTIONBAR_1, SHOW_MULTI_ACTIONBAR_2,
|
||||
SHOW_MULTI_ACTIONBAR_3, SHOW_MULTI_ACTIONBAR_4)
|
||||
UIParent_ManageFramePositions()
|
||||
end,
|
||||
"0", "Show Bottom Left ActionBar", "OPTION_TOOLTIP_SHOW_MULTIBAR1")
|
||||
</OnLoad></Scripts>
|
||||
</Frame>
|
||||
@@ -2886,7 +3019,15 @@
|
||||
<Scripts><OnLoad>
|
||||
OptionsFuncRow_OnLoad(self,
|
||||
function() return SHOW_MULTI_ACTIONBAR_2 end,
|
||||
function(value) BenillaMultiBar_SetShown(2, value) end,
|
||||
function(value)
|
||||
local on
|
||||
if value and value ~= "0" and value ~= 0 then on = 1 end
|
||||
SHOW_MULTI_ACTIONBAR_2 = on
|
||||
MultiActionBar_Update()
|
||||
SetActionBarToggles(SHOW_MULTI_ACTIONBAR_1, SHOW_MULTI_ACTIONBAR_2,
|
||||
SHOW_MULTI_ACTIONBAR_3, SHOW_MULTI_ACTIONBAR_4)
|
||||
UIParent_ManageFramePositions()
|
||||
end,
|
||||
"0", "Show Bottom Right ActionBar", "OPTION_TOOLTIP_SHOW_MULTIBAR2")
|
||||
</OnLoad></Scripts>
|
||||
</Frame>
|
||||
@@ -2898,7 +3039,15 @@
|
||||
<Scripts><OnLoad>
|
||||
OptionsFuncRow_OnLoad(self,
|
||||
function() return SHOW_MULTI_ACTIONBAR_3 end,
|
||||
function(value) BenillaMultiBar_SetShown(3, value) end,
|
||||
function(value)
|
||||
local on
|
||||
if value and value ~= "0" and value ~= 0 then on = 1 end
|
||||
SHOW_MULTI_ACTIONBAR_3 = on
|
||||
MultiActionBar_Update()
|
||||
SetActionBarToggles(SHOW_MULTI_ACTIONBAR_1, SHOW_MULTI_ACTIONBAR_2,
|
||||
SHOW_MULTI_ACTIONBAR_3, SHOW_MULTI_ACTIONBAR_4)
|
||||
UIParent_ManageFramePositions()
|
||||
end,
|
||||
"0", "Show Right ActionBar", "OPTION_TOOLTIP_SHOW_MULTIBAR3")
|
||||
</OnLoad></Scripts>
|
||||
</Frame>
|
||||
@@ -2919,7 +3068,15 @@
|
||||
<Scripts><OnLoad>
|
||||
OptionsFuncRow_OnLoad(self,
|
||||
function() return SHOW_MULTI_ACTIONBAR_4 end,
|
||||
function(value) BenillaMultiBar_SetShown(4, value) end,
|
||||
function(value)
|
||||
local on
|
||||
if value and value ~= "0" and value ~= 0 then on = 1 end
|
||||
SHOW_MULTI_ACTIONBAR_4 = on
|
||||
MultiActionBar_Update()
|
||||
SetActionBarToggles(SHOW_MULTI_ACTIONBAR_1, SHOW_MULTI_ACTIONBAR_2,
|
||||
SHOW_MULTI_ACTIONBAR_3, SHOW_MULTI_ACTIONBAR_4)
|
||||
UIParent_ManageFramePositions()
|
||||
end,
|
||||
"0", "Show Right ActionBar 2", "OPTION_TOOLTIP_SHOW_MULTIBAR4", 1)
|
||||
</OnLoad></Scripts>
|
||||
</Frame>
|
||||
@@ -2931,7 +3088,14 @@
|
||||
<Scripts><OnLoad>
|
||||
OptionsGlobalRow_OnLoad(self, "ALWAYS_SHOW_MULTIBARS",
|
||||
"Always Show ActionBars", "OPTION_TOOLTIP_ALWAYS_SHOW_MULTIBARS")
|
||||
self.applyFunc = MultiActionBar_UpdateGridVisibility
|
||||
-- Two arms, as the reference has them: a CLICK runs
|
||||
-- MultiActionBar_UpdateGridVisibility (UIOptionsFrame.xml:683);
|
||||
-- the LOAD arm only ever SHOWS (UIOptionsFrame.lua:218-220),
|
||||
-- because every hide is a counted decrement on each well
|
||||
-- (ActionButton_HideGrid) and a load-time hide against wells
|
||||
-- nobody opened would push the count negative.
|
||||
self.applyFunc = "MultiActionBar_UpdateGridVisibility"
|
||||
self.loadFunc = "OptionsFrame_MultibarGrid_Load"
|
||||
</OnLoad></Scripts>
|
||||
</Frame>
|
||||
<Frame name="$parentRowLockActionBar" inherits="OptionsCheckboxRowTemplate">
|
||||
@@ -3013,7 +3177,7 @@
|
||||
<Scripts><OnLoad>
|
||||
OptionsGlobalRow_OnLoad(self, "SHOW_TARGET_OF_TARGET",
|
||||
"Show Target of Target", "OPTION_TOOLTIP_SHOW_TARGET_OF_TARGET")
|
||||
self.applyFunc = TargetofTarget_Update
|
||||
self.applyFunc = "TargetofTarget_Update" -- by NAME, resolved when applied: this file loads before its definer now (1938)
|
||||
</OnLoad></Scripts>
|
||||
</Frame>
|
||||
<!-- The label is ours: the reference declares a Label
|
||||
@@ -3039,7 +3203,7 @@
|
||||
{ text = "Party or Raid", value = "4", tip = "OPTION_TOOLTIP_TARGETOFTARGET4" },
|
||||
{ text = "Always", value = "5", tip = "OPTION_TOOLTIP_TARGETOFTARGET5" },
|
||||
})
|
||||
self.applyFunc = TargetofTarget_Update
|
||||
self.applyFunc = "TargetofTarget_Update" -- by NAME, resolved when applied: this file loads before its definer now (1938)
|
||||
</OnLoad></Scripts>
|
||||
</Frame>
|
||||
<!-- The two equipment-display toggles (decision 1472,
|
||||
@@ -3094,7 +3258,7 @@
|
||||
<Scripts><OnLoad>
|
||||
OptionsGlobalRow_OnLoad(self, "SHOW_BUFF_DURATIONS",
|
||||
"Buff Durations", "OPTION_TOOLTIP_SHOW_BUFF_DURATION")
|
||||
self.applyFunc = BuffButtons_UpdatePositions
|
||||
self.applyFunc = "BuffButtons_UpdatePositions" -- by NAME, resolved when applied: this file loads before its definer now (1938)
|
||||
</OnLoad></Scripts>
|
||||
</Frame>
|
||||
<Frame name="$parentRowAutoQuestWatch" inherits="OptionsCheckboxRowTemplate">
|
||||
@@ -3180,7 +3344,7 @@
|
||||
OptionsGlobalRow_OnLoad(self, "REMOVE_CHAT_DELAY",
|
||||
"Remove Chat Hover Delay",
|
||||
"OPTION_TOOLTIP_REMOVE_CHAT_DELAY")
|
||||
self.applyFunc = ChatFrame_ApplyMouseOverDelay
|
||||
self.applyFunc = "ChatFrame_ApplyMouseOverDelay" -- by NAME, resolved when applied: this file loads before its definer now (1938)
|
||||
</OnLoad></Scripts>
|
||||
</Frame>
|
||||
<Frame name="$parentRowChatBubbles" inherits="OptionsCheckboxRowTemplate">
|
||||
|
||||
@@ -36,7 +36,8 @@
|
||||
- ONE refresh event. The app diffs the whole pushed bar and fires PET_BAR_UPDATE on any
|
||||
change, so the ref's PET_BAR_UPDATE / PET_BAR_UPDATE_COOLDOWN / UNIT_PET / the
|
||||
UNIT_FLAGS+UNIT_AURA pair it filters for arg1=="pet" collapse into one — the same
|
||||
collapse StanceBar.xml makes, for the same reason.
|
||||
collapse our stance bar made, for the same reason (the stock shapeshift bar,
|
||||
BonusActionBarFrame.xml since 1938, registers the reference's six).
|
||||
- The drag is ONE verb. PickupPetAction serves shift-click, OnDragStart and OnReceiveDrag
|
||||
alike (decision 1010) — that is the reference's own shape, not a collapse of ours: the
|
||||
binding itself forks on whether the cursor is already carrying a pet payload, so there is
|
||||
|
||||
@@ -1,374 +0,0 @@
|
||||
<Ui>
|
||||
<!--
|
||||
The stance/shapeshift bar (vanilla ShapeshiftBarFrame, ref-BonusActionBarFrame.xml/.lua —
|
||||
extracted from patch.MPQ). Warrior stances, druid forms, rogue Stealth: one CheckButton per
|
||||
known form, checked = the ACTIVE form (the descriptor's UNIT_FIELD_BYTES_1 byte-2 form,
|
||||
wow-re bonusbar-shapeshift.md), grey icon when not castable, cooldown swipe, click casts
|
||||
the form (`crate::ui_shapeshift` drains CastShapeshiftForm onto the wire).
|
||||
|
||||
Geometry QUOTED from the ref's ShapeshiftBar_UpdatePosition (BonusActionBarFrame.lua
|
||||
l.229-251), which forks on whether MultiBarBottomLeft is up: buttons 30x30, button1 at frame
|
||||
BOTTOMLEFT +(11,3), chained +7; NormalTexture UI-Quickslot2 at the RAISED mode's 50x50 (ref
|
||||
l.236-237), CENTER (0,-1) like the action template. Max 10 slots (NUM_SHAPESHIFT_SLOTS).
|
||||
|
||||
THE SEAT IS NOT THIS FILE'S — the XML anchor below (MainMenuBar TOPLEFT +(30,45)) is only
|
||||
the value the manage pass computes with the bottom-left bar up, written statically so the
|
||||
frame is somewhere sane before the first pass runs. `ShapeshiftBarFrame` is a managed row
|
||||
(UIParent.xml: baseY 0, bottomLeft 45, xOffset 30), so the pass re-derives +45 raised and
|
||||
+0 unraised — the ref's own two positions — and re-seats the frame on every bar change. That
|
||||
used to be a distinction without a difference (0270's bottom bars were always on, so raised
|
||||
was the only state); decision 1500 made all four extra bars player options, and the unraised
|
||||
state is now reachable. It is covered, and `multibar_stance_tests` pins both seats.
|
||||
|
||||
The ART half of that same fork IS this file's, and it is `ShapeshiftBar_UpdatePosition` in
|
||||
the script below: the ref's unraised branch also shows the ShapeshiftBarEnds/Middle shelf
|
||||
strips and grows each button's ring from 50x50 to 64x64. That branch was dead code while the
|
||||
bottom bars were always on, and 1500 made it the DEFAULT — every form class meets the
|
||||
unraised bar on a fresh character — so a shelf-less bar would be the shipped look rather than
|
||||
an edge case. It rides `UIParent_RegisterManagedPositionListener` (decision 1499's
|
||||
after-every-recompute hook, the same one the bag stack uses) so the art settles when the pass
|
||||
that decides raised-ness has finished, and it needs no edit to UIParent.xml.
|
||||
|
||||
Ref divergences (deliberate):
|
||||
- ONE refresh event: the app diffs the whole pushed form list (identity, active, castable,
|
||||
cooldown) and fires UPDATE_SHAPESHIFT_FORMS on any change — so the ref's separate
|
||||
Update/UpdateState split over six events (SPELL_UPDATE_COOLDOWN/_USABLE,
|
||||
PLAYER_AURAS_CHANGED, UPDATE_INVENTORY_ALERTS "Wha??", ACTIONBAR_PAGE_CHANGED) collapses
|
||||
into Update on UPDATE_SHAPESHIFT_FORMS + PLAYER_ENTERING_WORLD.
|
||||
- No lastSelected bookkeeping (ref ShapeshiftBar_ChangeForm) — checked state is always
|
||||
re-derived from the pushed `isActive`, never locally latched: the server's form byte is
|
||||
the truth and the app re-pushes on its every change. (Verified dead in the ref itself:
|
||||
`lastSelected` is written twice and read nowhere in the shipped FrameXML.)
|
||||
-->
|
||||
<Script><![CDATA[
|
||||
NUM_SHAPESHIFT_SLOTS = 10
|
||||
|
||||
-- RIGHT-CLICK IS DEAD HERE, and deliberately (decision 1030 reverting 1027). The ref's
|
||||
-- ShapeshiftButtonTemplate registers NOTHING: it inherits ActionButtonTemplate and then
|
||||
-- overrides <OnLoad> with a body that only scales the cooldown, so the
|
||||
-- `RegisterForClicks("LeftButtonUp","RightButtonUp")` that ActionButton_OnLoad gives every
|
||||
-- other action-style button (ref ActionButton.lua:109) is lost and the default
|
||||
-- {LeftButtonUp} stands. A right-press therefore neither flashes the pushed art
|
||||
-- (0x77924b gates that on the same mask) nor casts. It reads like a Blizzard oversight
|
||||
-- and it is the only bar that behaves this way — reproduced anyway, because the director
|
||||
-- checked it against the reference and called faithful.
|
||||
function BenillaShapeshiftButton_OnLoad(button, index)
|
||||
button.index = index
|
||||
button.icon = getglobal(button:GetName() .. "Icon")
|
||||
button.cooldown = getglobal(button:GetName() .. "Cooldown")
|
||||
end
|
||||
|
||||
-- The CheckButton widget toggles itself BEFORE OnClick fires (the widget contract); the
|
||||
-- ref immediately reverts that toggle (ShapeshiftButtonTemplate OnClick, ref-
|
||||
-- BonusActionBarFrame.xml l.31-38), so the checked ring never follows the click — only
|
||||
-- the isActive repaint. That is what keeps a warrior's ACTIVE stance lit through the
|
||||
-- click's silent wire no-op (stances never cancel — the 0x4b4963 guard), and a druid
|
||||
-- form/Stealth lit until the server actually confirms the shift-out. The revert is
|
||||
-- unconditional, which is why no click here can leave a lit form dark — unlike the pet
|
||||
-- bar's flat SetChecked(0), which needs a repaint to be correct.
|
||||
function BenillaShapeshiftButton_OnClick(button)
|
||||
if button:GetChecked() then
|
||||
button:SetChecked(nil)
|
||||
else
|
||||
button:SetChecked(1)
|
||||
end
|
||||
CastShapeshiftForm(button.index)
|
||||
end
|
||||
|
||||
-- Hover: the form's spell tooltip through the engine channel (ref ShapeshiftBar's
|
||||
-- GameTooltip:SetShapeshift; decision 0274 P2). Anchor: the ref's ShapeshiftButton
|
||||
-- OnEnter (BonusActionBarFrame.xml l.40-45) takes the default corner when UberTooltips
|
||||
-- is "1" — its byte-verified stock default — and sits beside the button otherwise. Like
|
||||
-- the action bar's, that fork was collapsed to the "1" leg until B230 registered the CVar
|
||||
-- (cvars.rs carries the byte-read); the stock look is unchanged, the off leg is reachable.
|
||||
function BenillaShapeshiftButton_OnEnter(button)
|
||||
if not GameTooltip then return end -- engine tests load file subsets
|
||||
if GetCVar("UberTooltips") == "0" then
|
||||
GameTooltip:SetOwner(button, "ANCHOR_RIGHT")
|
||||
else
|
||||
GameTooltip_SetDefaultAnchor(GameTooltip, button)
|
||||
end
|
||||
GameTooltip:SetShapeshift(button.index)
|
||||
end
|
||||
|
||||
function BenillaShapeshiftButton_OnLeave(button)
|
||||
GameTooltip:Hide()
|
||||
end
|
||||
|
||||
-- Ref ShapeshiftBar_Update + ShapeshiftBar_UpdateState fused (SCOPE note above): size the
|
||||
-- bar to the form count (hide the whole frame at 0 — a formless class never shows it),
|
||||
-- then paint each button — icon, checked (active), the 1.0/0.4 castable tint (ref l.210-214),
|
||||
-- cooldown swipe (CooldownFrame_SetTimer, Cooldown.xml's shared transcription).
|
||||
function ShapeshiftBar_Update()
|
||||
local numForms = GetNumShapeshiftForms()
|
||||
if numForms > 0 then
|
||||
-- ── The shelf's LENGTH is the form count (ref l.161-175) ─────────────────────
|
||||
-- The three strips are DECLARED as the >2-form chain (Left → Middle → Right), so
|
||||
-- the one- and two-form shelves have to re-point the end cap. Hiding the middle
|
||||
-- strip does NOT close its gap — a hidden region still resolves its rect, here
|
||||
-- and in the reference — so without this the 42x50 right cap sits 38px past the
|
||||
-- last button as a stray empty recess: a rogue's Stealth with a second, always-
|
||||
-- empty slot beside it. One form pulls the cap back INSIDE the left cap (+12, a
|
||||
-- 54px shelf around the single button); two butt the caps together; past two the
|
||||
-- middle strip carries one slot per extra form and the cap chains off it.
|
||||
if numForms == 1 then
|
||||
ShapeshiftBarRight:SetPoint("LEFT", ShapeshiftBarLeft, "LEFT", 12, 0)
|
||||
elseif numForms == 2 then
|
||||
ShapeshiftBarRight:SetPoint("LEFT", ShapeshiftBarLeft, "RIGHT", 0, 0)
|
||||
else
|
||||
ShapeshiftBarMiddle:SetPoint("LEFT", ShapeshiftBarLeft, "RIGHT", 0, 0)
|
||||
ShapeshiftBarMiddle:SetWidth(38 * (numForms - 2))
|
||||
ShapeshiftBarMiddle:SetTexCoord(0, numForms - 2, 0, 1)
|
||||
ShapeshiftBarRight:SetPoint("LEFT", ShapeshiftBarMiddle, "RIGHT", 0, 0)
|
||||
end
|
||||
ShapeshiftBarFrame:Show()
|
||||
else
|
||||
ShapeshiftBarFrame:Hide()
|
||||
end
|
||||
for i = 1, NUM_SHAPESHIFT_SLOTS do
|
||||
local button = getglobal("ShapeshiftButton" .. i)
|
||||
if i <= numForms then
|
||||
local texture, name, isActive, isCastable = GetShapeshiftFormInfo(i)
|
||||
button.icon:SetTexture(texture or BENILLA_FALLBACK_ICON)
|
||||
local start, duration, enable = GetShapeshiftFormCooldown(i)
|
||||
CooldownFrame_SetTimer(button.cooldown, start, duration, enable)
|
||||
if isActive then
|
||||
button:SetChecked(1)
|
||||
else
|
||||
button:SetChecked(0)
|
||||
end
|
||||
if isCastable then
|
||||
button.icon:SetVertexColor(1.0, 1.0, 1.0)
|
||||
else
|
||||
button.icon:SetVertexColor(0.4, 0.4, 0.4)
|
||||
end
|
||||
button:Show()
|
||||
else
|
||||
button:Hide()
|
||||
end
|
||||
end
|
||||
-- The art depends on the form COUNT too (the middle strip's `> 2`), so it is re-derived
|
||||
-- here as well as on every managed-position pass. Pure derivation, so running it twice
|
||||
-- costs nothing and missing it costs a strip.
|
||||
ShapeshiftBar_UpdatePosition()
|
||||
end
|
||||
|
||||
-- ── The raised/unraised ART fork (ref ShapeshiftBar_UpdatePosition, l.229-251) ──────────
|
||||
-- The reference's function does TWO things and we only owe it one. Its two SetPoints —
|
||||
-- MainMenuBar TOPLEFT +(30,45) with the bottom-left multibar up, +(30,0) without — are
|
||||
-- already `UIParent_ManageFramePositions`' job here: `ShapeshiftBarFrame` is a managed row
|
||||
-- (baseY 0, bottomLeft 45, xOffset 30), which IS those two positions, and a second
|
||||
-- SetPoint from this file would be a competing answer to a question already answered.
|
||||
-- What the pass does NOT carry is the art half, and that is this:
|
||||
--
|
||||
-- raised — the three shelf strips hidden, each button's ring 50x50. The shelf art IS
|
||||
-- the bar's border and only reads as one while the bar sits on the main bar;
|
||||
-- a row up it would draw across whatever is underneath (the same rule the
|
||||
-- pass applies to the pet bar's sliding strips).
|
||||
-- unraised — the two END-CAPS shown, the MIDDLE strip shown only past two forms, each
|
||||
-- button's ring 64x64.
|
||||
--
|
||||
-- The `> 2` on the middle strip is the ref's own and easy to miss: a two-form class (a
|
||||
-- priest with Shadowform, a warrior before Berserker) gets the end caps and no middle.
|
||||
--
|
||||
-- **This became load-bearing with 1500.** While the bottom bars were always on (0270) the
|
||||
-- raised branch was the only reachable state and this whole fork was dead code; now every
|
||||
-- form class sees the UNRAISED bar on a fresh character, so the shelf-less version would be
|
||||
-- the shipped default look rather than an edge case.
|
||||
--
|
||||
-- It keeps the reference's NAME because the corpus replaces that name wholesale:
|
||||
-- CT_BottomBar.lua:712/741 saves and overwrites `ShapeshiftBar_UpdatePosition` to take the
|
||||
-- bar over. The listener below therefore calls it through the GLOBAL, so an addon that
|
||||
-- replaces it really does take the art with it.
|
||||
function ShapeshiftBar_UpdatePosition()
|
||||
local raised = MultiBarBottomLeft and MultiBarBottomLeft:IsShown()
|
||||
local ring = 64
|
||||
if raised then
|
||||
ring = 50
|
||||
ShapeshiftBarLeft:Hide()
|
||||
ShapeshiftBarMiddle:Hide()
|
||||
ShapeshiftBarRight:Hide()
|
||||
else
|
||||
ShapeshiftBarLeft:Show()
|
||||
ShapeshiftBarRight:Show()
|
||||
if GetNumShapeshiftForms() > 2 then
|
||||
ShapeshiftBarMiddle:Show()
|
||||
else
|
||||
ShapeshiftBarMiddle:Hide()
|
||||
end
|
||||
end
|
||||
-- The ref sizes only the buttons a form occupies; ours sizes all ten. Same pixels (a
|
||||
-- hidden button draws nothing) and one less thing to get wrong — a button that appears
|
||||
-- later is already the right size, without this needing to run again for it.
|
||||
for i = 1, NUM_SHAPESHIFT_SLOTS do
|
||||
local ringTexture = getglobal("ShapeshiftButton" .. i .. "NormalTexture")
|
||||
ringTexture:SetWidth(ring)
|
||||
ringTexture:SetHeight(ring)
|
||||
end
|
||||
end
|
||||
|
||||
function ShapeshiftBar_OnLoad()
|
||||
local frame = this
|
||||
frame:RegisterEvent("PLAYER_ENTERING_WORLD")
|
||||
frame:RegisterEvent("UPDATE_SHAPESHIFT_FORMS")
|
||||
ShapeshiftBar_Update()
|
||||
end
|
||||
|
||||
function ShapeshiftBar_OnEvent()
|
||||
local frame = this
|
||||
ShapeshiftBar_Update()
|
||||
end
|
||||
|
||||
-- Re-derive the art after every managed-position recompute (decision 1499's hook — the same
|
||||
-- after-the-pass seam the bag stack registers on). The pass is what decides whether this bar
|
||||
-- is raised, so the art fork has to run when the pass has finished and not before. Called
|
||||
-- through the global name so an addon replacement is honoured (see the function's comment).
|
||||
-- Guarded for the harnesses that load this file without UIParent.xml, the way BagFrame.xml
|
||||
-- guards its own registration.
|
||||
if UIParent_RegisterManagedPositionListener then
|
||||
UIParent_RegisterManagedPositionListener(function() ShapeshiftBar_UpdatePosition() end)
|
||||
end
|
||||
]]></Script>
|
||||
|
||||
<!--
|
||||
One stance button (ref ShapeshiftButtonTemplate: 30x30, inherits ActionButtonTemplate's
|
||||
art set): icon fills (BACKGROUND); Cooldown child (the ref SetScale(.83)s the 36px
|
||||
template's — ours is sized 30x30 directly); the raised-mode 50x50 UI-Quickslot2 ring
|
||||
CENTER (0,-1); the action template's pushed/highlight/checked trio.
|
||||
-->
|
||||
<CheckButton name="BenillaShapeshiftButtonTemplate" virtual="true">
|
||||
<Size><AbsDimension x="30" y="30"/></Size>
|
||||
<Layers>
|
||||
<Layer level="BACKGROUND">
|
||||
<Texture name="$parentIcon"/>
|
||||
</Layer>
|
||||
</Layers>
|
||||
<Frames>
|
||||
<Cooldown name="$parentCooldown" hidden="true">
|
||||
<Size><AbsDimension x="30" y="30"/></Size>
|
||||
<Anchors>
|
||||
<Anchor point="CENTER" relativePoint="CENTER"><Offset><AbsDimension x="0" y="-1"/></Offset></Anchor>
|
||||
</Anchors>
|
||||
</Cooldown>
|
||||
</Frames>
|
||||
<NormalTexture name="$parentNormalTexture" file="Interface\Buttons\UI-Quickslot2">
|
||||
<Size><AbsDimension x="50" y="50"/></Size>
|
||||
<Anchors>
|
||||
<Anchor point="CENTER" relativePoint="CENTER"><Offset><AbsDimension x="0" y="-1"/></Offset></Anchor>
|
||||
</Anchors>
|
||||
</NormalTexture>
|
||||
<PushedTexture file="Interface\Buttons\UI-Quickslot-Depress"/>
|
||||
<HighlightTexture file="Interface\Buttons\ButtonHilight-Square" alphaMode="ADD"/>
|
||||
<CheckedTexture file="Interface\Buttons\CheckButtonHilight" alphaMode="ADD"/>
|
||||
<Scripts>
|
||||
<OnClick>BenillaShapeshiftButton_OnClick(self)</OnClick>
|
||||
<OnEnter>BenillaShapeshiftButton_OnEnter(self)</OnEnter>
|
||||
<OnLeave>BenillaShapeshiftButton_OnLeave(self)</OnLeave>
|
||||
</Scripts>
|
||||
</CheckButton>
|
||||
|
||||
<Frame name="ShapeshiftBarFrame" enableMouse="true" parent="MainMenuBar" hidden="true">
|
||||
<Size><AbsDimension x="29" y="32"/></Size>
|
||||
<Anchors>
|
||||
<Anchor point="BOTTOMLEFT" relativeTo="MainMenuBar" relativePoint="TOPLEFT"><Offset><AbsDimension x="30" y="45"/></Offset></Anchor>
|
||||
</Anchors>
|
||||
<!-- The shelf art (ref BonusActionBarFrame.xml l.220-249), declared HIDDEN.
|
||||
|
||||
The header above says this art "is not modeled at all" because raised mode hides it and
|
||||
we are permanently raised. That reasoning was right about the PIXELS and wrong about
|
||||
the NAMES: 1219/1223 settled that the reference does not choose between shown and
|
||||
absent, and "we don't show it" is not a reason for it not to exist. Four frames landed
|
||||
on that principle; these are the first REGIONS to, which is why the sweep that closed
|
||||
the class missed them — it ran over the frames/tables ranking, and a Texture is
|
||||
neither.
|
||||
|
||||
Two corpus addons index them, and both do pure LAYOUT — they do not use the art, they
|
||||
move it, exactly the shape 1223 found for BonusActionBarFrame:
|
||||
zBarEx/zMainBar.lua:288-290 SetParent(MainMenuBar) on all three
|
||||
CT_BottomBar.lua:681-692 ClearAllPoints + SetPoint off-screen
|
||||
Absent, `ShapeshiftBarLeft:ClearAllPoints()` is `attempt to index global` and takes the
|
||||
addon's whole handler with it.
|
||||
|
||||
Geometry, anchor chain and TexCoords quoted whole: Left and Right are two crops of ONE
|
||||
`ShapeshiftBarEnds` file (0..0.453125 and 0.453125..0.875), Middle its own, and Right
|
||||
hangs off Middle's RIGHT — the >2-form chain, which is the DECLARED state only. The
|
||||
cap is re-pointed per form count by `ShapeshiftBar_Update` above (the ref's l.161-175):
|
||||
hiding the middle strip leaves its 38px rect behind, so the declared chain alone
|
||||
strands the cap past the last button.
|
||||
|
||||
`hidden="true"` is the RAISED state, and it is only the pre-pass value now:
|
||||
`ShapeshiftBar_UpdatePosition` (above) shows and hides all three from the bottom-left
|
||||
multibar's state on every managed-position pass. Declared hidden rather than shown so a
|
||||
harness that never runs the pass draws nothing new. -->
|
||||
<Layers>
|
||||
<Layer level="OVERLAY">
|
||||
<Texture name="ShapeshiftBarLeft" file="Interface\ShapeshiftBar\ShapeshiftBarEnds" hidden="true">
|
||||
<Size><AbsDimension x="45" y="50"/></Size>
|
||||
<Anchors><Anchor point="BOTTOMLEFT"/></Anchors>
|
||||
<TexCoords left="0" right="0.453125" top="0" bottom="1"/>
|
||||
</Texture>
|
||||
<Texture name="ShapeshiftBarMiddle" file="Interface\ShapeshiftBar\ShapeshiftBarMiddle" hidden="true">
|
||||
<Size><AbsDimension x="38" y="50"/></Size>
|
||||
<Anchors><Anchor point="LEFT" relativeTo="ShapeshiftBarLeft" relativePoint="RIGHT"/></Anchors>
|
||||
<TexCoords left="0" right="1" top="0" bottom="1"/>
|
||||
</Texture>
|
||||
<Texture name="ShapeshiftBarRight" file="Interface\ShapeshiftBar\ShapeshiftBarEnds" hidden="true">
|
||||
<Size><AbsDimension x="42" y="50"/></Size>
|
||||
<Anchors><Anchor point="LEFT" relativeTo="ShapeshiftBarMiddle" relativePoint="RIGHT"/></Anchors>
|
||||
<TexCoords left="0.453125" right="0.875" top="0" bottom="1"/>
|
||||
</Texture>
|
||||
</Layer>
|
||||
</Layers>
|
||||
<Frames>
|
||||
<CheckButton name="ShapeshiftButton1" inherits="BenillaShapeshiftButtonTemplate" id="1">
|
||||
<Anchors><Anchor point="BOTTOMLEFT" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="11" y="3"/></Offset></Anchor></Anchors>
|
||||
<Scripts><OnLoad>BenillaShapeshiftButton_OnLoad(self, 1)</OnLoad></Scripts>
|
||||
</CheckButton>
|
||||
<CheckButton name="ShapeshiftButton2" inherits="BenillaShapeshiftButtonTemplate" id="2">
|
||||
<Anchors><Anchor point="LEFT" relativeTo="ShapeshiftButton1" relativePoint="RIGHT"><Offset><AbsDimension x="7" y="0"/></Offset></Anchor></Anchors>
|
||||
<Scripts><OnLoad>BenillaShapeshiftButton_OnLoad(self, 2)</OnLoad></Scripts>
|
||||
</CheckButton>
|
||||
<CheckButton name="ShapeshiftButton3" inherits="BenillaShapeshiftButtonTemplate" id="3">
|
||||
<Anchors><Anchor point="LEFT" relativeTo="ShapeshiftButton2" relativePoint="RIGHT"><Offset><AbsDimension x="7" y="0"/></Offset></Anchor></Anchors>
|
||||
<Scripts><OnLoad>BenillaShapeshiftButton_OnLoad(self, 3)</OnLoad></Scripts>
|
||||
</CheckButton>
|
||||
<CheckButton name="ShapeshiftButton4" inherits="BenillaShapeshiftButtonTemplate" id="4">
|
||||
<Anchors><Anchor point="LEFT" relativeTo="ShapeshiftButton3" relativePoint="RIGHT"><Offset><AbsDimension x="7" y="0"/></Offset></Anchor></Anchors>
|
||||
<Scripts><OnLoad>BenillaShapeshiftButton_OnLoad(self, 4)</OnLoad></Scripts>
|
||||
</CheckButton>
|
||||
<CheckButton name="ShapeshiftButton5" inherits="BenillaShapeshiftButtonTemplate" id="5">
|
||||
<Anchors><Anchor point="LEFT" relativeTo="ShapeshiftButton4" relativePoint="RIGHT"><Offset><AbsDimension x="7" y="0"/></Offset></Anchor></Anchors>
|
||||
<Scripts><OnLoad>BenillaShapeshiftButton_OnLoad(self, 5)</OnLoad></Scripts>
|
||||
</CheckButton>
|
||||
<CheckButton name="ShapeshiftButton6" inherits="BenillaShapeshiftButtonTemplate" id="6">
|
||||
<Anchors><Anchor point="LEFT" relativeTo="ShapeshiftButton5" relativePoint="RIGHT"><Offset><AbsDimension x="7" y="0"/></Offset></Anchor></Anchors>
|
||||
<Scripts><OnLoad>BenillaShapeshiftButton_OnLoad(self, 6)</OnLoad></Scripts>
|
||||
</CheckButton>
|
||||
<CheckButton name="ShapeshiftButton7" inherits="BenillaShapeshiftButtonTemplate" id="7">
|
||||
<Anchors><Anchor point="LEFT" relativeTo="ShapeshiftButton6" relativePoint="RIGHT"><Offset><AbsDimension x="7" y="0"/></Offset></Anchor></Anchors>
|
||||
<Scripts><OnLoad>BenillaShapeshiftButton_OnLoad(self, 7)</OnLoad></Scripts>
|
||||
</CheckButton>
|
||||
<CheckButton name="ShapeshiftButton8" inherits="BenillaShapeshiftButtonTemplate" id="8">
|
||||
<Anchors><Anchor point="LEFT" relativeTo="ShapeshiftButton7" relativePoint="RIGHT"><Offset><AbsDimension x="7" y="0"/></Offset></Anchor></Anchors>
|
||||
<Scripts><OnLoad>BenillaShapeshiftButton_OnLoad(self, 8)</OnLoad></Scripts>
|
||||
</CheckButton>
|
||||
<CheckButton name="ShapeshiftButton9" inherits="BenillaShapeshiftButtonTemplate" id="9">
|
||||
<Anchors><Anchor point="LEFT" relativeTo="ShapeshiftButton8" relativePoint="RIGHT"><Offset><AbsDimension x="7" y="0"/></Offset></Anchor></Anchors>
|
||||
<Scripts><OnLoad>BenillaShapeshiftButton_OnLoad(self, 9)</OnLoad></Scripts>
|
||||
</CheckButton>
|
||||
<CheckButton name="ShapeshiftButton10" inherits="BenillaShapeshiftButtonTemplate" id="10">
|
||||
<Anchors><Anchor point="LEFT" relativeTo="ShapeshiftButton9" relativePoint="RIGHT"><Offset><AbsDimension x="7" y="0"/></Offset></Anchor></Anchors>
|
||||
<Scripts><OnLoad>BenillaShapeshiftButton_OnLoad(self, 10)</OnLoad></Scripts>
|
||||
</CheckButton>
|
||||
</Frames>
|
||||
<Scripts>
|
||||
<OnLoad>ShapeshiftBar_OnLoad()</OnLoad>
|
||||
<OnEvent>ShapeshiftBar_OnEvent()</OnEvent>
|
||||
<!-- The stance bar is the pet/stance term of the managed bottom-stack positions
|
||||
(UIParent.xml, decision 0272): its show/hide re-fires the pass, exactly the
|
||||
ref's BonusActionBarFrame.xml:350-355. -->
|
||||
<OnShow>UIParent_ManageFramePositions()</OnShow>
|
||||
<OnHide>UIParent_ManageFramePositions()</OnHide>
|
||||
</Scripts>
|
||||
</Frame>
|
||||
</Ui>
|
||||
@@ -74,8 +74,8 @@
|
||||
XML anchor: every layout pass re-anchors it to a base offset plus per-bar deltas -
|
||||
either bottom multibar, the pet/stance bar, the reputation watch bar - fired from those
|
||||
bars' OnShow/OnHide (ref BonusActionBarFrame.xml:350-355 et al.) and applied once at
|
||||
startup. The loader's post-load call (ui_script/mod.rs) is our startup seam; the stance
|
||||
bar's OnShow/OnHide (StanceBar.xml) is the one live re-fire today.
|
||||
startup. The loader's post-load call (ui_script/mod.rs) is our startup seam; the pet
|
||||
bar's OnShow/OnHide and the options rows' setters (1938) are the live re-fires today.
|
||||
|
||||
Departures, each named:
|
||||
- Lua 5.1 dialect (the 1.12 client ran 5.0): pairs() iteration, table.insert for
|
||||
@@ -207,7 +207,7 @@
|
||||
-- `CMSG_SET_ACTIONBAR_TOGGLES` and waits for the field to come back (wow-re
|
||||
-- `system/ui/scratch/action-bar-toggles.md` §5: the setter's every store is into its own
|
||||
-- stack frame, and nothing in the binary fires on a change to that byte). So the live UI
|
||||
-- state is the four Lua globals `MultiBars.xml` owns, and `GetActionBarToggles()` is read
|
||||
-- state is the four Lua globals stock `MultiActionBars.lua` reads, and `GetActionBarToggles()` is read
|
||||
-- exactly ONCE, here, to seed them. Reading it again later would fight the optimistic
|
||||
-- value the options rows just set and lose a round trip's worth of clicks.
|
||||
--
|
||||
@@ -810,6 +810,12 @@
|
||||
-- while the stance bar is up starts 20px past the last stance button. That case is
|
||||
-- rare but real — a possessed or charmed unit gives a pet bar to a class that also
|
||||
-- has forms.
|
||||
-- Update shapeshift bar appearance — ref UIParent.lua l.1705-1732, the reference's own
|
||||
-- arm of this pass: with the bottom-left bar up the stance bar loses its shelf (end
|
||||
-- caps + middle strip) and its rings shrink to 50; otherwise the shelf shows (the
|
||||
-- middle only past two forms) and the rings are 64. Stock BonusActionBarFrame.lua
|
||||
-- carries a `ShapeshiftBar_UpdatePosition` with the same body that nothing calls; the
|
||||
-- pass IS the caller. (Our StanceBar.xml did this through a listener until 1938.)
|
||||
local bottomLeftUp = MultiBarBottomLeft and MultiBarBottomLeft:IsShown()
|
||||
if ( SlidingActionBarTexture0 and SlidingActionBarTexture1 ) then
|
||||
if ( bottomLeftUp ) then
|
||||
@@ -820,6 +826,28 @@
|
||||
SlidingActionBarTexture1:Show()
|
||||
end
|
||||
end
|
||||
if ( ShapeshiftBarFrame ) then
|
||||
local numForms = GetNumShapeshiftForms and GetNumShapeshiftForms() or 0
|
||||
if ( bottomLeftUp ) then
|
||||
ShapeshiftBarLeft:Hide()
|
||||
ShapeshiftBarRight:Hide()
|
||||
ShapeshiftBarMiddle:Hide()
|
||||
for i=1, numForms do
|
||||
getglobal("ShapeshiftButton"..i.."NormalTexture"):SetWidth(50)
|
||||
getglobal("ShapeshiftButton"..i.."NormalTexture"):SetHeight(50)
|
||||
end
|
||||
else
|
||||
if ( numForms > 2 ) then
|
||||
ShapeshiftBarMiddle:Show()
|
||||
end
|
||||
ShapeshiftBarLeft:Show()
|
||||
ShapeshiftBarRight:Show()
|
||||
for i=1, numForms do
|
||||
getglobal("ShapeshiftButton"..i.."NormalTexture"):SetWidth(64)
|
||||
getglobal("ShapeshiftButton"..i.."NormalTexture"):SetHeight(64)
|
||||
end
|
||||
end
|
||||
end
|
||||
if ( PetActionBarFrame ) then
|
||||
PETACTIONBAR_XPOS = 36
|
||||
local forms = GetNumShapeshiftForms and GetNumShapeshiftForms() or 0
|
||||
|
||||
@@ -24,6 +24,11 @@
|
||||
its comment names the reference row it stands in for.
|
||||
-->
|
||||
<Script><![CDATA[
|
||||
-- The placeholder icon for an item/spell whose texture has not arrived yet (Craft, Quest,
|
||||
-- QuestLog, Trainer and TradeSkill read it). Was ActionBar.xml's; that file went stock with
|
||||
-- 1938 and this kit is the constant's home now — a benilla name, so nothing stock reads it.
|
||||
BENILLA_FALLBACK_ICON = "Interface\\Icons\\INV_Misc_QuestionMark"
|
||||
|
||||
-- UIParent.lua l.14-42: UIPanelWindows["FrameName"] = { area, pushable, whileDead }. Only
|
||||
-- the rows benilla ships today; the ref rows with no benilla window yet (Tabard,
|
||||
-- Battlefield, WorldStateScore, Minigame) join as their windows land (audited
|
||||
|
||||
@@ -340,54 +340,104 @@ ConfirmSummon.xml
|
||||
# asks. The only entry in the registry that raises a money frame, so it needs MoneyFrame.xml (far
|
||||
# above, ahead of UiPanels.xml) as well as the StaticPopup engine itself.
|
||||
TalentWipeConfirm.xml
|
||||
# ── MEASUREMENT (1938): the options window at the reference's slot (FrameXML.toc l.21, before
|
||||
# MainMenuBar l.34) with the four rows it inherits from. ──
|
||||
# The faux-scroll kit (decisions 0247/0250/0251): BenillaScrollBarTemplate (the draggable
|
||||
# Slider scroll bar) + FauxScrollFrameTemplate + the reference's own FauxScrollFrame_*
|
||||
# API (Update/GetOffset/SetOffset/OnVerticalScroll + ScrollFrame_OnLoad) —
|
||||
# before its first customer (the trainer list) below.
|
||||
ScrollTemplates.xml
|
||||
|
||||
# ══ THE PANEL TEMPLATE KIT — the reference's own, off the player's chain (1751, window 31) ══
|
||||
# TWO entries, because the reference splits this kit the way it splits every other: the `.lua`
|
||||
# carries the verbs and the `.xml` the templates. Listing the `.xml` alone brings none of the verbs
|
||||
# (`every_chain_xml_brings_its_own_lua` is a gate because that already bit twice).
|
||||
#
|
||||
# ONE swap, THREE of our files, which is why it had to move as a unit and why 1837 measured it
|
||||
# before anyone executed it:
|
||||
# * `UIPanelTemplates.xml` — DELETED. Our 18 templates were a strict SUBSET of the reference's,
|
||||
# zero benilla-only additions, so the template half is a pure gain.
|
||||
# * the scroll half of `ScrollTemplates.xml` — `FauxScrollFrameTemplate`, `FauxScrollFrame_*`,
|
||||
# `ScrollFrame_*` — now the chain's. What stays there is `BenillaScrollBarTemplate` and
|
||||
# `BenillaScrollTroughTemplate`, which are ours and which six windows still inherit.
|
||||
# * the `PanelTemplates_*` family out of `UiPanels.xml`.
|
||||
#
|
||||
# It could not be split: `ScrollTemplates.xml` sits ABOVE this entry, so the chain's
|
||||
# `FauxScrollFrameTemplate` lands after ours and wins (registration is last-in). There is no
|
||||
# ordering that takes the templates without also taking Faux.
|
||||
#
|
||||
# GAINED: `TabButtonTemplate`, and the whole `ScrollingEdit_*` family, which we had NOWHERE — that
|
||||
# is HelpFrame's named blocker, answered as a side effect. And it unblocks MacroFrame (1835).
|
||||
Interface\FrameXML\UIPanelTemplates.lua
|
||||
|
||||
Interface\FrameXML\UIPanelTemplates.xml
|
||||
|
||||
# The Keybindings page MODULE (decision 1008, superseding 0997's standalone window):
|
||||
# the templates + script of the Options window's Keybindings category, over the
|
||||
# engine's binding table (benilla_ui::script::keybind + crate::bindings — the 1.12
|
||||
# GetBinding/SetBinding Lua API). Needs UiPanels (popup engine) and GameTooltip (the
|
||||
# character-specific checkbox's hover); MUST load before OptionsFrame.xml, whose
|
||||
# Keybindings body inherits these templates and calls this script at OnLoad.
|
||||
KeyBindingsPage.xml
|
||||
|
||||
# The options window (the 0985 provenance split: 0950's era structure, 0978's
|
||||
# 1.12-native art, 0981/0984's 1.14 chrome + working era search, 0989's directed
|
||||
# cuts — bare fully-live sliders, no corner X; 0992's dropdown rows + the Nameplates
|
||||
# page): the era-shaped OptionsFrame the game menu's Options button opens. Needs
|
||||
# Fonts' objects, UiPanels' panel manager, and UIDropDownMenu's capsule template
|
||||
# (all far above); sits right before GameMenuFrame so the menu — which reaches it by
|
||||
# name at click time — stays the last, outermost layer.
|
||||
OptionsFrame.xml
|
||||
|
||||
# ── The action-bar cluster, stock (1938): MainMenuBar.xml before Cooldown.xml as the reference's
|
||||
# toc has it (l.34 → l.36); ActionBarFrame.xml + MultiActionBars.xml right after ActionButtonTemplate.xml
|
||||
# (l.37-39); BonusActionBarFrame.xml — which also declares the shapeshift bar — after PetActionBar.xml
|
||||
# (l.73-74). Replaces ActionBar.xml, MultiBars.xml and StanceBar.xml.
|
||||
Interface\FrameXML\MainMenuBar.xml
|
||||
|
||||
# The shared CooldownFrame_SetTimer (the ref's Cooldown.lua file split) — before its
|
||||
# consumers (ActionBar's buttons, the multibars, the stance bar, BagFrame's slots).
|
||||
# consumers (the action, multibar, bonus and shapeshift buttons; the bag slots).
|
||||
Cooldown.xml
|
||||
# ══ THE ACTION-BUTTON REGIONS — the reference's own, off the player's chain (1751) ══
|
||||
# `ActionButtonTemplate`: the REGIONS-ONLY half of the reference's two-level split — icon, count,
|
||||
# hotkey, name, border, flash, cooldown and normal texture. It declares NO scripts and calls
|
||||
# nothing, so it needs no `.lua` beside it. The handler half is `ActionBarButtonTemplate`, which
|
||||
# ActionBar.xml still declares and which inherits this; those handlers stay ours until
|
||||
# `ActionButton.lua`'s 21 verbs migrate (we have 10 of them, under the reference's own names).
|
||||
# ActionBarFrame.xml below declares over `ActionButton.lua` (stock since 1938).
|
||||
#
|
||||
# Ours was a transcription of this exact file — the eight `$parent*` child names match to the
|
||||
# letter — so the swap is a deletion.
|
||||
#
|
||||
# **It must come BEFORE ActionBar.xml.** A template is registered by name and the later line
|
||||
# wins, so seated below it our transcription would take the name straight back.
|
||||
# **It must come BEFORE ActionBarFrame.xml**, whose template inherits it by name at load.
|
||||
Interface\FrameXML\ActionButtonTemplate.xml
|
||||
ActionBar.xml
|
||||
Interface\FrameXML\ActionBarFrame.xml
|
||||
# The four OPTIONAL extra bars (bottom left/right 61-72 / 49-60, vertical right/left
|
||||
# 25-36 / 37-48), all shipping hidden until the player ticks their row (decision 1500):
|
||||
# pure XML/Lua over ActionBar.xml's shared button template + handler set. Loads right
|
||||
# after it, and the order is a REQUIREMENT, not tidiness: it inherits
|
||||
# BenillaActionButtonTemplate, anchors to ActionButton1, its buttons join the
|
||||
# BENILLA_ACTION_BUTTONS roster ActionBar.xml declares, its grid verbs call that file's
|
||||
# ActionButton_ShowGrid/_HideGrid, and its BENILLA_MULTIBARS table reads that file's
|
||||
# *_ACTIONBAR_PAGE constants at FILE SCOPE. UIParent.xml (far above) owns the manage pass
|
||||
# MultiActionBar_Update ends with, and the PLAYER_ENTERING_WORLD seed that fills the four
|
||||
# SHOW_MULTI_ACTIONBAR_* globals from the server's byte.
|
||||
MultiBars.xml
|
||||
# The stance/shapeshift bar: hidden until `crate::ui_shapeshift`'s feed pushes a
|
||||
# non-empty form list (a formless class never shows it). After Cooldown.xml
|
||||
# (CooldownFrame_SetTimer) + ActionBar.xml (BENILLA_FALLBACK_ICON, the MainMenuBar
|
||||
# anchor target).
|
||||
StanceBar.xml
|
||||
# 25-36 / 37-48), all shipping hidden until the player ticks their row (decision 1500) — the
|
||||
# reference's own file, right after ActionBarFrame.xml as its toc has it (l.38-39): it inherits
|
||||
# ActionBarButtonTemplate, anchors to ActionButton1, and calls ActionButton_ShowGrid/_HideGrid.
|
||||
# Its OnLoad writes five rows of `UIOptionsFrameCheckButtons`, which is why OptionsFrame.xml sits
|
||||
# ABOVE this cluster (1938). UIParent.xml (far above) carries the PLAYER_ENTERING_WORLD seed
|
||||
# that fills the four SHOW_MULTI_ACTIONBAR_* globals from the server's byte; the manage pass runs
|
||||
# from the options rows' setters, as the stock options window runs it on hide.
|
||||
Interface\FrameXML\MultiActionBars.xml
|
||||
# The pet action bar (decision 0982): hidden until `crate::ui_pet`'s feed reports a bar,
|
||||
# i.e. until the server sends `SMSG_PET_SPELLS` for a live pet or charm — so a class that
|
||||
# never has one never sees it. Right after the stance bar, whose row it shares (either
|
||||
# one showing raises UIParent.xml's managed "pet" delta), and after Cooldown.xml
|
||||
# (CooldownFrame_SetTimer), ActionBar.xml (the `MainMenuBar` anchor target),
|
||||
# (CooldownFrame_SetTimer), MainMenuBar.xml (the `MainMenuBar` anchor target),
|
||||
# GameTooltip.xml (its hover), Fonts.xml (NORMAL_FONT_COLOR_CODE, which wraps the binding
|
||||
# a token slot's tooltip appends) and UIParent.xml (GetBindingText, which renders it).
|
||||
# It is also the pet ARC's file, so decision 1066's three
|
||||
# right-click-menu dialogs (ABANDON_PET / RENAME_PET / PETRENAMECONFIRM) register here —
|
||||
# which adds UiPanels.xml to its dependency set, already far above.
|
||||
PetActionBar.xml
|
||||
# The bonus (stance-page) bar AND the shapeshift bar — one stock file declares both, and the
|
||||
# reference's toc seats it right after PetActionBarFrame (l.73-74): the bonus buttons'
|
||||
# Up/Down call PetActionButtonUp/Down, which PetActionBar.xml above defines.
|
||||
Interface\FrameXML\BonusActionBarFrame.xml
|
||||
# The micro-button row in the bar's right-hand recess (the ref's own file split, its TOC
|
||||
# loads MainMenuBarMicroButtons right after MainMenuBar). After ActionBar.xml: it anchors
|
||||
# into MainMenuBarArtFrame by name and seats itself with that file's
|
||||
# BenillaActionBarArt_SeatAbove. It must come BEFORE the panels it toggles, because each of
|
||||
# loads MainMenuBarMicroButtons right after MainMenuBar). After MainMenuBar.xml: it anchors
|
||||
# into MainMenuBarArtFrame by name and seats itself above the art plate with its own
|
||||
# BenillaActionBarArt_SeatAbove (moved here from ActionBar.xml with 1938). It must come BEFORE the panels it toggles, because each of
|
||||
# their OnShow/OnHide calls the `UpdateMicroButtons` this file defines (the panel frames
|
||||
# themselves are read back through getglobal, so the reverse dependency is call-time).
|
||||
MicroMenu.xml
|
||||
@@ -440,33 +490,6 @@ Interface\FrameXML\FadingFrame.xml
|
||||
# strings, which came from ours as `X = X or "…"` fallbacks and now come from the player's own
|
||||
# GlobalStrings.lua like every other string in this interface.
|
||||
Interface\FrameXML\ZoneText.xml
|
||||
# The faux-scroll kit (decisions 0247/0250/0251): BenillaScrollBarTemplate (the draggable
|
||||
# Slider scroll bar) + FauxScrollFrameTemplate + the reference's own FauxScrollFrame_*
|
||||
# API (Update/GetOffset/SetOffset/OnVerticalScroll + ScrollFrame_OnLoad) —
|
||||
# before its first customer (the trainer list) below.
|
||||
ScrollTemplates.xml
|
||||
# ══ THE PANEL TEMPLATE KIT — the reference's own, off the player's chain (1751, window 31) ══
|
||||
# TWO entries, because the reference splits this kit the way it splits every other: the `.lua`
|
||||
# carries the verbs and the `.xml` the templates. Listing the `.xml` alone brings none of the verbs
|
||||
# (`every_chain_xml_brings_its_own_lua` is a gate because that already bit twice).
|
||||
#
|
||||
# ONE swap, THREE of our files, which is why it had to move as a unit and why 1837 measured it
|
||||
# before anyone executed it:
|
||||
# * `UIPanelTemplates.xml` — DELETED. Our 18 templates were a strict SUBSET of the reference's,
|
||||
# zero benilla-only additions, so the template half is a pure gain.
|
||||
# * the scroll half of `ScrollTemplates.xml` — `FauxScrollFrameTemplate`, `FauxScrollFrame_*`,
|
||||
# `ScrollFrame_*` — now the chain's. What stays there is `BenillaScrollBarTemplate` and
|
||||
# `BenillaScrollTroughTemplate`, which are ours and which six windows still inherit.
|
||||
# * the `PanelTemplates_*` family out of `UiPanels.xml`.
|
||||
#
|
||||
# It could not be split: `ScrollTemplates.xml` sits ABOVE this entry, so the chain's
|
||||
# `FauxScrollFrameTemplate` lands after ours and wins (registration is last-in). There is no
|
||||
# ordering that takes the templates without also taking Faux.
|
||||
#
|
||||
# GAINED: `TabButtonTemplate`, and the whole `ScrollingEdit_*` family, which we had NOWHERE — that
|
||||
# is HelpFrame's named blocker, answered as a side effect. And it unblocks MacroFrame (1835).
|
||||
Interface\FrameXML\UIPanelTemplates.lua
|
||||
Interface\FrameXML\UIPanelTemplates.xml
|
||||
# ══ THE OPTIONS WIDGET KIT — the reference's own, off the player's chain (1751, window 29) ══
|
||||
# The four templates addons name for an options panel: `OptionFrameBoxTemplate`,
|
||||
# `OptionsButtonTemplate`, `OptionsCheckButtonTemplate`, `OptionsSliderTemplate`. Self-contained —
|
||||
@@ -498,7 +521,7 @@ OptionsFrameTemplates.xml
|
||||
#
|
||||
# THE SEAT IS A REQUIREMENT, not tidiness: the Okay/Cancel buttons
|
||||
# `inherits="GameMenuButtonTemplate"`, resolved at LOAD, and that template lives in
|
||||
# UIPanelTemplates.xml six lines up (it moved there from GameMenuFrame.xml — the reference's own
|
||||
# UIPanelTemplates.xml far above (it moved there from GameMenuFrame.xml — the reference's own
|
||||
# file for it — precisely because GameMenuFrame.xml is deliberately LAST, which is also why this
|
||||
# entry does not follow the reference's own position below it). Everything else it needs is far
|
||||
# above: Fonts' GameFontNormal trio, and UiPanels' ShowUIPanel/HideUIPanel plus its UISpecialFrames
|
||||
@@ -641,7 +664,7 @@ Interface\FrameXML\ContainerFrame.xml
|
||||
# `UIPanelButtonTemplate`, and `PanelTemplates_SetNumTabs`, which `CharacterFrame_OnLoad` calls at
|
||||
# LOAD), TextStatusBar.xml (`SetTextStatusBarTextPrefix`, called at load too, plus the
|
||||
# `TextStatusBar` the pet page's XP bar inherits), UnitFrames.xml (`PlayerFrameHealthBar` /
|
||||
# `PlayerFrameManaBar`, named in that same OnLoad), ActionBar.xml (`MainMenuExpBar`, likewise),
|
||||
# `PlayerFrameManaBar`, named in that same OnLoad), MainMenuBar.xml (`MainMenuExpBar`, likewise),
|
||||
# MicroMenu.xml (`MicroButtonTooltipText`, the five tab hovers) and UIParent.xml
|
||||
# (`Model_OnLoad`/`_RotateLeft`/`_RotateRight`/`_OnUpdate` — the model pane's turntable, the
|
||||
# reference's own code living in our UIParent.lua counterpart).
|
||||
@@ -669,7 +692,7 @@ Interface\FrameXML\PetPaperDollFrame.xml
|
||||
# THE POSITION IS THE REQUIREMENT: `inherits="PaperDollItemSlotButtonTemplate"` on the four bag
|
||||
# slots and `inherits="ItemButtonTemplate"` on the backpack are resolved at LOAD, and so is
|
||||
# `parent="MainMenuBarArtFrame"` — so this sits below the character-window block (above, which is
|
||||
# where stock `PaperDollFrame.xml` declares that template) and below ActionBar.xml (which declares
|
||||
# where stock `PaperDollFrame.xml` declares that template) and below MainMenuBar.xml (which declares
|
||||
# the art frame). It sat below a transcribed copy of the template in `ItemSlotButtonTemplates.xml`
|
||||
# until the character window migrated and made that copy redundant (decision 1810). Being BELOW the sourced ContainerFrame.xml is
|
||||
# legibility, not necessity, in either direction: every verb each file calls of the other resolves
|
||||
@@ -679,8 +702,8 @@ Interface\FrameXML\PetPaperDollFrame.xml
|
||||
# than being edited into anything of Blizzard's: the item-push card (0887 — the reference's is a
|
||||
# `<Model>` playing an .mdx and this engine renders no FrameXML models, so ours samples that
|
||||
# model's own tracks and draws a quad), and the GlobalStrings fallbacks the install-less harnesses
|
||||
# need. The keyring's SHOW trigger moved the other way — into ActionBar.xml, our MainMenuBar.xml,
|
||||
# which is exactly where the reference keeps it (MainMenuBar.xml l.323-341).
|
||||
# need. The keyring's SHOW trigger moved the other way — into the main bar's file, which is exactly
|
||||
# where the reference keeps it (MainMenuBar.xml l.323-341; stock since 1938).
|
||||
Interface\FrameXML\MainMenuBarBagButtons.xml
|
||||
# The stack-split spinner (decision 0216 §6/slice 2): loaded beside the bag windows, whose slot
|
||||
# click handler (the reference's `ContainerFrameItemButton_OnClick`) calls `OpenStackSplitFrame`.
|
||||
@@ -700,8 +723,7 @@ ContainerFrameAdapters.xml
|
||||
# template's first benilla consumer), OptionsFrameTemplates.xml (`OptionsCheckButtonTemplate`, two
|
||||
# of the three detail boxes), GameTooltip.xml (the column newbie tips) and Fonts.xml. One
|
||||
# dependency runs the other way and is worth naming: the WATCH BAR half of the reference's
|
||||
# ReputationFrame lives in ActionBar.xml (entry 94) beside the MainMenuBar frames it drives — that
|
||||
# file's own comment carries the justification — so `FACTION_BAR_COLORS` and
|
||||
# ReputationFrame is declared in this same stock file (since 1875's co-edit, below) — so `FACTION_BAR_COLORS` and
|
||||
# `ReputationWatchBar_Update` are already defined when this page paints a row or ticks the
|
||||
# "Show as Experience Bar" box.
|
||||
# ══ THE REPUTATION PANE — the reference's own, off the player's chain (1751, scoped by 1844) ══
|
||||
@@ -712,7 +734,7 @@ ContainerFrameAdapters.xml
|
||||
#
|
||||
# THE CO-EDIT was compulsory. The reference declares the reputation WATCH BAR in this same file;
|
||||
# ours lived in `ActionBar.xml` behind a note defending the placement. Frame globals publish
|
||||
# NON-overwritingly and ActionBar.xml is manifest line 340 against this file's — so ours would have
|
||||
# NON-overwritingly and ActionBar.xml was manifest line 340 against this file's — so ours would have
|
||||
# kept the name and the chain's bar would have been built and unreachable, which is exactly what
|
||||
# `GroupLootFrame` did for months (1838). The bar, its update, its two text verbs,
|
||||
# `MAX_PLAYER_LEVEL` and `FACTION_BAR_COLORS` all went home with it (1875).
|
||||
@@ -1053,21 +1075,6 @@ Interface\FrameXML\HelpFrame.xml
|
||||
# nothing else, and nothing needs it — the engine reaches it purely by firing CINEMATIC_START.
|
||||
# Seated late because it is an outermost, screen-owning layer, like the two windows below it.
|
||||
Interface\FrameXML\CinematicFrame.xml
|
||||
# The Keybindings page MODULE (decision 1008, superseding 0997's standalone window):
|
||||
# the templates + script of the Options window's Keybindings category, over the
|
||||
# engine's binding table (benilla_ui::script::keybind + crate::bindings — the 1.12
|
||||
# GetBinding/SetBinding Lua API). Needs UiPanels (popup engine) and GameTooltip (the
|
||||
# character-specific checkbox's hover); MUST load before OptionsFrame.xml, whose
|
||||
# Keybindings body inherits these templates and calls this script at OnLoad.
|
||||
KeyBindingsPage.xml
|
||||
# The options window (the 0985 provenance split: 0950's era structure, 0978's
|
||||
# 1.12-native art, 0981/0984's 1.14 chrome + working era search, 0989's directed
|
||||
# cuts — bare fully-live sliders, no corner X; 0992's dropdown rows + the Nameplates
|
||||
# page): the era-shaped OptionsFrame the game menu's Options button opens. Needs
|
||||
# Fonts' objects, UiPanels' panel manager, and UIDropDownMenu's capsule template
|
||||
# (all far above); sits right before GameMenuFrame so the menu — which reaches it by
|
||||
# name at click time — stays the last, outermost layer.
|
||||
OptionsFrame.xml
|
||||
# The game menu (decision 0674): the frame ESC opens. LAST on purpose — it is the outermost
|
||||
# layer of the shell, it depends on everything it touches being loaded (UiPanels' popup engine
|
||||
# + panel manager, MicroMenu's UpdateMicroButtons, BagFrame's Disable_BagButtons), and nothing
|
||||
|
||||
@@ -307,10 +307,10 @@ const TABLE: &[Spec] = &[
|
||||
None
|
||||
),
|
||||
// The stance/shapeshift row (ref ShapeshiftBar_ChangeForm(n)) — ours clicks the bar's own
|
||||
// buttons, which carry the full form-switch law (StanceBar.xml).
|
||||
// buttons, which carry the full form-switch law (stock BonusActionBarFrame.xml, 1938).
|
||||
// ── The self-cast dozen (1.12 `Bindings.xml`:257-293) ───────────────────────────────
|
||||
// The same two halves as ACTIONBUTTON, with `ActionButtonUp`'s second argument set: the
|
||||
// reference's own `onSelf`, which `ActionBar.xml` has always forwarded to `UseAction`'s third
|
||||
// reference's own `onSelf`, which stock `ActionButtonUp` forwards to `UseAction`'s third
|
||||
// and the host used to drop (1745). `ALT-1`…`ALT-=` are byte-real from DefaultBindings.wtf,
|
||||
// and they sit one modifier off the plain bar exactly as the reference lays them out.
|
||||
spec!(
|
||||
@@ -567,8 +567,9 @@ const TABLE: &[Spec] = &[
|
||||
),
|
||||
// ── The action-bar PAGES (1.12 `Bindings.xml`:395-431) ──────────────────────────────
|
||||
// The bar is six pages of twelve (action slots 1..72) and it has been since 1500 shipped the
|
||||
// multibars; `ChangeActionBarPage` and the `ActionBar_Page{Up,Down}` wrap are ActionBar.xml's
|
||||
// own, quoted 1:1 from the reference. `SHIFT-1..6` and the SHIFT-arrow / SHIFT-wheel steps are
|
||||
// multibars; `ChangeActionBarPage` is the engine's (it fires ACTIONBAR_PAGE_CHANGED and nothing
|
||||
// else — 1938) and the `ActionBar_Page{Up,Down}` wrap is stock ActionButton.lua's. `SHIFT-1..6`
|
||||
// and the SHIFT-arrow / SHIFT-wheel steps are
|
||||
// byte-real from `WTF\\DefaultBindings.wtf`.
|
||||
spec!(
|
||||
"ACTIONPAGE1",
|
||||
@@ -645,7 +646,7 @@ const TABLE: &[Spec] = &[
|
||||
Some("SHIFT-MOUSEWHEELDOWN")
|
||||
),
|
||||
// The action-bar lock (decision 1136), the ref's own binding body verbatim (Bindings.xml:433-
|
||||
// 439) — it flips the `LOCK_ACTIONBAR` uvar `ActionBar.xml` declares, the same global the
|
||||
// 439) — it flips the `LOCK_ACTIONBAR` uvar OptionsFrame.xml declares (1938), the same global the
|
||||
// Options window's Action Bars row writes. It sits here because the reference files it under
|
||||
// this header (l.433 carries no `header=`, so it inherits l.121's ACTIONBAR), and it ships
|
||||
// **unbound**: no `TOGGLEACTIONBARLOCK` line in any of the install's three
|
||||
@@ -1148,10 +1149,10 @@ const TABLE: &[Spec] = &[
|
||||
None
|
||||
),
|
||||
// ── MultiActionBar (BINDING_HEADER_MULTIACTIONBAR) ──────────────────────────────────
|
||||
// The two bottom bars' buttons (MultiBars.xml renders exactly these; 1.12's right bars
|
||||
// The two bottom bars' buttons (stock MultiActionBars.xml renders exactly these; 1.12's right bars
|
||||
// and their MULTIACTIONBAR3/4 commands stay out — honest tree). Ref bodies are the
|
||||
// MultiActionButtonDown/Up runOnUp pair (Bindings.xml:799-966), transcribed in
|
||||
// MultiBars.xml; shipped UNBOUND like the ref (no MULTIACTIONBAR* line in any of the
|
||||
// MultiActionButtonDown/Up runOnUp pair (Bindings.xml:799-966), defined in stock
|
||||
// MultiActionBars.lua; shipped UNBOUND like the ref (no MULTIACTIONBAR* line in any of the
|
||||
// install's bindings-cache.wtf files). 1.12 files bar 2 under a BLANK spacer-header;
|
||||
// both bars sit under the one MULTIACTIONBAR header here (1008, recorded).
|
||||
spec!(
|
||||
|
||||
@@ -840,6 +840,7 @@ mod tests {
|
||||
..default()
|
||||
};
|
||||
let path = |grounded: bool| Spline {
|
||||
deck: None,
|
||||
points: vec![[0.0; 3], [1.0, 0.0, 0.0]],
|
||||
start: std::time::Instant::now(),
|
||||
duration: std::time::Duration::from_secs(1),
|
||||
|
||||
@@ -1453,6 +1453,7 @@ fn seed_bag_window(
|
||||
benilla_ui::script::ContainerSlot {
|
||||
petition: None,
|
||||
durability: None,
|
||||
duration_ms: None,
|
||||
bar_placeable: true,
|
||||
texture: icon(disp),
|
||||
count,
|
||||
@@ -1549,6 +1550,7 @@ fn seed_equipped_bags(
|
||||
|disp: u32, count: u32, name: &str, quality: u32| benilla_ui::script::ContainerSlot {
|
||||
petition: None,
|
||||
durability: None,
|
||||
duration_ms: None,
|
||||
bar_placeable: true,
|
||||
texture: icon(disp),
|
||||
count,
|
||||
|
||||
@@ -50,6 +50,8 @@ impl Plugin for LiveFpsPlugin {
|
||||
run: std::env::var("WOW_LIVE_FPS_MOVE").as_deref() == Ok("1"),
|
||||
phase: LiveFpsPhase::Waiting,
|
||||
samples: Vec::new(),
|
||||
cpu_samples: Vec::new(),
|
||||
cpu_prev: None,
|
||||
cpu_at_start: None,
|
||||
sys_at_start: None,
|
||||
occluded_now: false,
|
||||
@@ -80,6 +82,11 @@ struct LiveFps {
|
||||
run: bool,
|
||||
phase: LiveFpsPhase,
|
||||
samples: Vec<f32>,
|
||||
/// Process CPU per frame, ms, index-parallel with `samples` — the TAIL half of `cpu_ms`:
|
||||
/// a mean under budget hides the one frame in twenty that is over it, and whether that
|
||||
/// frame was CPU work or a wait is the first question about a stutter.
|
||||
cpu_samples: Vec<f32>,
|
||||
cpu_prev: Option<f64>,
|
||||
/// Process CPU seconds at the first sampled frame ([`crate::perf::process_cpu_secs`]) — the
|
||||
/// baseline for the window's `cpu_ms`/`cpu_pct`.
|
||||
cpu_at_start: Option<f64>,
|
||||
@@ -122,6 +129,21 @@ type ScreenParams<'w, 's> = (
|
||||
Query<'w, 's, &'static Camera>,
|
||||
// The UI's live texture-identity runs (`ui_batches=`): one `Mesh2d` draw each in the 2D pass.
|
||||
Query<'w, 's, (), With<crate::ui_pass::UiQuadBatch>>,
|
||||
// Every view-visible `Mesh3d` by lane (`vis_terrain/model/liquid/other=`): the 3D passes'
|
||||
// draw population. `drawn=` counts model submeshes only; the terrain is one entity per
|
||||
// ADT chunk and a city horizon is over a thousand of them — a per-draw cost is not
|
||||
// attributable until this split is known.
|
||||
Query<
|
||||
'w,
|
||||
's,
|
||||
(
|
||||
&'static bevy::camera::visibility::ViewVisibility,
|
||||
Has<MeshMaterial3d<benilla_assets::materials::TerrainMaterial>>,
|
||||
Has<benilla_world::model_render::ModelPart>,
|
||||
Has<MeshMaterial3d<benilla_assets::materials::LiquidMaterial>>,
|
||||
),
|
||||
With<Mesh3d>,
|
||||
>,
|
||||
);
|
||||
|
||||
#[derive(SystemParam)]
|
||||
@@ -165,13 +187,14 @@ fn drive_live_fps(
|
||||
// the leg line carries gpu percentiles beside the cpu ones (absent when the meter is off).
|
||||
mut screen: ScreenParams,
|
||||
) {
|
||||
let (monitors, gpu, gpu_samples, wgpu_census, cameras, ui_batches) = (
|
||||
let (monitors, gpu, gpu_samples, wgpu_census, cameras, ui_batches, vis_meshes) = (
|
||||
&screen.0,
|
||||
&screen.1,
|
||||
&mut screen.2,
|
||||
&screen.3,
|
||||
&screen.4,
|
||||
&screen.5,
|
||||
&screen.6,
|
||||
);
|
||||
// Drain every frame so the state is current whichever phase we're in — the window can be
|
||||
// occluded before sampling ever starts (a detached launch spawns behind whatever is open).
|
||||
@@ -246,6 +269,13 @@ fn drive_live_fps(
|
||||
}
|
||||
let ms = time.delta_secs() * 1000.0;
|
||||
probe.samples.push(ms);
|
||||
let cpu_now = crate::perf::process_cpu_secs();
|
||||
let cpu_ms = match (probe.cpu_prev, cpu_now) {
|
||||
(Some(a), Some(b)) => ((b - a) * 1000.0) as f32,
|
||||
_ => 0.0,
|
||||
};
|
||||
probe.cpu_prev = cpu_now;
|
||||
probe.cpu_samples.push(cpu_ms);
|
||||
if let Some(gpu) = gpu {
|
||||
let ns = gpu.0.load(std::sync::atomic::Ordering::Relaxed);
|
||||
if ns > 0 {
|
||||
@@ -344,9 +374,34 @@ fn drive_live_fps(
|
||||
let cpu = match (probe.cpu_at_start, crate::perf::process_cpu_secs()) {
|
||||
(Some(t0), Some(t1)) => {
|
||||
let per_frame_ms = (t1 - t0) * 1000.0 / v.len() as f64;
|
||||
// The tail: CPU percentiles over the same frames, how many frames ran over
|
||||
// the 60 Hz budget, and the worst six as `index:frame_ms/cpu_ms` — a frame
|
||||
// whose cpu is near its length was WORK; one far under it was a WAIT (GPU,
|
||||
// present, a lock).
|
||||
let mut c: Vec<f32> = probe.cpu_samples.clone();
|
||||
c.sort_by(f32::total_cmp);
|
||||
let cat = |q: f32| c[(((c.len() - 1) as f32) * q).round() as usize];
|
||||
let over = probe.samples.iter().filter(|&&f| f > 16.9).count();
|
||||
let mut worst: Vec<usize> = (0..probe.samples.len()).collect();
|
||||
worst.sort_by(|&a, &b| probe.samples[b].total_cmp(&probe.samples[a]));
|
||||
let worst = worst
|
||||
.iter()
|
||||
.take(6)
|
||||
.map(|&i| {
|
||||
format!(
|
||||
"{i}:{:.1}/{:.1}",
|
||||
probe.samples[i],
|
||||
probe.cpu_samples.get(i).copied().unwrap_or(0.0)
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.join(",");
|
||||
format!(
|
||||
" cpu_ms={per_frame_ms:.2} cpu_pct={:.0}",
|
||||
per_frame_ms / mean as f64 * 100.0
|
||||
" cpu_ms={per_frame_ms:.2} cpu_pct={:.0} cpu_p95={:.2} cpu_p99={:.2} \
|
||||
frames_over={over} worst=[{worst}]",
|
||||
per_frame_ms / mean as f64 * 100.0,
|
||||
cat(0.95),
|
||||
cat(0.99),
|
||||
)
|
||||
}
|
||||
_ => String::new(),
|
||||
@@ -431,7 +486,7 @@ fn drive_live_fps(
|
||||
})
|
||||
.unwrap_or_default();
|
||||
let residency_line = format!(
|
||||
" mats={} mats_parked={} meshes={} images={} uv={} tint={} views={} ui_batches={}",
|
||||
" mats={} mats_parked={} meshes={} images={} uv={} tint={} views={} ui_batches={}{vis}",
|
||||
seen.mats,
|
||||
seen.mats_parked,
|
||||
seen.meshes,
|
||||
@@ -440,6 +495,28 @@ fn drive_live_fps(
|
||||
seen.tint_anims,
|
||||
cameras.iter().filter(|c| c.is_active).count(),
|
||||
ui_batches.iter().count(),
|
||||
vis = {
|
||||
let mut n = [0usize; 4];
|
||||
for (vv, terrain, model, liquid) in vis_meshes.iter() {
|
||||
if !vv.get() {
|
||||
continue;
|
||||
}
|
||||
let k = if terrain {
|
||||
0
|
||||
} else if model {
|
||||
1
|
||||
} else if liquid {
|
||||
2
|
||||
} else {
|
||||
3
|
||||
};
|
||||
n[k] += 1;
|
||||
}
|
||||
format!(
|
||||
" vis_terrain={} vis_model={} vis_liquid={} vis_other={}",
|
||||
n[0], n[1], n[2], n[3]
|
||||
)
|
||||
},
|
||||
);
|
||||
println!(
|
||||
"FPS_PROBE scenario=live frames={} mean_ms={mean:.2} p50_ms={:.2} p95_ms={:.2} p99_ms={:.2} max_ms={:.2} fps={:.1} emitters={} active={} particles={} submeshes={} drawn={} streamed={} parked={} entities={}{rigs}{residency_line} px={}x{}{cpu}{sys}{present}{display}{gpu_line} occluded_frames={}{at_pin}{cam_pose}{gate}{sky}{ribbons}{culled}",
|
||||
|
||||
@@ -917,6 +917,7 @@ fn a_flying_spline_is_fly_before_backward_and_speed() {
|
||||
fn unify_stamps_flying_from_the_live_spline_on_every_leg() {
|
||||
use std::time::{Duration, Instant};
|
||||
let spline = |grounded| crate::net::Spline {
|
||||
deck: None,
|
||||
points: vec![[0.0, 0.0, 0.0], [10.0, 0.0, 0.0]],
|
||||
start: Instant::now(),
|
||||
duration: Duration::from_secs(10),
|
||||
@@ -958,6 +959,7 @@ fn the_granted_modes_fold_into_the_flags_word_on_every_leg_but_our_own() {
|
||||
|
||||
// Creature leg on a path: the granted bit rides alongside the spline's own FORWARD.
|
||||
let spline = crate::net::Spline {
|
||||
deck: None,
|
||||
points: vec![[0.0, 0.0, 0.0], [10.0, 0.0, 0.0]],
|
||||
start: Instant::now(),
|
||||
duration: Duration::from_secs(10),
|
||||
|
||||
@@ -227,7 +227,17 @@ fn drive_displayed_cursor(
|
||||
kind: CursorKind::Repair,
|
||||
unable: false,
|
||||
}
|
||||
} else if targeting.active() {
|
||||
} else if targeting.active()
|
||||
|| script
|
||||
.as_ref()
|
||||
.is_some_and(|s| s.gift_wrap_armed().is_some())
|
||||
{
|
||||
// An armed **gift wrap** parks the base at Cast(2) for the same reason a targeting spell
|
||||
// does, and by the same act: `0x5edea0`'s three calls are `LockItem`, **`SetCursorBaseMode(2)`**
|
||||
// and `CursorSetMode(2)` (decision 1934). Both cells, deliberately — the displayed one so the
|
||||
// cursor changes at the click, the BASE one so it survives the pointer crossing the world,
|
||||
// where the classifier would otherwise write its own verdict over it. Setting only the
|
||||
// displayed mode would lose the wrap cursor the moment the mouse left the bag.
|
||||
WorldCursor {
|
||||
kind: CursorKind::Cast,
|
||||
unable: false,
|
||||
|
||||
@@ -525,7 +525,7 @@ pub(crate) const REGISTERED: &[Registered] = &[
|
||||
// Enhanced Tooltips (B230): 1.12's `UberTooltips`, the *Enhanced Tooltips* checkbox
|
||||
// (`UIOptionsFrame.lua:15`, `USE_UBERTOOLTIPS`). **No host knob** — its consumers are Lua, and
|
||||
// there are three: PetActionBar.xml forks the whole tooltip on it (a token's own text with the
|
||||
// binding appended, vs the engine's pet-spell channel), ActionBar.xml and StanceBar.xml fork
|
||||
// binding appended, vs the engine's pet-spell channel), the stock action and shapeshift buttons fork
|
||||
// their anchor. Registered "1" — byte-read, not behaviour-derived: WoW.exe `0x48fdd9`, default
|
||||
// string `0x82e748`, with the sibling rows `BlockTrades`→"0" and `UnitNameRenderMode`→"2"
|
||||
// confirming the layout. Those three Lua sites each carried the reference's fork in prose and
|
||||
|
||||
@@ -122,7 +122,12 @@ mod tests {
|
||||
|
||||
let mut spawned: Vec<(Entity, Vec3, Entity)> = app
|
||||
.world_mut()
|
||||
.query::<(Entity, &PointLight, &Transform, &ChildOf)>()
|
||||
.query::<(
|
||||
Entity,
|
||||
&benilla_world::lighting::WorldPointLight,
|
||||
&Transform,
|
||||
&ChildOf,
|
||||
)>()
|
||||
.iter(app.world())
|
||||
.map(|(e, _, t, c)| (e, t.translation, c.parent()))
|
||||
.collect();
|
||||
@@ -145,12 +150,11 @@ mod tests {
|
||||
let pl = app
|
||||
.world()
|
||||
.entity(spawned[0].0)
|
||||
.get::<PointLight>()
|
||||
.get::<benilla_world::lighting::WorldPointLight>()
|
||||
.unwrap();
|
||||
let lin = pl.color.to_linear();
|
||||
let recovered = pl.intensity / (4.0 * std::f32::consts::PI);
|
||||
assert!(
|
||||
(lin.red * recovered - 1.4).abs() < 1e-3,
|
||||
(pl.color[0] * recovered - 1.4).abs() < 1e-3,
|
||||
"colour × intensity survives the packing"
|
||||
);
|
||||
}
|
||||
|
||||
+137
-17
@@ -320,6 +320,13 @@ pub(crate) struct Items {
|
||||
/// duration field, which the reference's tooltip never reads. Session state, so it clears with
|
||||
/// the objects (decision 0920).
|
||||
enchant_deadlines: HashMap<(u64, u32), std::time::Instant>,
|
||||
/// The **item-lifetime deadlines**, keyed by item guid — a duration-limited instance's
|
||||
/// remaining life (a conjured stone, a holiday gift, a timed quest item). Its only feed is
|
||||
/// `SMSG_ITEM_TIME_UPDATE`; the instance's own `ITEM_FIELD_DURATION` carries the same number
|
||||
/// and is sent to the owner, but vmangos's writer says in as many words that the field is not
|
||||
/// what the client displays from (`Item::SendTimeUpdate`, `Objects/Item.cpp:1094`). Same shape
|
||||
/// as [`Self::enchant_deadlines`] one field up, for the same reason (decision 1933).
|
||||
duration_deadlines: HashMap<u64, std::time::Instant>,
|
||||
}
|
||||
|
||||
impl Items {
|
||||
@@ -353,6 +360,7 @@ impl Items {
|
||||
self.object_epoch = self.object_epoch.wrapping_add(1);
|
||||
}
|
||||
self.enchant_deadlines.retain(|&(g, _), _| g != guid);
|
||||
self.duration_deadlines.remove(&guid);
|
||||
}
|
||||
|
||||
/// The instance-side broadcast counter — see the [`Self::object_epoch`] field.
|
||||
@@ -404,23 +412,27 @@ impl Items {
|
||||
}
|
||||
|
||||
/// The counter a gated feed watches instead of holding its gate open per-frame: moves exactly
|
||||
/// when some [`Self::enchant_remaining_display_ms`] can move. Each live deadline contributes
|
||||
/// `floor(seconds left) + 1` — the `+1` makes the final `Some(0) → None` collapse (the tooltip
|
||||
/// reverting to the plain enchant name) its own step, one frame after the deadline elapses.
|
||||
/// when some displayable countdown can — [`Self::enchant_remaining_display_ms`] or
|
||||
/// [`Self::duration_remaining_display_ms`], the two per-second timers a bag/equipment
|
||||
/// snapshot renders. Each live deadline contributes `floor(seconds left) + 1` — the `+1`
|
||||
/// makes the final `Some(0) → None` collapse (the tooltip reverting to the plain enchant
|
||||
/// name, the duration line vanishing) its own step, one frame after the deadline elapses.
|
||||
/// Landings and removals move [`Self::object_epoch`] on their own. Empty — the overwhelmingly
|
||||
/// common case — costs an empty iteration.
|
||||
pub(crate) fn enchant_display_epoch(&self) -> u64 {
|
||||
/// common case — costs two empty iterations.
|
||||
pub(crate) fn countdown_display_epoch(&self) -> u64 {
|
||||
let now = std::time::Instant::now();
|
||||
let step = |at: &std::time::Instant| {
|
||||
let left = at.saturating_duration_since(now);
|
||||
if left.is_zero() {
|
||||
0
|
||||
} else {
|
||||
left.as_secs() + 1
|
||||
}
|
||||
};
|
||||
self.enchant_deadlines
|
||||
.values()
|
||||
.map(|&at| {
|
||||
let left = at.saturating_duration_since(now);
|
||||
if left.is_zero() {
|
||||
0
|
||||
} else {
|
||||
left.as_secs() + 1
|
||||
}
|
||||
})
|
||||
.chain(self.duration_deadlines.values())
|
||||
.map(step)
|
||||
.sum()
|
||||
}
|
||||
|
||||
@@ -442,6 +454,56 @@ impl Items {
|
||||
)
|
||||
}
|
||||
|
||||
/// `SMSG_ITEM_TIME_UPDATE` landed: park the absolute deadline for that item instance's
|
||||
/// remaining lifetime. Same store shape as the enchant path one field up
|
||||
/// ([`Self::set_enchant_deadline`]) — byte-confirmed as such, not assumed (decision 1933's
|
||||
/// fold-back).
|
||||
///
|
||||
/// Three rules, each the reference's:
|
||||
///
|
||||
/// - **A non-POSITIVE duration clears.** `0x5d9c0c` tests the wire value **signed** (`jle`),
|
||||
/// so `0` and anything with the top bit set both zero the cell rather than parking a
|
||||
/// 68-year deadline. An expired countdown is an absent line, not a line reading `0`.
|
||||
/// - **An item we do not hold is DROPPED — no queue, no retry.** `0x1EA`'s arm resolves the
|
||||
/// guid and returns on a miss. This is the load-bearing asymmetry with its sibling: the
|
||||
/// `0x1EB` enchant arm *does* enqueue an unresolved update and replays it when the item
|
||||
/// appears (`0x5ebd40` → `0x5ebde0`); this one does not. Following the sibling would also
|
||||
/// grow this map without bound for items that never arrive.
|
||||
/// - The cell dies with the object, so a relog or a zone destroy loses it; the server re-sends
|
||||
/// for everything carried on world enter (`Player::SendItemDurations`).
|
||||
pub(crate) fn set_item_duration(&mut self, guid: u64, seconds: u32) {
|
||||
if !self.objects.contains_key(&guid) {
|
||||
debug!("item duration: {guid:#x} names an item we do not hold — dropped");
|
||||
return;
|
||||
}
|
||||
if seconds == 0 || seconds as i32 <= 0 {
|
||||
if self.duration_deadlines.remove(&guid).is_some() {
|
||||
self.object_epoch = self.object_epoch.wrapping_add(1);
|
||||
}
|
||||
return;
|
||||
}
|
||||
let at = std::time::Instant::now() + std::time::Duration::from_secs(u64::from(seconds));
|
||||
self.duration_deadlines.insert(guid, at);
|
||||
self.object_epoch = self.object_epoch.wrapping_add(1);
|
||||
debug!("item duration: item {guid:#x} → {seconds}s");
|
||||
}
|
||||
|
||||
/// Milliseconds left on an item instance's lifetime, or `None` when it carries no timer —
|
||||
/// including an elapsed one. Recomputed on read from the parked deadline, never ticked (the
|
||||
/// enchant path's `0x5d9d00` shape).
|
||||
pub(crate) fn duration_remaining_ms(&self, guid: u64) -> Option<u64> {
|
||||
let at = self.duration_deadlines.get(&guid)?;
|
||||
let left = at.saturating_duration_since(std::time::Instant::now());
|
||||
(!left.is_zero()).then_some(left.as_millis() as u64)
|
||||
}
|
||||
|
||||
/// [`Self::duration_remaining_ms`] at **display granularity** — floored to the whole second,
|
||||
/// for the same reason [`Self::enchant_remaining_display_ms`] is: a raw per-ms read makes
|
||||
/// every snapshot differ every frame and fires the inventory feeds at frame rate.
|
||||
pub(crate) fn duration_remaining_display_ms(&self, guid: u64) -> Option<u64> {
|
||||
self.duration_remaining_ms(guid).map(|ms| ms - ms % 1000)
|
||||
}
|
||||
|
||||
/// A tracked item object's merged descriptor fields.
|
||||
pub(crate) fn object(&self, guid: u64) -> Option<&ObjectFields> {
|
||||
self.objects.get(&guid)
|
||||
@@ -787,12 +849,12 @@ mod tests {
|
||||
/// to the whole second, and clearing the deadline zeroes the epoch (the `Some(0) → None`
|
||||
/// collapse is the term's own last step).
|
||||
#[test]
|
||||
fn the_enchant_display_epoch_steps_by_displayable_seconds() {
|
||||
fn the_countdown_display_epoch_steps_by_displayable_seconds() {
|
||||
let mut items = Items::default();
|
||||
assert_eq!(items.enchant_display_epoch(), 0, "no deadlines, no epoch");
|
||||
assert_eq!(items.countdown_display_epoch(), 0, "no deadlines, no epoch");
|
||||
|
||||
items.set_enchant_deadline(0x42, 1, 90);
|
||||
let epoch = items.enchant_display_epoch();
|
||||
let epoch = items.countdown_display_epoch();
|
||||
assert!(
|
||||
(90..=91).contains(&epoch),
|
||||
"a 90 s deadline contributes floor(secs)+1, got {epoch}"
|
||||
@@ -803,13 +865,71 @@ mod tests {
|
||||
|
||||
items.set_enchant_deadline(0x42, 1, 0);
|
||||
assert_eq!(
|
||||
items.enchant_display_epoch(),
|
||||
items.countdown_display_epoch(),
|
||||
0,
|
||||
"cleared deadline, term gone"
|
||||
);
|
||||
assert_eq!(items.enchant_remaining_display_ms(0x42, 1), None);
|
||||
}
|
||||
|
||||
/// The item-lifetime store (decision 1933) — the enchant timer's shape, one key narrower:
|
||||
/// `seconds == 0` stores ABSENCE, the read is second-floored, the deadline joins the shared
|
||||
/// display epoch, and destroying the item takes its timer with it.
|
||||
#[test]
|
||||
fn an_item_duration_parks_a_deadline_and_dies_with_the_item() {
|
||||
let mut items = Items::default();
|
||||
assert_eq!(
|
||||
items.duration_remaining_ms(0x42),
|
||||
None,
|
||||
"no timer by default"
|
||||
);
|
||||
|
||||
items.insert_object(0x42, ObjectFields::default());
|
||||
items.set_item_duration(0x42, 1800);
|
||||
let shown = items.duration_remaining_display_ms(0x42).expect("parked");
|
||||
assert_eq!(shown % 1000, 0, "the display read is second-floored");
|
||||
assert!(shown <= 1_800_000);
|
||||
let epoch = items.countdown_display_epoch();
|
||||
assert!(
|
||||
(1800..=1801).contains(&epoch),
|
||||
"the lifetime deadline joins the same per-second epoch as the enchant one, got {epoch}"
|
||||
);
|
||||
|
||||
// `0` is the wire's "no timer", never a zero-second timer — the same store the enchant
|
||||
// path takes for the same reason (an expired countdown is an absent line).
|
||||
items.set_item_duration(0x42, 0);
|
||||
assert_eq!(items.duration_remaining_ms(0x42), None);
|
||||
assert_eq!(items.countdown_display_epoch(), 0);
|
||||
|
||||
// A destroyed item takes its timer with it: the guid can be reissued by the server, and a
|
||||
// stale deadline would print a countdown on whatever lands there next.
|
||||
items.insert_object(0x99, ObjectFields::default());
|
||||
items.set_item_duration(0x99, 600);
|
||||
items.remove_object(0x99);
|
||||
assert_eq!(items.duration_remaining_ms(0x99), None);
|
||||
|
||||
// **An item we do not hold is dropped outright** — no queue, no retry, and nothing
|
||||
// parked. `0x1EA`'s arm resolves the guid and returns on a miss; its enchant sibling
|
||||
// `0x1EB` is the one that enqueues and replays, and copying that here would grow the map
|
||||
// without bound for guids that never arrive.
|
||||
items.set_item_duration(0xdead, 600);
|
||||
assert_eq!(items.duration_remaining_ms(0xdead), None);
|
||||
assert_eq!(items.countdown_display_epoch(), 0);
|
||||
|
||||
// **A non-POSITIVE duration clears** — the reference's test is SIGNED (`0x5d9c0c jle`),
|
||||
// so a wire value with the top bit set zeroes the cell instead of parking a 68-year
|
||||
// deadline.
|
||||
items.insert_object(0x7, ObjectFields::default());
|
||||
items.set_item_duration(0x7, 600);
|
||||
assert!(items.duration_remaining_ms(0x7).is_some());
|
||||
items.set_item_duration(0x7, 0x8000_0000);
|
||||
assert_eq!(
|
||||
items.duration_remaining_ms(0x7),
|
||||
None,
|
||||
"a negative-as-signed duration clears, it does not park a far future"
|
||||
);
|
||||
}
|
||||
|
||||
/// The push half of the tooltip store: a landed template is marked fresh exactly once (the
|
||||
/// drain empties), and a cached negative never is (there's nothing to push).
|
||||
#[test]
|
||||
|
||||
@@ -711,7 +711,7 @@ pub fn run(build: BuildId) -> AppExit {
|
||||
// in shape — a guid-carrying question over an already-closed gossip menu.
|
||||
.add_plugins(UiTalentWipePlugin)
|
||||
// The stance/shapeshift bar feed (wow-re shapeshift-bar-api.md): builds the form list from
|
||||
// PlayerActions.spells per the byte-verified admission/order, drives StanceBar.xml through
|
||||
// PlayerActions.spells per the byte-verified admission/order, drives the stock shapeshift bar through
|
||||
// the engine's shapeshift seam, and drains its clicks (cancel-if-active else cast). After
|
||||
// UiActionPlugin (shares `Spells`, the `usable` walk, and the cast tail).
|
||||
.add_plugins(UiShapeshiftPlugin)
|
||||
|
||||
@@ -770,6 +770,15 @@ pub(crate) enum ClientCommand {
|
||||
jump: Option<JumpInfo>,
|
||||
transport: Option<TransportPose>,
|
||||
},
|
||||
/// Report that our mover's simulation advanced through `lag_ms` without integrating
|
||||
/// (`CMSG_MOVE_TIME_SKIPPED`) — decision 1935. `guid` is the mover's own, the reference's
|
||||
/// `0x600be0` gate being that it equals the active-mover globals. The server folds the number
|
||||
/// into its copy of that mover's movement clock (`stime`/`ctime`), and — while it thinks we
|
||||
/// have just boarded a transport — answers by re-sending the transport's create update.
|
||||
MoveTimeSkipped {
|
||||
guid: u64,
|
||||
lag_ms: u32,
|
||||
},
|
||||
/// Claim `guid` as our mover (`CMSG_SET_ACTIVE_MOVER`). Login sends it for our own body; a
|
||||
/// possession handoff re-sends it for the unit we were handed, because the server drops every
|
||||
/// `MSG_MOVE_*` for a mover it has not confirmed.
|
||||
@@ -881,6 +890,21 @@ pub(crate) enum ClientCommand {
|
||||
bag_index: u8,
|
||||
slot: u8,
|
||||
},
|
||||
/// Wrap the item at `(item_bag, item_slot)` in the paper at `(gift_bag, gift_slot)`
|
||||
/// (`CMSG_WRAP_ITEM`, the same bag addressing) — the completion of the local wrap cursor a
|
||||
/// `ITEM_FLAG_WRAPPER` item's right-click armed. The **paper leads**, as the wire does.
|
||||
///
|
||||
/// The client applies no eligibility filter of its own (decision 1934): an equipped item, a
|
||||
/// bag, a soulbound or stackable or unique item, or an already-wrapped one all send and are
|
||||
/// refused server-side with one of the six `ERR_CANT_WRAP_*` reasons, arriving here as an
|
||||
/// `InventoryFailure` on the UI error line. Success is silent — field updates on the target
|
||||
/// and one paper destroyed.
|
||||
WrapItem {
|
||||
gift_bag: u8,
|
||||
gift_slot: u8,
|
||||
item_bag: u8,
|
||||
item_slot: u8,
|
||||
},
|
||||
/// Equip a bag item (`CMSG_AUTOEQUIP_ITEM`, same bag addressing) — the drain's fork for an
|
||||
/// *equippable* click, mirroring the real client's equip-vs-use decision. Refusals come back
|
||||
/// as `InventoryFailure` events onto the UI error line.
|
||||
|
||||
@@ -106,11 +106,17 @@ pub(super) fn apply_net_updates(
|
||||
mut reputations: ResMut<Reputations>,
|
||||
mut transforms: Query<&mut Transform>,
|
||||
mut stores: Query<&mut ObjectStore>,
|
||||
// One tuple param (the 16-SystemParam ceiling this signature already lives against): the two
|
||||
// One tuple param (the 16-SystemParam ceiling this signature already lives against): the three
|
||||
// per-unit motion states the drain writes. [`RemoteMotion`] is a relayed player's dead-reckon;
|
||||
// [`UnitMoveModes`] is any unit's server-granted movement modes (decision 1780). Different
|
||||
// components, one concern — what the wire says about how a body we don't control is moving.
|
||||
mut motion: (Query<&mut RemoteMotion>, Query<&mut UnitMoveModes>),
|
||||
// [`UnitMoveModes`] is any unit's server-granted movement modes (decision 1780);
|
||||
// [`crate::transport::TransportRider`] is the deck a unit is standing on and its pose there
|
||||
// (decision 1936). Different components, one concern — what the wire says about how a body we
|
||||
// don't control is moving.
|
||||
mut motion: (
|
||||
Query<&mut RemoteMotion>,
|
||||
Query<&mut UnitMoveModes>,
|
||||
Query<&mut crate::transport::TransportRider>,
|
||||
),
|
||||
// One tuple param (the 16-SystemParam ceiling): the session-lifecycle one-shot writers — the
|
||||
// player's teleport/worldport snaps + the glue-screen edges (decision 0193).
|
||||
session_msgs: (
|
||||
@@ -651,6 +657,16 @@ pub(super) fn apply_net_updates(
|
||||
&index,
|
||||
&mut transforms,
|
||||
),
|
||||
// **An observed mover skipped time** (decision 1935). No pose moved — only that
|
||||
// unit's clock ran on — so this touches its relay chain and nothing else, which is
|
||||
// the whole of what the reference's handler does (`0x603b40` → `0x61ab90`:
|
||||
// `[CMovement+0xac] += lag`). A guid we do not hold is dropped, faithfully: the
|
||||
// reference resolves under `TYPEMASK_UNIT` and returns on a miss.
|
||||
SessionEvent::MoveTimeSkipped { guid, lag_ms } => {
|
||||
if let Some(mut m) = index.0.get(&guid).and_then(|&e| motion.0.get_mut(e).ok()) {
|
||||
m.relay.skip_time(lag_ms);
|
||||
}
|
||||
}
|
||||
SessionEvent::UnitMove {
|
||||
guid,
|
||||
position,
|
||||
@@ -717,6 +733,7 @@ pub(super) fn apply_net_updates(
|
||||
}
|
||||
SessionEvent::MonsterMove {
|
||||
guid,
|
||||
transport,
|
||||
start,
|
||||
spline_id,
|
||||
path,
|
||||
@@ -727,6 +744,7 @@ pub(super) fn apply_net_updates(
|
||||
run_mode,
|
||||
} => objects::monster_move(
|
||||
guid,
|
||||
transport,
|
||||
start,
|
||||
spline_id,
|
||||
path,
|
||||
@@ -739,6 +757,7 @@ pub(super) fn apply_net_updates(
|
||||
&mut commands,
|
||||
&index,
|
||||
&mut transforms,
|
||||
&mut motion.2,
|
||||
),
|
||||
SessionEvent::Teleport {
|
||||
guid,
|
||||
@@ -1668,6 +1687,12 @@ pub(super) fn apply_net_updates(
|
||||
item_guid,
|
||||
spell_id,
|
||||
} => item_cooldown(item_guid, spell_id, &items, &mut ui_actions.10),
|
||||
// The item-lifetime countdown's ONLY feed (decision 1933): park the deadline on the
|
||||
// item store, exactly as the enchant timer below does — vmangos's own writer says the
|
||||
// `ITEM_FIELD_DURATION` field the client also holds is not what it displays from.
|
||||
SessionEvent::ItemTime { item_guid, seconds } => {
|
||||
items.set_item_duration(item_guid, seconds)
|
||||
}
|
||||
// The temporary-enchant countdown's ONLY feed (decision 0920): park the deadline on the
|
||||
// item store, which every tooltip surface reads back through `enchant_remaining_ms`.
|
||||
SessionEvent::ItemEnchantTime {
|
||||
|
||||
@@ -488,11 +488,20 @@ pub(super) fn objects_removed(guids: Vec<u64>, commands: &mut Commands, index: &
|
||||
}
|
||||
}
|
||||
|
||||
/// A creature path packet (`SMSG_MONSTER_MOVE`): apply the dictated facing snap, then attach or
|
||||
/// clear the travel spline.
|
||||
/// A creature path packet (`SMSG_MONSTER_MOVE`, or its deck twin `SMSG_MONSTER_MOVE_TRANSPORT`):
|
||||
/// settle the unit's transport membership, apply the dictated facing snap, then attach or clear
|
||||
/// the travel spline.
|
||||
///
|
||||
/// **`transport` changes the frame of everything else in the packet** (decision 1936). When it is
|
||||
/// `Some`, `start`, every `path` point and an `Angle` facing are offsets in that transport's frame,
|
||||
/// the unit becomes (or stays) a [`TransportRider`] on it, and the spline is sampled into the
|
||||
/// rider's local pose for `transport::compose_riders` to carry out to the world. When it is `None`
|
||||
/// on a unit we had riding, the unit has *left* the deck — vmangos drops it from the transport on
|
||||
/// exactly this edge (`MoveSplineInit::Launch`, `spline/MoveSplineInit.cpp:156-159`).
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub(super) fn monster_move(
|
||||
guid: u64,
|
||||
transport: Option<u64>,
|
||||
start: [f32; 3],
|
||||
spline_id: u32,
|
||||
path: Vec<[f32; 3]>,
|
||||
@@ -505,6 +514,7 @@ pub(super) fn monster_move(
|
||||
commands: &mut Commands,
|
||||
index: &GuidIndex,
|
||||
transforms: &mut Query<&mut Transform>,
|
||||
riders: &mut Query<&mut crate::transport::TransportRider>,
|
||||
) {
|
||||
if let Some(&e) = index.0.get(&guid) {
|
||||
// The DESYNC readout (decision 0708): how far this packet is about to teleport the unit — the
|
||||
@@ -512,11 +522,78 @@ pub(super) fn monster_move(
|
||||
// correctly-followed creature reads ~0; a frozen one reads the whole walk it slept through.
|
||||
trace_move_snap(
|
||||
guid,
|
||||
transforms.get(e).ok().map(|t| bevy_to_wow(t.translation)),
|
||||
// Both sides of the readout have to be in ONE frame. A deck packet's `start` is a
|
||||
// transport-local offset, so it is compared against the rider's own local pose, not
|
||||
// against the composed world position (which would read as the whole boat's travel).
|
||||
match transport {
|
||||
Some(_) => riders.get(e).ok().map(|r| r.local_pos),
|
||||
None => transforms.get(e).ok().map(|t| bevy_to_wow(t.translation)),
|
||||
},
|
||||
start,
|
||||
stop,
|
||||
duration_ms,
|
||||
);
|
||||
// **Transport membership, settled before anything reads a pose.** A packet naming a
|
||||
// transport attaches the unit to it (and re-seeds its deck-local pose from the packet's
|
||||
// own `start`, so the stop form places a body too); a packet naming none detaches a unit
|
||||
// we had riding. The facing that goes with it is deck-local for an `Angle` — vmangos runs
|
||||
// `CalculatePassengerOffset(…, &args.facing.angle)` on it (`MoveSplineInit.cpp:88`) — but
|
||||
// a `Spot`/`Target` facing is **not** converted server-side, so those resolve to a world
|
||||
// bearing and are rebased here by the deck's own yaw.
|
||||
//
|
||||
// **Above the root gate deliberately.** The gate below refuses the *path*, which is what
|
||||
// wow-re establishes; membership is not a path. A rooted body on a deck that was left
|
||||
// unattached would be abandoned in the sea as the boat sails out from under it, which is
|
||||
// a worse failure than carrying a pinned body along with the deck it is standing on.
|
||||
let deck_yaw = |t: u64| {
|
||||
index
|
||||
.0
|
||||
.get(&t)
|
||||
.and_then(|&te| transforms.get(te).ok())
|
||||
.map(|tf| tf.rotation.to_euler(EulerRot::YXZ).0)
|
||||
};
|
||||
if let Some(t) = transport {
|
||||
let local_facing = match facing {
|
||||
MonsterMoveFacing::None => None,
|
||||
MonsterMoveFacing::Angle(a) => Some(a),
|
||||
other => {
|
||||
let target_pos = |g: u64| {
|
||||
index
|
||||
.0
|
||||
.get(&g)
|
||||
.and_then(|&te| transforms.get(te).ok())
|
||||
.map(|tf| bevy_to_wow(tf.translation))
|
||||
};
|
||||
// The unit's own WORLD position anchors the bearing (the wire's `start` is
|
||||
// deck-local and would put the unit near the map origin).
|
||||
let world_pos = transforms.get(e).ok().map(|tf| bevy_to_wow(tf.translation));
|
||||
world_pos
|
||||
.and_then(|p| resolve_facing(other, p, target_pos))
|
||||
.zip(deck_yaw(t))
|
||||
.map(|(world, yaw)| world - yaw)
|
||||
}
|
||||
};
|
||||
match riders.get_mut(e) {
|
||||
Ok(mut rider) => {
|
||||
rider.transport_guid = t;
|
||||
rider.local_pos = start;
|
||||
if let Some(o) = local_facing {
|
||||
rider.local_orientation = o;
|
||||
}
|
||||
}
|
||||
Err(_) => {
|
||||
commands.entity(e).insert(crate::transport::TransportRider {
|
||||
transport_guid: t,
|
||||
local_pos: start,
|
||||
local_orientation: local_facing.unwrap_or_default(),
|
||||
});
|
||||
}
|
||||
}
|
||||
} else if riders.get(e).is_ok() {
|
||||
commands
|
||||
.entity(e)
|
||||
.remove::<crate::transport::TransportRider>();
|
||||
}
|
||||
// Apply the dictated final facing (moveType 2/3/4) as a **snap** — faithful to the
|
||||
// client, which stores it straight into the unit's **raw** movement facing (`0x7c6f30`).
|
||||
// This is the *packet*-driven re-face — a scripted/emote/aggro `SetFacingTo` the
|
||||
@@ -529,7 +606,9 @@ pub(super) fn monster_move(
|
||||
// frame (faithful — the client's spline-follow snaps the mesh yaw to the path tangent;
|
||||
// wow-re body-facing §4). The receipt snap thus only sticks for a path-less move (a
|
||||
// `Stop`/in-place re-face); a moving unit ends on its last tangent.
|
||||
if !matches!(facing, MonsterMoveFacing::None) {
|
||||
// The world-space facing snap — for a deck packet the rider's `local_orientation` above is
|
||||
// the one that counts, and `compose_riders` writes the rotation from it.
|
||||
if transport.is_none() && !matches!(facing, MonsterMoveFacing::None) {
|
||||
let target_pos = |g: u64| {
|
||||
index
|
||||
.0
|
||||
@@ -585,7 +664,15 @@ pub(super) fn monster_move(
|
||||
// root gate because it is the *install's* own consequence: with no path taken there is no
|
||||
// new authority to hand the pose to.
|
||||
commands.entity(e).remove::<RemoteMotion>();
|
||||
match monster_move_spline(path, spline_id, stop, duration_ms, flying, run_mode) {
|
||||
match monster_move_spline(
|
||||
path,
|
||||
spline_id,
|
||||
stop,
|
||||
duration_ms,
|
||||
flying,
|
||||
run_mode,
|
||||
transport,
|
||||
) {
|
||||
// A moving path: sample_splines drives the transform along every waypoint.
|
||||
Some(spline) => {
|
||||
commands.entity(e).insert(spline).remove::<SplineStopped>();
|
||||
@@ -1074,9 +1161,11 @@ mod tests {
|
||||
w.run_system_once(
|
||||
move |mut commands: Commands,
|
||||
index: Res<GuidIndex>,
|
||||
mut transforms: Query<&mut Transform>| {
|
||||
mut transforms: Query<&mut Transform>,
|
||||
mut riders: Query<&mut crate::transport::TransportRider>| {
|
||||
monster_move(
|
||||
MOB,
|
||||
None,
|
||||
[0.0, 0.0, 0.0],
|
||||
7,
|
||||
path.clone(),
|
||||
@@ -1089,6 +1178,7 @@ mod tests {
|
||||
&mut commands,
|
||||
&index,
|
||||
&mut transforms,
|
||||
&mut riders,
|
||||
);
|
||||
},
|
||||
)
|
||||
@@ -1100,6 +1190,115 @@ mod tests {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// **`SMSG_MONSTER_MOVE_TRANSPORT` attaches, and its plain twin detaches** (decision
|
||||
/// 1936). A packet naming a transport makes the unit a [`TransportRider`] on it, seeds the
|
||||
/// rider pose from the packet's own deck-local `start`, and hands the spline the deck so
|
||||
/// the sampler writes the local pose instead of the world transform. A packet naming none
|
||||
/// takes the unit off the deck, which is the edge vmangos itself drops the passenger on
|
||||
/// (`MoveSplineInit::Launch`, `spline/MoveSplineInit.cpp:156-159`).
|
||||
#[test]
|
||||
fn a_transport_path_attaches_the_rider_and_a_plain_one_lets_it_go() {
|
||||
const BOAT: u64 = 0x2000_0000_0000_0007;
|
||||
let mut w = World::new();
|
||||
let e = w
|
||||
.spawn((
|
||||
Transform::default(),
|
||||
NetEntity {
|
||||
kind: EntityKind::Unit,
|
||||
display_id: None,
|
||||
scale: 1.0,
|
||||
},
|
||||
))
|
||||
.id();
|
||||
let boat = w.spawn(Transform::default()).id();
|
||||
let mut index = GuidIndex::default();
|
||||
index.0.insert(MOB, e);
|
||||
index.0.insert(BOAT, boat);
|
||||
w.insert_resource(index);
|
||||
|
||||
let deck_path = vec![[1.5, -2.5, 0.75], [4.0, -2.5, 0.75]];
|
||||
w.run_system_once(
|
||||
move |mut commands: Commands,
|
||||
index: Res<GuidIndex>,
|
||||
mut transforms: Query<&mut Transform>,
|
||||
mut riders: Query<&mut crate::transport::TransportRider>| {
|
||||
monster_move(
|
||||
MOB,
|
||||
Some(BOAT),
|
||||
[1.5, -2.5, 0.75],
|
||||
7,
|
||||
deck_path.clone(),
|
||||
MonsterMoveFacing::Angle(1.25),
|
||||
false,
|
||||
3000,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
&mut commands,
|
||||
&index,
|
||||
&mut transforms,
|
||||
&mut riders,
|
||||
);
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
let rider = w
|
||||
.entity(e)
|
||||
.get::<crate::transport::TransportRider>()
|
||||
.expect("a transport path attaches its rider");
|
||||
assert_eq!(rider.transport_guid, BOAT);
|
||||
assert_eq!(rider.local_pos, [1.5, -2.5, 0.75], "seeded deck-local");
|
||||
assert!(
|
||||
(rider.local_orientation - 1.25).abs() < 1e-6,
|
||||
"Angle is deck-local"
|
||||
);
|
||||
assert_eq!(
|
||||
w.entity(e).get::<Spline>().expect("a path").deck,
|
||||
Some(BOAT),
|
||||
"the spline rides the deck frame, not the world"
|
||||
);
|
||||
// The rider's world transform is `compose_riders`' to write, not this apply's — the
|
||||
// deck-local start must never have been mistaken for a world position.
|
||||
assert_eq!(
|
||||
w.entity(e).get::<Transform>().unwrap().translation,
|
||||
Vec3::ZERO
|
||||
);
|
||||
|
||||
let world_path = vec![[100.0, 200.0, 30.0], [110.0, 200.0, 30.0]];
|
||||
w.run_system_once(
|
||||
move |mut commands: Commands,
|
||||
index: Res<GuidIndex>,
|
||||
mut transforms: Query<&mut Transform>,
|
||||
mut riders: Query<&mut crate::transport::TransportRider>| {
|
||||
monster_move(
|
||||
MOB,
|
||||
None,
|
||||
[100.0, 200.0, 30.0],
|
||||
8,
|
||||
world_path.clone(),
|
||||
MonsterMoveFacing::None,
|
||||
false,
|
||||
3000,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
&mut commands,
|
||||
&index,
|
||||
&mut transforms,
|
||||
&mut riders,
|
||||
);
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
assert!(
|
||||
w.entity(e)
|
||||
.get::<crate::transport::TransportRider>()
|
||||
.is_none(),
|
||||
"a plain path takes the unit off the deck"
|
||||
);
|
||||
assert_eq!(w.entity(e).get::<Spline>().expect("a path").deck, None);
|
||||
}
|
||||
}
|
||||
|
||||
/// A dismounted human's set — 1.12.1's base run 7.0 (the `--speed` probe's own golden).
|
||||
|
||||
@@ -832,6 +832,9 @@ fn writer_loop(
|
||||
orientation,
|
||||
spline_id,
|
||||
} => w.move_spline_done(flags, pos, orientation, spline_id),
|
||||
ClientCommand::MoveTimeSkipped { guid, lag_ms } => {
|
||||
w.move_time_skipped(guid, lag_ms)
|
||||
}
|
||||
ClientCommand::ForceSpeedAck {
|
||||
kind,
|
||||
guid,
|
||||
@@ -919,6 +922,12 @@ fn writer_loop(
|
||||
target,
|
||||
} => w.use_item(bag_index, slot, spell_index, target),
|
||||
ClientCommand::OpenItem { bag_index, slot } => w.open_item(bag_index, slot),
|
||||
ClientCommand::WrapItem {
|
||||
gift_bag,
|
||||
gift_slot,
|
||||
item_bag,
|
||||
item_slot,
|
||||
} => w.wrap_item(gift_bag, gift_slot, item_bag, item_slot),
|
||||
ClientCommand::AutoEquipItem { bag_index, slot } => {
|
||||
w.auto_equip_item(bag_index, slot)
|
||||
}
|
||||
|
||||
@@ -156,6 +156,25 @@ impl RelayChain {
|
||||
fire_ms
|
||||
}
|
||||
|
||||
/// **The sender skipped time** (`MSG_MOVE_TIME_SKIPPED`, decision 1935): advance this chain's
|
||||
/// copy of the mover's wire clock by `lag_ms`, without scheduling anything. The reference does
|
||||
/// exactly this and nothing else — `0x603b40` resolves the unit and `0x61ab90` runs
|
||||
/// `[CMovement+0xac] += lag`, which is this field.
|
||||
///
|
||||
/// It has to be applied even though no pose moved, because [`Self::schedule`] pays the whole
|
||||
/// chain off `last_wire_ms`: leave it short and the mover's next real packet reads as a `step`
|
||||
/// `lag` ms larger than it was, buys that much extra `wire_delta`, and fires `lag` late — a
|
||||
/// hitch on one unit that looks like packet loss and isn't. Wrapping, for
|
||||
/// [`Self::schedule`]'s reason: the stamp is a `u32` ms clock that wraps every 49.7 days.
|
||||
///
|
||||
/// A chain that has never seen a packet is left alone: there is no reference stamp to advance
|
||||
/// yet, and the first real packet seeds both cells from itself.
|
||||
pub(crate) fn skip_time(&mut self, lag_ms: u32) {
|
||||
if self.seeded {
|
||||
self.last_wire_ms = self.last_wire_ms.wrapping_add(lag_ms);
|
||||
}
|
||||
}
|
||||
|
||||
/// Record this packet's lateness and return the window's worst (`0x618b50`): store
|
||||
/// `base + lateness` at the cursor, advance it mod 32, and return the max over all 32 slots (the
|
||||
/// entry just written included — the reference seeds its scan with it and walks the whole ring).
|
||||
|
||||
@@ -42,6 +42,17 @@ pub(crate) struct Spline {
|
||||
/// gait is derived from [`Spline::speed`], the path's arc length over its duration, so its
|
||||
/// walk bit would select nothing.
|
||||
pub(crate) run_mode: bool,
|
||||
/// **A deck path** (`SMSG_MONSTER_MOVE_TRANSPORT`): the transport guid whose frame every
|
||||
/// point in `points` is expressed in. `None` — the overwhelmingly common case — is the plain
|
||||
/// `SMSG_MONSTER_MOVE`, whose points are absolute world coordinates.
|
||||
///
|
||||
/// A deck path is sampled into the rider's [`crate::transport::TransportRider`] local pose
|
||||
/// rather than straight into the [`Transform`]; `compose_riders` then carries it through the
|
||||
/// transport's live matrix, exactly as it already does for a rider whose pose came off a
|
||||
/// `MSG_MOVE_*` relay. It is also exempt from [`ground_clamp_creatures`]: the re-ground is a
|
||||
/// world-space terrain probe, there is no terrain under a deck, and the wire Z is already the
|
||||
/// deck-local height (decision 1936).
|
||||
pub(crate) deck: Option<u64>,
|
||||
}
|
||||
|
||||
/// **A spline the server ended by decree**, carrying the id the acknowledgement owes (decision
|
||||
@@ -234,6 +245,7 @@ fn catmull_rom(pts: &[[f32; 3]], i: usize, u: f32) -> ([f32; 3], [f32; 3]) {
|
||||
/// takes the Catmull-Rom family instead (the taxi/flight look — see [`Spline::sample`]'s INTERIM note).
|
||||
/// Returns `None` — "stationary, clear any path" — for a `Stop`, a zero duration, or a path with fewer
|
||||
/// than two points (nothing to travel along).
|
||||
#[allow(clippy::too_many_arguments)] // one argument per decoded wire field
|
||||
pub(in crate::net) fn monster_move_spline(
|
||||
path: Vec<[f32; 3]>,
|
||||
spline_id: u32,
|
||||
@@ -241,6 +253,7 @@ pub(in crate::net) fn monster_move_spline(
|
||||
duration_ms: u32,
|
||||
flying: bool,
|
||||
run_mode: bool,
|
||||
deck: Option<u64>,
|
||||
) -> Option<Spline> {
|
||||
if stop || duration_ms == 0 || path.len() < 2 {
|
||||
return None;
|
||||
@@ -250,8 +263,13 @@ pub(in crate::net) fn monster_move_spline(
|
||||
start: Instant::now(),
|
||||
duration: Duration::from_millis(u64::from(duration_ms)),
|
||||
id: spline_id,
|
||||
// A deck path's Z is the deck-local height and nothing re-derives it, so `grounded` — the
|
||||
// terrain-clamp opt-in — is meaningless there; `deck` is what the clamp actually skips on.
|
||||
// The flag still rides along because the SAMPLER reads it too: `grounded` selects the
|
||||
// linear follow, `!grounded` the Catmull-Rom flight family (decision 1936).
|
||||
grounded: !flying,
|
||||
run_mode,
|
||||
deck,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -286,6 +304,10 @@ pub(in crate::net) fn create_spline(spline: CreateSpline) -> Option<Spline> {
|
||||
id: spline.id,
|
||||
grounded: !spline.flying,
|
||||
run_mode: spline.run_mode,
|
||||
// The create block's spline tail carries no transport of its own — a unit created on a
|
||||
// deck takes its `TransportRider` from the same block's movement tail, and its ride is in
|
||||
// world coordinates like any other create spline.
|
||||
deck: None,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -391,6 +413,12 @@ pub(in crate::net) fn sample_splines(
|
||||
// The rider's server identity, for the `spl` trace alone — `Option` because the sampler
|
||||
// must never gate on it (a spline entity with no guid still has to be flown).
|
||||
Option<&super::super::Guid>,
|
||||
// A **deck path**'s destination: the sample is the rider's pose in the transport's frame,
|
||||
// and `transport::compose_riders` — which runs after this stage — carries it out to the
|
||||
// world. `Option` because only a `SMSG_MONSTER_MOVE_TRANSPORT` rider has one; a deck
|
||||
// spline whose component is missing falls back to the world write below rather than
|
||||
// freezing (decision 1936).
|
||||
Option<&mut crate::transport::TransportRider>,
|
||||
)>,
|
||||
mut trace_next: Local<f32>,
|
||||
time: Res<Time>,
|
||||
@@ -404,8 +432,30 @@ pub(in crate::net) fn sample_splines(
|
||||
if tick {
|
||||
*trace_next = time.elapsed_secs() + SPL_TRACE_SECS;
|
||||
}
|
||||
for (entity, spline, mut t, swimming, guid) in &mut q {
|
||||
for (entity, spline, mut t, swimming, guid, rider) in &mut q {
|
||||
let (wow_pos, facing, pitch) = spline.sample(now);
|
||||
// **The deck fork.** A transport path's points are offsets in the named transport's frame,
|
||||
// so the sample IS the rider's local pose; writing it to the `Transform` would place the
|
||||
// unit a few yards from the world origin. `compose_riders` composes it through the deck's
|
||||
// live matrix later this frame — the same route an observed rider's relayed pose takes.
|
||||
if let Some(deck) = spline.deck {
|
||||
// A deck sample lands on the rider or NOWHERE. Falling through to the world write
|
||||
// below would put the body a few yards from the map origin, because that is what a
|
||||
// transport-local offset reads as in world coordinates — so a rider component that is
|
||||
// missing (the insert is deferred by one sync point) or that names a different
|
||||
// transport freezes the unit for that frame instead. Freezing is recoverable; a body
|
||||
// teleported to the middle of the map is what a bug report looks like.
|
||||
if let Some(mut rider) = rider.filter(|r| r.transport_guid == deck) {
|
||||
rider.local_pos = wow_pos;
|
||||
if let Some(f) = facing {
|
||||
rider.local_orientation = f;
|
||||
}
|
||||
}
|
||||
if now.saturating_duration_since(spline.start) >= spline.duration {
|
||||
commands.entity(entity).remove::<Spline>();
|
||||
}
|
||||
continue;
|
||||
}
|
||||
// What the transform held *coming in* — i.e. whatever the previous frame left there after
|
||||
// every other writer had its turn. Traced beside the fresh sample as `was=`: a rider whose
|
||||
// `was` is not last frame's sample is being stomped by another system, which no amount of
|
||||
@@ -687,6 +737,10 @@ pub(in crate::net) fn ground_clamp_creatures(
|
||||
skipped += 1;
|
||||
continue; // a flying path is authoritative on Z
|
||||
}
|
||||
if spline.is_some_and(|s| s.deck.is_some()) {
|
||||
skipped += 1;
|
||||
continue; // a deck path: no terrain under a boat, and the wire Z is deck-local (1936)
|
||||
}
|
||||
if swimming {
|
||||
skipped += 1;
|
||||
continue; // in-liquid: the wire Z is the creature's swim depth
|
||||
@@ -1330,6 +1384,7 @@ mod tests {
|
||||
/// A spline `frac` of the way through its ride: 10 s duration, started `frac·10 s` ago.
|
||||
fn spline_at(points: Vec<[f32; 3]>, grounded: bool, frac: f32) -> (Spline, Instant) {
|
||||
let spline = Spline {
|
||||
deck: None,
|
||||
points,
|
||||
start: Instant::now() - Duration::from_secs_f32(10.0 * frac),
|
||||
duration: Duration::from_secs(10),
|
||||
@@ -1697,6 +1752,7 @@ mod server_moved_players {
|
||||
/// sends for a bot and for a charged/knocked-back/feared player alike.
|
||||
fn walking(app: &mut App, e: Entity) {
|
||||
app.world_mut().entity_mut(e).insert(Spline {
|
||||
deck: None,
|
||||
points: vec![RIM_A, RIM_B],
|
||||
start: Instant::now(),
|
||||
duration: Duration::from_secs(4),
|
||||
@@ -1781,6 +1837,7 @@ mod server_moved_players {
|
||||
fn a_flying_path_is_left_alone() {
|
||||
let (mut app, body) = hollow(EntityKind::Player);
|
||||
app.world_mut().entity_mut(body).insert(Spline {
|
||||
deck: None,
|
||||
points: vec![RIM_A, RIM_B],
|
||||
start: Instant::now(),
|
||||
duration: Duration::from_secs(4),
|
||||
|
||||
@@ -426,6 +426,7 @@ fn spline_interpolates_constant_speed_and_faces_travel() {
|
||||
// Two legs: 10 yd east (+X), then 10 yd north-ish (+Y), over 4s total (constant speed → 2s/leg).
|
||||
let start = Instant::now();
|
||||
let s = Spline {
|
||||
deck: None,
|
||||
points: vec![[0.0, 0.0, 0.0], [10.0, 0.0, 0.0], [10.0, 10.0, 0.0]],
|
||||
start,
|
||||
duration: Duration::from_secs(4),
|
||||
@@ -466,6 +467,7 @@ fn spline_travel_pitch_is_the_segment_climb_angle() {
|
||||
// observed-mover pitch rule `asin(dir.z)` the swimming-creature body pitch renders.
|
||||
let start = Instant::now();
|
||||
let s = Spline {
|
||||
deck: None,
|
||||
points: vec![[0.0, 0.0, 0.0], [10.0, 0.0, 10.0]],
|
||||
start,
|
||||
duration: Duration::from_secs(4),
|
||||
@@ -491,7 +493,7 @@ fn monster_move_carries_every_waypoint() {
|
||||
[10.0, 10.0, 0.0],
|
||||
[10.0, 10.0, 5.0],
|
||||
];
|
||||
let s = monster_move_spline(path.clone(), 42, false, 2000, false, true)
|
||||
let s = monster_move_spline(path.clone(), 42, false, 2000, false, true, None)
|
||||
.expect("a moving monster-move yields a spline");
|
||||
assert_eq!(
|
||||
s.points, path,
|
||||
@@ -508,6 +510,54 @@ fn monster_move_carries_every_waypoint() {
|
||||
);
|
||||
}
|
||||
|
||||
/// **`MSG_MOVE_TIME_SKIPPED` advances the chain's wire clock and nothing else** (decision 1935).
|
||||
/// The whole of the reference's handler is `[CMovement+0xac] += lag` (`0x603b40` → `0x61ab90`),
|
||||
/// and `+0xac` is this chain's `last_wire_ms`. The property that matters is downstream: after the
|
||||
/// skip, the mover's next packet — whose stamp is `lag` further on than it would otherwise have
|
||||
/// been — must schedule as though nothing unusual happened. Drop the skip and that same packet
|
||||
/// buys `lag` ms of extra `wire_delta` and fires that much late.
|
||||
#[test]
|
||||
fn a_reported_skip_keeps_the_relay_chain_level_with_the_sender() {
|
||||
let step = |chain: &mut RelayChain, wire_ms: u32, now_ms: f64| {
|
||||
chain.schedule(wire_ms, now_ms, 0, true)
|
||||
};
|
||||
// Two chains fed identically, except that one is told about the sender's 300 ms skip.
|
||||
let (mut told, mut untold) = (RelayChain::default(), RelayChain::default());
|
||||
step(&mut told, 10_000, 0.0);
|
||||
step(&mut untold, 10_000, 0.0);
|
||||
|
||||
told.skip_time(300);
|
||||
|
||||
// The sender's next packet: 100 ms of real play later, but its stamp has ALSO carried the
|
||||
// 300 ms it skipped — so the wire step is 400 while only 100 ms of our clock passed.
|
||||
let a = step(&mut told, 10_400, 100.0);
|
||||
let b = step(&mut untold, 10_400, 100.0);
|
||||
assert!(
|
||||
a < b,
|
||||
"the informed chain schedules earlier: it already knew 300 of those 400 ms were skipped, \
|
||||
not travelled (informed {a}, uninformed {b})"
|
||||
);
|
||||
assert!(
|
||||
(b - a - 300.0).abs() < 1e-6,
|
||||
"and the difference is exactly the skip it was told about: {} vs 300",
|
||||
b - a
|
||||
);
|
||||
}
|
||||
|
||||
/// A skip for a mover whose chain has never seen a packet is inert: there is no reference stamp
|
||||
/// to advance, and the first real packet seeds both cells off itself.
|
||||
#[test]
|
||||
fn a_skip_before_the_first_packet_changes_nothing() {
|
||||
let mut seeded = RelayChain::default();
|
||||
let mut cold = RelayChain::default();
|
||||
cold.skip_time(5_000);
|
||||
assert_eq!(
|
||||
seeded.schedule(10_000, 0.0, 0, true),
|
||||
cold.schedule(10_000, 0.0, 0, true),
|
||||
"an unseeded chain has no clock to move"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn monster_move_flying_spline_is_not_grounded() {
|
||||
// A FLYING path keeps the server's Z — the ground-clamp must leave it alone.
|
||||
@@ -518,6 +568,7 @@ fn monster_move_flying_spline_is_not_grounded() {
|
||||
2000,
|
||||
true,
|
||||
true,
|
||||
None,
|
||||
)
|
||||
.expect("a flying monster-move still yields a spline");
|
||||
assert!(
|
||||
@@ -535,7 +586,8 @@ fn monster_move_stop_clears_the_spline() {
|
||||
true,
|
||||
2000,
|
||||
false,
|
||||
true
|
||||
true,
|
||||
None
|
||||
)
|
||||
.is_none(),
|
||||
"a Stop move snaps and clears, never builds a path"
|
||||
@@ -551,7 +603,8 @@ fn monster_move_zero_duration_clears_the_spline() {
|
||||
false,
|
||||
0,
|
||||
false,
|
||||
true
|
||||
true,
|
||||
None
|
||||
)
|
||||
.is_none(),
|
||||
"a zero-duration move would divide by ~0 when sampled; treat as stationary"
|
||||
@@ -561,7 +614,7 @@ fn monster_move_zero_duration_clears_the_spline() {
|
||||
#[test]
|
||||
fn monster_move_without_a_travelable_path_clears_the_spline() {
|
||||
assert!(
|
||||
monster_move_spline(vec![[1.0, 2.0, 3.0]], 0, false, 2000, false, true).is_none(),
|
||||
monster_move_spline(vec![[1.0, 2.0, 3.0]], 0, false, 2000, false, true, None).is_none(),
|
||||
"a single point is nowhere to travel — no spline"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1063,6 +1063,15 @@ pub(super) fn control(
|
||||
orientation: (player.face_yaw - r.boat_yaw).rem_euclid(std::f32::consts::TAU),
|
||||
}
|
||||
});
|
||||
// The skipped-time clock (decision 1935): a held frame is a frame of movement simulation
|
||||
// we advanced through without integrating, and the mover we name is the one we are
|
||||
// actually driving — a possessed unit while we hold its reins. Read out before the call
|
||||
// takes `player` mutably.
|
||||
let skip = movement_net::SkipClock {
|
||||
dt,
|
||||
held: player.settling,
|
||||
mover: player.foreign_mover.or(self_guid),
|
||||
};
|
||||
movement_net::stream_self_movement(
|
||||
&net.0 .0,
|
||||
&mut player,
|
||||
@@ -1090,6 +1099,7 @@ pub(super) fn control(
|
||||
// byte is 0 → `0x615c80 je 0x616539`: no apply, and no ack).
|
||||
knockback.filter(|_| knocked),
|
||||
wire_transport,
|
||||
skip,
|
||||
);
|
||||
} else {
|
||||
// Free fly (pre-connect or detached): aim from the look angles, move the camera directly
|
||||
|
||||
@@ -216,6 +216,20 @@ pub(super) fn swim(feet_y: f32, surface_y: f32, swimming: bool, h: f32) {
|
||||
);
|
||||
}
|
||||
|
||||
/// One `sett` line per `CMSG_MOVE_TIME_SKIPPED` we send (decision 1935) — how many milliseconds
|
||||
/// of movement simulation the hold ran through without integrating, and for which mover. It rides
|
||||
/// the settle's own tag because it reports the settle's own cost: a `skipped` far larger than the
|
||||
/// `sett` line beside it means the hold outlived the stream it was waiting for.
|
||||
pub(super) fn skipped_time(guid: u64, lag_ms: u32) {
|
||||
if !trace::enabled() {
|
||||
return;
|
||||
}
|
||||
trace::line(
|
||||
"sett",
|
||||
&format!("skipped {lag_ms:5} ms for mover {guid:#x}"),
|
||||
);
|
||||
}
|
||||
|
||||
/// How the post-teleport settle hold **ended** — the whole diagnosis of a fall-through report.
|
||||
///
|
||||
/// `resident` means the destination's world arrived (scene spawned + collider queue quiet —
|
||||
|
||||
@@ -89,6 +89,19 @@ const IN_MOTION: u32 = move_flags::ANY_MOVE
|
||||
| move_flags::SWIMMING
|
||||
| move_flags::ON_TRANSPORT;
|
||||
|
||||
/// What this frame's **skipped-time report** ([`stream_skipped_time`]) needs: how long the frame
|
||||
/// was, whether the mover integrated it, and which mover to name.
|
||||
pub(super) struct SkipClock {
|
||||
/// This frame's length in seconds — the budget the mover either integrated or did not.
|
||||
pub(super) dt: f32,
|
||||
/// **The mover held instead of stepping this frame** ([`super::Player::settling`]): the world
|
||||
/// under us has not streamed in, so nothing integrated `dt`.
|
||||
pub(super) held: bool,
|
||||
/// The mover's own guid — a possessed unit's while we hold its reins, else our own body's.
|
||||
/// `None` before the server has named one, which is also before anything could be skipped.
|
||||
pub(super) mover: Option<u64>,
|
||||
}
|
||||
|
||||
/// This frame's **arc edges** — the airborne lifecycle as the send law reads it. One struct rather
|
||||
/// than four adjacent bools in the argument list, where a miscount is silent and the symptom is a
|
||||
/// wrong opcode on the wire.
|
||||
@@ -145,6 +158,7 @@ pub(super) fn stream_self_movement(
|
||||
speed_acks: &[crate::net::SpeedChangeMessage],
|
||||
knock_ack: Option<super::state::PendingKnockback>,
|
||||
transport: Option<TransportPose>,
|
||||
skip: SkipClock,
|
||||
) {
|
||||
let ArcEdges {
|
||||
jumped,
|
||||
@@ -153,6 +167,7 @@ pub(super) fn stream_self_movement(
|
||||
landed,
|
||||
fall_time,
|
||||
} = arc;
|
||||
stream_skipped_time(sender, player, skip);
|
||||
let wow_pos = bevy_to_wow(player.pos);
|
||||
// Normalize the facing into [0, 2π) before it goes on the wire. `face_yaw` is an unbounded
|
||||
// accumulator (mouse-look and A/D turning keep growing it), but the real client always sends a
|
||||
@@ -478,6 +493,65 @@ pub(super) fn stream_self_movement(
|
||||
/// Park our mover on the wire: flush a single `MSG_MOVE_STOP` (flags cleared) so the server — and the
|
||||
/// observers extrapolating from it — drop whatever locomotion flags we last reported, then zero our
|
||||
/// bookkeeping. Called when the controller stops driving the avatar with stale flags still live on the
|
||||
/// **`CMSG_MOVE_TIME_SKIPPED` — the milliseconds the mover advanced through without integrating**
|
||||
/// (decision 1935). Accumulate while the mover holds; report the total once, on the release edge.
|
||||
///
|
||||
/// **The law is "time the movement simulation advanced through without integrating"** — not "a
|
||||
/// long frame", which is what this packet is usually described as. wow-re's carve
|
||||
/// (`collision/scratch/move-time-skipped-law.md`, §5-verified, landed 2026-09-03) found **four**
|
||||
/// emission sites feeding one builder `0x600be0`, and the long-frame one (`0x616642 cmp esi,0xfa`
|
||||
/// → send `dt − 250`) is only one of them and not the common one. The two that dominate real
|
||||
/// traffic are the **no-geometry** pair: the resolve entry's swept query returning nothing
|
||||
/// (`0x634154`) and the fall integrator's TOI kernel finding no contact (`0x635742`), each
|
||||
/// reporting the substep budget they could not spend. Across 79 sniffs of the reference,
|
||||
/// 1277 of 1278 packets carry `lag ≤ 250` — the clamped substep budget those two draw from —
|
||||
/// and an exact-build 36-minute session sent **three**, at world entry and after a teleport.
|
||||
///
|
||||
/// **benilla's no-geometry state is the settle hold**, and that is what this reports: the frames
|
||||
/// after a login, teleport or worldport where the destination's terrain and building colliders
|
||||
/// have not streamed in, so the mover freezes rather than falling through a city that isn't there
|
||||
/// (decision 0737). Same cause, same quantity, same moments the reference's own captures show it.
|
||||
///
|
||||
/// **The coalescing is ours, deliberately.** The reference sends once per firing, which for it is
|
||||
/// once per skipped substep — and its no-geometry window is a substep or two wide, because it
|
||||
/// loads the world before it places you. Ours is a live stream that can hold for seconds, so
|
||||
/// per-frame parity would put hundreds of packets on the wire where the reference puts one. The
|
||||
/// *quantity* is what the wire means and the quantity is preserved whole; the packet count is
|
||||
/// kept at the reference's. What is NOT copied is the send's `[mgr+0x130] += lag` side effect
|
||||
/// (the 500 ms heartbeat deadline pushed out by the skipped time): that deadline is only ever read
|
||||
/// while moving, and a held body streams zeroed flags with its mover parked, so there is no
|
||||
/// heartbeat inside a settle for it to delay. Applying a multi-second delay to the first heartbeat
|
||||
/// *after* one would be our invention, not the reference's.
|
||||
///
|
||||
/// The other two sites are deliberately unbuilt. The long-frame one has no meaning here: benilla
|
||||
/// has no 250 ms substep clamp — it integrates the whole frame — so a long frame is time we
|
||||
/// *spent*, not time we skipped, and reporting it would be a lie about our own simulation. The
|
||||
/// `[CMovement+0x40] & 0x8000000` free-advance bit's own setter is not modelled at all. wow-re's
|
||||
/// own verdict on the build order says as much: a client implementing cases 1–3 matches every
|
||||
/// packet in the shipped captures.
|
||||
fn stream_skipped_time(sender: &Sender<ClientCommand>, player: &mut Player, skip: SkipClock) {
|
||||
if skip.held {
|
||||
player.skipped_ms += skip.dt * 1000.0;
|
||||
return;
|
||||
}
|
||||
// The release edge: report the whole hold at once, then disarm. Sub-millisecond residue is
|
||||
// dropped rather than carried — the wire field is whole milliseconds and a `0` lag is a
|
||||
// packet that says nothing.
|
||||
let lag_ms = player.skipped_ms as u32;
|
||||
player.skipped_ms = 0.0;
|
||||
if lag_ms == 0 {
|
||||
return;
|
||||
}
|
||||
let Some(guid) = skip.mover else {
|
||||
return;
|
||||
};
|
||||
// The reference's `0x600be0` refuses unless the named mover IS the active mover
|
||||
// (`0x600bec`/`0x600bfb` against `[0xc4da98]`/`[0xc4da9c]`); ours is the same statement, made
|
||||
// by naming the mover the controller is driving and nothing else.
|
||||
super::move_trace::skipped_time(guid, lag_ms);
|
||||
let _ = sender.send(ClientCommand::MoveTimeSkipped { guid, lag_ms });
|
||||
}
|
||||
|
||||
/// wire — entering free-fly (`F`), where [`stream_self_movement`] no longer runs each frame, so nothing
|
||||
/// else would ever clear them and observers would extrapolate a phantom walk/spin until we re-attach.
|
||||
/// **Idempotent**: a no-op once we've already reported stopped, so it's safe to call every free-fly
|
||||
@@ -509,10 +583,68 @@ pub(super) fn park_mover(sender: &Sender<ClientCommand>, player: &mut Player) {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
/// A frame that integrated everything it was given — the skipped-time report's no-op input,
|
||||
/// which is every frame in these tests but the one that exercises it.
|
||||
fn no_skip() -> SkipClock {
|
||||
SkipClock {
|
||||
dt: 0.0,
|
||||
held: false,
|
||||
mover: None,
|
||||
}
|
||||
}
|
||||
|
||||
use super::*;
|
||||
use std::f32::consts::TAU;
|
||||
|
||||
/// **The gait toggle announces itself standing perfectly still** — which is the whole reason
|
||||
/// **The skipped-time report** (decision 1935): held frames accumulate, the release edge sends
|
||||
/// the total ONCE, and nothing is sent for a frame that actually integrated. The three
|
||||
/// properties together are the whole contract — a per-frame send would be hundreds of packets
|
||||
/// per settle, a lost accumulator would tell the server nothing, and a send with no hold
|
||||
/// behind it would be a lie about our own simulation.
|
||||
#[test]
|
||||
fn a_settle_hold_reports_its_skipped_milliseconds_once_on_release() {
|
||||
const MOVER: u64 = 0x0000_0000_0000_002a;
|
||||
let (tx, rx) = crossbeam_channel::unbounded();
|
||||
let mut player = Player::default();
|
||||
let held = |dt: f32| SkipClock {
|
||||
dt,
|
||||
held: true,
|
||||
mover: Some(MOVER),
|
||||
};
|
||||
let released = || SkipClock {
|
||||
dt: 0.016,
|
||||
held: false,
|
||||
mover: Some(MOVER),
|
||||
};
|
||||
// Three held frames: 0.5 s of simulation advanced through, nothing integrated, and
|
||||
// NOTHING on the wire yet — the report is one packet, not one per frame.
|
||||
for _ in 0..3 {
|
||||
stream_skipped_time(&tx, &mut player, held(0.1));
|
||||
}
|
||||
stream_skipped_time(&tx, &mut player, held(0.2));
|
||||
assert!(
|
||||
rx.try_recv().is_err(),
|
||||
"a hold in progress sends nothing — the reference's own captures show one packet per \
|
||||
skip, not one per frame"
|
||||
);
|
||||
// The release: one packet, carrying the whole hold.
|
||||
stream_skipped_time(&tx, &mut player, released());
|
||||
match rx.try_recv().expect("the release edge reports") {
|
||||
ClientCommand::MoveTimeSkipped { guid, lag_ms } => {
|
||||
assert_eq!(guid, MOVER, "the packet names the mover we were driving");
|
||||
assert_eq!(lag_ms, 500, "0.1+0.1+0.1+0.2 s of un-integrated simulation");
|
||||
}
|
||||
other => panic!("expected MoveTimeSkipped, got {other:?}"),
|
||||
}
|
||||
// …and the accumulator disarms: a second released frame reports nothing.
|
||||
stream_skipped_time(&tx, &mut player, released());
|
||||
assert!(
|
||||
rx.try_recv().is_err(),
|
||||
"the report is an edge; a running mover skips nothing and says nothing"
|
||||
);
|
||||
}
|
||||
|
||||
/// the reference gives `ToggleRun` its own move-event enqueue instead of letting the
|
||||
/// move-state broadcaster find the flag change: that broadcaster gates every send on the
|
||||
/// locomotion nibble (`0x61a99d test al,0xf`), so a toggle with no direction bit set would
|
||||
@@ -541,6 +673,7 @@ mod tests {
|
||||
&[],
|
||||
None,
|
||||
None,
|
||||
no_skip(),
|
||||
);
|
||||
let ClientCommand::Move { kind, flags, .. } =
|
||||
rx.try_recv().expect("a standing walk toggle still sends")
|
||||
@@ -552,7 +685,18 @@ mod tests {
|
||||
assert_eq!(flags & move_flags::WALK_MODE, move_flags::WALK_MODE);
|
||||
|
||||
// …and leaving it is the OTHER opcode, not a repeat of the same one.
|
||||
stream_self_movement(&tx, &mut player, 0, 0.0, arc(), 0.0, &[], None, None);
|
||||
stream_self_movement(
|
||||
&tx,
|
||||
&mut player,
|
||||
0,
|
||||
0.0,
|
||||
arc(),
|
||||
0.0,
|
||||
&[],
|
||||
None,
|
||||
None,
|
||||
no_skip(),
|
||||
);
|
||||
let ClientCommand::Move { kind, flags, .. } =
|
||||
rx.try_recv().expect("the run half sends too")
|
||||
else {
|
||||
@@ -573,6 +717,7 @@ mod tests {
|
||||
&[],
|
||||
None,
|
||||
None,
|
||||
no_skip(),
|
||||
);
|
||||
while let Ok(ClientCommand::Move { kind, .. }) = rx.try_recv() {
|
||||
assert_eq!(kind, MoveKind::SetWalkMode, "the re-entry edge only");
|
||||
@@ -587,6 +732,7 @@ mod tests {
|
||||
&[],
|
||||
None,
|
||||
None,
|
||||
no_skip(),
|
||||
);
|
||||
assert!(
|
||||
rx.try_recv().is_err(),
|
||||
@@ -620,6 +766,7 @@ mod tests {
|
||||
&[],
|
||||
None,
|
||||
None,
|
||||
no_skip(),
|
||||
);
|
||||
let kinds: Vec<_> = rx
|
||||
.try_iter()
|
||||
@@ -661,6 +808,7 @@ mod tests {
|
||||
&[],
|
||||
None,
|
||||
None,
|
||||
no_skip(),
|
||||
);
|
||||
|
||||
let ClientCommand::Move { orientation, .. } = rx
|
||||
@@ -706,6 +854,7 @@ mod tests {
|
||||
&[],
|
||||
None,
|
||||
None,
|
||||
no_skip(),
|
||||
);
|
||||
|
||||
let ClientCommand::Move {
|
||||
@@ -749,6 +898,7 @@ mod tests {
|
||||
&[],
|
||||
None,
|
||||
None,
|
||||
no_skip(),
|
||||
);
|
||||
assert!(rx.try_recv().is_err(), "a mid-air release sends nothing");
|
||||
assert_eq!(
|
||||
@@ -773,6 +923,7 @@ mod tests {
|
||||
&[],
|
||||
None,
|
||||
None,
|
||||
no_skip(),
|
||||
);
|
||||
let ClientCommand::Move { kind, flags, .. } = rx.try_recv().expect("the landing packet")
|
||||
else {
|
||||
@@ -817,6 +968,7 @@ mod tests {
|
||||
&[],
|
||||
None,
|
||||
None,
|
||||
no_skip(),
|
||||
);
|
||||
};
|
||||
|
||||
@@ -882,6 +1034,7 @@ mod tests {
|
||||
&[],
|
||||
None,
|
||||
None,
|
||||
no_skip(),
|
||||
);
|
||||
let ClientCommand::Move {
|
||||
kind, flags, jump, ..
|
||||
@@ -922,6 +1075,7 @@ mod tests {
|
||||
&[],
|
||||
None,
|
||||
None,
|
||||
no_skip(),
|
||||
);
|
||||
assert!(
|
||||
rx.try_recv().is_err(),
|
||||
@@ -956,6 +1110,7 @@ mod tests {
|
||||
&[],
|
||||
None,
|
||||
None,
|
||||
no_skip(),
|
||||
);
|
||||
let ClientCommand::Move { kind, flags, .. } =
|
||||
rx.try_recv().expect("a mid-air turn broadcasts")
|
||||
@@ -985,6 +1140,7 @@ mod tests {
|
||||
&[],
|
||||
None,
|
||||
None,
|
||||
no_skip(),
|
||||
);
|
||||
let kinds: Vec<_> = rx
|
||||
.try_iter()
|
||||
@@ -1029,6 +1185,7 @@ mod tests {
|
||||
&[],
|
||||
None,
|
||||
None,
|
||||
no_skip(),
|
||||
);
|
||||
let ClientCommand::Move { kind, flags, .. } = rx
|
||||
.try_recv()
|
||||
@@ -1064,6 +1221,7 @@ mod tests {
|
||||
&[],
|
||||
None,
|
||||
None,
|
||||
no_skip(),
|
||||
);
|
||||
assert!(rx.try_recv().is_err(), "an unchanged facing is silent");
|
||||
}
|
||||
@@ -1097,6 +1255,7 @@ mod tests {
|
||||
&[],
|
||||
None,
|
||||
None,
|
||||
no_skip(),
|
||||
);
|
||||
assert!(
|
||||
rx.try_recv().is_err(),
|
||||
@@ -1121,6 +1280,7 @@ mod tests {
|
||||
&[],
|
||||
None,
|
||||
None,
|
||||
no_skip(),
|
||||
);
|
||||
let ClientCommand::Move { kind, .. } = rx.try_recv().expect("the STOP_TURN") else {
|
||||
panic!("expected a Move command");
|
||||
@@ -1159,6 +1319,7 @@ mod tests {
|
||||
&[],
|
||||
None,
|
||||
None,
|
||||
no_skip(),
|
||||
);
|
||||
let kinds: Vec<_> = rx
|
||||
.try_iter()
|
||||
@@ -1189,6 +1350,7 @@ mod tests {
|
||||
&[],
|
||||
None,
|
||||
None,
|
||||
no_skip(),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1327,6 +1489,7 @@ mod tests {
|
||||
launch,
|
||||
}),
|
||||
None,
|
||||
no_skip(),
|
||||
);
|
||||
|
||||
let ClientCommand::KnockBackAck {
|
||||
@@ -1388,6 +1551,7 @@ mod tests {
|
||||
&[],
|
||||
None,
|
||||
None,
|
||||
no_skip(),
|
||||
);
|
||||
let ClientCommand::Move { kind, jump, .. } =
|
||||
rx.try_recv().expect("a jump take-off is announced")
|
||||
|
||||
@@ -193,6 +193,14 @@ pub(super) fn drive_self_ride(
|
||||
let y = ride_z(
|
||||
&player,
|
||||
&points,
|
||||
// **A deck path re-derives its Z exactly like any other** (decision 1936's
|
||||
// correction). The tempting exemption — "there is no terrain under a boat" — is
|
||||
// refuted: the transport guid appears in neither `0x616cb0`'s predicate nor
|
||||
// `0x634040`'s dispatch, so a grounded deck spline takes the same fork, and the
|
||||
// probe does not miss because it runs at the **composed world position** against
|
||||
// a class mask that admits GameObject meshes. This system is in `WorldStage::Input`
|
||||
// and `transport::compose_riders` in the stage before it, so the translation read
|
||||
// here is already composed — nothing else is needed for the deck case.
|
||||
spline.grounded,
|
||||
transform.translation,
|
||||
floor,
|
||||
@@ -352,6 +360,7 @@ mod tests {
|
||||
.spawn((
|
||||
Transform::default(),
|
||||
Spline {
|
||||
deck: None,
|
||||
points: vec![[0.0, 0.0, 0.0], [10.0, 0.0, 0.0]],
|
||||
start: Instant::now(),
|
||||
duration: Duration::from_secs(600), // far from ending during the test
|
||||
|
||||
@@ -853,6 +853,12 @@ pub(crate) struct Player {
|
||||
pub(crate) owes_worldport_ack: bool,
|
||||
/// `Time::elapsed_secs` when we last sent a heartbeat.
|
||||
pub(super) last_heartbeat: f32,
|
||||
/// **Milliseconds of movement simulation this client advanced through without integrating** —
|
||||
/// the quantity `CMSG_MOVE_TIME_SKIPPED` reports (decision 1935). Accumulated while
|
||||
/// [`Self::settling`] holds the mover (the world under us has not streamed in, so no step
|
||||
/// runs), drained and sent by [`super::movement_net::stream_self_movement`] on the release
|
||||
/// edge. Fractional because it accumulates a frame `dt` at a time; the wire takes whole ms.
|
||||
pub(super) skipped_ms: f32,
|
||||
/// `Time::elapsed_secs` when the current airborne phase (jump or step-off) began, else `None` on the
|
||||
/// ground. Drives the wire `fall_time` (ms airborne) and detects the take-off / landing transitions
|
||||
/// that emit `MSG_MOVE_JUMP` / `MSG_MOVE_FALL_LAND` (decision 0053).
|
||||
|
||||
@@ -94,6 +94,7 @@ impl Plugin for TransportPlugin {
|
||||
tick_transports,
|
||||
republish_moved_collider_aabbs::<Collider>,
|
||||
compose_riders,
|
||||
ground_deck_riders,
|
||||
)
|
||||
.chain()
|
||||
.after(benilla_world::schedule::WorldStage::Net)
|
||||
@@ -538,6 +539,78 @@ fn compose_riders(
|
||||
}
|
||||
}
|
||||
|
||||
/// **Re-ground a deck-splined rider onto the deck it is walking on** (decision 1936's correction).
|
||||
///
|
||||
/// The tempting reading — "a transport spline's Z is authoritative, because there is no terrain
|
||||
/// under a boat" — is what benilla shipped for a few hours and what wow-re's carve refuted. The
|
||||
/// transport guid appears in **neither** `0x616cb0`'s predicate **nor** `0x634040`'s dispatch: a
|
||||
/// grounded `SMSG_MONSTER_MOVE_TRANSPORT` spline takes the *same* fork as a plain one,
|
||||
/// `0x616d03` zeroes its Z-delta and the WALK resolver `0x6367b0` re-derives Z off the world trace
|
||||
/// (`0x636e52`: `pos.z -= hitDistance`). The reason that does not drop the unit into the sea is
|
||||
/// that the query runs at the **composed world position**, and the class mask `0x6315f0` yields
|
||||
/// `0x100111` whose `0x100000` bit unconditionally admits GameObject meshes — a boat's `.wmo`
|
||||
/// being in the global map-object list precisely because it is a type-11/15 GameObject.
|
||||
///
|
||||
/// **So the deck's grounding is the ordinary grounding, moved one step later.** It cannot live in
|
||||
/// [`crate::net::ground_clamp_creatures`] with every other unit's, because that system runs inside
|
||||
/// `WorldStage::Net` — *before* [`compose_riders`] — where a deck rider's `Transform` is still
|
||||
/// last frame's composed pose and its fresh position exists only as a transport-local offset. Here
|
||||
/// the deck is at its this-frame pose, its collider AABB has just been republished
|
||||
/// ([`republish_moved_collider_aabbs`], decision 1663 — without which the probe would be pruned
|
||||
/// against the deck's previous-frame box) and the rider's world position is on the transform. One
|
||||
/// probe, at the reference's own moment.
|
||||
///
|
||||
/// The corrected Y is written to the `Transform` and not back into
|
||||
/// [`TransportRider::local_pos`], deliberately: [`compose_riders`] recomputes the world pose from
|
||||
/// the local one every frame, so a write-back would be re-derived away next frame anyway, and the
|
||||
/// correction is idempotent as it stands. [`crate::net::RemoteMotion`]'s canonical pose moves with
|
||||
/// it, as it does in `compose_riders`.
|
||||
///
|
||||
/// **A named gap.** The reference has a *third* regime this does not model: `0x616af0`'s anti-warp
|
||||
/// guard snaps `pos := base + sampledOffset` verbatim — wire Z included — and bypasses the
|
||||
/// re-ground entirely whenever the horizontal step exceeds 3 yd (`[0x80c5bc]` = 9.0, squared) or
|
||||
/// 60 yd/s. For a creature freshly placed on a deck that is the common *first* substep, so our
|
||||
/// first frame re-grounds where the reference would take the wire Z. It settles to the same place
|
||||
/// on the next frame either way.
|
||||
#[allow(clippy::type_complexity)] // one Bevy system's full input set
|
||||
fn ground_deck_riders(
|
||||
world: benilla_world::collision::WorldCollision,
|
||||
mut riders: Query<
|
||||
(
|
||||
&mut Transform,
|
||||
Option<&mut crate::net::RemoteMotion>,
|
||||
&crate::net::Spline,
|
||||
),
|
||||
(With<TransportRider>, Without<Transport>),
|
||||
>,
|
||||
) {
|
||||
/// The probe's reach above and below the rider's seat — the creature clamp's own numbers
|
||||
/// (`GROUND_CLAMP_UP`/`_DOWN`), for the same reason: enough to clear a slightly-high server Z
|
||||
/// and to follow a step down, not enough to grab a deck one storey up.
|
||||
const UP: f32 = 2.5;
|
||||
const DOWN: f32 = 4.0;
|
||||
for (mut transform, motion, spline) in &mut riders {
|
||||
// Only a GROUNDED deck path. A flying one owns its altitude (the `0x200` bit is the same
|
||||
// one that decides the point encoding and the resolver arm), and a world-frame spline is
|
||||
// `ground_clamp_creatures`' business, not ours.
|
||||
if spline.deck.is_none() || !spline.grounded {
|
||||
continue;
|
||||
}
|
||||
let origin = transform.translation + Vec3::Y * UP;
|
||||
let Some(hit) = world.ray_body(origin, Dir3::NEG_Y, UP + DOWN) else {
|
||||
continue; // nothing under the rider — leave the composed pose alone
|
||||
};
|
||||
let y = origin.y - hit.distance;
|
||||
if y == transform.translation.y {
|
||||
continue;
|
||||
}
|
||||
transform.translation.y = y;
|
||||
if let Some(mut rm) = motion {
|
||||
rm.wow_pos = benilla_assets::coords::bevy_to_wow(transform.translation);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Publish the **ride frame** onto every model standing on a transport — the reference's
|
||||
/// `SetMoveBase` install (`0x617170`/`0x618970` → `0x718910` → `[CM2Model+0x17c]`), which is the
|
||||
/// frame a rider's world-space effects are *stored* in ([`benilla_world::ride_frame`]).
|
||||
|
||||
@@ -22,12 +22,14 @@
|
||||
//! NO_POWER).
|
||||
//!
|
||||
//! **The argument arms, and what is still approximate.** Filled: `0x5e` REQUIRES_SPELL_FOCUS and
|
||||
//! `0x5d` REQUIRES_AREA here (decision 1313 — `0x6e1f62`/`0x6e1fad`), and `0x78` TOTEMS / `0x5c`
|
||||
//! `0x5d` REQUIRES_AREA here (decision 1313 — `0x6e1f62`/`0x6e1fad`), plus `0x8d`
|
||||
//! PREVENTED_BY_MECHANIC (decision 1948 — `0x6e2190`, and the one arm whose word is produced
|
||||
//! locally rather than read off the wire), and `0x78` TOTEMS / `0x5c`
|
||||
//! REAGENTS / `0x19`–`0x1b` EQUIPPED_ITEM_CLASS\* in the drain, which owns them because their
|
||||
//! fills need the item caches and the query-then-redisplay cache-miss behavior ("Requires Mining
|
||||
//! Pick", decisions 0545 + 0552). Still stripped rather than filled — each needs a DBC we do not
|
||||
//! load yet: `0x56` ONLY_SHAPESHIFT (form-name list), `0x8d` PREVENTED_BY_MECHANIC
|
||||
//! (`SpellMechanic.dbc`), `0x90` MIN_SKILL (`SkillLine.dbc`), `0x31` NEED_EXOTIC_AMMO and `0x84`
|
||||
//! load yet: `0x56` ONLY_SHAPESHIFT (form-name list), `0x90` MIN_SKILL (`SkillLine.dbc`),
|
||||
//! `0x31` NEED_EXOTIC_AMMO and `0x84`
|
||||
//! PROSPECT_NEED_MORE. Stripping is a **deliberate divergence**, now byte-confirmed as one: on a
|
||||
//! bad id or an absent word the reference jumps to the default arm with the pointer still on the
|
||||
//! *unfilled* template, so it displays a literal `Requires %s` (wow-re §WIRE-ARGS C3 — the fill
|
||||
@@ -36,7 +38,7 @@
|
||||
//! `0x0a`'s item-spell leg (`ERR_INVALID_ITEM_TARGET`) is unmodeled — the drain does not know
|
||||
//! item-ness.
|
||||
|
||||
use benilla_formats::{AreaTableCatalog, SpellDisplay, SpellFocusCatalog};
|
||||
use benilla_formats::{AreaTableCatalog, SpellDisplay, SpellFocusCatalog, SpellMechanicCatalog};
|
||||
|
||||
/// Wire reason → its `SPELL_FAILED_*` GlobalStrings key (the `0x6e23e0` table, byte-exact).
|
||||
pub(super) const CAST_FAIL_KEYS: [&str; 146] = [
|
||||
@@ -220,6 +222,8 @@ pub(super) struct FailArgs<'a> {
|
||||
pub(super) focus: Option<&'a SpellFocusCatalog>,
|
||||
/// `AreaTable.dbc` (`0xc0e048`) — the `0x5d` arm's `AreaName`.
|
||||
pub(super) areas: Option<&'a AreaTableCatalog>,
|
||||
/// `SpellMechanic.dbc` (`0xc0d7c4`) — the `0x8d` arm's mechanic name (decision 1948).
|
||||
pub(super) mechanics: Option<&'a SpellMechanicCatalog>,
|
||||
}
|
||||
|
||||
impl FailArgs<'_> {
|
||||
@@ -247,6 +251,13 @@ impl FailArgs<'_> {
|
||||
/// in `Spell.dbc` holds it, so an absent word means an unfilled message.
|
||||
fn fill(&self, reason: u8, spell: Option<&SpellDisplay>) -> Option<String> {
|
||||
match reason {
|
||||
// **`0x8d` PREVENTED_BY_MECHANIC** (`0x6e2190`) → `SpellMechanic.dbc` (`0xc0d7c4`),
|
||||
// so "Can't do that while %s" reads "Can't do that while stunned". Unlike its two
|
||||
// neighbours the word is not the wire's: this refusal is raised locally and the id
|
||||
// comes from the crowd-control ladder's own exemption scan (decisions 1941/1948).
|
||||
// A `0` or unknown id leaves the template to the strip fallback, which is also what
|
||||
// the arm does when the scan named no mechanic at all.
|
||||
0x8D => self.mechanics?.name(self.arg?).map(str::to_string),
|
||||
0x5D => self.areas?.name(self.arg?).map(str::to_string),
|
||||
0x5E => {
|
||||
let id = self
|
||||
@@ -634,11 +645,32 @@ mod tests {
|
||||
let focus =
|
||||
benilla_formats::load_spell_focus_catalog(&mut chain).expect("SpellFocusObject");
|
||||
let areas = benilla_formats::load_area_table_catalog(&mut chain).expect("AreaTable");
|
||||
let mechanics =
|
||||
benilla_formats::load_spell_mechanic_catalog(&mut chain).expect("SpellMechanic");
|
||||
let args = |arg: u32| FailArgs {
|
||||
arg: Some(arg),
|
||||
focus: Some(&focus),
|
||||
areas: Some(&areas),
|
||||
mechanics: Some(&mechanics),
|
||||
};
|
||||
// **0x8d PREVENTED_BY_MECHANIC** (decision 1948) — the crowd-control ladder's renamed
|
||||
// refusal, and the one argument arm whose word is produced locally rather than read off
|
||||
// the wire. The names are lower-case adjectives in the shipped data, which is what makes
|
||||
// them read as the tail of the sentence.
|
||||
assert_eq!(
|
||||
cast_fail_text(0x8D, None, args(12), &g).unwrap().text,
|
||||
"Can't do that while stunned"
|
||||
);
|
||||
assert_eq!(
|
||||
cast_fail_text(0x8D, None, args(5), &g).unwrap().text,
|
||||
"Can't do that while fleeing"
|
||||
);
|
||||
// An unknown or absent mechanic strips to the bare stem rather than showing a raw `%s`.
|
||||
assert!(!cast_fail_text(0x8D, None, args(999), &g)
|
||||
.unwrap()
|
||||
.text
|
||||
.contains('%'));
|
||||
|
||||
// 0x5e REQUIRES_SPELL_FOCUS: the Crown of the Earth phials' own refusal. Focus 12 is the
|
||||
// Starbreeze Village moonwell — using the Jade Phial at any *other* pool is the report.
|
||||
assert_eq!(
|
||||
|
||||
@@ -523,8 +523,15 @@ fn send_spell_cast(
|
||||
.unwrap_or_default()
|
||||
},
|
||||
) {
|
||||
debug!("ui_action: cast {spell_id} refused locally — crowd control ({reason:#x})");
|
||||
cast_errors.push_local(spell_id, reason);
|
||||
let (reason, mechanic) = reason;
|
||||
debug!(
|
||||
"ui_action: cast {spell_id} refused locally — crowd control ({reason:#x}, \
|
||||
mechanic {mechanic:?})"
|
||||
);
|
||||
match mechanic {
|
||||
Some(m) => cast_errors.push_local_arg(spell_id, reason, m),
|
||||
None => cast_errors.push_local(spell_id, reason),
|
||||
}
|
||||
return;
|
||||
}
|
||||
// The client-side mounted gate (decision 0481; wow-re `mounted-action-gate.md` §5:
|
||||
|
||||
@@ -42,10 +42,16 @@ pub(crate) struct CastErrors(pub Vec<CastFail>);
|
||||
/// word that fills the message's `%s` (the arm table at `0x6e1d8e` — a `SpellFocusObject.dbc` id
|
||||
/// for 0x5e, an `AreaTable.dbc` id for 0x5d).
|
||||
///
|
||||
/// `arg` is `None` for every **client-local** refusal, and that is faithful rather than a gap: the
|
||||
/// local refusals the reference raises itself go through `DisplayError` with no argText, and the
|
||||
/// two client-generated argument messages it *does* build (the lock toasts, MIN_SKILL) are
|
||||
/// `arg` is `None` for **almost** every client-local refusal, and that is faithful rather than a
|
||||
/// gap: the local refusals the reference raises itself go through `DisplayError` with no argText,
|
||||
/// and the two client-generated argument messages it *does* build (the lock toasts, MIN_SKILL) are
|
||||
/// [`UiError`]'s tenants, not this queue's.
|
||||
///
|
||||
/// The **one** exception is the crowd-control ladder's `0x8d` (decision 1948). That refusal is
|
||||
/// local — `0x6094f0` bails before any packet — yet it carries an argument all the same, because
|
||||
/// its own exemption scan produced one: the blocking aura's `SpellMechanic.dbc` id. It is the
|
||||
/// only place a locally-generated word reaches this queue, which is why `push_local` still cannot
|
||||
/// set one and [`CastErrors::push_local_arg`] exists beside it.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub(crate) struct CastFail {
|
||||
pub spell_id: u32,
|
||||
@@ -69,6 +75,16 @@ impl CastErrors {
|
||||
pub(crate) fn push_local(&mut self, spell_id: u32, reason: u8) {
|
||||
self.0.push(CastFail::local(spell_id, reason));
|
||||
}
|
||||
|
||||
/// Queue a client-local refusal that carries its own argument word — see [`CastFail::arg`]
|
||||
/// for why there is exactly one such tenant.
|
||||
pub(crate) fn push_local_arg(&mut self, spell_id: u32, reason: u8, arg: u32) {
|
||||
self.0.push(CastFail {
|
||||
spell_id,
|
||||
reason,
|
||||
arg: Some(arg),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// (Dis)mount refusals queued for the UI error line, as the wire pair `(mount, code)` from
|
||||
|
||||
@@ -75,6 +75,31 @@ pub(super) struct FeedMemory {
|
||||
macro_generation: u64,
|
||||
}
|
||||
|
||||
/// The DBC name tables the cast-fail **argument arms** read (`FailArgs`), as one parameter — three
|
||||
/// `Option<Res<…>>` that are one concept and were pushing this system past Bevy's parameter arity
|
||||
/// (decision 1948). Each is `Option` for the same reason `FailArgs`' fields are: a client with no
|
||||
/// game data has none, and the arm then declines and the template strips.
|
||||
#[derive(bevy::ecs::system::SystemParam)]
|
||||
pub(super) struct FailNameTables<'w> {
|
||||
/// `SpellFocusObject.dbc` — the crafting book's catalog, for `0x5e`.
|
||||
focus: Option<Res<'w, crate::ui_tradeskill::SpellFocus>>,
|
||||
/// `AreaTable.dbc` — the map arc's, for `0x5d`.
|
||||
areas: Option<Res<'w, crate::area::AreaTableRes>>,
|
||||
/// `SpellMechanic.dbc` — for `0x8d`, the one arm whose word is produced locally.
|
||||
mechanics: Option<Res<'w, super::SpellMechanics>>,
|
||||
}
|
||||
|
||||
impl FailNameTables<'_> {
|
||||
fn args(&self) -> cast_fail::FailArgs<'_> {
|
||||
cast_fail::FailArgs {
|
||||
arg: None,
|
||||
focus: self.focus.as_deref().map(|f| &f.catalog),
|
||||
areas: self.areas.as_deref().map(|a| &a.0),
|
||||
mechanics: self.mechanics.as_deref().map(|m| &m.catalog),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)] // a Bevy system's full input set
|
||||
pub(super) fn feed_actions(
|
||||
script: Option<NonSendMut<UiScript>>,
|
||||
@@ -88,10 +113,7 @@ pub(super) fn feed_actions(
|
||||
mut items: ResMut<Items>,
|
||||
icons: Option<Res<ItemDisplays>>,
|
||||
sub_classes: Option<Res<crate::ui_items::ItemSubClasses>>,
|
||||
// The two DBC name tables the cast-fail argument arms read (`FailArgs`): the crafting book's
|
||||
// SpellFocusObject catalog and the map arc's AreaTable one, both already loaded.
|
||||
spell_focus: Option<Res<crate::ui_tradeskill::SpellFocus>>,
|
||||
areas: Option<Res<crate::area::AreaTableRes>>,
|
||||
name_tables: FailNameTables,
|
||||
commands: Res<NetCommands>,
|
||||
mut memory: Local<crate::ui_script::VmMemo<FeedMemory>>,
|
||||
// Where a displayed message lands: the chat window (the combat log's own record of a failed
|
||||
@@ -122,11 +144,7 @@ pub(super) fn feed_actions(
|
||||
// things — "Not enough mana." on the screen, "You fail to cast Frostbolt: Not enough mana."
|
||||
// in the log.
|
||||
let mut fail_lines: Vec<crate::ui_chat::combat::PendingCombat> = Vec::new();
|
||||
let fail_args = cast_fail::FailArgs {
|
||||
arg: None,
|
||||
focus: spell_focus.as_deref().map(|f| &f.catalog),
|
||||
areas: areas.as_deref().map(|a| &a.0),
|
||||
};
|
||||
let fail_args = name_tables.args();
|
||||
let texts: Vec<cast_fail::CastFailLine> = cast_errors
|
||||
.0
|
||||
.drain(..)
|
||||
|
||||
@@ -291,6 +291,34 @@ fn track_learned_abilities(
|
||||
}
|
||||
}
|
||||
|
||||
/// `SpellMechanic.dbc` — the vocabulary that fills `SPELL_FAILED_PREVENTED_BY_MECHANIC`'s `%s`
|
||||
/// ([`benilla_formats::SpellMechanicCatalog`], decision 1948). Its one reader is the cast-failure
|
||||
/// resolver's `0x8d` arm.
|
||||
#[derive(Resource)]
|
||||
pub(crate) struct SpellMechanics {
|
||||
pub(crate) catalog: benilla_formats::SpellMechanicCatalog,
|
||||
}
|
||||
|
||||
fn load_spell_mechanics(mut commands: Commands, assets: Option<Res<benilla_assets::WorldAssets>>) {
|
||||
let Some(assets) = assets else { return };
|
||||
let loaded = {
|
||||
let mut chain = benilla_assets::LockRecover::lock_recover(&*assets.chain);
|
||||
benilla_formats::load_spell_mechanic_catalog(&mut chain)
|
||||
};
|
||||
match loaded {
|
||||
Ok(catalog) => {
|
||||
debug!("ui_action: {} spell-mechanic name(s)", catalog.len());
|
||||
commands.insert_resource(SpellMechanics { catalog });
|
||||
}
|
||||
// Absent data is the strip fallback, not a failure: the refusal still appears, it just
|
||||
// shows its bare stem instead of naming what is holding you.
|
||||
Err(e) => warn!(
|
||||
"ui_action: SpellMechanic.dbc failed to load — the crowd-control refusal drops the \
|
||||
mechanic name: {e:#}"
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct UiActionPlugin;
|
||||
|
||||
impl Plugin for UiActionPlugin {
|
||||
@@ -308,6 +336,7 @@ impl Plugin for UiActionPlugin {
|
||||
.init_resource::<targeting::SpellTargeting>()
|
||||
.init_resource::<targeting::EnchantConfirmItem>()
|
||||
.add_systems(Startup, load_spells.after(AssetSet::Open))
|
||||
.add_systems(Startup, load_spell_mechanics.after(AssetSet::Open))
|
||||
.add_systems(
|
||||
Update,
|
||||
(
|
||||
|
||||
@@ -140,7 +140,7 @@ pub(crate) fn cast_cc_refusal(
|
||||
charmed_by_other: bool,
|
||||
spell: Option<&SpellDisplay>,
|
||||
exempt: &mut impl FnMut(&[u32]) -> benilla_formats::CcExemption,
|
||||
) -> Option<u8> {
|
||||
) -> Option<(u8, Option<u32>)> {
|
||||
use crate::player::UNIT_FLAG_STUNNED;
|
||||
/// `UNIT_FIELD_FLAGS` bits 13/17/22/23 (vmangos `UnitDefines.h`).
|
||||
const UNIT_FLAG_SILENCED: u32 = 0x0000_2000;
|
||||
@@ -158,15 +158,17 @@ pub(crate) fn cast_cc_refusal(
|
||||
// immunity to whatever is blocking — the arm is SKIPPED and the cast proceeds. Otherwise the
|
||||
// refusal is `0x8d` "Can't do that while %s" when the scan named a mechanic, and the arm's own
|
||||
// reason when it did not (decision 1946).
|
||||
let mut arm = |aura_types: &[u32], own_reason: u8| -> Option<u8> {
|
||||
let mut arm = |aura_types: &[u32], own_reason: u8| -> Option<(u8, Option<u32>)> {
|
||||
let scan = exempt(aura_types);
|
||||
if scan.exempt {
|
||||
return None;
|
||||
}
|
||||
Some(if scan.mechanic != 0 {
|
||||
REASON_PREVENTED_BY_MECHANIC
|
||||
// The mechanic rides along as the message's `%s` — the one client-LOCAL refusal that
|
||||
// carries an argument word (decision 1948).
|
||||
(REASON_PREVENTED_BY_MECHANIC, Some(scan.mechanic))
|
||||
} else {
|
||||
own_reason
|
||||
(own_reason, None)
|
||||
})
|
||||
};
|
||||
|
||||
@@ -1209,9 +1211,14 @@ mod cc_refusal_tests {
|
||||
|_: &[u32]| benilla_formats::CcExemption::default()
|
||||
}
|
||||
|
||||
/// The reason alone, dropping the mechanic — most of these tests are about which arm fires.
|
||||
fn reason_of(v: Option<(u8, Option<u32>)>) -> Option<u8> {
|
||||
v.map(|(r, _)| r)
|
||||
}
|
||||
|
||||
/// A live caster, nobody else at the reins, no auras.
|
||||
fn cc(flags: u32, d: &SpellDisplay) -> Option<u8> {
|
||||
cast_cc_refusal(flags, Some(100), false, Some(d), &mut no_auras())
|
||||
cast_cc_refusal(flags, Some(100), false, Some(d), &mut no_auras()).map(|(r, _)| r)
|
||||
}
|
||||
|
||||
/// **The arms are not symmetric** (decision 1904, widened by 1925): stun, fear, charm and
|
||||
@@ -1234,7 +1241,13 @@ mod cc_refusal_tests {
|
||||
}
|
||||
// …and charm, which is not a flag at all but "somebody else holds the reins".
|
||||
assert_eq!(
|
||||
cast_cc_refusal(0, Some(100), true, Some(&neither), &mut no_auras()),
|
||||
reason_of(cast_cc_refusal(
|
||||
0,
|
||||
Some(100),
|
||||
true,
|
||||
Some(&neither),
|
||||
&mut no_auras()
|
||||
)),
|
||||
Some(0x14)
|
||||
);
|
||||
|
||||
@@ -1250,11 +1263,23 @@ mod cc_refusal_tests {
|
||||
// Nothing up, nothing refused; and no record claims no prevention.
|
||||
assert_eq!(cc(0, &cast), None);
|
||||
assert_eq!(
|
||||
cast_cc_refusal(SILENCED, Some(100), false, None, &mut no_auras()),
|
||||
reason_of(cast_cc_refusal(
|
||||
SILENCED,
|
||||
Some(100),
|
||||
false,
|
||||
None,
|
||||
&mut no_auras()
|
||||
)),
|
||||
None
|
||||
);
|
||||
assert_eq!(
|
||||
cast_cc_refusal(STUNNED, Some(100), false, None, &mut no_auras()),
|
||||
reason_of(cast_cc_refusal(
|
||||
STUNNED,
|
||||
Some(100),
|
||||
false,
|
||||
None,
|
||||
&mut no_auras()
|
||||
)),
|
||||
Some(0x64),
|
||||
"the stun needs no record"
|
||||
);
|
||||
@@ -1268,13 +1293,13 @@ mod cc_refusal_tests {
|
||||
let cast = spell(1);
|
||||
// Charm is arm 1: it beats a simultaneous stun.
|
||||
assert_eq!(
|
||||
cast_cc_refusal(
|
||||
reason_of(cast_cc_refusal(
|
||||
STUNNED | SILENCED,
|
||||
Some(100),
|
||||
true,
|
||||
Some(&cast),
|
||||
&mut no_auras()
|
||||
),
|
||||
)),
|
||||
Some(0x14)
|
||||
);
|
||||
// Stun is arm 2: it beats silence, pacify, fear and confuse.
|
||||
@@ -1298,25 +1323,43 @@ mod cc_refusal_tests {
|
||||
|
||||
// No aura of the arm's type: the arm's OWN reason, as everywhere else.
|
||||
assert_eq!(
|
||||
cast_cc_refusal(STUNNED, Some(100), false, Some(&cast), &mut scan(false, 0)),
|
||||
reason_of(cast_cc_refusal(
|
||||
STUNNED,
|
||||
Some(100),
|
||||
false,
|
||||
Some(&cast),
|
||||
&mut scan(false, 0)
|
||||
)),
|
||||
Some(0x64)
|
||||
);
|
||||
// A blocking aura the cast does NOT counter: the refusal survives but is renamed to
|
||||
// `0x8d` "Can't do that while %s", the mechanic naming the aura.
|
||||
assert_eq!(
|
||||
cast_cc_refusal(STUNNED, Some(100), false, Some(&cast), &mut scan(false, 12)),
|
||||
reason_of(cast_cc_refusal(
|
||||
STUNNED,
|
||||
Some(100),
|
||||
false,
|
||||
Some(&cast),
|
||||
&mut scan(false, 12)
|
||||
)),
|
||||
Some(0x8d)
|
||||
);
|
||||
// The cast grants immunity: the arm is SKIPPED and the cast goes out. This is the whole
|
||||
// point — Ice Block cast while stunned.
|
||||
assert_eq!(
|
||||
cast_cc_refusal(STUNNED, Some(100), false, Some(&cast), &mut scan(true, 0)),
|
||||
reason_of(cast_cc_refusal(
|
||||
STUNNED,
|
||||
Some(100),
|
||||
false,
|
||||
Some(&cast),
|
||||
&mut scan(true, 0)
|
||||
)),
|
||||
None
|
||||
);
|
||||
// …and skipping one arm does not skip the ladder: a silenced-and-stunned caster whose
|
||||
// spell counters only the stun still refuses on silence below it.
|
||||
assert_eq!(
|
||||
cast_cc_refusal(
|
||||
reason_of(cast_cc_refusal(
|
||||
STUNNED | SILENCED,
|
||||
Some(100),
|
||||
false,
|
||||
@@ -1328,32 +1371,83 @@ mod cc_refusal_tests {
|
||||
mechanic: 0,
|
||||
}
|
||||
}
|
||||
),
|
||||
)),
|
||||
Some(0x60)
|
||||
);
|
||||
}
|
||||
|
||||
/// **The mechanic rides out with the refusal** (decision 1948) — the half that turns
|
||||
/// "Can't do that while %s" into a sentence. It is the ONE client-local refusal that carries
|
||||
/// an argument word, and it is `None` for every arm that fell to its own reason.
|
||||
#[test]
|
||||
fn the_renamed_refusal_carries_the_mechanic_and_the_others_carry_nothing() {
|
||||
let cast = spell(1);
|
||||
let scan = |exempt: bool, mechanic: u32| {
|
||||
move |_: &[u32]| benilla_formats::CcExemption { exempt, mechanic }
|
||||
};
|
||||
|
||||
// Rejected by a blocking aura: reason `0x8d` AND the mechanic that names it.
|
||||
assert_eq!(
|
||||
cast_cc_refusal(STUNNED, Some(100), false, Some(&cast), &mut scan(false, 12)),
|
||||
Some((0x8d, Some(12)))
|
||||
);
|
||||
// No aura: the arm's own reason, and NO argument — the message has no `%s` to fill.
|
||||
assert_eq!(
|
||||
cast_cc_refusal(STUNNED, Some(100), false, Some(&cast), &mut scan(false, 0)),
|
||||
Some((0x64, None))
|
||||
);
|
||||
// Exempt: nothing at all.
|
||||
assert_eq!(
|
||||
cast_cc_refusal(STUNNED, Some(100), false, Some(&cast), &mut scan(true, 0)),
|
||||
None
|
||||
);
|
||||
}
|
||||
|
||||
/// **A dead caster skips all six** (`0x60980d`'s `jle` on `UNIT_FIELD_HEALTH`) — a corpse is
|
||||
/// refused by an earlier rung, and reporting a stun over it would be the wrong line.
|
||||
#[test]
|
||||
fn a_dead_caster_takes_no_crowd_control_refusal() {
|
||||
let cast = spell(1);
|
||||
assert_eq!(
|
||||
cast_cc_refusal(STUNNED, Some(0), false, Some(&cast), &mut no_auras()),
|
||||
reason_of(cast_cc_refusal(
|
||||
STUNNED,
|
||||
Some(0),
|
||||
false,
|
||||
Some(&cast),
|
||||
&mut no_auras()
|
||||
)),
|
||||
None
|
||||
);
|
||||
assert_eq!(
|
||||
cast_cc_refusal(0, Some(0), true, Some(&cast), &mut no_auras()),
|
||||
reason_of(cast_cc_refusal(
|
||||
0,
|
||||
Some(0),
|
||||
true,
|
||||
Some(&cast),
|
||||
&mut no_auras()
|
||||
)),
|
||||
None
|
||||
);
|
||||
// Alive again, and the arm is back.
|
||||
assert_eq!(
|
||||
cast_cc_refusal(STUNNED, Some(1), false, Some(&cast), &mut no_auras()),
|
||||
reason_of(cast_cc_refusal(
|
||||
STUNNED,
|
||||
Some(1),
|
||||
false,
|
||||
Some(&cast),
|
||||
&mut no_auras()
|
||||
)),
|
||||
Some(0x64)
|
||||
);
|
||||
// No health field at all is not death — the descriptor simply has not landed.
|
||||
assert_eq!(
|
||||
cast_cc_refusal(STUNNED, None, false, Some(&cast), &mut no_auras()),
|
||||
reason_of(cast_cc_refusal(
|
||||
STUNNED,
|
||||
None,
|
||||
false,
|
||||
Some(&cast),
|
||||
&mut no_auras()
|
||||
)),
|
||||
Some(0x64)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -95,7 +95,7 @@ struct CharFeedMemo {
|
||||
items_objects: gate::Watch,
|
||||
items_templates: gate::Watch,
|
||||
names_generation: gate::Watch,
|
||||
/// `Items::enchant_display_epoch` — one step per displayable countdown change (the slot
|
||||
/// `Items::countdown_display_epoch` — one step per displayable countdown change (the slot
|
||||
/// views read second-floored countdowns), including the last elapse's collapsing push.
|
||||
enchant_deadlines: gate::Watch,
|
||||
/// `PendingItemOps::epoch` — one step per change to the in-flight lock set. Watched BESIDE
|
||||
@@ -604,6 +604,8 @@ fn slot_view(
|
||||
// feed's twin.
|
||||
let enchant_ms: [Option<u64>; 7] =
|
||||
std::array::from_fn(|s| items.enchant_remaining_display_ms(guid, s as u32));
|
||||
// The item's own expiry countdown — the bag feed's twin (decision 1933).
|
||||
let duration_ms = items.duration_remaining_display_ms(guid);
|
||||
let obj = items.object(guid)?;
|
||||
let entry = obj.object_entry()?;
|
||||
let count = obj.item_stack_count().unwrap_or(1).max(1);
|
||||
@@ -710,6 +712,7 @@ fn slot_view(
|
||||
bar_placeable,
|
||||
creator,
|
||||
enchants,
|
||||
duration_ms,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -805,8 +808,10 @@ fn inventory_slots(
|
||||
bar_placeable: false,
|
||||
creator: None,
|
||||
// Ammo carries no enchant slots to read: the wire never streams an ammo instance
|
||||
// here, only the template id off the player descriptor.
|
||||
// here, only the template id off the player descriptor — and for the same reason it
|
||||
// carries no expiry countdown either (there is no instance guid to key one on).
|
||||
enchants: Vec::new(),
|
||||
duration_ms: None,
|
||||
});
|
||||
}
|
||||
// Equipment 1..=19, then Bag0Slot..Bag3Slot (ids 20..23, PaperDollItemFrame.dbc-verified) —
|
||||
@@ -1061,7 +1066,9 @@ pub(crate) fn feed_char(
|
||||
// second per ticking enchant, plus one final step at the elapse. Holding the gate open on
|
||||
// `live_enchant_deadlines() > 0` (the first 1439 shape) rebuilt both snapshots and fired
|
||||
// `UNIT_INVENTORY_CHANGED` at frame rate for the whole life of a poison.
|
||||
let deadlines_moved = memo.enchant_deadlines.moved(items.enchant_display_epoch());
|
||||
let deadlines_moved = memo
|
||||
.enchant_deadlines
|
||||
.moved(items.countdown_display_epoch());
|
||||
let self_changed = !changed_self.is_empty();
|
||||
// `is_added` for the icon catalog: the feeds read only its load-once icon column;
|
||||
// its model-cache half churns every frame (the containers gate's own note).
|
||||
|
||||
@@ -216,13 +216,12 @@ fn feed_craft(
|
||||
if reagents.is_empty() {
|
||||
num_available = 0;
|
||||
}
|
||||
// **Focus first, then the totems** — `0x4ff980`'s own push order, and
|
||||
// `GetCraftSpellFocus 0x4f78b0` returns the very same pair list despite its name
|
||||
// (wow-re `tradeskill-tools-and-spell-focus.md`). The focus's flag is the literal
|
||||
// `1.0` with no predicate: the reference never reddens it. See
|
||||
// [`crate::ui_tradeskill`]'s twin, where the law is written out.
|
||||
let mut tools = Vec::new();
|
||||
for &t in d.totems.iter().filter(|&&t| t != 0) {
|
||||
let have = count_of(&store.0, &items, t, InventoryScope::CARRIED) > 0;
|
||||
if let Some(info) = items.template(t, 0, &commands) {
|
||||
tools.push((info.name.clone(), have));
|
||||
}
|
||||
}
|
||||
if d.requires_spell_focus != 0 {
|
||||
if let Some(n) = focus
|
||||
.as_deref()
|
||||
@@ -231,6 +230,12 @@ fn feed_craft(
|
||||
tools.push((n.to_string(), true));
|
||||
}
|
||||
}
|
||||
for &t in d.totems.iter().filter(|&&t| t != 0) {
|
||||
let have = count_of(&store.0, &items, t, InventoryScope::CARRIED) > 0;
|
||||
if let Some(info) = items.template(t, 0, &commands) {
|
||||
tools.push((info.name.clone(), have));
|
||||
}
|
||||
}
|
||||
let needs_item_target = matches!(
|
||||
d.effects[0],
|
||||
SPELL_EFFECT_ENCHANT_ITEM | SPELL_EFFECT_ENCHANT_ITEM_TEMPORARY
|
||||
|
||||
@@ -301,6 +301,17 @@ pub(super) fn drain_container_uses(
|
||||
}
|
||||
}
|
||||
for (bag, slot) in script.take_container_uses() {
|
||||
// **A right-click cancels an armed gift wrap, whatever it then does** (decision 1934):
|
||||
// only a LEFT-click on a container slot spends the paper, and the reference's use path
|
||||
// clears the cursor on its way. Above every affordance below, because a sell or a
|
||||
// deposit is still a right-click. Re-arming is not a special case: a right-click on a
|
||||
// second piece of paper cancels the first here and arms itself in the dispatcher.
|
||||
if let Some(w) = script.cancel_gift_wrap() {
|
||||
debug!(
|
||||
"ui_items: right-click cancels the armed gift wrap on bag {} slot {}",
|
||||
w.bag, w.slot
|
||||
);
|
||||
}
|
||||
// Lua (bagID, 1-based slot) → the wire's player-array addressing.
|
||||
let slot0 = u8::try_from(slot.saturating_sub(1)).ok();
|
||||
// Sell affordance (decision 0081 v1): while a merchant is open, a bag-slot click sells the
|
||||
@@ -380,6 +391,7 @@ pub(super) fn drain_container_uses(
|
||||
spell_index: t.use_spell_index().unwrap_or(0),
|
||||
use_spell: t.use_spell.map(|u| u.spell_id),
|
||||
unwraps_gift: t.unwraps_gift(inst_flags),
|
||||
begins_gift_wrap: t.begins_gift_wrap(inst_flags),
|
||||
opens_loot: t.opens_loot(),
|
||||
page_text: t.page_text,
|
||||
is_charter: t.flags & benilla_protocol::messages::ITEM_FLAG_CHARTER != 0,
|
||||
@@ -435,6 +447,21 @@ pub(super) fn drain_container_uses(
|
||||
});
|
||||
continue;
|
||||
}
|
||||
// …and arm #2's OTHER side: a piece of wrapping paper (`0x5d8d9d`'s clear branch →
|
||||
// `0x5edea0`). **Purely local — nothing is sent.** The paper's slot locks, the displayed
|
||||
// cursor becomes mode 2, and the next LEFT-click on a container slot is what sends
|
||||
// `CMSG_WRAP_ITEM` (`ui_items::feed`'s wrap drain, through the engine's
|
||||
// `pickup_container_item`). Decision 1934.
|
||||
if let Some(c) = clicked.filter(|c| c.begins_gift_wrap) {
|
||||
debug!(
|
||||
"ui_items: arm gift wrap with {:#x} (lua bag {bag} slot {slot})",
|
||||
c.guid
|
||||
);
|
||||
// `arm_gift_wrap` queues the slot's own `ITEM_LOCK_CHANGED` — the paper dims from
|
||||
// the lock the arm takes, exactly as a held item's source slot does.
|
||||
script.arm_gift_wrap(bag, slot);
|
||||
continue;
|
||||
}
|
||||
// #3 — the quest-starter (`0x5d8dd2`, decision 0664): the item's own guid is the
|
||||
// questgiver. Placed at the reference's position rather than at the tail, so a starter
|
||||
// that is *also* readable or lootable offers its quest instead of reading/opening. Routed
|
||||
@@ -602,6 +629,9 @@ struct Clicked {
|
||||
use_spell: Option<u32>,
|
||||
/// `ItemInfo::unwraps_gift` for this instance — dispatcher arm #2.
|
||||
unwraps_gift: bool,
|
||||
/// `ItemInfo::begins_gift_wrap` — arm #2's OTHER side: a piece of wrapping paper. Arms the
|
||||
/// local wrap cursor and sends nothing (decision 1934).
|
||||
begins_gift_wrap: bool,
|
||||
/// `ItemInfo::opens_loot` for this template — dispatcher arm #8.
|
||||
opens_loot: bool,
|
||||
/// The template's `PageText` — dispatcher arm #5's book gate (decision 1105); `0` = not a
|
||||
@@ -645,6 +675,31 @@ pub(super) fn drain_container_moves(
|
||||
return;
|
||||
};
|
||||
let store = self_q.iter().next();
|
||||
// **The completed gift wraps** (decision 1934): a left-click that spent an armed paper. Both
|
||||
// pairs go out in the wire's own order — the paper first — and the whole eligibility question
|
||||
// is the server's: it answers an ineligible target with one of the six `ERR_CANT_WRAP_*`
|
||||
// reasons through the ordinary `SMSG_INVENTORY_CHANGE_FAILURE` line.
|
||||
for (gift_bag, gift_slot, item_bag, item_slot) in script.take_container_wraps() {
|
||||
let (Some((gift_bag_index, gift_wire_slot)), Some((item_bag_index, item_wire_slot))) =
|
||||
(wire_pos(gift_bag, gift_slot), wire_pos(item_bag, item_slot))
|
||||
else {
|
||||
debug!(
|
||||
"ui_items: WrapItem({gift_bag}/{gift_slot} → {item_bag}/{item_slot}) out of \
|
||||
range — ignored"
|
||||
);
|
||||
continue;
|
||||
};
|
||||
debug!(
|
||||
"ui_items: wrap {item_bag}/{item_slot} with the paper at {gift_bag}/{gift_slot} \
|
||||
(wire {gift_bag_index}/{gift_wire_slot} → {item_bag_index}/{item_wire_slot})"
|
||||
);
|
||||
let _ = commands.0.send(ClientCommand::WrapItem {
|
||||
gift_bag: gift_bag_index,
|
||||
gift_slot: gift_wire_slot,
|
||||
item_bag: item_bag_index,
|
||||
item_slot: item_wire_slot,
|
||||
});
|
||||
}
|
||||
for mv in script.take_container_moves() {
|
||||
send_container_move(
|
||||
&mut script,
|
||||
@@ -932,6 +987,46 @@ mod tests {
|
||||
"the slot greys at the click"
|
||||
);
|
||||
}
|
||||
|
||||
/// **Right-clicking wrapping paper arms the wrap and sends NOTHING** (decision 1934) — the
|
||||
/// same dispatcher arm the wrapped-gift unwrap takes, on its other side. This is the bug the
|
||||
/// carve found: benilla fell through to `CMSG_USE_ITEM`, which casts a spell the paper does
|
||||
/// not have.
|
||||
#[test]
|
||||
fn a_wrapper_right_click_arms_the_cursor_and_ships_no_packet() {
|
||||
let (mut app, rx) = open_the_clam();
|
||||
while rx.try_recv().is_ok() {} // drain the clam's own send
|
||||
// Re-dress backpack slot 1 as a piece of wrapping paper: WRAPPER on the template, and no
|
||||
// WRAPPED bit on the instance (that combination is the begin-wrap arm; with the bit set it
|
||||
// would be a present, and would send `CMSG_OPEN_ITEM`).
|
||||
let mut items = app.world_mut().resource_mut::<Items>();
|
||||
items.insert_template(
|
||||
CLAM_ENTRY,
|
||||
Some(ItemInfo {
|
||||
flags: benilla_protocol::messages::ITEM_FLAG_WRAPPER,
|
||||
..crate::items::test_template("Red Ribboned Wrapping Paper")
|
||||
}),
|
||||
);
|
||||
{
|
||||
let script = app.world().non_send_resource::<UiScript>();
|
||||
script.run("UseContainerItem(0, 1)").unwrap();
|
||||
}
|
||||
app.world_mut()
|
||||
.run_system_once(drain_container_uses)
|
||||
.unwrap();
|
||||
|
||||
assert!(
|
||||
rx.try_recv().is_err(),
|
||||
"the begin-wrap arm is purely local — nothing goes on the wire until a container \
|
||||
click spends it"
|
||||
);
|
||||
let script = app.world().non_send_resource::<UiScript>();
|
||||
assert_eq!(
|
||||
script.gift_wrap_armed(),
|
||||
Some(benilla_ui::script::PendingWrap { bag: 0, slot: 1 }),
|
||||
"…and the paper is armed"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// **The bind confirmations' answers** (decision 1750) — `EquipPendingItem`/`CancelPendingEquip`
|
||||
@@ -1091,6 +1186,7 @@ mod bind_confirm_tests {
|
||||
slots.insert(
|
||||
1,
|
||||
ContainerSlot {
|
||||
duration_ms: None,
|
||||
petition: None,
|
||||
already_bound: false,
|
||||
bar_placeable: true,
|
||||
|
||||
@@ -41,7 +41,7 @@ pub(crate) struct FeedMemory {
|
||||
/// what a charter slot's tooltip lines are built from. Lazy, so its arrival moves nothing else
|
||||
/// here (the same reason `names_generation` exists one line up).
|
||||
petition_records: gate::Watch,
|
||||
/// `Items::enchant_display_epoch` — one step per displayable countdown change (the slot
|
||||
/// `Items::countdown_display_epoch` — one step per displayable countdown change (the slot
|
||||
/// views read second-floored countdowns), including the last elapse's collapsing push.
|
||||
enchant_deadlines: gate::Watch,
|
||||
/// The item guid **of the bag itself** in each of the ten bag slots — container ids 1..=10
|
||||
@@ -565,6 +565,9 @@ fn resolve_slot(
|
||||
// (both live on `Items`): one `Option<ms>` per enchant slot.
|
||||
let enchant_ms: [Option<u64>; 7] =
|
||||
std::array::from_fn(|s| items.enchant_remaining_display_ms(guid, s as u32));
|
||||
// The item's own expiry countdown, read off the same deadline store and floored the same way
|
||||
// — `SMSG_ITEM_TIME_UPDATE`'s only surface (decision 1933).
|
||||
let duration_ms = items.duration_remaining_display_ms(guid);
|
||||
let (entry, count, durability, readable, creator, flags, already_bound, roll, enchant_lines) =
|
||||
match items.object(guid) {
|
||||
Some(fields) => (
|
||||
@@ -631,6 +634,7 @@ fn resolve_slot(
|
||||
flags,
|
||||
already_bound,
|
||||
enchants: enchant_lines,
|
||||
duration_ms,
|
||||
..Default::default()
|
||||
});
|
||||
};
|
||||
@@ -678,6 +682,7 @@ fn resolve_slot(
|
||||
flags,
|
||||
already_bound,
|
||||
enchants: enchant_lines,
|
||||
duration_ms,
|
||||
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| {
|
||||
@@ -830,7 +835,7 @@ pub(crate) fn feed_containers(
|
||||
// that rebuilt every bag snapshot for the whole life of a ticking enchant.
|
||||
let deadlines_moved = memory
|
||||
.enchant_deadlines
|
||||
.moved(items.enchant_display_epoch());
|
||||
.moved(items.countdown_display_epoch());
|
||||
// Bound as `let`s (not a bare OR-chain) so the gate trace below can name each input.
|
||||
let sweep = cooldowns.sweep_pending(clock.anchor);
|
||||
let self_changed = !self_q.1.is_empty();
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
//! The connection-telemetry feed — the app side of `benilla_ui::script::net_stats`' seam.
|
||||
//!
|
||||
//! One number: the latency `GetNetStats()` reports, which is the main bar's performance ("ping")
|
||||
//! meter's whole input (`ActionBar.xml`'s `MainMenuBarPerformanceBarFrame` polls it every 10 s and
|
||||
//! meter's whole input (stock `MainMenuBar.xml`'s `MainMenuBarPerformanceBarFrame` polls it every 10 s and
|
||||
//! colors the bar green/yellow/red). The measurement itself belongs to the **net read thread** —
|
||||
//! every `SMSG_PONG` is timed where it lands and filed in the connection clock's RTT history at the
|
||||
//! reference's own depth — and this feed only carries its average across the engine boundary (decision
|
||||
|
||||
@@ -18,11 +18,25 @@ fn shipped_action_bar_drives_end_to_end() {
|
||||
// `load_ui` returns the same `report.frames` the disk reader asserted on, so this
|
||||
// count is the one that always stood here — moved, not re-derived.
|
||||
super::test_ui::load_ui(&s, "Interface\\FrameXML\\ActionButtonTemplate.xml");
|
||||
let frames = super::test_ui::load_ui(&s, "ActionBar.xml");
|
||||
super::test_ui::load_ui(&s, "Interface\\FrameXML\\TextStatusBar.lua");
|
||||
super::test_ui::load_ui(&s, "Interface\\FrameXML\\TextStatusBar.xml");
|
||||
super::test_ui::load_ui(&s, "Interface\\FrameXML\\Fonts.xml");
|
||||
super::test_ui::load_ui(&s, "UIParent.xml");
|
||||
super::test_ui::load_ui(&s, "MoneyFrame.xml");
|
||||
super::test_ui::load_ui(&s, "GameTooltip.xml");
|
||||
super::test_ui::load_ui(&s, "Interface\\FrameXML\\GlobalStrings.lua");
|
||||
let frames = super::test_ui::load_ui(&s, "Interface\\FrameXML\\MainMenuBar.xml")
|
||||
+ super::test_ui::load_ui(&s, "Interface\\FrameXML\\ActionBarFrame.xml")
|
||||
+ super::test_ui::load_ui(&s, "Interface\\FrameXML\\BonusActionBarFrame.xml");
|
||||
assert_eq!(
|
||||
frames, 59,
|
||||
"bar + XP StatusBar (+ its numerals overlay) + exhaustion tick + max-level rail + art frame + 12 buttons (each with a Cooldown child) + 2 page buttons + the performance meter and its hover button, \
|
||||
+ BonusActionBarFrame and its 12 buttons with their Cooldown children (25 — hidden, as the reference's is; decision 1223) — and NO ReputationWatchBar: its three frames went home to the reference's own ReputationFrame.xml with 1875"
|
||||
frames, 80,
|
||||
"what the three stock files declare (1938): MainMenuBar.xml's 8 — the bar, the XP StatusBar, \
|
||||
the overlay frame, the max-level rail, the art frame, the performance bar and its button, \
|
||||
the exhaustion tick; ActionBarFrame.xml's 14 — 12 ActionButtons and the 2 page arrows; \
|
||||
BonusActionBarFrame.xml's 24 — the bonus frame with its 12 buttons and the shapeshift frame \
|
||||
with its 10; plus one $parentCooldown per action, bonus and shapeshift button (12 + 12 + 10). \
|
||||
Ours built 59 for the same seats: no shapeshift bar in the file (StanceBar.xml's), no \
|
||||
overlay frame, no performance-bar button"
|
||||
);
|
||||
|
||||
// `ExhaustionTick_Update` indexes `ReputationWatchBar` UNGUARDED — the reference's own code,
|
||||
@@ -60,13 +74,20 @@ fn shipped_action_bar_drives_end_to_end() {
|
||||
}),
|
||||
);
|
||||
s.fire_event("PLAYER_ENTERING_WORLD", vec![]);
|
||||
// The app fires this on the offset's edge (ui_action/feed.rs); the stock bonus frame shows on
|
||||
// it and slides up over the main bar for BONUSACTIONBAR_SLIDETIME (0.15 s) — one OnUpdate
|
||||
// paints the start, the next lands it.
|
||||
s.fire_event("UPDATE_BONUS_ACTIONBAR", vec![]);
|
||||
s.tick(10.0);
|
||||
s.tick(0.2);
|
||||
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
|
||||
|
||||
// Button 1 paints action 73's icon seated in the art frame's first well: the 1024-wide bar
|
||||
// centers at BOTTOM of the 1024-wide screen ⇒ bar/art-frame left edge = (1024-1024)/2 = 0.
|
||||
// Button1 anchors the art frame's BOTTOMLEFT +(8,4), 36×36 ⇒ x[8,44] y[4,40]; the icon fills
|
||||
// the button (owner-sized) ⇒ the same rect. The chain stride is 36 + 6 = 42, so button i's left
|
||||
// edge is 8 + (i-1)*42.
|
||||
// The stance page is painted by BonusActionButton1/2, not by the main bar (1897/1938). The
|
||||
// 1024-wide bar centers at BOTTOM of the 1024-wide screen ⇒ bar left edge = 0; the bonus
|
||||
// frame lands at the bar's BOTTOMLEFT + (BONUSACTIONBAR_XPOS 4, BONUSACTIONBAR_YPOS 43), is
|
||||
// 43 high, and its button 1 sits at its own BOTTOMLEFT + (5, 4), 36×36 ⇒ x[9,45] y[4,40] —
|
||||
// one pixel right of the main bar's (8, 4). The reference's own geometry, off
|
||||
// BonusActionBarFrame.xml:54-96; the chain stride is 36 + 6 = 42.
|
||||
s.resolve();
|
||||
let quads = s.extract();
|
||||
let icon = |path: &str| {
|
||||
@@ -75,11 +96,12 @@ fn shipped_action_bar_drives_end_to_end() {
|
||||
.find(|q| matches!(&q.content, QuadContent::Texture { path: Some(p), .. } if p == path))
|
||||
.and_then(|q| q.rect)
|
||||
};
|
||||
let r = icon("Interface\\Icons\\Ability_SteelMelee").expect("button 1 icon");
|
||||
assert_eq!((r.left, r.bottom, r.right, r.top), (8.0, 4.0, 44.0, 40.0));
|
||||
let r2 = icon("Interface\\Icons\\Ability_Rogue_Ambush").expect("button 2 icon");
|
||||
assert_eq!(r2.left, 8.0 + 42.0); // button 2 left = 50
|
||||
// Twelve quickslot rings (every button draws its NormalTexture), two icons only.
|
||||
let r = icon("Interface\\Icons\\Ability_SteelMelee").expect("bonus button 1 icon");
|
||||
assert_eq!((r.left, r.bottom, r.right, r.top), (9.0, 4.0, 45.0, 40.0));
|
||||
let r2 = icon("Interface\\Icons\\Ability_Rogue_Ambush").expect("bonus button 2 icon");
|
||||
assert_eq!(r2.left, 9.0 + 42.0); // bonus button 2 left = 51
|
||||
// Only an OCCUPIED button is shown — an empty one hides while showgrid == 0
|
||||
// (ActionButton.lua:69-70) — so two rings, two icons.
|
||||
let rings = quads
|
||||
.iter()
|
||||
.filter(|q| {
|
||||
@@ -87,7 +109,10 @@ fn shipped_action_bar_drives_end_to_end() {
|
||||
if p.contains("UI-Quickslot2"))
|
||||
})
|
||||
.count();
|
||||
assert_eq!(rings, 12);
|
||||
assert_eq!(
|
||||
rings, 2,
|
||||
"the two occupied bonus buttons; every empty main and bonus slot is hidden"
|
||||
);
|
||||
let icons = quads
|
||||
.iter()
|
||||
.filter(|q| {
|
||||
@@ -97,8 +122,8 @@ fn shipped_action_bar_drives_end_to_end() {
|
||||
.count();
|
||||
assert_eq!(icons, 2, "empty slots draw no icon quad");
|
||||
|
||||
// A physical click on button 1 queues UseAction(73) — the stance page's id, not 1. Button 1's
|
||||
// center is (8+18, 4+18) = (26, 22).
|
||||
// A physical click at (26, 22) lands on BonusActionButton1 (9..45 × 4..40, two frame levels
|
||||
// above the hidden main slot) and queues UseAction(73) — the stance page's id, not 1.
|
||||
s.mouse_button(26.0, 22.0, "LeftButton", true);
|
||||
s.mouse_button(26.0, 22.0, "LeftButton", false);
|
||||
assert_eq!(action_ids(&mut s), vec![73]);
|
||||
@@ -124,9 +149,10 @@ fn shipped_action_bar_drives_end_to_end() {
|
||||
s.run("ActionButtonDown(2)").unwrap();
|
||||
assert_eq!(depressed(&s), 1, "key DOWN shows the pushed texture");
|
||||
assert_eq!(
|
||||
s.eval::<String>("return ActionButton2:GetButtonState()")
|
||||
s.eval::<String>("return BonusActionButton2:GetButtonState()")
|
||||
.unwrap(),
|
||||
"PUSHED"
|
||||
"PUSHED",
|
||||
"with the overlay up a key drives the BONUS button (ActionButton.lua:15-22)"
|
||||
);
|
||||
s.run("ActionButtonUp(2)").unwrap();
|
||||
assert_eq!(action_ids(&mut s), vec![74], "key '2' fires action 74");
|
||||
@@ -135,6 +161,10 @@ fn shipped_action_bar_drives_end_to_end() {
|
||||
// Stance drops (offset 0): the bar re-pages to actions 1..12 — all empty here, icons clear.
|
||||
s.set_bonus_bar_offset(0);
|
||||
s.fire_event("UPDATE_BONUS_ACTIONBAR", vec![]);
|
||||
// The overlay descends carrying the old form's page (`lastBonusBar`) and hides at the end
|
||||
// of its slide — one OnUpdate more than BONUSACTIONBAR_SLIDETIME (paint, then advance).
|
||||
s.tick(0.2);
|
||||
s.tick(0.01);
|
||||
s.resolve();
|
||||
let icons_after = s
|
||||
.extract()
|
||||
@@ -151,7 +181,16 @@ fn shipped_action_bar_drives_end_to_end() {
|
||||
fn load_action_bar(s: &UiScript) {
|
||||
super::test_ui::load_ui(s, "Cooldown.xml");
|
||||
super::test_ui::load_ui(s, "Interface\\FrameXML\\ActionButtonTemplate.xml");
|
||||
super::test_ui::load_ui(s, "ActionBar.xml");
|
||||
super::test_ui::load_ui(s, "Interface\\FrameXML\\TextStatusBar.lua");
|
||||
super::test_ui::load_ui(s, "Interface\\FrameXML\\TextStatusBar.xml");
|
||||
super::test_ui::load_ui(s, "Interface\\FrameXML\\Fonts.xml");
|
||||
super::test_ui::load_ui(s, "UIParent.xml");
|
||||
super::test_ui::load_ui(s, "Interface\\FrameXML\\GlobalStrings.lua");
|
||||
super::test_ui::load_ui(s, "Interface\\FrameXML\\MainMenuBar.xml");
|
||||
super::test_ui::load_ui(s, "MoneyFrame.xml");
|
||||
super::test_ui::load_ui(s, "GameTooltip.xml");
|
||||
super::test_ui::load_ui(s, "Interface\\FrameXML\\ActionBarFrame.xml");
|
||||
super::test_ui::load_ui(s, "Interface\\FrameXML\\BonusActionBarFrame.xml");
|
||||
|
||||
// `ExhaustionTick_Update` indexes `ReputationWatchBar` UNGUARDED — the reference's own code,
|
||||
// safe there because `ReputationFrame.xml` is always loaded and always declares the bar. Since
|
||||
@@ -164,6 +203,14 @@ fn load_action_bar(s: &UiScript) {
|
||||
super::test_ui::load_ui(s, r"Interface\FrameXML\UIPanelTemplates.xml");
|
||||
super::test_ui::load_ui(s, r"Interface\FrameXML\OptionsFrameTemplates.xml");
|
||||
super::test_ui::load_ui(s, r"Interface\FrameXML\ReputationFrame.xml");
|
||||
// The options window: `LOCK_ACTIONBAR` and `ALWAYS_SHOW_MULTIBARS` are declared there, as the
|
||||
// reference declares them in UIOptionsFrame_Init (1938) — and the manifest loads it before
|
||||
// the bars.
|
||||
super::test_ui::load_ui(s, "UiPanels.xml");
|
||||
super::test_ui::load_ui(s, "Interface\\FrameXML\\UIDropDownMenu.xml");
|
||||
super::test_ui::load_ui(s, "ScrollTemplates.xml");
|
||||
super::test_ui::load_ui(s, "KeyBindingsPage.xml");
|
||||
super::test_ui::load_ui(s, "OptionsFrame.xml");
|
||||
}
|
||||
|
||||
/// The state/feedback layer (decision 0137 phase 4) through the REAL shipped XML: a pushed
|
||||
@@ -454,7 +501,7 @@ fn the_action_bar_lock_stops_the_drag_and_leaves_shift_click_alone() {
|
||||
s.resolve();
|
||||
|
||||
s.run(r#"LOCK_ACTIONBAR = "1""#).unwrap();
|
||||
s.run("BenillaActionButton_OnDragStart(ActionButton1)")
|
||||
s.run("this = ActionButton1 ActionButton1:GetScript(\"OnDragStart\")()")
|
||||
.unwrap();
|
||||
assert!(
|
||||
s.cursor_payload().is_none(),
|
||||
@@ -480,7 +527,7 @@ fn the_action_bar_lock_stops_the_drag_and_leaves_shift_click_alone() {
|
||||
);
|
||||
|
||||
// The receiving end is guarded too: the held action cannot be dropped back by a drag…
|
||||
s.run("BenillaActionButton_OnReceiveDrag(ActionButton1)")
|
||||
s.run("this = ActionButton1 ActionButton1:GetScript(\"OnReceiveDrag\")()")
|
||||
.unwrap();
|
||||
assert!(
|
||||
s.cursor_payload().is_some(),
|
||||
@@ -488,7 +535,7 @@ fn the_action_bar_lock_stops_the_drag_and_leaves_shift_click_alone() {
|
||||
);
|
||||
// …and unlocking makes both ends live again.
|
||||
s.run(r#"LOCK_ACTIONBAR = "0""#).unwrap();
|
||||
s.run("BenillaActionButton_OnReceiveDrag(ActionButton1)")
|
||||
s.run("this = ActionButton1 ActionButton1:GetScript(\"OnReceiveDrag\")()")
|
||||
.unwrap();
|
||||
assert!(s.cursor_payload().is_none(), "unlocked, the drop lands");
|
||||
assert_eq!(
|
||||
@@ -779,7 +826,16 @@ fn shipped_bag_frame_drives_end_to_end() {
|
||||
let frames = load_ui(&s, file);
|
||||
if *file == "Cooldown.xml" {
|
||||
load_ui(&s, "Interface\\FrameXML\\ActionButtonTemplate.xml");
|
||||
load_ui(&s, "ActionBar.xml");
|
||||
load_ui(&s, "Interface\\FrameXML\\TextStatusBar.lua");
|
||||
load_ui(&s, "Interface\\FrameXML\\TextStatusBar.xml");
|
||||
load_ui(&s, "Interface\\FrameXML\\Fonts.xml");
|
||||
load_ui(&s, "UIParent.xml");
|
||||
load_ui(&s, "Interface\\FrameXML\\GlobalStrings.lua");
|
||||
load_ui(&s, "Interface\\FrameXML\\MainMenuBar.xml");
|
||||
load_ui(&s, "MoneyFrame.xml");
|
||||
load_ui(&s, "GameTooltip.xml");
|
||||
load_ui(&s, "Interface\\FrameXML\\ActionBarFrame.xml");
|
||||
load_ui(&s, "Interface\\FrameXML\\BonusActionBarFrame.xml");
|
||||
}
|
||||
if *file == "Interface\\FrameXML\\MainMenuBarBagButtons.xml" {
|
||||
bar_frames = frames;
|
||||
@@ -803,6 +859,7 @@ fn shipped_bag_frame_drives_end_to_end() {
|
||||
slots.insert(
|
||||
1,
|
||||
ContainerSlot {
|
||||
duration_ms: None,
|
||||
petition: None,
|
||||
already_bound: false,
|
||||
bar_placeable: true,
|
||||
@@ -1112,7 +1169,20 @@ fn the_bonus_action_bar_exists_hidden_and_takes_layout_calls() {
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
// A hidden bar changes nothing about the visible one.
|
||||
// A hidden bar changes nothing about the visible one. (An EMPTY main-bar button is itself
|
||||
// hidden under the reference — `ActionButton_Update` hides a slot with no action while
|
||||
// `showgrid == 0`, ActionButton.lua:69-70 — so slot 1 is occupied first.)
|
||||
s.set_action(
|
||||
1,
|
||||
Some(ActionSlot {
|
||||
texture: Some("Interface\\Icons\\Ability_SteelMelee".into()),
|
||||
kind: 0x00,
|
||||
action: 100,
|
||||
count: 0,
|
||||
consumable: false,
|
||||
}),
|
||||
);
|
||||
s.fire_event("ACTIONBAR_SLOT_CHANGED", vec![ScriptValue::Int(1)]);
|
||||
assert!(
|
||||
s.eval::<bool>("return ActionButton1:IsShown()").unwrap(),
|
||||
"the main bar is untouched"
|
||||
@@ -1193,7 +1263,7 @@ fn both_reference_action_button_templates_are_inheritable() {
|
||||
// zBar's exact shape: inherit the bar template, supply your own OnLoad.
|
||||
let doc = benilla_ui::framexml::parse(
|
||||
r#"<Ui>
|
||||
<CheckButton name="ZLikeButton" inherits="ActionBarButtonTemplate" id="1">
|
||||
<CheckButton name="ZLikeButton" inherits="ActionBarButtonTemplate" parent="UIParent" id="1">
|
||||
<Anchors><Anchor point="CENTER"/></Anchors>
|
||||
</CheckButton>
|
||||
<CheckButton name="BareLikeButton" inherits="ActionButtonTemplate" id="1">
|
||||
@@ -1279,7 +1349,14 @@ fn the_main_bar_pages_and_a_bonus_page_still_outranks_it() {
|
||||
"UIParent.xml",
|
||||
"Cooldown.xml",
|
||||
"Interface\\FrameXML\\ActionButtonTemplate.xml",
|
||||
"ActionBar.xml",
|
||||
"Interface\\FrameXML\\TextStatusBar.lua",
|
||||
"Interface\\FrameXML\\TextStatusBar.xml",
|
||||
"Interface\\FrameXML\\GlobalStrings.lua",
|
||||
"Interface\\FrameXML\\MainMenuBar.xml",
|
||||
"MoneyFrame.xml",
|
||||
"GameTooltip.xml",
|
||||
"Interface\\FrameXML\\ActionBarFrame.xml",
|
||||
"Interface\\FrameXML\\BonusActionBarFrame.xml",
|
||||
// The reference declares the reputation WATCH BAR in `ReputationFrame.xml`, and
|
||||
// `ExhaustionTick_Update` reads `ReputationWatchBar:IsShown()` twice — the reference's own
|
||||
// coupling of MainMenuBar to that pane. So an action-bar harness loads it, and with it the
|
||||
@@ -1288,7 +1365,15 @@ fn the_main_bar_pages_and_a_bonus_page_still_outranks_it() {
|
||||
r"Interface\FrameXML\UIPanelTemplates.xml",
|
||||
r"Interface\FrameXML\OptionsFrameTemplates.xml",
|
||||
r"Interface\FrameXML\ReputationFrame.xml",
|
||||
"MultiBars.xml",
|
||||
"Interface\\FrameXML\\ActionBarFrame.xml",
|
||||
"UiPanels.xml",
|
||||
"Interface\\FrameXML\\UIDropDownMenu.xml",
|
||||
"ScrollTemplates.xml",
|
||||
r"Interface\FrameXML\UIPanelTemplates.lua",
|
||||
r"Interface\FrameXML\UIPanelTemplates.xml",
|
||||
"KeyBindingsPage.xml",
|
||||
"OptionsFrame.xml",
|
||||
"Interface\\FrameXML\\MultiActionBars.xml",
|
||||
] {
|
||||
// `test_ui::load_ui`, not a disk read: this list names chain entries now (the reputation
|
||||
// pane and the templates it inherits through), and `assets/ui` cannot answer for those.
|
||||
@@ -1378,14 +1463,31 @@ fn the_main_bar_pages_and_a_bonus_page_still_outranks_it() {
|
||||
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.
|
||||
// The bonus branch is GUARDED by the page (ActionButton.lua:447: `button.isBonus and
|
||||
// CURRENT_ACTIONBAR_PAGE == 1`) — and a MAIN-bar button has no isBonus at all, so it never
|
||||
// takes that branch: the bonus offset is BonusActionBarFrame's own twelve buttons' business
|
||||
// (1897, adopted with 1938). Main-bar slot 1 on page 3 is action 25 whatever the offset.
|
||||
s.run("CURRENT_ACTIONBAR_PAGE = 3").unwrap();
|
||||
s.set_bonus_bar_offset(1);
|
||||
assert_eq!(
|
||||
s.eval::<i64>("return ActionButton_GetPagedID(ActionButton1)")
|
||||
.unwrap(),
|
||||
25,
|
||||
"a main-bar button follows its page, never the bonus offset (ActionButton.lua:447-456)"
|
||||
);
|
||||
s.run("CURRENT_ACTIONBAR_PAGE = 1").unwrap();
|
||||
assert_eq!(
|
||||
s.eval::<i64>("return ActionButton_GetPagedID(BonusActionButton1)")
|
||||
.unwrap(),
|
||||
73,
|
||||
"bonus offset 1 is action 73, whatever page the main bar is on"
|
||||
"the bonus frame's own button, on page 1 with offset 1, is 72 + 1"
|
||||
);
|
||||
s.run("CURRENT_ACTIONBAR_PAGE = 3").unwrap();
|
||||
assert_eq!(
|
||||
s.eval::<i64>("return ActionButton_GetPagedID(BonusActionButton1)")
|
||||
.unwrap(),
|
||||
25,
|
||||
"and off page 1 even the bonus button follows the page: the conjunct 1897 found missing"
|
||||
);
|
||||
s.set_bonus_bar_offset(0);
|
||||
assert_eq!(
|
||||
@@ -1410,7 +1512,16 @@ fn bonus_bar_slides_up_with_sound_and_down_without() {
|
||||
s.set_screen_size(1024.0, 768.0);
|
||||
super::test_ui::load_ui(&s, "Cooldown.xml");
|
||||
super::test_ui::load_ui(&s, "Interface\\FrameXML\\ActionButtonTemplate.xml");
|
||||
super::test_ui::load_ui(&s, "ActionBar.xml");
|
||||
super::test_ui::load_ui(&s, "Interface\\FrameXML\\TextStatusBar.lua");
|
||||
super::test_ui::load_ui(&s, "Interface\\FrameXML\\TextStatusBar.xml");
|
||||
super::test_ui::load_ui(&s, "Interface\\FrameXML\\Fonts.xml");
|
||||
super::test_ui::load_ui(&s, "UIParent.xml");
|
||||
super::test_ui::load_ui(&s, "Interface\\FrameXML\\GlobalStrings.lua");
|
||||
super::test_ui::load_ui(&s, "Interface\\FrameXML\\MainMenuBar.xml");
|
||||
super::test_ui::load_ui(&s, "MoneyFrame.xml");
|
||||
super::test_ui::load_ui(&s, "GameTooltip.xml");
|
||||
super::test_ui::load_ui(&s, "Interface\\FrameXML\\ActionBarFrame.xml");
|
||||
super::test_ui::load_ui(&s, "Interface\\FrameXML\\BonusActionBarFrame.xml");
|
||||
|
||||
// `ExhaustionTick_Update` indexes `ReputationWatchBar` UNGUARDED — the reference's own code,
|
||||
// safe there because `ReputationFrame.xml` is always loaded and always declares the bar. Since
|
||||
@@ -1474,12 +1585,34 @@ fn bonus_bar_slides_up_with_sound_and_down_without() {
|
||||
.into_iter()
|
||||
.map(|u| (u.action, u.on_self))
|
||||
.collect::<Vec<_>>(),
|
||||
vec![(73, true)],
|
||||
"ActionButtonUp's onSelf reaches UseAction's third argument"
|
||||
vec![(73, false)],
|
||||
"the BONUS branch of stock ActionButtonUp passes a literal 0 for onSelf (ActionButton.lua:38) \
|
||||
— in a stance, ALT-1 drives the bonus page without self-cast; the reference's own rule, \
|
||||
which 1745's expectation of (73, true) had smoothed over"
|
||||
);
|
||||
s.run("BonusActionBarFrame:Hide() ActionButtonDown(1) ActionButtonUp(1, 1)")
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
s.take_action_uses()
|
||||
.into_iter()
|
||||
.map(|u| (u.action, u.on_self))
|
||||
.collect::<Vec<_>>(),
|
||||
vec![(1, true)],
|
||||
"on the main-bar branch onSelf reaches UseAction's third argument (ActionButton.lua:51)"
|
||||
);
|
||||
s.run("BonusActionBarFrame:Show()").unwrap();
|
||||
|
||||
// Half the slide: the replica is half-risen (top = 0.5 * 43 over the bar's bottom edge at
|
||||
// y=0), the paint suppression still holds, still silent.
|
||||
// The slide, frame by frame. Stock `BonusActionBar_OnUpdate` paints the position the timer
|
||||
// has REACHED and then advances it (BonusActionBarFrame.lua:37-49), so the first OnUpdate
|
||||
// after a Show paints the start (top = 0 over the bar's bottom edge at y=0), the second the
|
||||
// half-risen replica (0.5 * 43), and the one that finds the timer past BONUSACTIONBAR_SLIDETIME
|
||||
// lands it. Silent until then.
|
||||
s.tick(0.075);
|
||||
let top = s
|
||||
.eval::<f64>("return BonusActionBarFrame:GetTop()")
|
||||
.unwrap();
|
||||
assert!(top.abs() < 0.01, "first frame top = {top}, want 0");
|
||||
assert!(s.take_sounds().is_empty());
|
||||
s.tick(0.075);
|
||||
let top = s
|
||||
.eval::<f64>("return BonusActionBarFrame:GetTop()")
|
||||
@@ -1495,8 +1628,10 @@ fn bonus_bar_slides_up_with_sound_and_down_without() {
|
||||
);
|
||||
assert!(s.take_sounds().is_empty());
|
||||
|
||||
// The landing edge: snap to 43, THE sound, and the main bar adopts the bonus page.
|
||||
s.tick(0.08);
|
||||
// The landing edge: snap to 43 and THE sound. The main bar does NOT adopt the bonus page:
|
||||
// a main-bar button has no `isBonus`, so `ActionButton_GetPagedID` keeps it on its page and the
|
||||
// overlay IS the stance page (ActionButton.lua:447-456; 1897, adopted with 1938).
|
||||
s.tick(0.01);
|
||||
assert_eq!(
|
||||
s.take_sounds(),
|
||||
vec![benilla_ui::script::SoundRequest::KitName(
|
||||
@@ -1519,8 +1654,8 @@ fn bonus_bar_slides_up_with_sound_and_down_without() {
|
||||
assert_eq!(
|
||||
s.eval::<i64>("return ActionButton_GetPagedID(ActionButton1)")
|
||||
.unwrap(),
|
||||
73,
|
||||
"the main bar adopts the bonus page at landing"
|
||||
1,
|
||||
"the main bar keeps its page under the landed overlay"
|
||||
);
|
||||
s.tick(0.5);
|
||||
assert!(s.take_sounds().is_empty(), "a landed bar never re-sounds");
|
||||
@@ -1540,7 +1675,8 @@ fn bonus_bar_slides_up_with_sound_and_down_without() {
|
||||
assert_eq!(
|
||||
s.eval::<i64>("return ActionButton_GetPagedID(ActionButton1)")
|
||||
.unwrap(),
|
||||
97
|
||||
1,
|
||||
"the main bar stays on its page through a form swap"
|
||||
);
|
||||
s.tick(0.2);
|
||||
assert!(
|
||||
@@ -1571,7 +1707,11 @@ fn bonus_bar_slides_up_with_sound_and_down_without() {
|
||||
// stand-in while the frame is still shown.
|
||||
s.run("ActionButtonDown(1) ActionButtonUp(1)").unwrap();
|
||||
assert_eq!(action_ids(&mut s), vec![97]);
|
||||
// Paint-then-advance again: the frame that finds the timer past the slide time is the one
|
||||
// that hides (BonusActionBarFrame.lua:51-66), so the descent takes one OnUpdate more than
|
||||
// the time itself.
|
||||
s.tick(0.2);
|
||||
s.tick(0.01);
|
||||
assert!(
|
||||
!s.eval::<bool>("return BonusActionBarFrame:IsShown()")
|
||||
.unwrap(),
|
||||
@@ -1593,7 +1733,16 @@ fn bonus_bar_turnaround_continues_from_position() {
|
||||
s.set_screen_size(1024.0, 768.0);
|
||||
super::test_ui::load_ui(&s, "Cooldown.xml");
|
||||
super::test_ui::load_ui(&s, "Interface\\FrameXML\\ActionButtonTemplate.xml");
|
||||
super::test_ui::load_ui(&s, "ActionBar.xml");
|
||||
super::test_ui::load_ui(&s, "Interface\\FrameXML\\TextStatusBar.lua");
|
||||
super::test_ui::load_ui(&s, "Interface\\FrameXML\\TextStatusBar.xml");
|
||||
super::test_ui::load_ui(&s, "Interface\\FrameXML\\Fonts.xml");
|
||||
super::test_ui::load_ui(&s, "UIParent.xml");
|
||||
super::test_ui::load_ui(&s, "Interface\\FrameXML\\GlobalStrings.lua");
|
||||
super::test_ui::load_ui(&s, "Interface\\FrameXML\\MainMenuBar.xml");
|
||||
super::test_ui::load_ui(&s, "MoneyFrame.xml");
|
||||
super::test_ui::load_ui(&s, "GameTooltip.xml");
|
||||
super::test_ui::load_ui(&s, "Interface\\FrameXML\\ActionBarFrame.xml");
|
||||
super::test_ui::load_ui(&s, "Interface\\FrameXML\\BonusActionBarFrame.xml");
|
||||
|
||||
// `ExhaustionTick_Update` indexes `ReputationWatchBar` UNGUARDED — the reference's own code,
|
||||
// safe there because `ReputationFrame.xml` is always loaded and always declares the bar. Since
|
||||
@@ -1620,15 +1769,28 @@ fn bonus_bar_turnaround_continues_from_position() {
|
||||
"hide"
|
||||
);
|
||||
// A third of the way back down from the turnaround point: 43 * (0.5 - 0.03/0.15).
|
||||
// `HideBonusActionBar` keeps a running timer (it resets it only when `completed`,
|
||||
// BonusActionBarFrame.lua:86-88), so the descent starts where the rise had got to: the rise's
|
||||
// one OnUpdate advanced the timer to 0.075 (half), and the hide arm paints (1 − 0.075/0.15) · 43
|
||||
// = 21.5 on its first frame, then 12.9 on the next — paint, then advance.
|
||||
s.tick(0.03);
|
||||
let top = s
|
||||
.eval::<f64>("return BonusActionBarFrame:GetTop()")
|
||||
.unwrap();
|
||||
assert!(
|
||||
(top - 21.5).abs() < 0.6,
|
||||
"turnaround descends from 21.5, top = {top}"
|
||||
);
|
||||
s.tick(0.03);
|
||||
let top = s
|
||||
.eval::<f64>("return BonusActionBarFrame:GetTop()")
|
||||
.unwrap();
|
||||
assert!(
|
||||
(top - 12.9).abs() < 0.6,
|
||||
"turnaround descends from 21.5, top = {top}"
|
||||
"next frame top = {top}, want ~12.9"
|
||||
);
|
||||
s.tick(0.2);
|
||||
s.tick(0.01);
|
||||
assert!(!s
|
||||
.eval::<bool>("return BonusActionBarFrame:IsShown()")
|
||||
.unwrap());
|
||||
@@ -1652,7 +1814,16 @@ fn the_page_arrows_do_not_steal_each_other_s_clicks() {
|
||||
s.set_screen_size(1024.0, 768.0);
|
||||
super::test_ui::load_ui(&s, "Cooldown.xml");
|
||||
super::test_ui::load_ui(&s, "Interface\\FrameXML\\ActionButtonTemplate.xml");
|
||||
super::test_ui::load_ui(&s, "ActionBar.xml");
|
||||
super::test_ui::load_ui(&s, "Interface\\FrameXML\\TextStatusBar.lua");
|
||||
super::test_ui::load_ui(&s, "Interface\\FrameXML\\TextStatusBar.xml");
|
||||
super::test_ui::load_ui(&s, "Interface\\FrameXML\\Fonts.xml");
|
||||
super::test_ui::load_ui(&s, "UIParent.xml");
|
||||
super::test_ui::load_ui(&s, "Interface\\FrameXML\\GlobalStrings.lua");
|
||||
super::test_ui::load_ui(&s, "Interface\\FrameXML\\MainMenuBar.xml");
|
||||
super::test_ui::load_ui(&s, "MoneyFrame.xml");
|
||||
super::test_ui::load_ui(&s, "GameTooltip.xml");
|
||||
super::test_ui::load_ui(&s, "Interface\\FrameXML\\ActionBarFrame.xml");
|
||||
super::test_ui::load_ui(&s, "Interface\\FrameXML\\BonusActionBarFrame.xml");
|
||||
|
||||
// `ExhaustionTick_Update` indexes `ReputationWatchBar` UNGUARDED — the reference's own code,
|
||||
// safe there because `ReputationFrame.xml` is always loaded and always declares the bar. Since
|
||||
|
||||
@@ -415,6 +415,7 @@ fn bag_tooltip_hangs_left_when_the_slot_sits_in_the_right_half() {
|
||||
slots.insert(
|
||||
1,
|
||||
ContainerSlot {
|
||||
duration_ms: None,
|
||||
petition: None,
|
||||
already_bound: false,
|
||||
bar_placeable: true,
|
||||
@@ -491,6 +492,7 @@ fn hovered_bag_tooltip_fills_itself_when_the_stats_land() {
|
||||
slots.insert(
|
||||
1,
|
||||
ContainerSlot {
|
||||
duration_ms: None,
|
||||
petition: None,
|
||||
already_bound: false,
|
||||
bar_placeable: true,
|
||||
@@ -594,6 +596,7 @@ fn vendor_bag_hover_shows_sell_price_and_arms_the_pouch_cursor() {
|
||||
slots.insert(
|
||||
1,
|
||||
ContainerSlot {
|
||||
duration_ms: None,
|
||||
petition: None,
|
||||
already_bound: false,
|
||||
bar_placeable: true,
|
||||
@@ -615,6 +618,7 @@ fn vendor_bag_hover_shows_sell_price_and_arms_the_pouch_cursor() {
|
||||
slots.insert(
|
||||
2,
|
||||
ContainerSlot {
|
||||
duration_ms: None,
|
||||
petition: None,
|
||||
already_bound: false,
|
||||
bar_placeable: true,
|
||||
@@ -798,6 +802,7 @@ fn drag_across_two_slots_queues_the_same_move_a_click_pickup_would() {
|
||||
slots.insert(
|
||||
1,
|
||||
ContainerSlot {
|
||||
duration_ms: None,
|
||||
petition: None,
|
||||
already_bound: false,
|
||||
bar_placeable: true,
|
||||
@@ -881,6 +886,7 @@ fn a_second_bag_window_feeds_and_paints_via_the_bag_bar() {
|
||||
slots.insert(
|
||||
1,
|
||||
ContainerSlot {
|
||||
duration_ms: None,
|
||||
petition: None,
|
||||
already_bound: false,
|
||||
bar_placeable: true,
|
||||
@@ -1013,6 +1019,7 @@ fn open_backpack_with_a_five_stack(s: &mut UiScript) -> (f32, f32) {
|
||||
slots.insert(
|
||||
1,
|
||||
ContainerSlot {
|
||||
duration_ms: None,
|
||||
petition: None,
|
||||
already_bound: false,
|
||||
bar_placeable: true,
|
||||
@@ -1454,6 +1461,7 @@ fn the_bar_bag_buttons_name_themselves_on_hover() {
|
||||
// SetInventoryItem arm. Bar slot 1 is inventory slot 20 (Bag0Slot).
|
||||
let mut inv: benilla_ui::script::InventorySlots = Default::default();
|
||||
inv[20] = Some(benilla_ui::script::InvSlotView {
|
||||
duration_ms: None,
|
||||
already_bound: false,
|
||||
bar_placeable: true,
|
||||
durability: None,
|
||||
@@ -1635,13 +1643,15 @@ fn the_first_key_puts_the_keyring_on_the_bar() {
|
||||
bar_right - perf_right
|
||||
);
|
||||
|
||||
// And it reverts: destroying the last key takes the keyring back off the bar (our divergence
|
||||
// from the ref's one-way saved-variable latch — see MainMenuBar_UpdateKeyRing).
|
||||
// And it does NOT revert: the reference's latch is one-way — `MainMenuBar_UpdateKeyRing`
|
||||
// only ever Shows, and SHOW_KEYRING is a saved variable (stock MainMenuBar.lua:174-183,
|
||||
// MainMenuBar.xml:323-336). Our file used to take the button away with the last key; that
|
||||
// divergence went with the file (1938).
|
||||
s.set_has_key(false);
|
||||
s.fire_event("BAG_UPDATE", vec![benilla_ui::script::ScriptValue::Int(-2)]);
|
||||
assert!(
|
||||
!s.eval::<bool>("return KeyRingButton:IsShown()").unwrap(),
|
||||
"losing the last key takes the button away again"
|
||||
s.eval::<bool>("return KeyRingButton:IsShown()").unwrap(),
|
||||
"losing the last key leaves the button on the bar — the reference's one-way latch"
|
||||
);
|
||||
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
|
||||
}
|
||||
|
||||
@@ -40,15 +40,19 @@ fn harness() -> UiScript {
|
||||
load_xml(&s, "UIParent.xml");
|
||||
load_xml(&s, "Cooldown.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\ActionButtonTemplate.xml");
|
||||
load_xml(&s, "ActionBar.xml"); // BENILLA_FALLBACK_ICON (the unknown-icon fallback)
|
||||
// The timer switch, PLANTED ON — not the shipped value. 1.12 declares it in
|
||||
// UIOptionsFrame.lua (default "0"); we have no counterpart to that file, so it lives with the
|
||||
// row that drives it (OptionsFrame.xml), where it shipped "1" from 0255/1139 until 1804 put
|
||||
// it back on the reference's "0". These tests are about the timer text and the geometry it
|
||||
// buys, so the harness turns it on the way the Interface page's row does. Order matters: the
|
||||
// reference's `BuffFrame_OnLoad` calls `BuffButtons_UpdatePositions`, which seats the debuff
|
||||
// row 20px differently depending on this value, so setting it afterwards leaves the bar laid
|
||||
// out for the wrong one.
|
||||
load_xml(&s, "Interface\\FrameXML\\TextStatusBar.lua");
|
||||
load_xml(&s, "Interface\\FrameXML\\TextStatusBar.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\MainMenuBar.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\ActionBarFrame.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\BonusActionBarFrame.xml"); // BENILLA_FALLBACK_ICON (the unknown-icon fallback)
|
||||
// The timer switch, PLANTED ON — not the shipped value. 1.12 declares it in
|
||||
// UIOptionsFrame.lua (default "0"); we have no counterpart to that file, so it lives with the
|
||||
// row that drives it (OptionsFrame.xml), where it shipped "1" from 0255/1139 until 1804 put
|
||||
// it back on the reference's "0". These tests are about the timer text and the geometry it
|
||||
// buys, so the harness turns it on the way the Interface page's row does. Order matters: the
|
||||
// reference's `BuffFrame_OnLoad` calls `BuffButtons_UpdatePositions`, which seats the debuff
|
||||
// row 20px differently depending on this value, so setting it afterwards leaves the bar laid
|
||||
// out for the wrong one.
|
||||
s.run("SHOW_BUFF_DURATIONS = \"1\"").unwrap();
|
||||
load_xml(&s, "Interface\\FrameXML\\BuffFrame.xml");
|
||||
// …and APPLIED, the way the app applies it (`manifest::apply_buff_durations`).
|
||||
|
||||
@@ -412,6 +412,14 @@ fn managed_positions_track_the_bottom_bar_stack() {
|
||||
assert_eq!(bottom(&s, "ChatFrame1"), 102.0, "85 + bottomLeft 17");
|
||||
|
||||
// The stance bar shows (the warrior at login): the pet term, plus chat's both-flags extra.
|
||||
// The pass's shapeshift-appearance arm (the reference's own, UIParent.lua:1705-1732 — in ours
|
||||
// since 1938) touches the bar's three shelf textures by name, unguarded as the reference has
|
||||
// it; a stand-in frame needs stand-in textures.
|
||||
s.run(
|
||||
"local t = { Show = function() end, Hide = function() end } \
|
||||
ShapeshiftBarLeft, ShapeshiftBarMiddle, ShapeshiftBarRight = t, t, t",
|
||||
)
|
||||
.unwrap();
|
||||
s.run("ShapeshiftBarFrame = { IsShown = function() return true end, SetPoint = function() end, ClearAllPoints = function() end }; UIParent_ManageFramePositions()")
|
||||
.unwrap();
|
||||
s.resolve();
|
||||
|
||||
@@ -68,6 +68,7 @@ fn combat_stats() -> UnitCombatStats {
|
||||
fn inventory_with_head_item() -> InventorySlots {
|
||||
let mut slots: InventorySlots = Default::default();
|
||||
slots[1] = Some(InvSlotView {
|
||||
duration_ms: None,
|
||||
already_bound: false,
|
||||
bar_placeable: true,
|
||||
durability: None,
|
||||
@@ -94,6 +95,7 @@ fn backpack_with_fitting_helm() -> benilla_ui::script::ContainerState {
|
||||
slots.insert(
|
||||
1,
|
||||
benilla_ui::script::ContainerSlot {
|
||||
duration_ms: None,
|
||||
petition: None,
|
||||
already_bound: false,
|
||||
bar_placeable: true,
|
||||
|
||||
@@ -315,6 +315,7 @@ fn chat_click_dismisses_a_stuck_spell_but_not_an_item() {
|
||||
slots.insert(
|
||||
1,
|
||||
ContainerSlot {
|
||||
duration_ms: None,
|
||||
petition: None,
|
||||
already_bound: false,
|
||||
bar_placeable: true,
|
||||
|
||||
@@ -22,6 +22,7 @@ fn one_item_backpack(item_id: u32, name: &str, quality: u32) -> ContainerState {
|
||||
slots.insert(
|
||||
1,
|
||||
ContainerSlot {
|
||||
duration_ms: None,
|
||||
petition: None,
|
||||
already_bound: false,
|
||||
bar_placeable: true,
|
||||
|
||||
@@ -24,6 +24,7 @@ fn one_item_backpack() -> ContainerState {
|
||||
slots.insert(
|
||||
1,
|
||||
ContainerSlot {
|
||||
duration_ms: None,
|
||||
petition: None,
|
||||
already_bound: false,
|
||||
bar_placeable: true,
|
||||
@@ -244,6 +245,7 @@ fn escape_closes_an_open_stack_split_frame() {
|
||||
slots.insert(
|
||||
1,
|
||||
ContainerSlot {
|
||||
duration_ms: None,
|
||||
petition: None,
|
||||
already_bound: false,
|
||||
bar_placeable: true,
|
||||
|
||||
@@ -23,7 +23,10 @@ fn exp_bar_harness() -> UiScript {
|
||||
"Interface\\FrameXML\\TextStatusBar.xml",
|
||||
"Cooldown.xml",
|
||||
"Interface\\FrameXML\\ActionButtonTemplate.xml",
|
||||
"ActionBar.xml",
|
||||
"Interface\\FrameXML\\GlobalStrings.lua",
|
||||
"Interface\\FrameXML\\MainMenuBar.xml",
|
||||
"Interface\\FrameXML\\ActionBarFrame.xml",
|
||||
"Interface\\FrameXML\\BonusActionBarFrame.xml",
|
||||
// The reference declares the reputation WATCH BAR in `ReputationFrame.xml`, and
|
||||
// `ExhaustionTick_Update` reads `ReputationWatchBar:IsShown()` twice — the reference's own
|
||||
// coupling of MainMenuBar to that pane. So an action-bar harness loads it, and with it the
|
||||
@@ -63,7 +66,8 @@ fn the_xp_bar_takes_the_mouse_and_explains_itself() {
|
||||
"the XP strip must be mouse-enabled or the hover never fires"
|
||||
);
|
||||
|
||||
s.run("BenillaExpBar_OnEnter(MainMenuExpBar)").unwrap();
|
||||
s.run("this = MainMenuExpBar MainMenuExpBar:GetScript(\"OnEnter\")()")
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
s.eval::<String>("return GameTooltipTextLeft1:GetText()")
|
||||
.unwrap(),
|
||||
@@ -81,7 +85,8 @@ fn the_xp_bar_takes_the_mouse_and_explains_itself() {
|
||||
"the default-corner anchor"
|
||||
);
|
||||
|
||||
s.run("BenillaExpBar_OnLeave()").unwrap();
|
||||
s.run("this = MainMenuExpBar MainMenuExpBar:GetScript(\"OnLeave\")()")
|
||||
.unwrap();
|
||||
assert!(
|
||||
!s.eval::<bool>("return GameTooltip:IsVisible()").unwrap(),
|
||||
"leaving hides the plate"
|
||||
@@ -301,7 +306,8 @@ fn the_xp_bar_numerals_show_on_hover() {
|
||||
s.set_player_xp(1234, 5678);
|
||||
s.fire_event("PLAYER_ENTERING_WORLD", vec![]);
|
||||
|
||||
s.run("BenillaExpBar_OnEnter(MainMenuExpBar)").unwrap();
|
||||
s.run("this = MainMenuExpBar MainMenuExpBar:GetScript(\"OnEnter\")()")
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
s.eval::<String>("return MainMenuBarExpText:GetText()")
|
||||
.unwrap(),
|
||||
@@ -315,7 +321,7 @@ fn the_xp_bar_numerals_show_on_hover() {
|
||||
// `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")
|
||||
s.run("this = MainMenuExpBar MainMenuExpBar:GetScript(\"OnLeave\")() this = nil")
|
||||
.unwrap();
|
||||
assert!(
|
||||
!s.eval::<bool>("return MainMenuBarExpText:IsShown()")
|
||||
@@ -338,7 +344,8 @@ fn the_rest_state_line_joins_the_held_open_tooltip() {
|
||||
|
||||
// 1.12's default posture (0661): detailed newbie tips ON.
|
||||
s.run("SHOW_NEWBIE_TIPS = \"1\"").unwrap();
|
||||
s.run("BenillaExpBar_OnEnter(MainMenuExpBar)").unwrap();
|
||||
s.run("this = MainMenuExpBar MainMenuExpBar:GetScript(\"OnEnter\")()")
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
s.eval::<f64>("return ExhaustionTick.timer").unwrap(),
|
||||
1.0,
|
||||
|
||||
@@ -1509,7 +1509,7 @@ fn convert_entry(
|
||||
// A slice that runs PAST the texture is the reference's tiling idiom, not a crop:
|
||||
// `SetTexCoord(0, n, 0, 1)` on an n-slots-wide strip repeats the art n times — the
|
||||
// stance shelf's middle carries one slot per extra form exactly that way
|
||||
// (`StanceBar.xml`, `ShapeshiftBar_Update`). Clamp-sampled it smears the last column
|
||||
// (stock `ShapeshiftBar_Update`). Clamp-sampled it smears the last column
|
||||
// across the extra width instead. Clamp/repeat bake into the `Image`, so this picks
|
||||
// the tiled GPU image + cache entry, as the `Backdrop` arm's `tile` does below.
|
||||
let tiled = uv
|
||||
|
||||
@@ -667,7 +667,16 @@ fn the_bag_row_greys_under_the_menu_without_any_of_it_disappearing() {
|
||||
&[
|
||||
"Cooldown.xml",
|
||||
"Interface\\FrameXML\\ActionButtonTemplate.xml",
|
||||
"ActionBar.xml",
|
||||
"Interface\\FrameXML\\TextStatusBar.lua",
|
||||
"Interface\\FrameXML\\TextStatusBar.xml",
|
||||
"Interface\\FrameXML\\Fonts.xml",
|
||||
"UIParent.xml",
|
||||
"Interface\\FrameXML\\GlobalStrings.lua",
|
||||
"Interface\\FrameXML\\MainMenuBar.xml",
|
||||
"MoneyFrame.xml",
|
||||
"GameTooltip.xml",
|
||||
"Interface\\FrameXML\\ActionBarFrame.xml",
|
||||
"Interface\\FrameXML\\BonusActionBarFrame.xml",
|
||||
],
|
||||
&["Interface\\FrameXML\\MerchantFrame.xml"],
|
||||
);
|
||||
@@ -758,6 +767,7 @@ fn backpack() -> ContainerState {
|
||||
slots.insert(
|
||||
1,
|
||||
ContainerSlot {
|
||||
duration_ms: None,
|
||||
petition: None,
|
||||
already_bound: false,
|
||||
bar_placeable: true,
|
||||
|
||||
@@ -49,7 +49,7 @@ fn text_color(quads: &[ExtractedQuad], t: &str) -> Option<[f32; 4]> {
|
||||
/// `UIParent.xml` comes with it because the `START_LOOT_ROLL` router lives there now — the
|
||||
/// reference's own slot for it — where our file used to carry a dedicated hidden driver frame.
|
||||
fn load_group_loot(s: &UiScript) {
|
||||
load_xml(s, "UIParent.xml");
|
||||
// `UIParent.xml` is loaded by `setup()` above — the stock main bar needs it first (1938).
|
||||
// `LootFrame.xml` brings the whole loot window, and its `GroupLootDropDown` calls
|
||||
// `UIDropDownMenu_Initialize` from its own OnLoad — the dropdown kit is on the chain too.
|
||||
load_xml(s, r"Interface\FrameXML\UIDropDownMenu.xml");
|
||||
@@ -81,8 +81,14 @@ fn setup() -> UiScript {
|
||||
load_xml(&s, "GameTooltip.xml"); // PASS/NEED/GREED + item hovers
|
||||
load_xml(&s, "Cooldown.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\ActionButtonTemplate.xml");
|
||||
load_xml(&s, "ActionBar.xml"); // BENILLA_FALLBACK_ICON (the in-flight icon fallback) —
|
||||
// buff_tests.rs's own load-order precedent for this same global.
|
||||
load_xml(&s, "Interface\\FrameXML\\TextStatusBar.lua");
|
||||
load_xml(&s, "Interface\\FrameXML\\TextStatusBar.xml");
|
||||
load_xml(&s, "UIParent.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\GlobalStrings.lua");
|
||||
load_xml(&s, "Interface\\FrameXML\\MainMenuBar.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\ActionBarFrame.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\BonusActionBarFrame.xml"); // BENILLA_FALLBACK_ICON (the in-flight icon fallback) —
|
||||
// buff_tests.rs's own load-order precedent for this same global.
|
||||
s
|
||||
}
|
||||
|
||||
@@ -569,6 +575,14 @@ fn managed_positions_engage_for_the_bare_frame_name() {
|
||||
assert_eq!(bottom(&s), 102.0, "60 + bottomEither 42 — the row engaged");
|
||||
|
||||
// The stance bar shows on top of them.
|
||||
// The pass's shapeshift-appearance arm (the reference's own, UIParent.lua:1705-1732 — in ours
|
||||
// since 1938) touches the bar's three shelf textures by name, unguarded as the reference has
|
||||
// it; a stand-in frame needs stand-in textures.
|
||||
s.run(
|
||||
"local t = { Show = function() end, Hide = function() end } \
|
||||
ShapeshiftBarLeft, ShapeshiftBarMiddle, ShapeshiftBarRight = t, t, t",
|
||||
)
|
||||
.unwrap();
|
||||
s.run("ShapeshiftBarFrame = { IsShown = function() return true end, SetPoint = function() end, ClearAllPoints = function() end }; UIParent_ManageFramePositions()")
|
||||
.unwrap();
|
||||
s.resolve();
|
||||
|
||||
@@ -36,7 +36,13 @@ fn setup() -> UiScript {
|
||||
// happens. Ours never called it. MicroMenu needs the bar's button kit under it.
|
||||
"Cooldown.xml",
|
||||
"Interface\\FrameXML\\ActionButtonTemplate.xml",
|
||||
"ActionBar.xml",
|
||||
"Interface\\FrameXML\\TextStatusBar.lua",
|
||||
"Interface\\FrameXML\\TextStatusBar.xml",
|
||||
"UIParent.xml",
|
||||
"Interface\\FrameXML\\GlobalStrings.lua",
|
||||
"Interface\\FrameXML\\MainMenuBar.xml",
|
||||
"Interface\\FrameXML\\ActionBarFrame.xml",
|
||||
"Interface\\FrameXML\\BonusActionBarFrame.xml",
|
||||
"MicroMenu.xml",
|
||||
// `TicketStatusFrame_OnEvent` re-anchors TemporaryEnchantFrame before it arms the repoll
|
||||
// timer, so without BuffFrame the handler raises and `refreshTime` is never set. This is
|
||||
|
||||
@@ -25,7 +25,15 @@ fn harness_with(extra: &[&str]) -> UiScript {
|
||||
"Interface\\FrameXML\\Fonts.xml",
|
||||
"Cooldown.xml",
|
||||
"Interface\\FrameXML\\ActionButtonTemplate.xml",
|
||||
"ActionBar.xml",
|
||||
"Interface\\FrameXML\\TextStatusBar.lua",
|
||||
"Interface\\FrameXML\\TextStatusBar.xml",
|
||||
"UIParent.xml",
|
||||
"Interface\\FrameXML\\GlobalStrings.lua",
|
||||
"Interface\\FrameXML\\MainMenuBar.xml",
|
||||
"MoneyFrame.xml",
|
||||
"GameTooltip.xml",
|
||||
"Interface\\FrameXML\\ActionBarFrame.xml",
|
||||
"Interface\\FrameXML\\BonusActionBarFrame.xml",
|
||||
"MicroMenu.xml",
|
||||
]
|
||||
.into_iter()
|
||||
|
||||
@@ -651,7 +651,7 @@ fn demo_unit_feed(script: Option<NonSendMut<UiScript>>, mut fired: Local<VmMemo<
|
||||
),
|
||||
(80, "Interface\\Icons\\INV_Misc_Food_16", 0x80, 117, 5),
|
||||
(84, "Interface\\Icons\\Spell_Holy_SealOfMight", 0x00, 103, 0),
|
||||
// The always-on multibars (MultiBars.xml): BottomLeft = actions 61..72, BottomRight
|
||||
// The always-on multibars (stock MultiActionBars.xml): BottomLeft = actions 61..72, BottomRight
|
||||
// = 49..60. A few occupied wells on each so the capture shows both rows seated
|
||||
// (empty multibar wells hide — the ref's own default — so without these the rows
|
||||
// would be invisible).
|
||||
@@ -696,7 +696,7 @@ fn demo_unit_feed(script: Option<NonSendMut<UiScript>>, mut fired: Local<VmMemo<
|
||||
}),
|
||||
);
|
||||
}
|
||||
// The stance bar (StanceBar.xml): the synthetic warrior's three stances, battle active —
|
||||
// The stance bar (stock BonusActionBarFrame.xml): the synthetic warrior's three stances, battle active —
|
||||
// matches the bonus offset 1 above (battle stance page). Defensive shows the not-castable
|
||||
// grey; berserker a running cooldown swipe.
|
||||
script.set_shapeshift_forms(vec![
|
||||
|
||||
@@ -24,7 +24,12 @@ fn load_action_bar(s: &UiScript) {
|
||||
"GameTooltip.xml",
|
||||
"Cooldown.xml",
|
||||
"Interface\\FrameXML\\ActionButtonTemplate.xml",
|
||||
"ActionBar.xml",
|
||||
"Interface\\FrameXML\\TextStatusBar.lua",
|
||||
"Interface\\FrameXML\\TextStatusBar.xml",
|
||||
"Interface\\FrameXML\\GlobalStrings.lua",
|
||||
"Interface\\FrameXML\\MainMenuBar.xml",
|
||||
"Interface\\FrameXML\\ActionBarFrame.xml",
|
||||
"Interface\\FrameXML\\BonusActionBarFrame.xml",
|
||||
// The reference declares the reputation WATCH BAR in `ReputationFrame.xml`, and
|
||||
// `ExhaustionTick_Update` reads `ReputationWatchBar:IsShown()` twice — the reference's own
|
||||
// coupling of MainMenuBar to that pane. So an action-bar harness loads it, and with it the
|
||||
@@ -33,6 +38,14 @@ fn load_action_bar(s: &UiScript) {
|
||||
r"Interface\FrameXML\UIPanelTemplates.xml",
|
||||
r"Interface\FrameXML\OptionsFrameTemplates.xml",
|
||||
r"Interface\FrameXML\ReputationFrame.xml",
|
||||
// The stock multibar file's OnLoad writes `UIOptionsFrameCheckButtons`, which our options
|
||||
// window publishes — the reference's own load order (UIOptionsFrame.xml l.21 before
|
||||
// MultiActionBars.xml l.39), and the manifest's since 1938.
|
||||
"Interface\\FrameXML\\UIDropDownMenu.xml",
|
||||
"UiPanels.xml", // StaticPopupDialogs, which the options window's pages register into
|
||||
"ScrollTemplates.xml",
|
||||
"KeyBindingsPage.xml",
|
||||
"OptionsFrame.xml",
|
||||
] {
|
||||
load_xml(s, file);
|
||||
}
|
||||
@@ -47,7 +60,9 @@ fn show_bars(s: &UiScript, bars: &[u32]) {
|
||||
if bars.contains(&bar) { "1" } else { "nil" }
|
||||
));
|
||||
}
|
||||
lua.push_str("MultiActionBar_Update()");
|
||||
// …then the manage pass, which the options window runs after a toggle (stock
|
||||
// UIOptionsFrame.xml:1192, and our rows the same); stock MultiActionBar_Update does not.
|
||||
lua.push_str("MultiActionBar_Update() UIParent_ManageFramePositions()");
|
||||
s.run(&lua).unwrap();
|
||||
}
|
||||
|
||||
@@ -65,21 +80,11 @@ fn shipped_multibars_drive_end_to_end() {
|
||||
let mut s = UiScript::new().unwrap();
|
||||
s.set_screen_size(1024.0, 768.0);
|
||||
load_action_bar(&s);
|
||||
let text = std::fs::read_to_string(
|
||||
std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("assets/ui/MultiBars.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
|
||||
);
|
||||
let frames = super::test_ui::load_ui(&s, "Interface\\FrameXML\\MultiActionBars.xml");
|
||||
assert_eq!(
|
||||
report.frames, 100,
|
||||
"4 bar frames + 48 buttons, each with a Cooldown child — the two VERTICAL bars joined \
|
||||
(hidden, as the reference's VerticalMultiBar3/4 are), which doubled this count"
|
||||
frames, 100,
|
||||
"what stock MultiActionBars.xml declares (1938): the four bar frames and their 48 buttons, \
|
||||
each with a $parentCooldown — the same 100 ours built for the same seats"
|
||||
);
|
||||
|
||||
// Occupy main slot 1, BottomLeft slot 1 (action 61), BottomRight slot 1 (action 49).
|
||||
@@ -152,8 +157,9 @@ fn shipped_multibars_drive_end_to_end() {
|
||||
};
|
||||
assert_eq!(
|
||||
rings(&quads, "Interface\\Buttons\\UI-Quickslot2"),
|
||||
14,
|
||||
"12 main wells + 2 occupied multibar buttons; 22 empty multibar wells hidden"
|
||||
3,
|
||||
"the occupied main slot + the 2 occupied multibar buttons; every empty well is hidden, \
|
||||
the main bar's included (ActionButton.lua:69-70)"
|
||||
);
|
||||
|
||||
// A click on BottomLeft button 1 (center (26,75)) queues the FIXED id 61 — and stays 61 when
|
||||
@@ -183,8 +189,10 @@ fn shipped_multibars_drive_end_to_end() {
|
||||
s.fire_event("UPDATE_BONUS_ACTIONBAR", vec![]);
|
||||
// The offset edges above showed the bonus overlay (1524) and its descent takes 0.15s of
|
||||
// frame time this event-only test never used to pass; without it the 12 overlay wells are
|
||||
// still on screen and correctly answer the SHOWGRID below as 12 extra drop targets.
|
||||
// still on screen and correctly answer the SHOWGRID below as 12 extra drop targets. The
|
||||
// stock slide paints, THEN advances, so the frame that hides it is one OnUpdate past the time.
|
||||
s.tick(0.2);
|
||||
s.tick(0.01);
|
||||
|
||||
// While a payload is held (SHOWGRID), the hidden empty wells appear as drop-target rings
|
||||
// (UI-Quickslot, the "no action" ring): 11 empty main wells swap texture + 22 multibar wells
|
||||
@@ -199,10 +207,19 @@ fn shipped_multibars_drive_end_to_end() {
|
||||
s.fire_event("ACTIONBAR_HIDEGRID", vec![]);
|
||||
s.resolve();
|
||||
assert_eq!(rings(&s.extract(), "Interface\\Buttons\\UI-Quickslot"), 0);
|
||||
assert_eq!(rings(&s.extract(), "Interface\\Buttons\\UI-Quickslot2"), 14);
|
||||
assert_eq!(
|
||||
rings(&s.extract(), "Interface\\Buttons\\UI-Quickslot2"),
|
||||
3,
|
||||
"letting go hides every empty well again, the main bar's included; the three occupied \
|
||||
buttons keep their rings"
|
||||
);
|
||||
|
||||
// The unbound multibar hotkey corner carries the ref's RANGE_INDICATOR dot: out of range paints
|
||||
// the red dot, back in range clears it (the main bar's labels tint instead).
|
||||
// the red dot, back in range clears it (the main bar's labels tint instead). Two stock
|
||||
// mechanisms, in order: `ActionButton_UpdateHotkeys` decides whether the corner's TEXT is the
|
||||
// dot at all — only while `IsActionInRange` answers non-nil, i.e. with a target — and it runs
|
||||
// on PLAYER_TARGET_CHANGED (ActionButton.lua:121-145, 333-334); every `ActionButton_OnUpdate`
|
||||
// then shows or hides that text by range (l.389-395) and tints it red past the 0.2 s recheck.
|
||||
use benilla_ui::script::ActionState;
|
||||
s.set_action_state(
|
||||
61,
|
||||
@@ -213,6 +230,7 @@ fn shipped_multibars_drive_end_to_end() {
|
||||
..Default::default()
|
||||
}),
|
||||
);
|
||||
s.fire_event("PLAYER_TARGET_CHANGED", vec![]);
|
||||
s.tick(0.5); // past the 0.2 s range recheck
|
||||
s.resolve();
|
||||
let dot_shown = |quads: &[benilla_ui::script::ExtractedQuad]| {
|
||||
@@ -255,22 +273,27 @@ fn shipped_stance_bar_drives_end_to_end() {
|
||||
// bottom multibar is UP to raise the flag. Since 1500 that is a player option rather than a
|
||||
// given, so this test raises it; `the_stance_bar_sits_where_the_pass_puts_it` below owns the
|
||||
// other state.
|
||||
load_xml(&s, "MultiBars.xml");
|
||||
load_xml(&s, "Cooldown.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\ActionButtonTemplate.xml");
|
||||
load_xml(&s, "MoneyFrame.xml");
|
||||
load_xml(&s, "GameTooltip.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\Fonts.xml");
|
||||
load_xml(&s, "UIParent.xml");
|
||||
load_xml(&s, "UiPanels.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\UIDropDownMenu.xml");
|
||||
load_xml(&s, "ScrollTemplates.xml");
|
||||
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
|
||||
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
|
||||
load_xml(&s, "KeyBindingsPage.xml");
|
||||
load_xml(&s, "OptionsFrame.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\MultiActionBars.xml");
|
||||
show_bars(&s, &[1]);
|
||||
let text = std::fs::read_to_string(
|
||||
std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("assets/ui/StanceBar.xml"),
|
||||
)
|
||||
.unwrap();
|
||||
let doc = benilla_ui::framexml::parse(&text).unwrap();
|
||||
let report = benilla_ui::loader::load(&s, &doc, &|_| None);
|
||||
// The shapeshift bar is declared by stock BonusActionBarFrame.xml (ShapeshiftBarFrame and
|
||||
// ShapeshiftButton1..10 beside the bonus bar), which load_action_bar loaded above (1938).
|
||||
assert!(
|
||||
report.errors.is_empty(),
|
||||
"loader errors: {:?}",
|
||||
report.errors
|
||||
);
|
||||
assert_eq!(
|
||||
report.frames, 21,
|
||||
"the bar frame + 10 buttons, each with a Cooldown child"
|
||||
s.eval::<bool>("return ShapeshiftBarFrame ~= nil and ShapeshiftButton10 ~= nil")
|
||||
.unwrap(),
|
||||
"the stance bar's frames come with the stock bonus-bar file"
|
||||
);
|
||||
|
||||
// No forms pushed (a mage): the frame is hidden.
|
||||
@@ -440,7 +463,12 @@ fn multibar_hover_renders_the_buttons_own_action() {
|
||||
"GameTooltip.xml",
|
||||
"Cooldown.xml",
|
||||
"Interface\\FrameXML\\ActionButtonTemplate.xml",
|
||||
"ActionBar.xml",
|
||||
"Interface\\FrameXML\\TextStatusBar.lua",
|
||||
"Interface\\FrameXML\\TextStatusBar.xml",
|
||||
"Interface\\FrameXML\\GlobalStrings.lua",
|
||||
"Interface\\FrameXML\\MainMenuBar.xml",
|
||||
"Interface\\FrameXML\\ActionBarFrame.xml",
|
||||
"Interface\\FrameXML\\BonusActionBarFrame.xml",
|
||||
// The reference declares the reputation WATCH BAR in `ReputationFrame.xml`, and
|
||||
// `ExhaustionTick_Update` reads `ReputationWatchBar:IsShown()` twice — the reference's own
|
||||
// coupling of MainMenuBar to that pane. So an action-bar harness loads it, and with it the
|
||||
@@ -449,7 +477,15 @@ fn multibar_hover_renders_the_buttons_own_action() {
|
||||
r"Interface\FrameXML\UIPanelTemplates.xml",
|
||||
r"Interface\FrameXML\OptionsFrameTemplates.xml",
|
||||
r"Interface\FrameXML\ReputationFrame.xml",
|
||||
"MultiBars.xml",
|
||||
"Interface\\FrameXML\\ActionBarFrame.xml",
|
||||
"UiPanels.xml",
|
||||
"Interface\\FrameXML\\UIDropDownMenu.xml",
|
||||
"ScrollTemplates.xml",
|
||||
r"Interface\FrameXML\UIPanelTemplates.lua",
|
||||
r"Interface\FrameXML\UIPanelTemplates.xml",
|
||||
"KeyBindingsPage.xml",
|
||||
"OptionsFrame.xml",
|
||||
"Interface\\FrameXML\\MultiActionBars.xml",
|
||||
] {
|
||||
load_xml(&s, file);
|
||||
}
|
||||
@@ -483,7 +519,7 @@ fn multibar_hover_renders_the_buttons_own_action() {
|
||||
|
||||
let hover_name = |s: &UiScript, button: &str| -> Option<String> {
|
||||
s.run(&format!(
|
||||
"GameTooltip:Hide() BenillaActionButton_OnEnter({button})"
|
||||
"GameTooltip:Hide() this = {button} ActionButton_SetTooltip()"
|
||||
))
|
||||
.unwrap();
|
||||
s.eval::<Option<String>>(
|
||||
@@ -537,7 +573,20 @@ fn multibar_hover_renders_the_buttons_own_action() {
|
||||
fn the_vertical_multibars_exist_hidden_on_the_reference_pages() {
|
||||
let s = UiScript::new().unwrap();
|
||||
load_action_bar(&s);
|
||||
load_xml(&s, "MultiBars.xml");
|
||||
load_xml(&s, "Cooldown.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\ActionButtonTemplate.xml");
|
||||
load_xml(&s, "MoneyFrame.xml");
|
||||
load_xml(&s, "GameTooltip.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\Fonts.xml");
|
||||
load_xml(&s, "UIParent.xml");
|
||||
load_xml(&s, "UiPanels.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\UIDropDownMenu.xml");
|
||||
load_xml(&s, "ScrollTemplates.xml");
|
||||
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
|
||||
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
|
||||
load_xml(&s, "KeyBindingsPage.xml");
|
||||
load_xml(&s, "OptionsFrame.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\MultiActionBars.xml");
|
||||
|
||||
for bar in ["MultiBarRight", "MultiBarLeft"] {
|
||||
assert!(
|
||||
@@ -556,13 +605,13 @@ fn the_vertical_multibars_exist_hidden_on_the_reference_pages() {
|
||||
// ref ActionButton.lua:8-9 — RIGHT_ACTIONBAR_PAGE = 3 (actions 25..36), LEFT = 4 (37..48).
|
||||
for (bar, first, last) in [("MultiBarRight", 25, 36), ("MultiBarLeft", 37, 48)] {
|
||||
assert_eq!(
|
||||
s.eval::<i64>(&format!("return {bar}Button1.base + {bar}Button1.index"))
|
||||
s.eval::<i64>(&format!("return ActionButton_GetPagedID({bar}Button1)"))
|
||||
.unwrap(),
|
||||
first,
|
||||
"{bar}'s first slot"
|
||||
);
|
||||
assert_eq!(
|
||||
s.eval::<i64>(&format!("return {bar}Button12.base + {bar}Button12.index"))
|
||||
s.eval::<i64>(&format!("return ActionButton_GetPagedID({bar}Button12)"))
|
||||
.unwrap(),
|
||||
last,
|
||||
"{bar}'s last slot"
|
||||
@@ -586,7 +635,20 @@ fn every_extra_bar_stays_down_until_its_own_toggle_is_set() {
|
||||
let mut s = UiScript::new().unwrap();
|
||||
s.set_screen_size(1024.0, 768.0);
|
||||
load_action_bar(&s);
|
||||
load_xml(&s, "MultiBars.xml");
|
||||
load_xml(&s, "Cooldown.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\ActionButtonTemplate.xml");
|
||||
load_xml(&s, "MoneyFrame.xml");
|
||||
load_xml(&s, "GameTooltip.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\Fonts.xml");
|
||||
load_xml(&s, "UIParent.xml");
|
||||
load_xml(&s, "UiPanels.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\UIDropDownMenu.xml");
|
||||
load_xml(&s, "ScrollTemplates.xml");
|
||||
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
|
||||
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
|
||||
load_xml(&s, "KeyBindingsPage.xml");
|
||||
load_xml(&s, "OptionsFrame.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\MultiActionBars.xml");
|
||||
|
||||
const BARS: [&str; 4] = [
|
||||
"MultiBarBottomLeft",
|
||||
@@ -660,7 +722,20 @@ fn raising_a_bottom_bar_moves_the_managed_bottom_stack() {
|
||||
let mut s = UiScript::new().unwrap();
|
||||
s.set_screen_size(1024.0, 768.0);
|
||||
load_action_bar(&s);
|
||||
load_xml(&s, "MultiBars.xml");
|
||||
load_xml(&s, "Cooldown.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\ActionButtonTemplate.xml");
|
||||
load_xml(&s, "MoneyFrame.xml");
|
||||
load_xml(&s, "GameTooltip.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\Fonts.xml");
|
||||
load_xml(&s, "UIParent.xml");
|
||||
load_xml(&s, "UiPanels.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\UIDropDownMenu.xml");
|
||||
load_xml(&s, "ScrollTemplates.xml");
|
||||
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
|
||||
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
|
||||
load_xml(&s, "KeyBindingsPage.xml");
|
||||
load_xml(&s, "OptionsFrame.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\MultiActionBars.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\CastingBarFrame.xml");
|
||||
|
||||
// The pass writes the y of a frame row into its anchor; read it back off the anchor rather
|
||||
@@ -711,7 +786,20 @@ fn viewable_action_bar_pages_follow_the_bar_toggles() {
|
||||
let mut s = UiScript::new().unwrap();
|
||||
s.set_screen_size(1024.0, 768.0);
|
||||
load_action_bar(&s);
|
||||
load_xml(&s, "MultiBars.xml");
|
||||
load_xml(&s, "Cooldown.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\ActionButtonTemplate.xml");
|
||||
load_xml(&s, "MoneyFrame.xml");
|
||||
load_xml(&s, "GameTooltip.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\Fonts.xml");
|
||||
load_xml(&s, "UIParent.xml");
|
||||
load_xml(&s, "UiPanels.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\UIDropDownMenu.xml");
|
||||
load_xml(&s, "ScrollTemplates.xml");
|
||||
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
|
||||
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
|
||||
load_xml(&s, "KeyBindingsPage.xml");
|
||||
load_xml(&s, "OptionsFrame.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\MultiActionBars.xml");
|
||||
|
||||
let viewable = |s: &UiScript| {
|
||||
s.eval::<String>(
|
||||
@@ -764,7 +852,20 @@ fn the_grid_option_holds_the_extra_bars_empty_wells_open() {
|
||||
let mut s = UiScript::new().unwrap();
|
||||
s.set_screen_size(1024.0, 768.0);
|
||||
load_action_bar(&s);
|
||||
load_xml(&s, "MultiBars.xml");
|
||||
load_xml(&s, "Cooldown.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\ActionButtonTemplate.xml");
|
||||
load_xml(&s, "MoneyFrame.xml");
|
||||
load_xml(&s, "GameTooltip.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\Fonts.xml");
|
||||
load_xml(&s, "UIParent.xml");
|
||||
load_xml(&s, "UiPanels.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\UIDropDownMenu.xml");
|
||||
load_xml(&s, "ScrollTemplates.xml");
|
||||
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
|
||||
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
|
||||
load_xml(&s, "KeyBindingsPage.xml");
|
||||
load_xml(&s, "OptionsFrame.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\MultiActionBars.xml");
|
||||
show_bars(&s, &[1]);
|
||||
|
||||
let well = |s: &UiScript| {
|
||||
@@ -798,12 +899,36 @@ fn the_grid_option_holds_the_extra_bars_empty_wells_open() {
|
||||
|
||||
// Re-applying an "off" that is already off must not owe anything: with a payload in hand the
|
||||
// wells stay open through it (the reference's own idempotence bug, closed by the latch).
|
||||
// The reference's hide is a COUNTED decrement on every well (`ActionButton_HideGrid`), never
|
||||
// a no-op: with the option off, one more apply closes a payload-held well. That is why our
|
||||
// row runs `MultiActionBar_UpdateGridVisibility` only on a CLICK (stock UIOptionsFrame.xml:683)
|
||||
// and its load arm only ever SHOWS (UIOptionsFrame.lua:218-220) — a load-time hide would push
|
||||
// counts nobody raised below zero. (Ours used to guard the no-op; the guard went with the
|
||||
// file, 1938.)
|
||||
// The load arm, both ways: off touches no count, on opens the wells.
|
||||
s.run("ALWAYS_SHOW_MULTIBARS = \"0\" OptionsFrame_ApplySavedSettings()")
|
||||
.unwrap();
|
||||
s.fire_event("ACTIONBAR_SHOWGRID", vec![]);
|
||||
s.run("MultiActionBar_UpdateGridVisibility()").unwrap();
|
||||
s.run("MultiActionBar_UpdateGridVisibility()").unwrap();
|
||||
assert!(well(&s), "a no-op apply may not decrement");
|
||||
assert!(well(&s));
|
||||
s.fire_event("ACTIONBAR_HIDEGRID", vec![]);
|
||||
assert!(!well(&s), "a load with the option off left the count alone");
|
||||
s.run("ALWAYS_SHOW_MULTIBARS = \"1\" OptionsFrame_ApplySavedSettings()")
|
||||
.unwrap();
|
||||
assert!(well(&s), "a load with the option on opens the wells");
|
||||
s.run("ALWAYS_SHOW_MULTIBARS = \"0\" MultiActionBar_UpdateGridVisibility()")
|
||||
.unwrap();
|
||||
assert!(!well(&s));
|
||||
|
||||
// …and the click arm against a held payload: one apply closes the well the payload opened.
|
||||
// (The payload's own HIDEGRID that follows takes the count to −1 in the reference too — its
|
||||
// quirk, not modelled here.)
|
||||
s.fire_event("ACTIONBAR_SHOWGRID", vec![]);
|
||||
assert!(well(&s));
|
||||
s.run("MultiActionBar_UpdateGridVisibility()").unwrap();
|
||||
assert!(
|
||||
!well(&s),
|
||||
"the option's hide counts against the payload's open"
|
||||
);
|
||||
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
|
||||
}
|
||||
|
||||
@@ -830,7 +955,20 @@ fn a_held_payload_ghosts_the_empty_wells_it_opens() {
|
||||
let mut s = UiScript::new().unwrap();
|
||||
s.set_screen_size(1024.0, 768.0);
|
||||
load_action_bar(&s);
|
||||
load_xml(&s, "MultiBars.xml");
|
||||
load_xml(&s, "Cooldown.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\ActionButtonTemplate.xml");
|
||||
load_xml(&s, "MoneyFrame.xml");
|
||||
load_xml(&s, "GameTooltip.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\Fonts.xml");
|
||||
load_xml(&s, "UIParent.xml");
|
||||
load_xml(&s, "UiPanels.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\UIDropDownMenu.xml");
|
||||
load_xml(&s, "ScrollTemplates.xml");
|
||||
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
|
||||
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
|
||||
load_xml(&s, "KeyBindingsPage.xml");
|
||||
load_xml(&s, "OptionsFrame.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\MultiActionBars.xml");
|
||||
s.set_action(
|
||||
1,
|
||||
Some(ActionSlot {
|
||||
@@ -869,8 +1007,9 @@ fn a_held_payload_ghosts_the_empty_wells_it_opens() {
|
||||
let resting = ring_alphas(&s, quickslot2);
|
||||
assert_eq!(
|
||||
resting.len(),
|
||||
12,
|
||||
"the 12 main wells; the multibar's are hidden"
|
||||
1,
|
||||
"only the occupied main slot draws a ring: every empty well, main bar included, is \
|
||||
hidden while nothing is held (ActionButton.lua:69-70)"
|
||||
);
|
||||
assert!(
|
||||
resting.iter().all(|a| *a == 1.0),
|
||||
@@ -891,9 +1030,14 @@ fn a_held_payload_ghosts_the_empty_wells_it_opens() {
|
||||
"every grid ring is the ref's half-alpha ghost, not an opaque plate: {ghosts:?}"
|
||||
);
|
||||
|
||||
// …and the one OCCUPIED button is untouched by it.
|
||||
// …and the one OCCUPIED button ghosts with it: `ActionButton_ShowGrid` sets the half alpha
|
||||
// (ActionButton.lua:246) before anything asks whether the slot holds an action, and
|
||||
// `HideGrid` touches no colour at all. What restores an occupied ring is its next
|
||||
// `ActionButton_UpdateUsable` (l.271-283), whose `SetVertexColor(1, 1, 1)` carries the default
|
||||
// alpha — from Update, and on SPELL_UPDATE_USABLE. Ours skipped occupied rings; the stock
|
||||
// file does not (1938).
|
||||
let occupied = ring_alphas(&s, quickslot2);
|
||||
assert_eq!(occupied, vec![1.0], "the occupied ring does not ghost");
|
||||
assert_eq!(occupied, vec![0.5], "the occupied ring ghosts too");
|
||||
|
||||
// A well that was ghosted and is then FILLED comes back opaque. This is the assertion that
|
||||
// outlives the engine's own `SetVertexColor` divergence (1782): the day an absent alpha stops
|
||||
@@ -914,21 +1058,28 @@ fn a_held_payload_ghosts_the_empty_wells_it_opens() {
|
||||
s.resolve();
|
||||
assert_eq!(
|
||||
ring_alphas(&s, quickslot2),
|
||||
vec![1.0, 1.0],
|
||||
"a ghosted well that gets an action is opaque again, payload still held"
|
||||
vec![0.5, 1.0],
|
||||
"the well that got an action is opaque again (its Update ran UpdateUsable); the other \
|
||||
occupied ring keeps its ghost, payload still held"
|
||||
);
|
||||
|
||||
// Letting go restores both halves — art and tint, which is why they share one setter.
|
||||
// Letting go takes the empty wells away; the colour it leaves alone.
|
||||
s.fire_event("ACTIONBAR_HIDEGRID", vec![]);
|
||||
s.resolve();
|
||||
assert_eq!(ring_alphas(&s, quickslot), Vec::<f32>::new());
|
||||
let after = ring_alphas(&s, quickslot2);
|
||||
assert_eq!(after.len(), 12);
|
||||
// (10 empty main wells back to the hollow ring + the 2 occupied ones.)
|
||||
assert!(
|
||||
after.iter().all(|a| *a == 1.0),
|
||||
"the dim does not outlive the payload (the ref's own stuck-dim bug): {after:?}"
|
||||
assert_eq!(
|
||||
after,
|
||||
vec![0.5, 1.0],
|
||||
"HideGrid touches no colour: a ring the grid ghosted stays dim past the payload — the \
|
||||
reference's own rule (ours used to clear it here)"
|
||||
);
|
||||
// What happens to that ring NEXT is decision 1782's open question, deliberately not asserted:
|
||||
// the reference's `SetVertexColor(1, 1, 1)` in `ActionButton_UpdateUsable` keeps the alpha
|
||||
// already on the region (byte-pinned, wow-re button-state-texture-path-setter.md §7 — the
|
||||
// "stuck dim" a 1.12 player sees after a drag), while this engine's three-argument call still
|
||||
// resets it to 1.0 until the director calls 1782. A test that pinned either outcome would be
|
||||
// enshrining a divergence as fidelity.
|
||||
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
|
||||
}
|
||||
|
||||
@@ -947,25 +1098,42 @@ fn a_bar_toggle_sends_the_byte_its_globals_pack_to() {
|
||||
let mut s = UiScript::new().unwrap();
|
||||
s.set_screen_size(1024.0, 768.0);
|
||||
load_action_bar(&s);
|
||||
load_xml(&s, "MultiBars.xml");
|
||||
load_xml(&s, "Cooldown.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\ActionButtonTemplate.xml");
|
||||
load_xml(&s, "MoneyFrame.xml");
|
||||
load_xml(&s, "GameTooltip.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\Fonts.xml");
|
||||
load_xml(&s, "UIParent.xml");
|
||||
load_xml(&s, "UiPanels.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\UIDropDownMenu.xml");
|
||||
load_xml(&s, "ScrollTemplates.xml");
|
||||
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
|
||||
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
|
||||
load_xml(&s, "KeyBindingsPage.xml");
|
||||
load_xml(&s, "OptionsFrame.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\MultiActionBars.xml");
|
||||
let _ = s.take_action_bar_toggle_sends();
|
||||
|
||||
// The row's own setter, with the row's own "1"/"0" strings.
|
||||
s.run("BenillaMultiBar_SetShown(1, \"1\")").unwrap();
|
||||
s.run("SHOW_MULTI_ACTIONBAR_1 = 1 MultiActionBar_Update() SetActionBarToggles(SHOW_MULTI_ACTIONBAR_1, SHOW_MULTI_ACTIONBAR_2, SHOW_MULTI_ACTIONBAR_3, SHOW_MULTI_ACTIONBAR_4)")
|
||||
.unwrap();
|
||||
assert_eq!(s.take_action_bar_toggle_sends(), vec![0x01]);
|
||||
s.run("BenillaMultiBar_SetShown(3, \"1\")").unwrap();
|
||||
s.run("SHOW_MULTI_ACTIONBAR_3 = 1 MultiActionBar_Update() SetActionBarToggles(SHOW_MULTI_ACTIONBAR_1, SHOW_MULTI_ACTIONBAR_2, SHOW_MULTI_ACTIONBAR_3, SHOW_MULTI_ACTIONBAR_4)")
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
s.take_action_bar_toggle_sends(),
|
||||
vec![0x05],
|
||||
"the WHOLE byte re-sent, not a delta"
|
||||
);
|
||||
s.run("BenillaMultiBar_SetShown(4, \"1\")").unwrap();
|
||||
s.run("SHOW_MULTI_ACTIONBAR_4 = 1 MultiActionBar_Update() SetActionBarToggles(SHOW_MULTI_ACTIONBAR_1, SHOW_MULTI_ACTIONBAR_2, SHOW_MULTI_ACTIONBAR_3, SHOW_MULTI_ACTIONBAR_4)")
|
||||
.unwrap();
|
||||
assert_eq!(s.take_action_bar_toggle_sends(), vec![0x0d]);
|
||||
s.run("BenillaMultiBar_SetShown(1, \"0\")").unwrap();
|
||||
s.run("SHOW_MULTI_ACTIONBAR_1 = nil MultiActionBar_Update() SetActionBarToggles(SHOW_MULTI_ACTIONBAR_1, SHOW_MULTI_ACTIONBAR_2, SHOW_MULTI_ACTIONBAR_3, SHOW_MULTI_ACTIONBAR_4)")
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
s.take_action_bar_toggle_sends(),
|
||||
vec![0x0c],
|
||||
"the row hands the setter a STRING, and \"0\" is off"
|
||||
"the row turns its \"0\" into nil before the send, and nil is off"
|
||||
);
|
||||
|
||||
// What is stored is 1 or nil and nothing else — GetActionBarToggles' own shape, and what the
|
||||
@@ -1017,7 +1185,20 @@ fn the_shipped_setter_passes_exactly_four_arguments() {
|
||||
let mut s = UiScript::new().unwrap();
|
||||
s.set_screen_size(1024.0, 768.0);
|
||||
load_action_bar(&s);
|
||||
load_xml(&s, "MultiBars.xml");
|
||||
load_xml(&s, "Cooldown.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\ActionButtonTemplate.xml");
|
||||
load_xml(&s, "MoneyFrame.xml");
|
||||
load_xml(&s, "GameTooltip.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\Fonts.xml");
|
||||
load_xml(&s, "UIParent.xml");
|
||||
load_xml(&s, "UiPanels.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\UIDropDownMenu.xml");
|
||||
load_xml(&s, "ScrollTemplates.xml");
|
||||
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
|
||||
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
|
||||
load_xml(&s, "KeyBindingsPage.xml");
|
||||
load_xml(&s, "OptionsFrame.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\MultiActionBars.xml");
|
||||
|
||||
// `r##`: the Lua contains `select("#", ...)`, and `"#` would close a single-hash raw string.
|
||||
s.run(
|
||||
@@ -1029,7 +1210,11 @@ fn the_shipped_setter_passes_exactly_four_arguments() {
|
||||
"##,
|
||||
)
|
||||
.unwrap();
|
||||
s.run("BenillaMultiBar_SetShown(2, \"1\")").unwrap();
|
||||
// The shipped setter is the Action Bars row's own closure (OptionsFrame.xml), which is
|
||||
// stock's three lines — assign the global, MultiActionBar_Update(), SetActionBarToggles(…) —
|
||||
// plus the manage pass.
|
||||
s.run("OptionsFrameContainerBodyActionBarsRowMultiBar2Check:Click()")
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
s.eval::<i64>("return BENILLA_TEST_TOGGLE_ARGC").unwrap(),
|
||||
4,
|
||||
@@ -1052,8 +1237,22 @@ fn the_stance_bar_sits_where_the_pass_puts_it() {
|
||||
let mut s = UiScript::new().unwrap();
|
||||
s.set_screen_size(1024.0, 768.0);
|
||||
load_action_bar(&s);
|
||||
load_xml(&s, "MultiBars.xml");
|
||||
load_xml(&s, "StanceBar.xml");
|
||||
load_xml(&s, "Cooldown.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\ActionButtonTemplate.xml");
|
||||
load_xml(&s, "MoneyFrame.xml");
|
||||
load_xml(&s, "GameTooltip.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\Fonts.xml");
|
||||
load_xml(&s, "UIParent.xml");
|
||||
load_xml(&s, "UiPanels.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\UIDropDownMenu.xml");
|
||||
load_xml(&s, "ScrollTemplates.xml");
|
||||
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
|
||||
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
|
||||
load_xml(&s, "KeyBindingsPage.xml");
|
||||
load_xml(&s, "OptionsFrame.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\MultiActionBars.xml");
|
||||
load_xml(&s, "Cooldown.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\ActionButtonTemplate.xml");
|
||||
s.set_shapeshift_forms(vec![ShapeshiftFormView {
|
||||
spell_id: 2457,
|
||||
texture: Some("Interface\\Icons\\Stance_A".into()),
|
||||
@@ -1110,8 +1309,22 @@ fn the_stance_shelf_follows_the_bottom_left_bar() {
|
||||
let mut s = UiScript::new().unwrap();
|
||||
s.set_screen_size(1024.0, 768.0);
|
||||
load_action_bar(&s);
|
||||
load_xml(&s, "MultiBars.xml");
|
||||
load_xml(&s, "StanceBar.xml");
|
||||
load_xml(&s, "Cooldown.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\ActionButtonTemplate.xml");
|
||||
load_xml(&s, "MoneyFrame.xml");
|
||||
load_xml(&s, "GameTooltip.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\Fonts.xml");
|
||||
load_xml(&s, "UIParent.xml");
|
||||
load_xml(&s, "UiPanels.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\UIDropDownMenu.xml");
|
||||
load_xml(&s, "ScrollTemplates.xml");
|
||||
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
|
||||
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
|
||||
load_xml(&s, "KeyBindingsPage.xml");
|
||||
load_xml(&s, "OptionsFrame.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\MultiActionBars.xml");
|
||||
load_xml(&s, "Cooldown.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\ActionButtonTemplate.xml");
|
||||
|
||||
let form = |id: u32| ShapeshiftFormView {
|
||||
spell_id: id,
|
||||
@@ -1175,8 +1388,17 @@ fn the_stance_shelf_follows_the_bottom_left_bar() {
|
||||
|
||||
// Even at three forms, raised keeps the middle strip down — the fork is on the BAR, not the
|
||||
// form count; the count only decides the middle strip within the unraised branch.
|
||||
// A third form learned while the bar is up: the reference's `ShapeshiftBar_Update` shows the
|
||||
// middle strip for three forms regardless of the bar (BonusActionBarFrame.lua:198-206), and
|
||||
// nothing runs the pass on that event — the strip draws across the row until something
|
||||
// moves. The pass, not Update, owns the raised look (UIParent.lua:1705-1720).
|
||||
s.set_shapeshift_forms(vec![form(2457), form(71), form(2458)]);
|
||||
s.fire_event("UPDATE_SHAPESHIFT_FORMS", vec![]);
|
||||
assert!(
|
||||
shown(&s, "ShapeshiftBarMiddle"),
|
||||
"Update shows the strip; only the pass takes it down"
|
||||
);
|
||||
s.run("UIParent_ManageFramePositions()").unwrap();
|
||||
assert!(!shown(&s, "ShapeshiftBarMiddle"));
|
||||
assert_eq!(ring(&s), 50.0);
|
||||
|
||||
@@ -1208,8 +1430,22 @@ fn the_stance_shelf_is_as_long_as_the_form_count() {
|
||||
let mut s = UiScript::new().unwrap();
|
||||
s.set_screen_size(1024.0, 768.0);
|
||||
load_action_bar(&s);
|
||||
load_xml(&s, "MultiBars.xml");
|
||||
load_xml(&s, "StanceBar.xml");
|
||||
load_xml(&s, "Cooldown.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\ActionButtonTemplate.xml");
|
||||
load_xml(&s, "MoneyFrame.xml");
|
||||
load_xml(&s, "GameTooltip.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\Fonts.xml");
|
||||
load_xml(&s, "UIParent.xml");
|
||||
load_xml(&s, "UiPanels.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\UIDropDownMenu.xml");
|
||||
load_xml(&s, "ScrollTemplates.xml");
|
||||
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
|
||||
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
|
||||
load_xml(&s, "KeyBindingsPage.xml");
|
||||
load_xml(&s, "OptionsFrame.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\MultiActionBars.xml");
|
||||
load_xml(&s, "Cooldown.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\ActionButtonTemplate.xml");
|
||||
show_bars(&s, &[]); // unraised: the shelf art is the state under test
|
||||
|
||||
let form = |id: u32| ShapeshiftFormView {
|
||||
@@ -1311,7 +1547,20 @@ fn an_extra_bars_empty_well_keeps_its_bound_hotkey_label() {
|
||||
// the player binds it, as the reporter had).
|
||||
s.register_bindings(&crate::bindings::registry_commands());
|
||||
load_action_bar(&s);
|
||||
load_xml(&s, "MultiBars.xml");
|
||||
load_xml(&s, "Cooldown.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\ActionButtonTemplate.xml");
|
||||
load_xml(&s, "MoneyFrame.xml");
|
||||
load_xml(&s, "GameTooltip.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\Fonts.xml");
|
||||
load_xml(&s, "UIParent.xml");
|
||||
load_xml(&s, "UiPanels.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\UIDropDownMenu.xml");
|
||||
load_xml(&s, "ScrollTemplates.xml");
|
||||
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
|
||||
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
|
||||
load_xml(&s, "KeyBindingsPage.xml");
|
||||
load_xml(&s, "OptionsFrame.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\MultiActionBars.xml");
|
||||
show_bars(&s, &[1]);
|
||||
s.set_action(
|
||||
61,
|
||||
|
||||
@@ -750,7 +750,10 @@ fn interface_harness() -> UiScript {
|
||||
"Interface\\FrameXML\\ColorPickerFrame.xml",
|
||||
"Cooldown.xml",
|
||||
"Interface\\FrameXML\\ActionButtonTemplate.xml",
|
||||
"ActionBar.xml",
|
||||
"Interface\\FrameXML\\GlobalStrings.lua",
|
||||
"Interface\\FrameXML\\MainMenuBar.xml",
|
||||
"Interface\\FrameXML\\ActionBarFrame.xml",
|
||||
"Interface\\FrameXML\\BonusActionBarFrame.xml",
|
||||
// The reference declares the reputation WATCH BAR in `ReputationFrame.xml`, and
|
||||
// `ExhaustionTick_Update` reads `ReputationWatchBar:IsShown()` twice — the reference's own
|
||||
// coupling of MainMenuBar to that pane. So an action-bar harness loads it, and with it the
|
||||
@@ -811,7 +814,14 @@ fn actionbars_harness() -> UiScript {
|
||||
"UIParent.xml",
|
||||
"Cooldown.xml",
|
||||
"Interface\\FrameXML\\ActionButtonTemplate.xml",
|
||||
"ActionBar.xml",
|
||||
"Interface\\FrameXML\\TextStatusBar.lua",
|
||||
"Interface\\FrameXML\\TextStatusBar.xml",
|
||||
"Interface\\FrameXML\\GlobalStrings.lua",
|
||||
"Interface\\FrameXML\\MainMenuBar.xml",
|
||||
"MoneyFrame.xml",
|
||||
"GameTooltip.xml",
|
||||
"Interface\\FrameXML\\ActionBarFrame.xml",
|
||||
"Interface\\FrameXML\\BonusActionBarFrame.xml",
|
||||
// The reference declares the reputation WATCH BAR in `ReputationFrame.xml`, and
|
||||
// `ExhaustionTick_Update` reads `ReputationWatchBar:IsShown()` twice — the reference's own
|
||||
// coupling of MainMenuBar to that pane. So an action-bar harness loads it, and with it the
|
||||
@@ -820,7 +830,15 @@ fn actionbars_harness() -> UiScript {
|
||||
r"Interface\FrameXML\UIPanelTemplates.xml",
|
||||
r"Interface\FrameXML\OptionsFrameTemplates.xml",
|
||||
r"Interface\FrameXML\ReputationFrame.xml",
|
||||
"MultiBars.xml",
|
||||
"Interface\\FrameXML\\ActionBarFrame.xml",
|
||||
"UiPanels.xml",
|
||||
"Interface\\FrameXML\\UIDropDownMenu.xml",
|
||||
"ScrollTemplates.xml",
|
||||
r"Interface\FrameXML\UIPanelTemplates.lua",
|
||||
r"Interface\FrameXML\UIPanelTemplates.xml",
|
||||
"KeyBindingsPage.xml",
|
||||
"OptionsFrame.xml",
|
||||
"Interface\\FrameXML\\MultiActionBars.xml",
|
||||
],
|
||||
);
|
||||
harness_on(s)
|
||||
@@ -2695,7 +2713,7 @@ fn the_action_bars_page_locks_the_real_bar() {
|
||||
);
|
||||
s.fire_event("PLAYER_ENTERING_WORLD", vec![]);
|
||||
s.resolve();
|
||||
s.run("BenillaActionButton_OnDragStart(ActionButton1)")
|
||||
s.run("this = ActionButton1 ActionButton1:GetScript(\"OnDragStart\")()")
|
||||
.unwrap();
|
||||
assert!(
|
||||
s.cursor_payload().is_none(),
|
||||
@@ -2705,7 +2723,7 @@ fn the_action_bars_page_locks_the_real_bar() {
|
||||
// Defaults walks it back to ActionBar.xml's own assignment, and the bar drags again.
|
||||
s.run("OptionsFrameContainerDefaults:Click()").unwrap();
|
||||
assert_eq!(s.eval::<String>("return LOCK_ACTIONBAR").unwrap(), "0");
|
||||
s.run("BenillaActionButton_OnDragStart(ActionButton1)")
|
||||
s.run("this = ActionButton1 ActionButton1:GetScript(\"OnDragStart\")()")
|
||||
.unwrap();
|
||||
assert!(s.cursor_payload().is_some(), "unlocked again");
|
||||
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
|
||||
@@ -2846,9 +2864,9 @@ fn the_target_of_target_rows_gate_each_other_and_write_their_globals() {
|
||||
assert!(
|
||||
s.eval::<bool>(
|
||||
"return OptionsFrameContainerBodyInterfaceRowTargetOfTarget.applyFunc \
|
||||
== TargetofTarget_Update \
|
||||
== \"TargetofTarget_Update\" \
|
||||
and OptionsFrameContainerBodyInterfaceRowTargetOfTargetMode.applyFunc \
|
||||
== TargetofTarget_Update"
|
||||
== \"TargetofTarget_Update\""
|
||||
)
|
||||
.unwrap(),
|
||||
"both rows re-decide the frame when they are written"
|
||||
|
||||
@@ -17,7 +17,15 @@ fn harness(extra: &[&str]) -> UiScript {
|
||||
load_xml(&s, "Interface\\FrameXML\\Fonts.xml");
|
||||
load_xml(&s, "Cooldown.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\ActionButtonTemplate.xml");
|
||||
load_xml(&s, "ActionBar.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\TextStatusBar.lua");
|
||||
load_xml(&s, "Interface\\FrameXML\\TextStatusBar.xml");
|
||||
load_xml(&s, "UIParent.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\GlobalStrings.lua");
|
||||
load_xml(&s, "Interface\\FrameXML\\MainMenuBar.xml");
|
||||
load_xml(&s, "MoneyFrame.xml");
|
||||
load_xml(&s, "GameTooltip.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\ActionBarFrame.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\BonusActionBarFrame.xml");
|
||||
for f in extra {
|
||||
load_xml(&s, f);
|
||||
}
|
||||
@@ -230,7 +238,7 @@ fn hovering_the_meter_shows_the_live_latency() {
|
||||
"the button over the meter takes the mouse"
|
||||
);
|
||||
|
||||
s.run("BenillaPerformanceBar_OnEnter(MainMenuBarPerformanceBarFrameButton)")
|
||||
s.run("this = MainMenuBarPerformanceBarFrameButton MainMenuBarPerformanceBarFrameButton:GetScript(\"OnEnter\")()")
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
s.eval::<String>("return GameTooltipTextLeft1:GetText()")
|
||||
@@ -277,7 +285,7 @@ fn hovering_the_meter_shows_the_live_latency() {
|
||||
"the refresh rebuilds BOTH lines — a held-open plate never decays to one"
|
||||
);
|
||||
|
||||
s.run("BenillaPerformanceBar_OnLeave()").unwrap();
|
||||
s.run("this = MainMenuBarPerformanceBarFrameButton MainMenuBarPerformanceBarFrameButton:GetScript(\"OnLeave\")()").unwrap();
|
||||
assert!(
|
||||
!s.eval::<bool>("return GameTooltip:IsVisible()").unwrap(),
|
||||
"leaving hides the plate"
|
||||
|
||||
@@ -19,7 +19,13 @@ fn load_pet_bar(s: &UiScript) {
|
||||
"UIParent.xml",
|
||||
"Cooldown.xml",
|
||||
"Interface\\FrameXML\\ActionButtonTemplate.xml",
|
||||
"ActionBar.xml",
|
||||
"Interface\\FrameXML\\TextStatusBar.lua",
|
||||
"Interface\\FrameXML\\TextStatusBar.xml",
|
||||
"Interface\\FrameXML\\GlobalStrings.lua",
|
||||
"Interface\\FrameXML\\MainMenuBar.xml",
|
||||
"GameTooltip.xml",
|
||||
"Interface\\FrameXML\\ActionBarFrame.xml",
|
||||
"Interface\\FrameXML\\BonusActionBarFrame.xml",
|
||||
// The reference declares the reputation WATCH BAR in `ReputationFrame.xml`, and
|
||||
// `ExhaustionTick_Update` reads `ReputationWatchBar:IsShown()` twice — the reference's own
|
||||
// coupling of MainMenuBar to that pane. So an action-bar harness loads it, and with it the
|
||||
@@ -370,7 +376,13 @@ fn pet_bar_row(with_multibar: bool) -> (usize, f32) {
|
||||
"UIParent.xml",
|
||||
"Cooldown.xml",
|
||||
"Interface\\FrameXML\\ActionButtonTemplate.xml",
|
||||
"ActionBar.xml",
|
||||
"Interface\\FrameXML\\TextStatusBar.lua",
|
||||
"Interface\\FrameXML\\TextStatusBar.xml",
|
||||
"Interface\\FrameXML\\GlobalStrings.lua",
|
||||
"Interface\\FrameXML\\MainMenuBar.xml",
|
||||
"GameTooltip.xml",
|
||||
"Interface\\FrameXML\\ActionBarFrame.xml",
|
||||
"Interface\\FrameXML\\BonusActionBarFrame.xml",
|
||||
// The reference declares the reputation WATCH BAR in `ReputationFrame.xml`, and
|
||||
// `ExhaustionTick_Update` reads `ReputationWatchBar:IsShown()` twice — the reference's own
|
||||
// coupling of MainMenuBar to that pane. So an action-bar harness loads it, and with it the
|
||||
@@ -383,7 +395,14 @@ fn pet_bar_row(with_multibar: bool) -> (usize, f32) {
|
||||
] {
|
||||
load_xml(&s, file);
|
||||
}
|
||||
load_xml(&s, "MultiBars.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\ActionBarFrame.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\UIDropDownMenu.xml");
|
||||
load_xml(&s, "ScrollTemplates.xml");
|
||||
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
|
||||
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
|
||||
load_xml(&s, "KeyBindingsPage.xml");
|
||||
load_xml(&s, "OptionsFrame.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\MultiActionBars.xml");
|
||||
if with_multibar {
|
||||
s.run("SHOW_MULTI_ACTIONBAR_1 = 1 MultiActionBar_Update()")
|
||||
.unwrap();
|
||||
@@ -665,7 +684,12 @@ fn load_pet_bar_with_tooltip(s: &UiScript) {
|
||||
"GameTooltip.xml",
|
||||
"Cooldown.xml",
|
||||
"Interface\\FrameXML\\ActionButtonTemplate.xml",
|
||||
"ActionBar.xml",
|
||||
"Interface\\FrameXML\\TextStatusBar.lua",
|
||||
"Interface\\FrameXML\\TextStatusBar.xml",
|
||||
"Interface\\FrameXML\\GlobalStrings.lua",
|
||||
"Interface\\FrameXML\\MainMenuBar.xml",
|
||||
"Interface\\FrameXML\\ActionBarFrame.xml",
|
||||
"Interface\\FrameXML\\BonusActionBarFrame.xml",
|
||||
// The reference declares the reputation WATCH BAR in `ReputationFrame.xml`, and
|
||||
// `ExhaustionTick_Update` reads `ReputationWatchBar:IsShown()` twice — the reference's own
|
||||
// coupling of MainMenuBar to that pane. So an action-bar harness loads it, and with it the
|
||||
|
||||
@@ -1075,8 +1075,7 @@ mod tests {
|
||||
/// 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`,
|
||||
/// That half doubles as the map nothing else holds: `FloatingChatFrame.xml` is our `ChatFrame.xml`,
|
||||
/// `MainMenuBarMicroButtons.xml` is our `MicroMenu.xml`, `StaticPopup.xml` is our
|
||||
/// `UiPanels.xml`. (The row this map used to lead with — `PlayerFrame.xml`/`TargetFrame.xml`/
|
||||
/// `PetFrame.xml` all being our one `UnitFrames.xml` — is retired: those four are the
|
||||
@@ -2037,11 +2036,6 @@ mod tests {
|
||||
flow works without it",
|
||||
),
|
||||
("ITEM_TEXT_TRANSLATION", "ItemTextFrame.lua"),
|
||||
(
|
||||
"PARTY_MEMBER_DISABLE",
|
||||
"PartyMemberFrame.lua — the offline grey-out",
|
||||
),
|
||||
("PARTY_MEMBER_ENABLE", "PartyMemberFrame.lua"),
|
||||
("PET_UI_CLOSE", "PetPaperDollFrame.lua"),
|
||||
("PET_UI_UPDATE", "PetPaperDollFrame.lua"),
|
||||
("PLAYER_DAMAGE_DONE_MODS", "PaperDollFrame.lua"),
|
||||
@@ -2049,10 +2043,6 @@ mod tests {
|
||||
"PLAYER_FLAGS_CHANGED",
|
||||
"TargetFrame.lua — the AFK/DND badge",
|
||||
),
|
||||
(
|
||||
"PLAYER_REGEN_ENABLED",
|
||||
"PlayerFrame.lua — and _DISABLED is unfired too, so the pair is whole",
|
||||
),
|
||||
("PLAYTIME_CHANGED", "PlayerFrame.lua"),
|
||||
("SHOW_COMPARE_TOOLTIP", "PaperDollFrame.lua"),
|
||||
("SYSMSG", "UIErrorsFrame.lua"),
|
||||
@@ -2080,47 +2070,59 @@ mod tests {
|
||||
} else if p.extension().is_some_and(|x| x == "rs") {
|
||||
let text = std::fs::read_to_string(&p).unwrap_or_default();
|
||||
// Split so this file cannot match its own walker (see the sibling gate).
|
||||
const CALL: &str = concat!("fire_event", "(");
|
||||
let mut from = 0;
|
||||
// Two producer shapes: `UiScript::fire_event(` and its `&Lua` twin
|
||||
// `tick::fire_event_into(` (1924), which an engine verb fires from inside a
|
||||
// binding — `UpdateSpells` → SPELLS_CHANGED, `ChangeActionBarPage` →
|
||||
// ACTIONBAR_PAGE_CHANGED (1938).
|
||||
const CALLS: [&str; 2] =
|
||||
[concat!("fire_event", "("), concat!("fire_event_into", "(")];
|
||||
let mut fires_indirectly = false;
|
||||
while let Some(i) = text[from..].find(CALL) {
|
||||
let at = from + i + CALL.len();
|
||||
from = at;
|
||||
let rest = text[at..].trim_start();
|
||||
if !rest.starts_with('"') {
|
||||
// `fire_event(event, args)` — the name reached the call through a
|
||||
// variable, so adjacency cannot see it. `ui_cast.rs` does exactly
|
||||
// this: a `match edge` yields `("SPELLCAST_INTERRUPTED", vec![])` and
|
||||
// one call site fires whatever it produced. Treated below.
|
||||
fires_indirectly = true;
|
||||
}
|
||||
if let Some(body) = rest.strip_prefix('"') {
|
||||
if let Some(end) = body.find('"') {
|
||||
fired.insert(body[..end].to_string());
|
||||
for call in CALLS {
|
||||
let mut from = 0;
|
||||
while let Some(i) = text[from..].find(call) {
|
||||
let at = from + i + call.len();
|
||||
from = at;
|
||||
let rest = text[at..].trim_start();
|
||||
let rest = rest.strip_prefix("lua,").map_or(rest, str::trim_start);
|
||||
if !rest.starts_with('"') {
|
||||
fires_indirectly = true;
|
||||
}
|
||||
if let Some(body) = rest.strip_prefix('"') {
|
||||
if let Some(end) = body.find('"') {
|
||||
fired.insert(body[..end].to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if fires_indirectly {
|
||||
// The name reached `fire_event` through a variable. Rather than guess, we
|
||||
// match the SHAPE that produced it: `("EVENT_NAME", vec!` — a tuple whose
|
||||
// second element is the argument vector, which is how `ui_cast.rs`'s
|
||||
// `match edge` hands a name and its args to one shared call site.
|
||||
//
|
||||
// The first attempt at this counted EVERY SCREAMING_CASE literal in such a
|
||||
// file, and that swept up eighteen names off the UNPRODUCED list — a gate
|
||||
// made vacuous by its own conservatism, which is the failure mode this
|
||||
// file has now hit twice. The shape is the discipline: it admits the
|
||||
// literals that are actually being fired and nothing else.
|
||||
// An indirect fire names its event either as `"NAME", vec![…]` or as one
|
||||
// arm of an `if … { "A" } else { "B" }` chosen before the call — a literal
|
||||
// standing alone on its line (`ui_action/state.rs`'s START/STOP_AUTOREPEAT).
|
||||
const SHAPE: &str = "\", vec!";
|
||||
let caps = |lit: &str| {
|
||||
!lit.is_empty()
|
||||
&& lit.chars().all(|c| {
|
||||
c.is_ascii_uppercase() || c == '_' || c.is_ascii_digit()
|
||||
})
|
||||
};
|
||||
for (i, _) in text.match_indices(SHAPE) {
|
||||
let before = &text[..i];
|
||||
let Some(q) = before.rfind('"') else { continue };
|
||||
let lit = &before[q + 1..];
|
||||
if !lit.is_empty()
|
||||
&& lit.chars().all(|c| {
|
||||
c.is_ascii_uppercase() || c == '_' || c.is_ascii_digit()
|
||||
})
|
||||
{
|
||||
if caps(lit) {
|
||||
fired.insert(lit.to_string());
|
||||
}
|
||||
}
|
||||
let lines: Vec<&str> = text.lines().map(str::trim).collect();
|
||||
for (i, t) in lines.iter().enumerate() {
|
||||
let Some(lit) = t.strip_prefix('"').and_then(|x| x.strip_suffix('"'))
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
let arm = i > 0
|
||||
&& lines[i - 1].ends_with('{')
|
||||
&& lines.get(i + 1).is_some_and(|n| n.starts_with('}'));
|
||||
if arm && caps(lit) {
|
||||
fired.insert(lit.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -662,7 +662,7 @@ fn a_hover_sweep_across_owners_costs_no_graph_derivation() {
|
||||
///
|
||||
/// The two guards above drive `SetOwner(button, "ANCHOR_RIGHT")` — the bag slot's idiom. The bars
|
||||
/// do not use it. With `UberTooltips` at its shipped default of `"1"`, every action button routes
|
||||
/// through `GameTooltip_SetDefaultAnchor` (`ActionBar.xml:510`, and the stance/pet/bonus bars the
|
||||
/// through `GameTooltip_SetDefaultAnchor` (stock `ActionButton_SetTooltip`, and the stance/pet/bonus bars the
|
||||
/// same), which is `SetOwner(owner, "ANCHOR_NONE")` followed by an explicit `SetPoint` — a
|
||||
/// completely different arm of the same verb, and the one that DROPS the tooltip's anchors.
|
||||
///
|
||||
|
||||
@@ -1280,7 +1280,7 @@ fn a_cinematic_leaves_nothing_of_the_interface_on_screen() {
|
||||
// The sweep is only worth anything if there was something to hide in the first place.
|
||||
let before = names.iter().filter(|n| visible(&s, n)).count();
|
||||
assert!(
|
||||
before > 50,
|
||||
before > 20, // 50 until 1938 took the three bar files (and their visible frames) stock
|
||||
"only {before} frames visible before the cinematic — the sweep found no interface to \
|
||||
hide, so it would pass no matter what the cascade did"
|
||||
);
|
||||
@@ -1350,7 +1350,7 @@ fn every_declared_parent_really_attaches() {
|
||||
|
||||
let declared = shipped_frame_parents();
|
||||
assert!(
|
||||
declared.len() > 60,
|
||||
declared.len() > 30, // 67 before 1938 took the three bar files (25 of them) stock
|
||||
"only {} parent declarations found — the scan broke",
|
||||
declared.len()
|
||||
);
|
||||
|
||||
@@ -127,7 +127,13 @@ fn shipped_spellbook_drives_end_to_end() {
|
||||
load_xml(&s, "GameTooltip.xml");
|
||||
load_xml(&s, "Cooldown.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\ActionButtonTemplate.xml");
|
||||
load_xml(&s, "ActionBar.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\TextStatusBar.lua");
|
||||
load_xml(&s, "Interface\\FrameXML\\TextStatusBar.xml");
|
||||
load_xml(&s, "UIParent.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\GlobalStrings.lua");
|
||||
load_xml(&s, "Interface\\FrameXML\\MainMenuBar.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\ActionBarFrame.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\BonusActionBarFrame.xml");
|
||||
// The reference declares the reputation WATCH BAR in `ReputationFrame.xml`, and
|
||||
// `ExhaustionTick_Update` reads `ReputationWatchBar:IsShown()` twice — the reference's own
|
||||
// coupling of MainMenuBar to that pane. So an action-bar harness loads it, and with it the
|
||||
@@ -203,6 +209,14 @@ fn shipped_spellbook_drives_end_to_end() {
|
||||
// (`cursor::bar::place_action`) a bar-to-bar drag uses: a plain click on an action button
|
||||
// routes through UseAction's checkCursor=1 fork to a place. Packs kind 0x00 (SPELL, decision
|
||||
// 0216 §1) with the spell id — `action_sets` is the app's own CMSG_SET_ACTION_BUTTON queue.
|
||||
// An EMPTY main-bar slot is hidden under the reference (ActionButton.lua:69-70) until a held
|
||||
// payload opens the grid: the engine derives ACTIONBAR_SHOWGRID from the cursor's edge and
|
||||
// fires it on the next tick, as the app ticks between any two mouse events.
|
||||
s.tick(0.016);
|
||||
assert!(
|
||||
s.eval::<bool>("return ActionButton1:IsVisible()").unwrap(),
|
||||
"the held spell opened the empty well"
|
||||
);
|
||||
let (ax, ay) = center(&s, "ActionButton1");
|
||||
s.mouse_button(ax, ay, "LeftButton", true);
|
||||
s.mouse_button(ax, ay, "LeftButton", false);
|
||||
@@ -684,7 +698,12 @@ fn the_macro_editor_takes_a_shift_click_and_only_a_shift_click() {
|
||||
r"Interface\FrameXML\ClassTrainerFrameTemplates.xml",
|
||||
"MicroMenu.xml",
|
||||
"Interface\\FrameXML\\ActionButtonTemplate.xml",
|
||||
"ActionBar.xml",
|
||||
"Interface\\FrameXML\\TextStatusBar.lua",
|
||||
"Interface\\FrameXML\\TextStatusBar.xml",
|
||||
"Interface\\FrameXML\\GlobalStrings.lua",
|
||||
"Interface\\FrameXML\\MainMenuBar.xml",
|
||||
"Interface\\FrameXML\\ActionBarFrame.xml",
|
||||
"Interface\\FrameXML\\BonusActionBarFrame.xml",
|
||||
// The reference declares the reputation WATCH BAR in `ReputationFrame.xml`, and
|
||||
// `ExhaustionTick_Update` reads `ReputationWatchBar:IsShown()` twice — the reference's own
|
||||
// coupling of MainMenuBar to that pane. So an action-bar harness loads it, and with it the
|
||||
|
||||
@@ -25,10 +25,15 @@ fn harness() -> UiScript {
|
||||
load_xml(&s, "Interface\\FrameXML\\UIDropDownMenu.xml"); // the unit popups' kit (TargetFrameDropDown's template)
|
||||
load_xml(&s, "Cooldown.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\ActionButtonTemplate.xml");
|
||||
load_xml(&s, "ActionBar.xml"); // BENILLA_FALLBACK_ICON
|
||||
// Before UnitPopup: that file reads ITEM_QUALITY_COLORS at FILE SCOPE, and its
|
||||
// declarer is UIParent (ref UIParent.lua:65) since 1888.
|
||||
load_xml(&s, "Interface\\FrameXML\\TextStatusBar.lua");
|
||||
load_xml(&s, "Interface\\FrameXML\\TextStatusBar.xml");
|
||||
load_xml(&s, "UIParent.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\GlobalStrings.lua");
|
||||
load_xml(&s, "Interface\\FrameXML\\MainMenuBar.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\ActionBarFrame.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\BonusActionBarFrame.xml"); // BENILLA_FALLBACK_ICON
|
||||
// Before UnitPopup: that file reads ITEM_QUALITY_COLORS at FILE SCOPE, and its
|
||||
// declarer is UIParent (ref UIParent.lua:65) since 1888.
|
||||
load_xml(&s, "UnitPopup.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\TextStatusBar.lua");
|
||||
load_xml(&s, "Interface\\FrameXML\\TextStatusBar.xml");
|
||||
|
||||
@@ -202,7 +202,7 @@ pub(super) const LOOT_UI: &[&str] = &[
|
||||
/// so `UnitFrame.xml` and `CombatFeedback.xml` come first — the reference's own toc order.
|
||||
/// (This entry used to be our one `UnitFrames.xml`, and this note used to say "when they are
|
||||
/// migrated, this becomes the stock pair". They are; it did.)
|
||||
/// * **`ActionBar.xml`** for `MainMenuExpBar` — the third bar of that prefix call — and for
|
||||
/// * **`Interface\FrameXML\MainMenuBar.xml`** for `MainMenuExpBar` — the third bar of that prefix call — and for
|
||||
/// `ShowWatchedReputationBarText` / `HideWatchedReputationBarText`, which the window's
|
||||
/// show/hide pair calls.
|
||||
/// * **`UIPanelTemplates.xml`** for `PanelTemplates_SetNumTabs`/`_SetTab`, the last two lines of
|
||||
@@ -259,7 +259,9 @@ pub(super) const CHARACTER_UI: &[&str] = &[
|
||||
"Interface\\FrameXML\\PartyFrameTemplates.xml",
|
||||
"Interface\\FrameXML\\PetFrame.xml",
|
||||
"Interface\\FrameXML\\ActionButtonTemplate.xml",
|
||||
"ActionBar.xml",
|
||||
"Interface\\FrameXML\\MainMenuBar.xml",
|
||||
"Interface\\FrameXML\\ActionBarFrame.xml",
|
||||
"Interface\\FrameXML\\BonusActionBarFrame.xml",
|
||||
"MicroMenu.xml",
|
||||
// The two page files these three need before the window can be OPENED, which is not the same
|
||||
// as before it can load: `CHARACTERFRAME_SUBFRAMES` lists all five pages by name and
|
||||
@@ -315,7 +317,11 @@ pub(super) const BAG_UI: &[&str] = &[
|
||||
// six buttons fall back to UIParent and sit at a level no production run ever puts them at.
|
||||
// It also carries `MainMenuBar_UpdateKeyRing`, which is what puts the keyring on the bar.
|
||||
"Interface\\FrameXML\\ActionButtonTemplate.xml",
|
||||
"ActionBar.xml",
|
||||
"Interface\\FrameXML\\TextStatusBar.lua",
|
||||
"Interface\\FrameXML\\TextStatusBar.xml",
|
||||
"Interface\\FrameXML\\MainMenuBar.xml",
|
||||
"Interface\\FrameXML\\ActionBarFrame.xml",
|
||||
"Interface\\FrameXML\\BonusActionBarFrame.xml",
|
||||
// `UpdateMicroButtons` — the KEYRING's own OnShow/OnHide calls it (ContainerFrame.lua l.117,
|
||||
// l.137), because in the reference the keyring's existence moves the micro-button row.
|
||||
"MicroMenu.xml",
|
||||
|
||||
@@ -231,10 +231,20 @@ fn action_button_hover_takes_the_default_corner() {
|
||||
let mut s = harness(&[
|
||||
"Cooldown.xml",
|
||||
"Interface\\FrameXML\\ActionButtonTemplate.xml",
|
||||
"ActionBar.xml",
|
||||
"Interface\\FrameXML\\TextStatusBar.lua",
|
||||
"Interface\\FrameXML\\TextStatusBar.xml",
|
||||
"Interface\\FrameXML\\Fonts.xml",
|
||||
"UIParent.xml",
|
||||
"Interface\\FrameXML\\GlobalStrings.lua",
|
||||
"Interface\\FrameXML\\MainMenuBar.xml",
|
||||
"MoneyFrame.xml",
|
||||
"GameTooltip.xml",
|
||||
"Interface\\FrameXML\\ActionBarFrame.xml",
|
||||
"Interface\\FrameXML\\BonusActionBarFrame.xml",
|
||||
]);
|
||||
s.register_cvars(crate::cvars::registered_pairs());
|
||||
s.run("BenillaActionButton_OnEnter(ActionButton3)").unwrap();
|
||||
s.run("this = ActionButton3 ActionButton_SetTooltip()")
|
||||
.unwrap();
|
||||
assert!(s.errors().is_empty(), "hover errors: {:?}", s.errors());
|
||||
let ok: bool = s
|
||||
.eval(
|
||||
@@ -264,8 +274,25 @@ fn ubertooltips_off_seats_action_bar_plates_beside_the_button() {
|
||||
let mut s = harness(&[
|
||||
"Cooldown.xml",
|
||||
"Interface\\FrameXML\\ActionButtonTemplate.xml",
|
||||
"ActionBar.xml",
|
||||
"MultiBars.xml",
|
||||
"Interface\\FrameXML\\TextStatusBar.lua",
|
||||
"Interface\\FrameXML\\TextStatusBar.xml",
|
||||
"Interface\\FrameXML\\Fonts.xml",
|
||||
"UIParent.xml",
|
||||
"Interface\\FrameXML\\GlobalStrings.lua",
|
||||
"Interface\\FrameXML\\MainMenuBar.xml",
|
||||
"MoneyFrame.xml",
|
||||
"GameTooltip.xml",
|
||||
"Interface\\FrameXML\\ActionBarFrame.xml",
|
||||
"Interface\\FrameXML\\BonusActionBarFrame.xml",
|
||||
"Interface\\FrameXML\\ActionBarFrame.xml",
|
||||
"UiPanels.xml",
|
||||
"Interface\\FrameXML\\UIDropDownMenu.xml",
|
||||
"ScrollTemplates.xml",
|
||||
r"Interface\FrameXML\UIPanelTemplates.lua",
|
||||
r"Interface\FrameXML\UIPanelTemplates.xml",
|
||||
"KeyBindingsPage.xml",
|
||||
"OptionsFrame.xml",
|
||||
"Interface\\FrameXML\\MultiActionBars.xml",
|
||||
]);
|
||||
s.register_cvars(crate::cvars::registered_pairs());
|
||||
s.set_cvar_engine("UberTooltips", "0");
|
||||
@@ -278,7 +305,8 @@ fn ubertooltips_off_seats_action_bar_plates_beside_the_button() {
|
||||
.unwrap()
|
||||
};
|
||||
|
||||
s.run("BenillaActionButton_OnEnter(ActionButton3)").unwrap();
|
||||
s.run("this = ActionButton3 ActionButton_SetTooltip()")
|
||||
.unwrap();
|
||||
assert!(
|
||||
s.eval::<bool>("return GameTooltip.default == nil").unwrap(),
|
||||
"off: the main bar's plate is owner-anchored, not the default corner"
|
||||
@@ -297,7 +325,7 @@ fn ubertooltips_off_seats_action_bar_plates_beside_the_button() {
|
||||
// All three members of the ref's LEFT set, by frame — membership is not gated on visibility,
|
||||
// and the two vertical bars are hidden until their option is ticked.
|
||||
for bar in ["MultiBarBottomRight", "MultiBarRight", "MultiBarLeft"] {
|
||||
s.run(&format!("BenillaActionButton_OnEnter({bar}Button1)"))
|
||||
s.run(&format!("this = {bar}Button1 ActionButton_SetTooltip()"))
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
seat(&s),
|
||||
@@ -308,7 +336,7 @@ fn ubertooltips_off_seats_action_bar_plates_beside_the_button() {
|
||||
|
||||
// And the CVar back on restores the corner — the fork is a fork, not a one-way door.
|
||||
s.set_cvar_engine("UberTooltips", "1");
|
||||
s.run("BenillaActionButton_OnEnter(MultiBarBottomRightButton1)")
|
||||
s.run("this = MultiBarBottomRightButton1 ActionButton_SetTooltip()")
|
||||
.unwrap();
|
||||
assert!(
|
||||
s.eval::<bool>("return GameTooltip.default ~= nil").unwrap(),
|
||||
@@ -326,8 +354,18 @@ fn ubertooltips_off_seats_stance_plates_beside_the_button() {
|
||||
let mut s = harness(&[
|
||||
"Cooldown.xml",
|
||||
"Interface\\FrameXML\\ActionButtonTemplate.xml",
|
||||
"ActionBar.xml",
|
||||
"StanceBar.xml",
|
||||
"Interface\\FrameXML\\TextStatusBar.lua",
|
||||
"Interface\\FrameXML\\TextStatusBar.xml",
|
||||
"Interface\\FrameXML\\Fonts.xml",
|
||||
"UIParent.xml",
|
||||
"Interface\\FrameXML\\GlobalStrings.lua",
|
||||
"Interface\\FrameXML\\MainMenuBar.xml",
|
||||
"MoneyFrame.xml",
|
||||
"GameTooltip.xml",
|
||||
"Interface\\FrameXML\\ActionBarFrame.xml",
|
||||
"Interface\\FrameXML\\BonusActionBarFrame.xml",
|
||||
"Interface\\FrameXML\\ActionBarFrame.xml",
|
||||
"Interface\\FrameXML\\BonusActionBarFrame.xml",
|
||||
]);
|
||||
s.register_cvars(crate::cvars::registered_pairs());
|
||||
s.set_shapeshift_forms(vec![benilla_ui::script::ShapeshiftFormView {
|
||||
@@ -341,7 +379,7 @@ fn ubertooltips_off_seats_stance_plates_beside_the_button() {
|
||||
s.fire_event("UPDATE_SHAPESHIFT_FORMS", vec![]);
|
||||
s.resolve();
|
||||
|
||||
s.run("BenillaShapeshiftButton_OnEnter(ShapeshiftButton1)")
|
||||
s.run("this = ShapeshiftButton1 ShapeshiftButton1:GetScript(\"OnEnter\")()")
|
||||
.unwrap();
|
||||
assert!(
|
||||
s.eval::<bool>("return GameTooltip.default ~= nil").unwrap(),
|
||||
@@ -349,7 +387,7 @@ fn ubertooltips_off_seats_stance_plates_beside_the_button() {
|
||||
);
|
||||
|
||||
s.set_cvar_engine("UberTooltips", "0");
|
||||
s.run("BenillaShapeshiftButton_OnEnter(ShapeshiftButton1)")
|
||||
s.run("this = ShapeshiftButton1 ShapeshiftButton1:GetScript(\"OnEnter\")()")
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
s.eval::<String>(
|
||||
@@ -371,7 +409,16 @@ fn buff_hover_hangs_below_left_of_the_button() {
|
||||
let mut s = harness(&[
|
||||
"Cooldown.xml",
|
||||
"Interface\\FrameXML\\ActionButtonTemplate.xml",
|
||||
"ActionBar.xml",
|
||||
"Interface\\FrameXML\\TextStatusBar.lua",
|
||||
"Interface\\FrameXML\\TextStatusBar.xml",
|
||||
"Interface\\FrameXML\\Fonts.xml",
|
||||
"UIParent.xml",
|
||||
"Interface\\FrameXML\\GlobalStrings.lua",
|
||||
"Interface\\FrameXML\\MainMenuBar.xml",
|
||||
"MoneyFrame.xml",
|
||||
"GameTooltip.xml",
|
||||
"Interface\\FrameXML\\ActionBarFrame.xml",
|
||||
"Interface\\FrameXML\\BonusActionBarFrame.xml",
|
||||
"Interface\\FrameXML\\TextStatusBar.lua",
|
||||
"Interface\\FrameXML\\TextStatusBar.xml",
|
||||
"Interface\\FrameXML\\BuffFrame.xml",
|
||||
|
||||
@@ -102,6 +102,7 @@ fn harness_with_bags() -> UiScript {
|
||||
fn seed_items(s: &mut UiScript) {
|
||||
let mut inv: InventorySlots = Default::default();
|
||||
inv[1] = Some(InvSlotView {
|
||||
duration_ms: None,
|
||||
already_bound: false,
|
||||
bar_placeable: true,
|
||||
durability: None,
|
||||
@@ -148,6 +149,7 @@ fn seed_items(s: &mut UiScript) {
|
||||
slots.insert(
|
||||
1,
|
||||
ContainerSlot {
|
||||
duration_ms: None,
|
||||
petition: None,
|
||||
already_bound: false,
|
||||
bar_placeable: true,
|
||||
@@ -308,6 +310,7 @@ fn doll_hover_renders_the_live_instance_and_never_self_compares() {
|
||||
// Break the equipped helm: instance pair (0, 40); the template stays authored-full.
|
||||
let mut inv: InventorySlots = Default::default();
|
||||
inv[1] = Some(InvSlotView {
|
||||
duration_ms: None,
|
||||
already_bound: false,
|
||||
bar_placeable: true,
|
||||
durability: Some((0, 40)),
|
||||
|
||||
@@ -68,7 +68,12 @@ fn load_tot() -> UiScript {
|
||||
load_xml(&s, "UnitPopup.xml");
|
||||
load_xml(&s, "Cooldown.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\ActionButtonTemplate.xml");
|
||||
load_xml(&s, "ActionBar.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\TextStatusBar.lua");
|
||||
load_xml(&s, "Interface\\FrameXML\\TextStatusBar.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\GlobalStrings.lua");
|
||||
load_xml(&s, "Interface\\FrameXML\\MainMenuBar.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\ActionBarFrame.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\BonusActionBarFrame.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\TextStatusBar.lua");
|
||||
load_xml(&s, "Interface\\FrameXML\\TextStatusBar.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\BuffFrame.xml");
|
||||
|
||||
@@ -483,7 +483,10 @@ fn load_pet_menu_frames(s: &UiScript) {
|
||||
"Interface\\FrameXML\\PetFrame.xml",
|
||||
"Cooldown.xml",
|
||||
"Interface\\FrameXML\\ActionButtonTemplate.xml",
|
||||
"ActionBar.xml",
|
||||
"Interface\\FrameXML\\GlobalStrings.lua",
|
||||
"Interface\\FrameXML\\MainMenuBar.xml",
|
||||
"Interface\\FrameXML\\ActionBarFrame.xml",
|
||||
"Interface\\FrameXML\\BonusActionBarFrame.xml",
|
||||
"PetActionBar.xml",
|
||||
] {
|
||||
load_xml(s, file);
|
||||
|
||||
@@ -31,7 +31,8 @@
|
||||
//! The refresh model is one diff-and-fire: the feed rebuilds the pushed list each frame
|
||||
//! (identity, active, castable, cooldown) and fires `UPDATE_SHAPESHIFT_FORMS` on any change — the
|
||||
//! real client's learn/unlearn edge (`0x5e9c20`/`0x5e9fe0`) plus its separate per-state repaint events
|
||||
//! collapse into one, which `StanceBar.xml` documents as its deliberate divergence. Cooldown
|
||||
//! collapse into one (our `StanceBar.xml` documented that as its deliberate divergence; since 1938 the
|
||||
//! stock shapeshift bar registers the reference's six events, and this one event drives them all). Cooldown
|
||||
//! remainders re-push only on a [`Cooldowns::generation`] change (`state.rs`'s own churn gate) —
|
||||
//! the engine extrapolates the sweep from the absolute start between pushes.
|
||||
|
||||
|
||||
@@ -19,8 +19,11 @@
|
||||
//! Craft-vs-TradeSkill routing are all byte-confirmed. **The header law landed too:** rows group
|
||||
//! by the created item's `(ItemClass, ItemSubClass)`, named from `ItemSubClass.dbc`, two-level
|
||||
//! sort (0446; the filter family on top of it, 0452) — this feed resolves each recipe's `group`
|
||||
//! ([`resolve_recipe`]), so the book is no longer flat. Remaining gap: the spell-focus tool never
|
||||
//! rendered red (no client-side proximity model — the server refuses the cast).
|
||||
//! ([`resolve_recipe`]), so the book is no longer flat. The spell-focus tool never renders red, and
|
||||
//! that is **faithful, not a gap** (wow-re `tradeskill-tools-and-spell-focus.md`, §5-carved): the
|
||||
//! reference's own `hasTool` for a focus is the literal `1.0` with no predicate anywhere, and the
|
||||
//! client holds neither a focus id nor a radius to test proximity with. The server refusing the
|
||||
//! cast is the whole of the feedback, there as here.
|
||||
|
||||
use std::time::Instant;
|
||||
|
||||
@@ -310,20 +313,35 @@ fn resolve_recipe(
|
||||
.flatten()
|
||||
.map_or((None, 0, 0), |(g, it, il)| (Some(g), it, il));
|
||||
|
||||
// Tools: the two totem items (present-not-consumed — a Blacksmith Hammer) + the spell focus
|
||||
// (Anvil/Forge/Cooking Fire; never red — module doc INTERIM).
|
||||
// Tools — **the spell FOCUS first, then `Totem[0]`, then `Totem[1]`** (wow-re
|
||||
// `tradeskill/scratch/tradeskill-tools-and-spell-focus.md`, §5-carved: `0x4ff980`'s own push
|
||||
// order). We had the totems leading, which reverses the Requirements line for every recipe
|
||||
// that needs both — a Blacksmithing anvil recipe reads "Blacksmith Hammer, Anvil" instead of
|
||||
// "Anvil, Blacksmith Hammer".
|
||||
//
|
||||
// **The focus is unconditionally satisfied, and that is VERIFIED rather than a gap.** Its
|
||||
// `hasTool` is the instruction immediate `push 0x3ff00000 / push 0` — the literal `1.0`, with
|
||||
// no predicate at all: an exhaustive absence (17 calls, 7 absolute operands, **zero FPU**;
|
||||
// `SpellRec+0x3c` has exactly two readers image-wide; the GameObject descriptor block carries
|
||||
// no focus id or radius, so the client could not test proximity even if it wanted to). The
|
||||
// reference NEVER reddens an Anvil line. `BuildColoredListString` is still byte-faithful and
|
||||
// still reds an unmet TOTEM — that half is live.
|
||||
//
|
||||
// A tool whose template has not landed contributes **no pair at all** (the reference drops it
|
||||
// and fires `CMSG_ITEM_QUERY_SINGLE`, so the arity grows between calls) — which is what the
|
||||
// `if let Some(info)` below already does, for the same reason.
|
||||
let mut tools = Vec::new();
|
||||
if d.requires_spell_focus != 0 {
|
||||
if let Some(name) = focus.and_then(|f| f.catalog.name(d.requires_spell_focus)) {
|
||||
tools.push((name.to_string(), true));
|
||||
}
|
||||
}
|
||||
for &t in d.totems.iter().filter(|&&t| t != 0) {
|
||||
let have = count_of(&store.0, items, t, InventoryScope::CARRIED) > 0;
|
||||
if let Some(info) = items.template(t, 0, commands) {
|
||||
tools.push((info.name.clone(), have));
|
||||
}
|
||||
}
|
||||
if d.requires_spell_focus != 0 {
|
||||
if let Some(name) = focus.and_then(|f| f.catalog.name(d.requires_spell_focus)) {
|
||||
tools.push((name.to_string(), true));
|
||||
}
|
||||
}
|
||||
|
||||
let cd = cooldowns.info(spell_id, 0, Some(d), now);
|
||||
Some(TradeSkillRecipe {
|
||||
|
||||
@@ -110,38 +110,62 @@ impl Default for ChunkShading {
|
||||
///
|
||||
/// Returns `None` for a chunk the hole mask emptied (it draws nothing: no mesh, no entity).
|
||||
pub fn chunk_to_mesh(chunk: &ChunkMesh, shading: &ChunkShading) -> Option<Mesh> {
|
||||
if chunk.indices.len() < 3 {
|
||||
chunks_to_mesh(&[(chunk, shading)])
|
||||
}
|
||||
|
||||
/// [`chunk_to_mesh`] over several chunks of ONE tile as one mesh — the terrain CELL (decision
|
||||
/// 1944): every per-chunk fact the shader reads (the layer indices, the alpha/shadow slot) is
|
||||
/// already baked per vertex, and a tile's chunks share its material, so concatenating them
|
||||
/// changes nothing a pixel can see and turns sixteen draws into one. Positions are absolute,
|
||||
/// so no per-chunk transform is folded. A hole-emptied chunk contributes nothing; a cell of
|
||||
/// nothing but holes is `None`.
|
||||
pub fn chunks_to_mesh(parts: &[(&ChunkMesh, &ChunkShading)]) -> Option<Mesh> {
|
||||
let live: Vec<&(&ChunkMesh, &ChunkShading)> =
|
||||
parts.iter().filter(|(c, _)| c.indices.len() >= 3).collect();
|
||||
if live.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let positions: Vec<[f32; 3]> = chunk
|
||||
.positions
|
||||
.iter()
|
||||
.map(|p| wow_to_bevy(*p).to_array())
|
||||
.collect();
|
||||
// Prefer authored MCNR normals (continuous across MCNK borders — no seams); else flat.
|
||||
let normals: Vec<[f32; 3]> = if chunk.normals.len() == chunk.positions.len() {
|
||||
chunk
|
||||
.normals
|
||||
let total: usize = live.iter().map(|(c, _)| c.positions.len()).sum();
|
||||
let mut positions: Vec<[f32; 3]> = Vec::with_capacity(total);
|
||||
let mut normals: Vec<[f32; 3]> = Vec::with_capacity(total);
|
||||
let mut uvs: Vec<[f32; 2]> = Vec::with_capacity(total);
|
||||
let mut uv1: Vec<[f32; 2]> = Vec::with_capacity(total);
|
||||
let mut colors: Vec<[f32; 4]> = Vec::with_capacity(total);
|
||||
let mut indices: Vec<u32> = Vec::with_capacity(live.iter().map(|(c, _)| c.indices.len()).sum());
|
||||
for (chunk, shading) in live {
|
||||
let base = u32::try_from(positions.len()).expect("a cell's vertex count fits u32");
|
||||
let chunk_positions: Vec<[f32; 3]> = chunk
|
||||
.positions
|
||||
.iter()
|
||||
.map(|n| wow_to_bevy(*n).to_array())
|
||||
.collect()
|
||||
} else {
|
||||
computed_normals(&positions, &chunk.indices)
|
||||
};
|
||||
let li = shading.layers;
|
||||
let col = [li[0] as f32, li[1] as f32, li[2] as f32, li[3] as f32];
|
||||
let uv1 = [shading.alpha as f32, shading.shadow];
|
||||
let n = positions.len();
|
||||
.map(|p| wow_to_bevy(*p).to_array())
|
||||
.collect();
|
||||
if chunk.normals.len() == chunk.positions.len() {
|
||||
normals.extend(chunk.normals.iter().map(|n| wow_to_bevy(*n).to_array()));
|
||||
} else {
|
||||
normals.extend(computed_normals(&chunk_positions, &chunk.indices));
|
||||
}
|
||||
let li = shading.layers;
|
||||
let col = [li[0] as f32, li[1] as f32, li[2] as f32, li[3] as f32];
|
||||
let n = chunk_positions.len();
|
||||
uv1.extend(std::iter::repeat_n(
|
||||
[shading.alpha as f32, shading.shadow],
|
||||
n,
|
||||
));
|
||||
colors.extend(std::iter::repeat_n(col, n));
|
||||
uvs.extend_from_slice(&chunk.uvs);
|
||||
indices.extend(chunk.indices.iter().map(|i| i + base));
|
||||
positions.extend(chunk_positions);
|
||||
}
|
||||
let mut mesh = Mesh::new(
|
||||
PrimitiveTopology::TriangleList,
|
||||
RenderAssetUsages::RENDER_WORLD,
|
||||
);
|
||||
mesh.insert_attribute(Mesh::ATTRIBUTE_POSITION, positions);
|
||||
mesh.insert_attribute(Mesh::ATTRIBUTE_NORMAL, normals);
|
||||
mesh.insert_attribute(Mesh::ATTRIBUTE_UV_0, chunk.uvs.clone());
|
||||
mesh.insert_attribute(Mesh::ATTRIBUTE_UV_1, vec![uv1; n]);
|
||||
mesh.insert_attribute(Mesh::ATTRIBUTE_COLOR, vec![col; n]);
|
||||
mesh.insert_indices(Indices::U32(chunk.indices.clone()));
|
||||
mesh.insert_attribute(Mesh::ATTRIBUTE_UV_0, uvs);
|
||||
mesh.insert_attribute(Mesh::ATTRIBUTE_UV_1, uv1);
|
||||
mesh.insert_attribute(Mesh::ATTRIBUTE_COLOR, colors);
|
||||
mesh.insert_indices(Indices::U32(indices));
|
||||
Some(mesh)
|
||||
}
|
||||
|
||||
|
||||
@@ -41,7 +41,7 @@ pub use model::{
|
||||
mod adt;
|
||||
mod terrain;
|
||||
mod wdt;
|
||||
pub use adt::{chunk_to_mesh, AdtLoader, AdtTile, ChunkShading};
|
||||
pub use adt::{chunk_to_mesh, chunks_to_mesh, AdtLoader, AdtTile, ChunkShading};
|
||||
pub use wdt::{WdtIndex, WdtIndexLoader};
|
||||
mod blp;
|
||||
pub use blp::{BlpImageLoader, BlpLoaderSettings, BlpVariant};
|
||||
|
||||
@@ -207,7 +207,9 @@ pub use skill_lines::{
|
||||
load_skill_line_catalog, SkillLineCatalog, SkillLineInfo, SkillRaceClass, SlaInfo,
|
||||
};
|
||||
mod spell_focus;
|
||||
mod spell_mechanic;
|
||||
pub use spell_focus::{load_spell_focus_catalog, SpellFocusCatalog};
|
||||
pub use spell_mechanic::{load_spell_mechanic_catalog, SpellMechanicCatalog};
|
||||
mod talents;
|
||||
pub use talents::{load_talent_catalog, Talent, TalentCatalog, TalentTabInfo, MAX_TALENT_RANK};
|
||||
mod spell_visual;
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
//! SpellMechanic.dbc — the vocabulary that fills `SPELL_FAILED_PREVENTED_BY_MECHANIC`'s `%s`,
|
||||
//! turning "Can't do that while %s" into "Can't do that while stunned" (decision 1948).
|
||||
//!
|
||||
//! It is the crowd-control ladder's other half: the exemption scan (decision 1941) reports the
|
||||
//! blocking aura's mechanic as an **id**, and this names it. Without the table the arm's refusal
|
||||
//! displayed its template with the specifier unfilled, which is the loose end 1941 recorded.
|
||||
//!
|
||||
//! The reference reads the same store (`0xc0d7c4`) from `0x6e2190`, the `0x8d` argument arm
|
||||
//! (wow-re `cast-fail-strings.md` line 120).
|
||||
//!
|
||||
//! Layout byte-checked on the raw 5875 file (a struct-unpack dump: **27 records × 10 fields,
|
||||
//! record size 40**, string block 246) — the same shape as `SpellFocusObject.dbc`: `ID@0` · the
|
||||
//! 8-locale `Name` block (enUS first ⇒ **Name = column 1**) · its flags word (9). Anchor rows:
|
||||
//! 5 "fleeing" · 7 "rooted" · 12 "stunned" · 17 "polymorphed".
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use benilla_dbc::{FieldType, Schema, SchemaField};
|
||||
|
||||
use crate::dbc::{parse, str_at, u32_at};
|
||||
use crate::Chain;
|
||||
|
||||
const SPELL_MECHANIC: &str = "DBFilesClient\\SpellMechanic.dbc";
|
||||
const SPELL_MECHANIC_FIELDS: usize = 10;
|
||||
const COL_NAME_ENUS: usize = 1;
|
||||
|
||||
/// `SpellMechanic.Id → Name` — the "Can't do that while stunned" vocabulary.
|
||||
///
|
||||
/// The names are **lower-case and adjectival** in the shipped data ("stunned", "asleep",
|
||||
/// "polymorphed"), which is what makes them read as the tail of that sentence rather than as a
|
||||
/// title. Nothing here capitalises them; the reference does not either.
|
||||
pub struct SpellMechanicCatalog {
|
||||
names: HashMap<u32, String>,
|
||||
}
|
||||
|
||||
impl SpellMechanicCatalog {
|
||||
/// The display name for a mechanic id, or `None` for 0/unknown — which is exactly the case
|
||||
/// the refusal treats as "no mechanic named", falling back to the arm's own reason.
|
||||
pub fn name(&self, mechanic_id: u32) -> Option<&str> {
|
||||
self.names.get(&mechanic_id).map(String::as_str)
|
||||
}
|
||||
|
||||
pub fn len(&self) -> usize {
|
||||
self.names.len()
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.names.is_empty()
|
||||
}
|
||||
}
|
||||
|
||||
fn schema() -> Schema {
|
||||
let mut s = Schema::new("SpellMechanic");
|
||||
for i in 0..SPELL_MECHANIC_FIELDS {
|
||||
let ty = if i == COL_NAME_ENUS {
|
||||
FieldType::String
|
||||
} else {
|
||||
FieldType::UInt32
|
||||
};
|
||||
s.add_field(SchemaField::new(format!("F{i}"), ty));
|
||||
}
|
||||
s
|
||||
}
|
||||
|
||||
/// Load SpellMechanic.dbc from the patch chain into a [`SpellMechanicCatalog`].
|
||||
pub fn load_spell_mechanic_catalog(chain: &mut Chain) -> Result<SpellMechanicCatalog> {
|
||||
let bytes = chain
|
||||
.read_file(SPELL_MECHANIC)
|
||||
.with_context(|| format!("reading {SPELL_MECHANIC}"))?;
|
||||
let rs = parse(&bytes, schema(), "SpellMechanic.dbc")?;
|
||||
let mut names = HashMap::new();
|
||||
for r in rs.records() {
|
||||
let Some(id) = u32_at(r, 0) else { continue };
|
||||
if let Some(name) = str_at(&rs, r, COL_NAME_ENUS) {
|
||||
names.insert(id, name);
|
||||
}
|
||||
}
|
||||
Ok(SpellMechanicCatalog { names })
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// The rows the crowd-control ladder actually names, on the real build-5875 file. A column
|
||||
/// slip lands on another locale or the flags word and fails loudly. Skips without client data.
|
||||
///
|
||||
/// The four ids here are the ones decision 1941's arms can report, cross-checked against the
|
||||
/// `Spell.dbc` mechanic columns pinned in `spell_catalog`: Fear's `Mechanic` is 5, Frost Nova's
|
||||
/// `EffectMechanic[1]` is 7, Polymorph's is 17.
|
||||
#[test]
|
||||
fn real_spell_mechanic_names_the_crowd_control_rows() {
|
||||
let data = crate::wow_data_or_skip!();
|
||||
let mut chain = crate::open_chain(&data).expect("open chain");
|
||||
let cat = load_spell_mechanic_catalog(&mut chain).expect("load SpellMechanic.dbc");
|
||||
|
||||
assert_eq!(cat.name(5), Some("fleeing"));
|
||||
assert_eq!(cat.name(7), Some("rooted"));
|
||||
assert_eq!(cat.name(11), Some("ensnared"));
|
||||
assert_eq!(cat.name(12), Some("stunned"));
|
||||
assert_eq!(cat.name(17), Some("polymorphed"));
|
||||
assert_eq!(cat.name(0), None, "0 = no mechanic, and no line to fill");
|
||||
assert_eq!(cat.name(999), None);
|
||||
assert_eq!(cat.len(), 27, "the 5875 file's full row count");
|
||||
}
|
||||
}
|
||||
@@ -346,8 +346,21 @@ pub enum SessionEvent {
|
||||
/// `[start, …waypoints…, endpoint]` — at constant (arc-length) speed over `duration_ms`. Every
|
||||
/// waypoint is carried, so a curved patrol reads as its real path, not a straight `start → endpoint`
|
||||
/// shortcut. A `Stop` / zero-duration / <2-point move clears the path (`path` empty).
|
||||
/// `MSG_MOVE_TIME_SKIPPED` — an observed mover reported that its own client skipped `lag_ms`
|
||||
/// of movement simulation. **Not a pose**: nothing about where the unit is has changed, only
|
||||
/// how far its clock has run. The app advances that mover's relay-chain wire stamp by it, the
|
||||
/// reference's `[CMovement+0xac] += lag` (`0x603b40` → `0x601560` → `0x61ab90`). Ignoring it
|
||||
/// leaves our copy of the chain permanently short, so the mover's next real packet reads as a
|
||||
/// step `lag` too large and is scheduled that much late. Decision 1935.
|
||||
MoveTimeSkipped { guid: u64, lag_ms: u32 },
|
||||
MonsterMove {
|
||||
guid: u64,
|
||||
/// `SMSG_MONSTER_MOVE_TRANSPORT` only: the transport whose frame `start` and every `path`
|
||||
/// point are expressed in — deck-local offsets, composed through the transport's live pose
|
||||
/// rather than read as world coordinates. `None` = the ordinary absolute path. A packet
|
||||
/// naming a transport is also the *attach* signal: the unit rides that deck from here
|
||||
/// (decision 1936).
|
||||
transport: Option<u64>,
|
||||
start: [f32; 3],
|
||||
/// The server's per-move spline counter — echoed in `CMSG_MOVE_SPLINE_DONE` when this spline
|
||||
/// drives our own player (Charge/knockback/taxi); ignored for a creature's walk.
|
||||
@@ -361,6 +374,8 @@ pub enum SessionEvent {
|
||||
duration_ms: u32,
|
||||
/// `true` ⇒ a 3-D flight path (keep the spline's Z); `false` ⇒ a ground walk whose Z the app
|
||||
/// re-derives from the terrain under the unit (see the renderer's creature ground-clamp).
|
||||
/// The re-derive is world-space and so does not apply to a `transport` path: there is no
|
||||
/// terrain under a deck, and the wire Z is already the deck-local height (decision 1936).
|
||||
flying: bool,
|
||||
/// `SPLINEFLAG_RUNMODE` — the path is travelled at run speed. Its **absence** forces
|
||||
/// `MOVEFLAG_WALK_MODE` on for the unit the spline moves (decision 1758).
|
||||
@@ -777,6 +792,13 @@ pub enum SessionEvent {
|
||||
},
|
||||
/// Put an item instance on the client's fixed 30 s use cooldown (`SMSG_ITEM_COOLDOWN`).
|
||||
ItemCooldown { item_guid: u64, spell_id: u32 },
|
||||
/// `SMSG_ITEM_TIME_UPDATE` — the seconds left on one duration-limited item instance (a
|
||||
/// conjured stone, a holiday gift, a timed quest item). The item's own `ITEM_FIELD_DURATION`
|
||||
/// carries the same number and is sent to the owner, but vmangos's writer says outright that
|
||||
/// the field is not what the client displays from (`Item::SendTimeUpdate`,
|
||||
/// `Objects/Item.cpp:1094`) — same shape as the enchant countdown (decision 0920).
|
||||
/// `seconds == 0` = expired / no timer. Decision 1933.
|
||||
ItemTime { item_guid: u64, seconds: u32 },
|
||||
/// `SMSG_ITEM_ENCHANT_TIME_UPDATE` — the seconds left on one item's TEMPORARY enchant, in the
|
||||
/// named enchant slot. The **only** feed for the tooltip's countdown: the item's own
|
||||
/// `ITEM_FIELD_ENCHANTMENT` duration field is never read for it (wow-re
|
||||
|
||||
@@ -257,6 +257,9 @@ pub fn decode(packet: ServerPacket) -> Vec<SessionEvent> {
|
||||
item_guid,
|
||||
spell_id,
|
||||
}],
|
||||
ServerPacket::ItemTime { item_guid, seconds } => {
|
||||
vec![SessionEvent::ItemTime { item_guid, seconds }]
|
||||
}
|
||||
ServerPacket::ItemEnchantTime {
|
||||
item_guid,
|
||||
slot,
|
||||
@@ -640,8 +643,12 @@ pub fn decode(packet: ServerPacket) -> Vec<SessionEvent> {
|
||||
ServerPacket::TriggerCinematic { cinematic_id } => {
|
||||
vec![SessionEvent::CinematicTriggered { cinematic_id }]
|
||||
}
|
||||
ServerPacket::MoveTimeSkipped { guid, lag_ms } => {
|
||||
vec![SessionEvent::MoveTimeSkipped { guid, lag_ms }]
|
||||
}
|
||||
ServerPacket::MonsterMove {
|
||||
guid,
|
||||
transport,
|
||||
start,
|
||||
spline_id,
|
||||
path,
|
||||
@@ -652,6 +659,7 @@ pub fn decode(packet: ServerPacket) -> Vec<SessionEvent> {
|
||||
run_mode,
|
||||
} => vec![SessionEvent::MonsterMove {
|
||||
guid,
|
||||
transport,
|
||||
start: v3(start),
|
||||
spline_id,
|
||||
path: path.into_iter().map(v3).collect(),
|
||||
|
||||
@@ -283,6 +283,19 @@ pub fn movement(info: &MovementInfo) -> Vec<u8> {
|
||||
body
|
||||
}
|
||||
|
||||
/// Body of `CMSG_MOVE_TIME_SKIPPED` (client): the mover's **full 8-byte** guid then the skipped
|
||||
/// milliseconds (vmangos `MoveTimeSkipped::ReadFromWorldPacket`, `Server/Packets/Movement.cpp:10-14`
|
||||
/// — `recv_data >> guid` is `ObjectGuid`'s plain `read<uint64>`, `ObjectGuid.cpp:180`, not the
|
||||
/// packed reader `PackedGuidReader` takes). Note the asymmetry with the server's own relay of the
|
||||
/// same fact, `MSG_MOVE_TIME_SKIPPED`, which writes `GetPackGUID()` (`MovementHandler.cpp:1014`):
|
||||
/// inbound is full, outbound is packed.
|
||||
pub fn move_time_skipped(guid: u64, lag_ms: u32) -> Vec<u8> {
|
||||
let mut body = Vec::with_capacity(12);
|
||||
body.extend_from_slice(&guid.to_le_bytes());
|
||||
body.extend_from_slice(&lag_ms.to_le_bytes());
|
||||
body
|
||||
}
|
||||
|
||||
/// Body of `CMSG_MOVE_SPLINE_DONE` (client): the `MovementInfo` at the ride's endpoint, the
|
||||
/// `spline_id` being acknowledged (matched against the server's newest spline for our mover), and a
|
||||
/// trailing float the server reads-and-discards (`MoveSplineDone::ReadFromWorldPacket` does an
|
||||
|
||||
@@ -247,12 +247,25 @@ impl ItemInfo {
|
||||
///
|
||||
/// It is a separate predicate from [`Self::opens_loot`] because it sits at a different point
|
||||
/// in the dispatcher's order — *before* the quest-starter and readable arms, where the loot
|
||||
/// arm sits after them. A wrapper template whose instance is no longer wrapped takes the
|
||||
/// begin-wrap cursor path instead (local, no packet; not built yet).
|
||||
/// arm sits after them. A wrapper template whose instance is *not* wrapped takes the
|
||||
/// begin-wrap cursor path instead ([`Self::begins_gift_wrap`]) — local, no packet.
|
||||
pub fn unwraps_gift(&self, instance_flags: u32) -> bool {
|
||||
self.flags & ITEM_FLAG_WRAPPER != 0 && instance_flags & ITEM_DYNFLAG_WRAPPED != 0
|
||||
}
|
||||
|
||||
/// Does a right-click on this instance **arm the gift-wrap cursor**? The *other* side of the
|
||||
/// same fork [`Self::unwraps_gift`] tests (`0x5d8d00` arm 2): template [`ITEM_FLAG_WRAPPER`]
|
||||
/// with the instance's [`ITEM_DYNFLAG_WRAPPED`] **clear** — a piece of wrapping paper rather
|
||||
/// than a wrapped present.
|
||||
///
|
||||
/// **Nothing is sent on this click.** `0x5d8dba` calls `0x5edea0`, whose only three acts are
|
||||
/// `LockItem 0x4953e0` on the paper, `SetCursorBaseMode(2)` and `CursorSetMode(2)` — a purely
|
||||
/// local arm (wow-re `object-layer/scratch/gift-wrap-law.md`, §5-carved). What completes it is
|
||||
/// the next **left**-click on a container slot, which sends [`wrap_item`]. Decision 1934.
|
||||
pub fn begins_gift_wrap(&self, instance_flags: u32) -> bool {
|
||||
self.flags & ITEM_FLAG_WRAPPER != 0 && instance_flags & ITEM_DYNFLAG_WRAPPED == 0
|
||||
}
|
||||
|
||||
/// Does a right-click on this template send `CMSG_OPEN_ITEM` to **loot it open**? The
|
||||
/// reference's arm #8 (`0x5d8f7c: test al,4` → the `0x5edc80` emitter): a **bare** template
|
||||
/// [`ITEM_FLAG_LOOTABLE`] test.
|
||||
@@ -663,6 +676,21 @@ pub fn open_item(bag_index: u8, slot: u8) -> Vec<u8> {
|
||||
vec![bag_index, slot]
|
||||
}
|
||||
|
||||
/// Body of `CMSG_WRAP_ITEM` (VERIFIED vmangos `WrapItem::ReadFromWorldPacket`,
|
||||
/// `Server/Packets/Item.cpp:121-127` + `.h:191-201`; opcode 467 `Opcodes_1_12_1.h:468`):
|
||||
/// `giftBag`, `giftSlot`, `itemBag`, `itemSlot` — all `uint8`, the **paper's** position first and
|
||||
/// the target's second, the same bag addressing as [`use_item`].
|
||||
///
|
||||
/// The server refuses every ineligible target with an `EQUIP_ERR_*` of its own
|
||||
/// (`HandleWrapItemOpcode`: equipped, a bag, soulbound, stackable, unique, already wrapped, or
|
||||
/// mid-cast) — so a refusal reaches the player as an `SMSG_INVENTORY_CHANGE_FAILURE` line, not as
|
||||
/// a silently eaten click. On success the *target* item keeps its guid and takes the paper's
|
||||
/// `WrappedGift` entry, gains `ITEM_FIELD_GIFTCREATOR` and `ITEM_DYNFLAG_WRAPPED`, and one paper
|
||||
/// is destroyed — all as ordinary field updates, no answering packet of its own.
|
||||
pub fn wrap_item(gift_bag: u8, gift_slot: u8, item_bag: u8, item_slot: u8) -> Vec<u8> {
|
||||
vec![gift_bag, gift_slot, item_bag, item_slot]
|
||||
}
|
||||
|
||||
/// Body of `CMSG_AUTOEQUIP_ITEM` (VERIFIED vmangos `AutoEquipItem::ReadFromWorldPacket`,
|
||||
/// `Server/Packets/Item.cpp:17-21` + `.h:31-39`; opcode 266 `Opcodes_1_12_1.h:269`): source
|
||||
/// `srcbag`/`srcslot` (both `uint8`), the same bag addressing as [`use_item`]. The real client
|
||||
@@ -760,6 +788,21 @@ pub(super) fn read_inventory_change_failure(
|
||||
Ok((reason, required_level, item_guid, bag_slot))
|
||||
}
|
||||
|
||||
/// Read `SMSG_ITEM_TIME_UPDATE` (VERIFIED vmangos `Item::SendTimeUpdate`,
|
||||
/// `Objects/Item.cpp:1096-1106`): the item guid, then the remaining **seconds**
|
||||
/// (`ITEM_FIELD_DURATION`, which vmangos counts down in seconds — `Item::UpdateDuration`,
|
||||
/// `Item.cpp:243-257`). No trailing player guid on this one, unlike its enchant sibling: the
|
||||
/// packet is `(8 + 4)` bytes and vmangos sizes it exactly so.
|
||||
///
|
||||
/// Sent on world enter for every carried duration item (`Player::SendItemDurations`,
|
||||
/// `Player.cpp:12007`) and again whenever one is created or its duration changes
|
||||
/// (`Player.cpp:19782-19785`).
|
||||
pub(super) fn read_item_time(r: &mut &[u8]) -> io::Result<(u64, u32)> {
|
||||
let item_guid = read_u64_le(r)?;
|
||||
let seconds = read_u32_le(r)?;
|
||||
Ok((item_guid, seconds))
|
||||
}
|
||||
|
||||
/// Read `SMSG_ITEM_ENCHANT_TIME_UPDATE` (VERIFIED vmangos
|
||||
/// `WorldPackets::Item::ItemEnchantTimeUpdate::AppendBodyTo`, `Server/Packets/Item.cpp:161-169`):
|
||||
/// item guid, enchant **slot**, remaining **seconds**, then the owning player's guid (present from
|
||||
@@ -792,6 +835,12 @@ mod tests {
|
||||
assert_eq!(auto_equip_item(255, 30), vec![255, 30]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wrap_item_body_paper_first() {
|
||||
// 467: giftbag, giftslot, itembag, itemslot (Item.cpp:121-127) — the PAPER's pair leads.
|
||||
assert_eq!(wrap_item(255, 23, 255, 24), vec![255, 23, 255, 24]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn auto_store_bag_item_body() {
|
||||
// 267: srcbag, srcslot, dstbag (Item.cpp:23-28).
|
||||
@@ -878,6 +927,22 @@ mod tests {
|
||||
assert_eq!(r.len(), 8);
|
||||
}
|
||||
|
||||
/// `SMSG_ITEM_TIME_UPDATE`'s body: guid then seconds, and **nothing after** — the enchant
|
||||
/// twin's trailing player guid is not on this packet (vmangos sizes it `8 + 4`). Pinned
|
||||
/// because the two share a handler and it would be easy to copy the wrong tail.
|
||||
#[test]
|
||||
fn item_time_reads_guid_then_seconds() {
|
||||
let mut buf = Vec::new();
|
||||
buf.extend_from_slice(&0x4000_0000_0000_00f7u64.to_le_bytes()); // itemGuid
|
||||
buf.extend_from_slice(&1800u32.to_le_bytes()); // seconds
|
||||
let mut r = &buf[..];
|
||||
assert_eq!(
|
||||
read_item_time(&mut r).unwrap(),
|
||||
(0x4000_0000_0000_00f7, 1800)
|
||||
);
|
||||
assert!(r.is_empty(), "the body is exactly 12 bytes");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn inventory_failure_nonlevel_branch_has_no_u32() {
|
||||
// Any failed reason != 1 skips requiredLevel: item1Guid u64, item2Guid u64, bagSlot u8.
|
||||
|
||||
@@ -100,8 +100,8 @@ pub use client::{
|
||||
channel_set_owner, channel_unban, channel_unmoderator, channel_unmute, char_create,
|
||||
creature_query, force_speed_ack, full_guid, join_channel, knock_back_ack, leave_channel,
|
||||
messagechat, messagechat_channel, messagechat_kind, messagechat_whisper, move_flag_ack,
|
||||
move_spline_done, movement, pet_name_query, ping, played_time, query_time, random_roll,
|
||||
teleport_ack, text_emote,
|
||||
move_spline_done, move_time_skipped, movement, pet_name_query, ping, played_time, query_time,
|
||||
random_roll, teleport_ack, text_emote,
|
||||
};
|
||||
pub use combat_log::{
|
||||
DamageShield, DispelFailed, EnchantmentLog, EnvironmentalDamageLog, ExecuteLog, PartyKillLog,
|
||||
@@ -150,9 +150,9 @@ pub use instance::{
|
||||
};
|
||||
pub use items::{
|
||||
auto_equip_item, auto_store_bag_item, destroy_item, item_query, open_item, set_ammo,
|
||||
split_item, swap_inv_item, swap_item, use_item, ItemDamage, ItemInfo, ItemSpellEntry,
|
||||
ItemUseSpell, UseItemTarget, BAG_PLAYER_INVENTORY, ITEM_DYNFLAG_UNLOCKED, ITEM_DYNFLAG_WRAPPED,
|
||||
ITEM_FLAG_LOOTABLE, ITEM_FLAG_WRAPPER, SLOT_BAG_FIRST, SLOT_PACK_FIRST,
|
||||
split_item, swap_inv_item, swap_item, use_item, wrap_item, ItemDamage, ItemInfo,
|
||||
ItemSpellEntry, ItemUseSpell, UseItemTarget, BAG_PLAYER_INVENTORY, ITEM_DYNFLAG_UNLOCKED,
|
||||
ITEM_DYNFLAG_WRAPPED, ITEM_FLAG_LOOTABLE, ITEM_FLAG_WRAPPER, SLOT_BAG_FIRST, SLOT_PACK_FIRST,
|
||||
};
|
||||
pub use loot::{
|
||||
autostore_loot_item, loot, loot_error, loot_master_give, loot_money, loot_release, loot_roll,
|
||||
|
||||
@@ -1,10 +1,19 @@
|
||||
//! `SMSG_MONSTER_MOVE` — the server-dictated creature movement path.
|
||||
//! `SMSG_MONSTER_MOVE` — the server-dictated creature movement path — and its transport twin.
|
||||
//!
|
||||
//! One packet: a mover, its `start`, a spline id, a `moveType`-switched final facing, then (unless it is
|
||||
//! a `Stop`) the spline block — a flag word, a duration, and the waypoints. Decodes into the full
|
||||
//! travel-order polyline [`ServerPacket::MonsterMove::path`] the app rides at constant (arc-length) speed
|
||||
//! (decision 0097). The wire packs the waypoints two ways, keyed by the `Flying`/`Mask_CatmullRom` flag —
|
||||
//! see [`read_monster_move_spline`].
|
||||
//!
|
||||
//! **`SMSG_MONSTER_MOVE_TRANSPORT` is the same packet with one field inserted**: a second packed guid,
|
||||
//! the transport's, immediately after the mover's — and every coordinate in the body is then an
|
||||
//! **offset in that transport's frame**, not world space (decision 1936). vmangos writes exactly that
|
||||
//! (`Movement::MoveSplineInit::Launch`, `spline/MoveSplineInit.cpp:138-170`: the same builder, the
|
||||
//! opcode swapped and the transport packed guid appended, after `CalculatePassengerOffset` has moved
|
||||
//! the start into deck space), and it computes such a path against the transport's **model** navmesh
|
||||
//! rather than the map's (`PathFinder::calculate`, `Maps/PathFinder.cpp:76-84`), so the waypoints are
|
||||
//! deck-local too. One reader serves both, keyed by [`read_monster_move`]'s `on_transport`.
|
||||
|
||||
use std::io;
|
||||
|
||||
@@ -39,8 +48,12 @@ const SPLINE_FLAG_RUNMODE: u32 = 0x100;
|
||||
/// reading a tail anyway over-ran the body, the `0x00dd: failed to fill whole buffer` skip that dropped
|
||||
/// every creature stop). Otherwise the spline block yields the full polyline `[start, …waypoints…,
|
||||
/// endpoint]`.
|
||||
pub(super) fn read_monster_move(r: &mut &[u8]) -> io::Result<ServerPacket> {
|
||||
pub(super) fn read_monster_move(r: &mut &[u8], on_transport: bool) -> io::Result<ServerPacket> {
|
||||
let guid = read_packed_guid(r)?;
|
||||
// `SMSG_MONSTER_MOVE_TRANSPORT` only: the deck the whole body is expressed on, packed like the
|
||||
// mover's guid and read immediately after it. Everything below is unchanged — the two opcodes
|
||||
// share the builder server-side and differ by this one field.
|
||||
let transport = on_transport.then(|| read_packed_guid(r)).transpose()?;
|
||||
let start = Vector3d::read(r)?;
|
||||
// The server's per-move spline counter. Discarded for a creature (nothing acks its walk), but
|
||||
// the client echoes it back in `CMSG_MOVE_SPLINE_DONE` when a spline drives its OWN player
|
||||
@@ -62,6 +75,7 @@ pub(super) fn read_monster_move(r: &mut &[u8]) -> io::Result<ServerPacket> {
|
||||
Ok(if move_type == MONSTER_MOVE_STOP {
|
||||
ServerPacket::MonsterMove {
|
||||
guid,
|
||||
transport,
|
||||
start,
|
||||
spline_id,
|
||||
path: Vec::new(),
|
||||
@@ -93,6 +107,7 @@ pub(super) fn read_monster_move(r: &mut &[u8]) -> io::Result<ServerPacket> {
|
||||
};
|
||||
ServerPacket::MonsterMove {
|
||||
guid,
|
||||
transport,
|
||||
start,
|
||||
spline_id,
|
||||
path,
|
||||
@@ -204,6 +219,52 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
/// `SMSG_MONSTER_MOVE_TRANSPORT` is `SMSG_MONSTER_MOVE` with the transport's packed guid
|
||||
/// inserted between the mover's guid and the start position — everything after it parses
|
||||
/// identically, and every coordinate is then a deck-local offset. Reading the *plain* opcode's
|
||||
/// layout for it would consume the transport guid as the start's X and Y and land the creature
|
||||
/// somewhere near the map origin; reading the transport layout for a plain packet is the same
|
||||
/// mistake in reverse. Both directions are pinned here (decision 1936).
|
||||
#[test]
|
||||
fn monster_move_transport_reads_the_deck_guid_first() {
|
||||
let mut body = Vec::new();
|
||||
write_packed_guid(0x55, &mut body).unwrap(); // the mover
|
||||
write_packed_guid(0x2000_0000_0000_0007, &mut body).unwrap(); // the transport
|
||||
for f in [1.5f32, -2.5, 0.75] {
|
||||
body.extend_from_slice(&f.to_le_bytes()); // deck-local start
|
||||
}
|
||||
body.extend_from_slice(&7u32.to_le_bytes()); // splineId
|
||||
body.push(0); // moveType: a plain move
|
||||
body.extend_from_slice(&0u32.to_le_bytes()); // spline flags: ground, walk
|
||||
body.extend_from_slice(&2_000u32.to_le_bytes()); // duration
|
||||
append_linear_path(&mut body, &[[1.5, -2.5, 0.75], [4.0, -2.5, 0.75]]);
|
||||
|
||||
match parse_server(opcode::SMSG_MONSTER_MOVE_TRANSPORT, &body).expect("parses") {
|
||||
ServerPacket::MonsterMove {
|
||||
guid,
|
||||
transport,
|
||||
start,
|
||||
path,
|
||||
..
|
||||
} => {
|
||||
assert_eq!(guid, 0x55);
|
||||
assert_eq!(transport, Some(0x2000_0000_0000_0007));
|
||||
assert_eq!((start.x, start.y, start.z), (1.5, -2.5, 0.75));
|
||||
assert_eq!(path.len(), 2, "start + endpoint, both deck-local");
|
||||
assert_eq!((path[1].x, path[1].y, path[1].z), (4.0, -2.5, 0.75));
|
||||
}
|
||||
_ => panic!("expected MonsterMove"),
|
||||
}
|
||||
|
||||
// The plain opcode over the SAME bytes must NOT come back with a transport — and it
|
||||
// mis-reads the head, which is exactly why the two layouts cannot share one arm.
|
||||
match parse_server(opcode::SMSG_MONSTER_MOVE, &body) {
|
||||
Ok(ServerPacket::MonsterMove { transport, .. }) => assert_eq!(transport, None),
|
||||
Ok(_) => panic!("expected MonsterMove"),
|
||||
Err(_) => {} // an under-run is equally fine: the point is that it is not the same read
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn monster_move_stop_has_no_tail() {
|
||||
// A STOP (moveType 1) is head-only: no flags/duration/points. Parsing the tail anyway
|
||||
|
||||
@@ -18,6 +18,20 @@ pub(super) const MOVEMENT_FLAG_SWIMMING: u32 = 0x20_0000;
|
||||
pub(super) const MOVEMENT_FLAG_SPLINE_ENABLED: u32 = 0x40_0000;
|
||||
pub(super) const MOVEMENT_FLAG_SPLINE_ELEVATION: u32 = 0x400_0000;
|
||||
|
||||
/// Read `MSG_MOVE_TIME_SKIPPED` — one observed mover's **packed** guid and the milliseconds its
|
||||
/// own client skipped (VERIFIED: the reference's handler `0x603b40` reads a packed guid through
|
||||
/// `0x642ed0`, resolves under `TYPEMASK_UNIT`, then reads a plain `u32`; vmangos relays exactly
|
||||
/// that shape, `MovementHandler.cpp:1011-1017`).
|
||||
///
|
||||
/// Note the **asymmetry with the client's own send**, which is the same fact in the other
|
||||
/// direction and writes a *plain* 8-byte guid (see [`super::client::move_time_skipped`]).
|
||||
/// Inbound packed, outbound plain — that is the reference's own encoding, not a slip.
|
||||
pub(super) fn read_move_time_skipped(r: &mut &[u8]) -> io::Result<(u64, u32)> {
|
||||
let guid = crate::wire::read_packed_guid(r)?;
|
||||
let lag_ms = read_u32_le(r)?;
|
||||
Ok((guid, lag_ms))
|
||||
}
|
||||
|
||||
/// Read a wire `MovementInfo` — the body shared by every `MSG_MOVE_*` (and the teleport ack). Surfaces
|
||||
/// `flags`/`position`/`orientation`/`timestamp`/`fall_time`, the **transport pose** ([`TransportPose`],
|
||||
/// present iff `MOVEFLAG_ON_TRANSPORT` — this is how a boarded rider's `MSG_MOVE_*` heartbeat carries
|
||||
|
||||
@@ -60,6 +60,13 @@ pub const SMSG_TRIGGER_CINEMATIC: u16 = 0x00FA;
|
||||
pub const CMSG_NEXT_CINEMATIC_CAMERA: u16 = 0x00FB;
|
||||
pub const CMSG_COMPLETE_CINEMATIC: u16 = 0x00FC;
|
||||
pub const SMSG_MONSTER_MOVE: u16 = 0x00DD;
|
||||
/// `SMSG_MONSTER_MOVE_TRANSPORT` (VERIFIED vmangos `Opcodes_1_12_1.h`: 686) — the transport twin of
|
||||
/// [`SMSG_MONSTER_MOVE`]: the identical body with the **transport's packed guid inserted after the
|
||||
/// mover's**, and every coordinate in it a deck-local offset instead of a world position. Sent for a
|
||||
/// unit pathing *on* a transport — a pet following its owner aboard a boat, anything a player pulls
|
||||
/// onto a deck (`Movement::MoveSplineInit::Launch`, `spline/MoveSplineInit.cpp:146-154`). Body in
|
||||
/// [`super::monster_move::read_monster_move`]; decision 1936.
|
||||
pub const SMSG_MONSTER_MOVE_TRANSPORT: u16 = 0x02AE; // 686
|
||||
pub const SMSG_INITIALIZE_FACTIONS: u16 = 0x0122;
|
||||
/// A faction became visible in the reputation pane (VERIFIED vmangos `Opcodes_1_12_1.h`: 291,
|
||||
/// sender `ReputationMgr::SendVisible`) — body one `u32` reputation-list slot. The server sets the
|
||||
@@ -198,6 +205,30 @@ pub const SMSG_PET_CAST_FAILED: u16 = 0x0138; // 312
|
||||
/// and a trailing float the server `read_skip`s (VERIFIED vmangos `Opcodes_1_12_1.h`: 713;
|
||||
/// `MoveSplineDone::ReadFromWorldPacket`).
|
||||
pub const CMSG_MOVE_SPLINE_DONE: u16 = 0x02C9; // 713
|
||||
/// The client's report that its movement clock **skipped** — a stall, a long frame, a window that
|
||||
/// went to the background — carrying the mover and how many milliseconds went missing (VERIFIED
|
||||
/// vmangos `Opcodes_1_12_1.h`: 718). Body in [`super::client::move_time_skipped`].
|
||||
///
|
||||
/// vmangos adds the reported `lag` to the mover's `stime`/`ctime` so its own movement clock stays
|
||||
/// aligned with ours — **and hangs a 1.12-specific transport fix off it**: a player whose last
|
||||
/// movement packet was the *first* one carrying `MOVEFLAG_ONTRANSPORT` is flagged `JustBoarded`,
|
||||
/// and the next `CMSG_MOVE_TIME_SKIPPED` makes the server re-send that transport's out-of-range
|
||||
/// update followed by a fresh create (`HandleMoveTimeSkippedOpcode`,
|
||||
/// `Handlers/MovementHandler.cpp:989-1019`, comment *"fix an 1.12 client problem with
|
||||
/// transports"*). A client that never sends this never gets that refresh. Decision 1935.
|
||||
pub const CMSG_MOVE_TIME_SKIPPED: u16 = 0x02CE; // 718
|
||||
/// The **broadcast twin** of [`CMSG_MOVE_TIME_SKIPPED`] (VERIFIED vmangos `Opcodes_1_12_1.h`: 793):
|
||||
/// the server relays one mover's skip to everyone watching it, so their copies of that unit's
|
||||
/// movement clock move with it (`MovementHandler.cpp:1011-1017`, packed guid + the same `u32`).
|
||||
///
|
||||
/// **Not ignorable.** The reference registers a handler for it (`0x6035e0` → `0x603b40`) that reads
|
||||
/// a packed guid, resolves under `TYPEMASK_UNIT`, and adds the lag to `[CMovement+0xac]` — the
|
||||
/// observed unit's last-seen **wire** movement timestamp, the monotonic maximum the relay chain
|
||||
/// hangs off (`0x601560` → `0x61ab90`). That cell is benilla-app's relay-chain `last_wire_ms`
|
||||
/// (`net::motion::relay::RelayChain`, decision 0615, which models `+0xa8`/`+0xac` by name). Drop the packet and the chain's copy stays `lag` ms short of the sender's
|
||||
/// forever: the mover's next real packet then reads as a step `lag` too large and is scheduled
|
||||
/// that much late — a hitch on that unit, not a lost packet. Decision 1935.
|
||||
pub const MSG_MOVE_TIME_SKIPPED: u16 = 0x0319; // 793
|
||||
pub const SMSG_ATTACKSTART: u16 = 0x0143; // 323
|
||||
pub const SMSG_ATTACKSTOP: u16 = 0x0144; // 324
|
||||
pub const SMSG_ATTACKERSTATEUPDATE: u16 = 0x014A; // 330
|
||||
@@ -228,6 +259,21 @@ pub const SMSG_COOLDOWN_EVENT: u16 = 0x0135; // 309
|
||||
pub const SMSG_CLEAR_COOLDOWN: u16 = 0x01DE; // 478
|
||||
pub const SMSG_COOLDOWN_CHEAT: u16 = 0x01E1; // 481
|
||||
|
||||
/// `SMSG_ITEM_TIME_UPDATE` (VERIFIED vmangos `Opcodes_1_12_1.h`: 490) — the
|
||||
/// remaining lifetime, in **seconds**, of one duration-limited item instance (a conjured stone, a
|
||||
/// holiday gift, a timed quest item). Shares its handler with
|
||||
/// [`SMSG_ITEM_ENCHANT_TIME_UPDATE`]: both read an 8-byte item guid first and then fork on the
|
||||
/// opcode (`0x5e4f69 sub eax,0x1ea ; je …` / `0x5e4f74 dec eax` — wow-re
|
||||
/// `ui/scratch/weapon-enchant-info.md` §7).
|
||||
///
|
||||
/// The item's own `ITEM_FIELD_DURATION` (wire field 15) carries the same number and is sent to the
|
||||
/// owner, but vmangos's writer says in as many words that it is not what the client displays —
|
||||
/// *"Though the client has the information in the item's data field, we have to send
|
||||
/// SMSG_ITEM_TIME_UPDATE to display the remaining time"* (`Item::SendTimeUpdate`,
|
||||
/// `Objects/Item.cpp:1094`). Same shape as the enchant countdown (decision 0920): a client-local
|
||||
/// deadline whose only feed is the packet. Body in [`super::items::read_item_time`]; decision 1933.
|
||||
pub const SMSG_ITEM_TIME_UPDATE: u16 = 0x01EA; // 490
|
||||
|
||||
/// `SMSG_ITEM_ENCHANT_TIME_UPDATE` (VERIFIED vmangos `Opcodes_1_12_1.h`: 491) — the **only** source
|
||||
/// of a temporary enchant's remaining time. The item's `ITEM_FIELD_ENCHANTMENT` duration field is
|
||||
/// never read for it: the reference's tooltip reads a client-side per-slot deadline array
|
||||
@@ -365,6 +411,17 @@ pub const CMSG_USE_ITEM: u16 = 0x00AB; // 171
|
||||
/// guid** (`HandleOpenItemOpcode` → `SendLoot(item, LOOT_CORPSE)`), so the loot window opens over
|
||||
/// a thing in your bag rather than a corpse in the world.
|
||||
pub const CMSG_OPEN_ITEM: u16 = 0x00AC; // 172
|
||||
/// Wrap an item in a piece of gift wrapping (VERIFIED vmangos `Opcodes_1_12_1.h`: 467). Body in
|
||||
/// [`super::items::wrap_item`]: `giftBag`, `giftSlot`, `itemBag`, `itemSlot` — the **paper first**,
|
||||
/// then the thing being wrapped.
|
||||
///
|
||||
/// The other half of the gift arc: [`CMSG_OPEN_ITEM`] above *unwraps* one, and this is how a gift
|
||||
/// gets made. Right-clicking a `ITEM_FLAG_WRAPPER` template whose instance is **not** already
|
||||
/// wrapped sends nothing at all — it arms a purely local wrap cursor (`0x5edea0`: the item lock
|
||||
/// `0x4953e0` @`0x5edeb5` + `CursorSetMode(2)`, disarmed by `0x5eded0` from inside `ClearCursor`;
|
||||
/// wow-re `ui/scratch/right-click-open.md`, use-dispatcher arm 2, VERIFIED) — and *this* opcode
|
||||
/// goes out when the armed cursor is then clicked on a target. Decision 1934.
|
||||
pub const CMSG_WRAP_ITEM: u16 = 0x01D3; // 467
|
||||
/// VERIFIED vmangos `Opcodes_1_12_1.h`: 177. Body in [`super::gameobj_use`] — a full guid, the
|
||||
/// GameObject to use (decision 0236). Not interchangeable with `CMSG_LOOT`: the server rejects a
|
||||
/// GameObject guid on `CMSG_LOOT`, so a chest opens its loot through this opcode.
|
||||
|
||||
@@ -135,8 +135,19 @@ pub enum ServerPacket {
|
||||
TriggerCinematic {
|
||||
cinematic_id: u32,
|
||||
},
|
||||
/// `MSG_MOVE_TIME_SKIPPED` — an observed mover's own client skipped `lag_ms` of movement
|
||||
/// simulation, so every watcher's copy of its wire clock must move with it (layout in
|
||||
/// [`super::movement::read_move_time_skipped`]). Decision 1935.
|
||||
MoveTimeSkipped {
|
||||
guid: u64,
|
||||
lag_ms: u32,
|
||||
},
|
||||
MonsterMove {
|
||||
guid: u64,
|
||||
/// `SMSG_MONSTER_MOVE_TRANSPORT` only: the transport whose frame `start` and every `path`
|
||||
/// point are expressed in (deck-local offsets, not world coordinates). `None` = the plain
|
||||
/// `SMSG_MONSTER_MOVE`, whose coordinates are absolute. Decision 1936.
|
||||
transport: Option<u64>,
|
||||
start: Vector3d,
|
||||
/// The server's per-move spline counter — echoed back in `CMSG_MOVE_SPLINE_DONE` when the
|
||||
/// spline drives our own player (Charge/knockback/taxi); ignored for a creature's walk.
|
||||
@@ -645,6 +656,12 @@ pub enum ServerPacket {
|
||||
item_guid: u64,
|
||||
spell_id: u32,
|
||||
},
|
||||
/// `SMSG_ITEM_TIME_UPDATE` — how long one duration-limited item instance has left, in
|
||||
/// **seconds** (layout in [`super::items::read_item_time`]). Decision 1933.
|
||||
ItemTime {
|
||||
item_guid: u64,
|
||||
seconds: u32,
|
||||
},
|
||||
/// `SMSG_ITEM_ENCHANT_TIME_UPDATE` — how long one item's TEMPORARY enchant has left (layout in
|
||||
/// [`super::items::read_item_enchant_time`]). The tooltip's countdown has no other source
|
||||
/// (decision 0920).
|
||||
@@ -1496,7 +1513,14 @@ impl ServerPacket {
|
||||
ServerPacket::CompressedMoves { .. } => "SMSG_COMPRESSED_MOVES".into(),
|
||||
ServerPacket::DestroyObject { .. } => "SMSG_DESTROY_OBJECT".into(),
|
||||
ServerPacket::TriggerCinematic { .. } => "SMSG_TRIGGER_CINEMATIC".into(),
|
||||
ServerPacket::MonsterMove { .. } => "SMSG_MONSTER_MOVE".into(),
|
||||
ServerPacket::MoveTimeSkipped { .. } => "MSG_MOVE_TIME_SKIPPED".into(),
|
||||
ServerPacket::MonsterMove { transport, .. } => {
|
||||
if transport.is_some() {
|
||||
"SMSG_MONSTER_MOVE_TRANSPORT".into()
|
||||
} else {
|
||||
"SMSG_MONSTER_MOVE".into()
|
||||
}
|
||||
}
|
||||
ServerPacket::PlayerMove { opcode, .. } => format!("MSG_MOVE relay ({opcode:#06x})"),
|
||||
ServerPacket::Teleport { .. } => "MSG_MOVE_TELEPORT_ACK".into(),
|
||||
ServerPacket::NewWorld { .. } => "SMSG_NEW_WORLD".into(),
|
||||
@@ -1572,6 +1596,7 @@ impl ServerPacket {
|
||||
ServerPacket::CancelAutoRepeat => "SMSG_CANCEL_AUTO_REPEAT".into(),
|
||||
ServerPacket::SpellCooldownList { .. } => "SMSG_SPELL_COOLDOWN".into(),
|
||||
ServerPacket::ItemCooldown { .. } => "SMSG_ITEM_COOLDOWN".into(),
|
||||
ServerPacket::ItemTime { .. } => "SMSG_ITEM_TIME_UPDATE".into(),
|
||||
ServerPacket::ItemEnchantTime { .. } => "SMSG_ITEM_ENCHANT_TIME_UPDATE".into(),
|
||||
ServerPacket::CooldownEvent { .. } => "SMSG_COOLDOWN_EVENT".into(),
|
||||
ServerPacket::ClearCooldown { .. } => "SMSG_CLEAR_COOLDOWN".into(),
|
||||
|
||||
@@ -244,7 +244,12 @@ pub fn parse_server(opcode: u16, body: &[u8]) -> io::Result<ServerPacket> {
|
||||
opcode::SMSG_COMPRESSED_MOVES => ServerPacket::CompressedMoves {
|
||||
packets: read_compressed_moves(&mut r)?,
|
||||
},
|
||||
opcode::SMSG_MONSTER_MOVE => monster_move::read_monster_move(&mut r)?,
|
||||
opcode::MSG_MOVE_TIME_SKIPPED => {
|
||||
let (guid, lag_ms) = movement::read_move_time_skipped(&mut r)?;
|
||||
ServerPacket::MoveTimeSkipped { guid, lag_ms }
|
||||
}
|
||||
opcode::SMSG_MONSTER_MOVE => monster_move::read_monster_move(&mut r, false)?,
|
||||
opcode::SMSG_MONSTER_MOVE_TRANSPORT => monster_move::read_monster_move(&mut r, true)?,
|
||||
opcode::MSG_MOVE_TELEPORT_ACK => {
|
||||
let guid = read_packed_guid(&mut r)?;
|
||||
let counter = read_u32_le(&mut r)?;
|
||||
@@ -575,6 +580,10 @@ pub fn parse_server(opcode: u16, body: &[u8]) -> io::Result<ServerPacket> {
|
||||
spell_id,
|
||||
}
|
||||
}
|
||||
opcode::SMSG_ITEM_TIME_UPDATE => {
|
||||
let (item_guid, seconds) = items::read_item_time(&mut r)?;
|
||||
ServerPacket::ItemTime { item_guid, seconds }
|
||||
}
|
||||
opcode::SMSG_ITEM_ENCHANT_TIME_UPDATE => {
|
||||
let (item_guid, slot, seconds) = items::read_item_enchant_time(&mut r)?;
|
||||
ServerPacket::ItemEnchantTime {
|
||||
|
||||
@@ -55,6 +55,23 @@ impl WorldWriter {
|
||||
)
|
||||
}
|
||||
|
||||
/// Wrap an item in gift paper (`CMSG_WRAP_ITEM`, layout in [`messages::wrap_item`]) — the
|
||||
/// completion of the local wrap cursor a `ITEM_FLAG_WRAPPER` item's right-click arms. The
|
||||
/// paper's `(bag, slot)` leads, the target's follows. Success is silent (field updates on the
|
||||
/// target, one paper destroyed); every refusal comes back as `SMSG_INVENTORY_CHANGE_FAILURE`.
|
||||
pub fn wrap_item(
|
||||
&mut self,
|
||||
gift_bag: u8,
|
||||
gift_slot: u8,
|
||||
item_bag: u8,
|
||||
item_slot: u8,
|
||||
) -> Result<()> {
|
||||
self.send(
|
||||
opcode::CMSG_WRAP_ITEM,
|
||||
&messages::wrap_item(gift_bag, gift_slot, item_bag, item_slot),
|
||||
)
|
||||
}
|
||||
|
||||
/// Equip a bag item (`CMSG_AUTOEQUIP_ITEM`, layout in [`messages::auto_equip_item`]) — the
|
||||
/// server picks the destination slot. Success arrives as inventory-slot values deltas (and the
|
||||
/// visible-item change everyone renders); refusal as `SMSG_INVENTORY_CHANGE_FAILURE`.
|
||||
|
||||
@@ -92,6 +92,18 @@ impl WorldWriter {
|
||||
self.send(opcode::CMSG_MOVE_NOT_ACTIVE_MOVER, &body)
|
||||
}
|
||||
|
||||
/// Report that our movement clock skipped `lag_ms` (`CMSG_MOVE_TIME_SKIPPED`, layout in
|
||||
/// [`messages::move_time_skipped`]) for the named mover. The server folds the number into its
|
||||
/// own copy of our movement clock — and, when we have just boarded a transport, answers by
|
||||
/// re-sending that transport's create update (the 1.12 boarding fix; see
|
||||
/// [`opcode::CMSG_MOVE_TIME_SKIPPED`]).
|
||||
pub fn move_time_skipped(&mut self, guid: u64, lag_ms: u32) -> Result<()> {
|
||||
self.send(
|
||||
opcode::CMSG_MOVE_TIME_SKIPPED,
|
||||
&messages::move_time_skipped(guid, lag_ms),
|
||||
)
|
||||
}
|
||||
|
||||
/// Acknowledge that a server-authored spline (Charge/knockback/taxi — an `SMSG_MONSTER_MOVE`
|
||||
/// addressed to our own guid) finished: `CMSG_MOVE_SPLINE_DONE` with a `MovementInfo` at the
|
||||
/// ride's endpoint and the `spline_id` we were driven by. The server sets `SplineDonePending` for
|
||||
|
||||
@@ -229,18 +229,20 @@ fn set_faction_standing_parses_and_decodes() {
|
||||
/// opcode numbers.
|
||||
#[test]
|
||||
fn unknown_opcode_decodes_to_packet_dropped() {
|
||||
// 0x0319 = MSG_MOVE_TIME_SKIPPED — assigned in 1.12.1 but deliberately unparsed by benilla.
|
||||
let packet = messages::parse_server(0x0319, &hx("0102030405")).unwrap();
|
||||
assert!(matches!(packet, ServerPacket::Other { opcode: 0x0319 }));
|
||||
// 0x0324 = SMSG_PET_ACTION_SOUND — assigned in 1.12.1 but deliberately unparsed by benilla.
|
||||
// (The sentinel used to be 0x0319 `MSG_MOVE_TIME_SKIPPED`, which decision 1935 gave a parse
|
||||
// arm: an observed mover's skipped time has to reach its relay chain or that unit hitches.)
|
||||
let packet = messages::parse_server(0x0324, &hx("0102030405")).unwrap();
|
||||
assert!(matches!(packet, ServerPacket::Other { opcode: 0x0324 }));
|
||||
match decode(packet).as_slice() {
|
||||
[SessionEvent::PacketDropped {
|
||||
opcode: 0x0319,
|
||||
opcode: 0x0324,
|
||||
unparseable: false,
|
||||
}] => {}
|
||||
other => panic!("expected one PacketDropped event, got {other:?}"),
|
||||
}
|
||||
// The generated name table: a known opcode resolves, an unassigned number doesn't.
|
||||
assert_eq!(messages::opcode_name(0x0319), Some("MSG_MOVE_TIME_SKIPPED"));
|
||||
assert_eq!(messages::opcode_name(0x0324), Some("SMSG_PET_ACTION_SOUND"));
|
||||
assert_eq!(
|
||||
messages::opcode_name(messages::opcode::SMSG_UPDATE_OBJECT),
|
||||
Some("SMSG_UPDATE_OBJECT")
|
||||
|
||||
@@ -273,7 +273,7 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
)?;
|
||||
|
||||
// UseAction(action [, checkCursor [, onSelf]]). **`onSelf` is the self-cast modifier** — the
|
||||
// third argument `ActionBar.xml`'s `ActionButtonUp(id, onSelf)` has always forwarded and this
|
||||
// third argument stock `ActionButtonUp(id, onSelf)` forwards on its main-bar branch and this
|
||||
// host used to drop on the floor, which is why 1.12's twelve `SELFACTIONBUTTON` bindings
|
||||
// (`ALT-1`…`ALT-=`) had no home. It rides out on [`ActionUse::on_self`] and the app's cast
|
||||
// resolver reads it. `checkCursor` TRUTHY (numeric nonzero — the
|
||||
@@ -327,6 +327,21 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
})?,
|
||||
)?;
|
||||
|
||||
// `ChangeActionBarPage()` — four instructions in the reference (`0x4e7650`: `mov ecx,0xd4;
|
||||
// call FrameScript_SignalEvent; xor eax,eax; ret` — wow-re `action-bar-toggles.md` §6, `ui.md`):
|
||||
// it fires `ACTIONBAR_PAGE_CHANGED` and does NOTHING else. The page is FrameXML state
|
||||
// (`CURRENT_ACTIONBAR_PAGE`, which `ActionBar_PageUp/Down` and the SHIFT-n bindings write before
|
||||
// calling this), the buttons repaint from `ActionButton_OnEvent`, and the up-arrow's OnEvent
|
||||
// restamps the numeral. Synchronous, like the reference's `SignalEvent` (1938; the pattern is
|
||||
// `UpdateSpells` → `SPELLS_CHANGED`, 1924). Zero return values on every path.
|
||||
g.set(
|
||||
"ChangeActionBarPage",
|
||||
lua.create_function(|lua, ()| {
|
||||
super::tick::fire_event_into(lua, "ACTIONBAR_PAGE_CHANGED", Vec::new());
|
||||
Ok(())
|
||||
})?,
|
||||
)?;
|
||||
|
||||
// ── The dynamic-state read API (decision 0137 phase 4): the reference ActionButton.lua's
|
||||
// whole input surface, answered from the app-pushed per-action state. Return conventions are
|
||||
// the 1.12 API's own (1/nil booleans; IsActionInRange's nil/0/1 tri-state) — the transcribed
|
||||
@@ -465,6 +480,24 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn change_action_bar_page_fires_the_event_and_nothing_else() {
|
||||
let s = UiScript::new().unwrap();
|
||||
s.run(
|
||||
"CURRENT_ACTIONBAR_PAGE = 3 \
|
||||
local f = CreateFrame('Frame') f:RegisterEvent('ACTIONBAR_PAGE_CHANGED') \
|
||||
f:SetScript('OnEvent', function() SEEN = event .. ':' .. CURRENT_ACTIONBAR_PAGE end) \
|
||||
N = table.getn({ChangeActionBarPage()})",
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
s.eval::<String>("return SEEN").unwrap(),
|
||||
"ACTIONBAR_PAGE_CHANGED:3",
|
||||
"the event fires synchronously, and the page is whatever FrameXML wrote"
|
||||
);
|
||||
assert_eq!(s.eval::<i64>("return N").unwrap(), 0, "zero return values");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn action_snapshot_reads_and_use_queues() {
|
||||
let mut s = UiScript::new().unwrap();
|
||||
|
||||
@@ -287,6 +287,11 @@ pub struct InvSlotView {
|
||||
/// and the reference renders all 7 from it — but a 1.12 server fills only PERM and TEMP, so in
|
||||
/// practice that is what an inspect hover shows.
|
||||
pub enchants: Vec<super::EnchantView>,
|
||||
/// **The instance's remaining LIFETIME in milliseconds** — the doll twin of
|
||||
/// [`super::container::ContainerSlot::duration_ms`]. `None` = no timer. In practice an
|
||||
/// equipped duration item is rare (the holiday masks are the shipped case), but the line law
|
||||
/// is one law and the doll hover runs the same builder. Decision 1933.
|
||||
pub duration_ms: Option<u64>,
|
||||
}
|
||||
|
||||
/// The inventory-slot snapshot: index 0 = ammo, 1..=19 the equipment slots, 20..=23 the four
|
||||
|
||||
@@ -105,6 +105,13 @@ pub struct ContainerSlot {
|
||||
/// hands over the row's name plus the three facts the line law needs to place and paint it.
|
||||
/// Empty = unenchanted, or the enchant DBC never loaded. Decisions 0915/0920.
|
||||
pub enchants: Vec<super::EnchantView>,
|
||||
/// **The instance's remaining LIFETIME in milliseconds** — a duration-limited item counting
|
||||
/// down to its own destruction (a conjured stone, a holiday gift, a timed quest item). Its
|
||||
/// only feed is `SMSG_ITEM_TIME_UPDATE`, parked as an absolute deadline and recomputed on
|
||||
/// read ([`crate::items::Items::duration_remaining_display_ms`] in benilla-app), floored to
|
||||
/// the whole second so the snapshot moves once a second rather than every frame.
|
||||
/// `None` = no timer, which is nearly every item. Decision 1933.
|
||||
pub duration_ms: Option<u64>,
|
||||
/// The petition this item names, when it is a signable charter — **line 3 of the tooltip's
|
||||
/// emission law**, between the NAME and the green `ITEM_SIGNABLE` line.
|
||||
///
|
||||
@@ -263,6 +270,47 @@ impl super::UiScript {
|
||||
std::mem::take(&mut self.model_mut().container_destroys)
|
||||
}
|
||||
|
||||
/// **Arm the gift-wrap cursor** on the paper at `(bag, slot)` — the app's call when a
|
||||
/// right-click takes the reference's begin-wrap arm (`ItemInfo::begins_gift_wrap`).
|
||||
///
|
||||
/// Three acts, exactly the three `0x5edea0` performs (wow-re
|
||||
/// `object-layer/scratch/gift-wrap-law.md`): the paper's slot reads **locked**, the displayed
|
||||
/// cursor becomes **mode 2** (`Interface\Cursor\Cast.blp` — the same art the cast cursor
|
||||
/// uses, table `0x853b88` slot 2), and **nothing goes on the wire**. What completes it is the
|
||||
/// next left-click on a container slot. Decision 1934.
|
||||
pub fn arm_gift_wrap(&mut self, bag: i64, slot: u32) {
|
||||
let mut model = self.model_mut();
|
||||
model.pending_wrap = Some(PendingWrap { bag, slot });
|
||||
model.ui_cursor = Some(UiCursorMode::Cast);
|
||||
model.ui_cursor_dirty = true;
|
||||
cursor::queue_lock_changed(&mut model, bag, slot);
|
||||
}
|
||||
|
||||
/// **Cancel an armed gift wrap and nothing else** — disarm, unlock the paper, reset the
|
||||
/// cursor. Deliberately narrower than `ClearCursor`, which also drops any held payload:
|
||||
/// what wow-re records is that a right-click "uses the item and cancels the wrap", not by
|
||||
/// which of the two routes, so this touches only the half the finding actually names.
|
||||
pub fn cancel_gift_wrap(&mut self) -> Option<PendingWrap> {
|
||||
let mut model = self.model_mut();
|
||||
let wrap = model.pending_wrap.take()?;
|
||||
cursor::queue_lock_changed(&mut model, wrap.bag, wrap.slot);
|
||||
model.ui_cursor = None;
|
||||
model.ui_cursor_dirty = true;
|
||||
Some(wrap)
|
||||
}
|
||||
|
||||
/// Is a gift wrap armed, and on which slot? Read by the app to decide whether a right-click
|
||||
/// is cancelling one.
|
||||
pub fn gift_wrap_armed(&self) -> Option<PendingWrap> {
|
||||
self.model_ref().pending_wrap
|
||||
}
|
||||
|
||||
/// Drain the `(giftBag, giftSlot, itemBag, itemSlot)` quads a completed wrap queued — the app
|
||||
/// resolves each pair to the wire's bag/slot addressing and sends `CMSG_WRAP_ITEM`.
|
||||
pub fn take_container_wraps(&mut self) -> Vec<(i64, u32, i64, u32)> {
|
||||
std::mem::take(&mut self.model_mut().container_wraps)
|
||||
}
|
||||
|
||||
/// The mode the last FrameXML cursor call armed — `None` after a `ResetCursor`. This is the
|
||||
/// *value* of the most recent write; whether a write happened at all is
|
||||
/// [`Self::take_cursor_write`], and that is what the app acts on (decision 1061).
|
||||
@@ -285,6 +333,19 @@ impl super::UiScript {
|
||||
}
|
||||
}
|
||||
|
||||
/// **The armed gift wrap** — which piece of wrapping paper the next container click will spend.
|
||||
///
|
||||
/// The reference holds this as globals beside the displayed-cursor mode rather than as a cursor
|
||||
/// *payload*; the distinction is load-bearing and is why this is its own type instead of a
|
||||
/// [`cursor::CursorPayload`] variant (see [`super::Model::pending_wrap`]).
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
||||
pub struct PendingWrap {
|
||||
/// The paper's container id, in the same bag space every cursor verb speaks.
|
||||
pub bag: i64,
|
||||
/// The paper's 1-based slot within that container.
|
||||
pub slot: u32,
|
||||
}
|
||||
|
||||
/// A **displayed-cursor override** the FrameXML cursor family arms (wow-re cursor-system.md §7): the
|
||||
/// single "displayed mode" (`0xbe2c2c`) the real client swaps to while a UI element wants a non-base
|
||||
/// cursor, restored to the base mode by `ResetCursor`. The app maps each to the matching
|
||||
@@ -330,6 +391,49 @@ pub enum UiCursorMode {
|
||||
///
|
||||
/// Returns whether the caller should repaint (the source-slot lock or the held payload changed).
|
||||
fn pickup_container_item(model: &mut super::Model, bag: i64, slot: u32) -> bool {
|
||||
// **The gift-wrap completion, ahead of the whole gesture** (decision 1934). `0x4f9b30` is the
|
||||
// *only* entry to the wrap sender `0x5edfc0` image-wide — which is why the paper doll cannot
|
||||
// finish a wrap and an equipped item is therefore unwrappable — and it takes this branch
|
||||
// before it looks at the cursor at all.
|
||||
if let Some(wrap) = model.pending_wrap {
|
||||
// An empty or unresolved slot **bails and leaves the wrap armed** — the reference's own
|
||||
// edge. Nothing is spent and the cursor keeps its mode, so the next click can still land
|
||||
// on a real item.
|
||||
if !model
|
||||
.containers
|
||||
.get(&bag)
|
||||
.and_then(|c| c.slots.get(&slot))
|
||||
.is_some_and(|s| s.item_id != 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
// **The paper has to still be there.** `0x5edfef` bails when the stashed gift GUID no
|
||||
// longer resolves, and — the edge worth transcribing — it then sends *and clears*
|
||||
// nothing: the wrap stays armed and the paper's slot stays locked. So this returns
|
||||
// without touching either.
|
||||
if !model
|
||||
.containers
|
||||
.get(&wrap.bag)
|
||||
.and_then(|c| c.slots.get(&wrap.slot))
|
||||
.is_some_and(|s| s.item_id != 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
// No eligibility test of any kind. The wrap arm reads each item's `ITEM_FIELD_CONTAINED`
|
||||
// and nothing else — not flags, not stack count, not bind state — and the client ships six
|
||||
// `ERR_CANT_WRAP_*` strings for the server to answer with (`SMSG_INVENTORY_CHANGE_FAILURE`
|
||||
// reasons 43-48). Same law as the openable click (decision 0896): gating locally would eat
|
||||
// the click in silence and cost the player their error line.
|
||||
model.pending_wrap = None;
|
||||
model.container_wraps.push((wrap.bag, wrap.slot, bag, slot));
|
||||
// The paper unlocks and the cursor resets **immediately**, not on the server's answer:
|
||||
// `0x5edfc0` calls `0x5eded0` at `0x5ee0aa`, right after the send. The target is never
|
||||
// locked at all.
|
||||
cursor::queue_lock_changed(model, wrap.bag, wrap.slot);
|
||||
model.ui_cursor = None;
|
||||
model.ui_cursor_dirty = true;
|
||||
return true;
|
||||
}
|
||||
match model.cursor.take() {
|
||||
None => {
|
||||
let picked = model
|
||||
@@ -653,7 +757,14 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
let held = matches!(
|
||||
&model.cursor,
|
||||
Some(CursorPayload::Item(c)) if c.bag == bag && c.slot == slot
|
||||
);
|
||||
) ||
|
||||
// An armed gift wrap locks its paper for as long as it stands (`0x4953e0` at
|
||||
// `0x5edeb5`) — the same dimming a held item's source slot shows, and released by
|
||||
// the send or by any cancel rather than by the server (decision 1934).
|
||||
matches!(
|
||||
&model.pending_wrap,
|
||||
Some(w) if w.bag == bag && w.slot == slot
|
||||
);
|
||||
(
|
||||
model
|
||||
.containers
|
||||
@@ -858,7 +969,7 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{ContainerMove, ContainerSlot, ContainerState};
|
||||
use super::{ContainerMove, ContainerSlot, ContainerState, PendingWrap, UiCursorMode};
|
||||
use crate::script::UiScript;
|
||||
|
||||
fn backpack() -> ContainerState {
|
||||
@@ -866,6 +977,7 @@ mod tests {
|
||||
slots.insert(
|
||||
1,
|
||||
ContainerSlot {
|
||||
duration_ms: None,
|
||||
petition: None,
|
||||
already_bound: false,
|
||||
bar_placeable: true,
|
||||
@@ -903,6 +1015,71 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
/// **The gift-wrap state machine** (decision 1934), all four of its edges. Armed by the app's
|
||||
/// right-click on a wrapper, the arm locks the paper and paints cursor mode 2 and sends
|
||||
/// nothing; the next LEFT-click on a container slot spends it; an empty slot bails and leaves
|
||||
/// it armed; and any `ClearCursor` cancels it.
|
||||
#[test]
|
||||
fn an_armed_gift_wrap_spends_on_the_next_container_click() {
|
||||
let mut s = UiScript::new().unwrap();
|
||||
s.set_container(0, Some(backpack()));
|
||||
// The arm: nothing on the wire, the paper reads locked, the cursor is mode 2, and the
|
||||
// held-item predicate stays FALSE — this is a cursor mode, not a payload.
|
||||
s.arm_gift_wrap(0, 1);
|
||||
assert_eq!(s.gift_wrap_armed(), Some(PendingWrap { bag: 0, slot: 1 }));
|
||||
assert!(s.take_container_wraps().is_empty(), "arming sends nothing");
|
||||
assert_eq!(s.take_cursor_write(), Some(Some(UiCursorMode::Cast)));
|
||||
assert!(
|
||||
s.eval::<bool>("local _, _, locked = GetContainerItemInfo(0, 1) return locked")
|
||||
.unwrap(),
|
||||
"the paper dims while the wrap stands"
|
||||
);
|
||||
assert!(
|
||||
!s.eval::<bool>("return CursorHasItem()").unwrap(),
|
||||
"the wrap writes no payload, so every stock CursorHasItem() gate stays shut"
|
||||
);
|
||||
// A click on an EMPTY slot bails and leaves the wrap armed — the reference's own edge.
|
||||
s.eval::<()>("PickupContainerItem(0, 9)").unwrap();
|
||||
assert_eq!(s.gift_wrap_armed(), Some(PendingWrap { bag: 0, slot: 1 }));
|
||||
assert!(s.take_container_wraps().is_empty());
|
||||
// A click on a real item spends it: the paper's pair leads, as the wire does.
|
||||
s.eval::<()>("PickupContainerItem(0, 4)").unwrap();
|
||||
assert_eq!(s.take_container_wraps(), vec![(0, 1, 0, 4)]);
|
||||
assert_eq!(s.gift_wrap_armed(), None);
|
||||
assert_eq!(
|
||||
s.take_cursor_write(),
|
||||
Some(None),
|
||||
"the cursor resets on the send, not on the server's answer"
|
||||
);
|
||||
assert!(
|
||||
s.cursor_payload().is_none(),
|
||||
"the completing click is spent on the wrap, not on picking the target up"
|
||||
);
|
||||
}
|
||||
|
||||
/// Any `ClearCursor` cancels an armed wrap — the reference tests it FIRST inside
|
||||
/// `ClearCursor 0x495190` and does so ungated by either parameter, so all 70 of its call
|
||||
/// sites cancel (decision 1934).
|
||||
#[test]
|
||||
fn clear_cursor_cancels_an_armed_gift_wrap() {
|
||||
let mut s = UiScript::new().unwrap();
|
||||
s.set_container(0, Some(backpack()));
|
||||
s.arm_gift_wrap(0, 1);
|
||||
let _ = s.take_cursor_write();
|
||||
s.eval::<()>("ClearCursor()").unwrap();
|
||||
assert_eq!(s.gift_wrap_armed(), None);
|
||||
assert_eq!(s.take_cursor_write(), Some(None), "and the mode goes back");
|
||||
assert!(
|
||||
!s.eval::<bool>("local _, _, locked = GetContainerItemInfo(0, 1) return locked")
|
||||
.unwrap(),
|
||||
"the paper unlocks with the cancel — the lock never waited on a server"
|
||||
);
|
||||
// …and the next container click is an ordinary pickup again.
|
||||
s.eval::<()>("PickupContainerItem(0, 1)").unwrap();
|
||||
assert!(s.take_container_wraps().is_empty());
|
||||
assert!(s.cursor_payload().is_some(), "a plain pickup, as before");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn container_snapshot_reads() {
|
||||
let mut s = UiScript::new().unwrap();
|
||||
@@ -1032,6 +1209,7 @@ mod tests {
|
||||
state.slots.insert(
|
||||
5,
|
||||
ContainerSlot {
|
||||
duration_ms: None,
|
||||
petition: None,
|
||||
already_bound: false,
|
||||
bar_placeable: true,
|
||||
|
||||
@@ -321,6 +321,16 @@ pub(super) fn item_link_name(link: Option<&str>) -> String {
|
||||
/// item payload also un-locks its source slot; an already-empty cursor is not a transition (no
|
||||
/// event).
|
||||
pub(crate) fn clear_cursor(model: &mut Model) {
|
||||
// **An armed gift wrap dies with any cancel** (decision 1934), and this is the FIRST thing
|
||||
// the reference's `ClearCursor 0x495190` does — `0x5edf10` is its opening act and it is
|
||||
// **ungated by either parameter**, including `ClearCursor(0)`, the flavour that deliberately
|
||||
// keeps an ordinary held item's lock. So all 70 of its call sites cancel a wrap. The paper
|
||||
// unlocks and the cursor mode goes back to the base.
|
||||
if let Some(wrap) = model.pending_wrap.take() {
|
||||
queue_lock_changed(model, wrap.bag, wrap.slot);
|
||||
model.ui_cursor = None;
|
||||
model.ui_cursor_dirty = true;
|
||||
}
|
||||
match model.cursor.take() {
|
||||
Some(CursorPayload::Item(item)) => {
|
||||
queue_cursor_update(model);
|
||||
@@ -741,6 +751,7 @@ mod tests {
|
||||
slots.insert(
|
||||
1,
|
||||
crate::script::container::ContainerSlot {
|
||||
duration_ms: None,
|
||||
petition: None,
|
||||
already_bound: false,
|
||||
bar_placeable: true,
|
||||
|
||||
@@ -286,6 +286,7 @@ mod tests {
|
||||
fn doll_slots() -> crate::script::InventorySlots {
|
||||
let mut slots: crate::script::InventorySlots = Default::default();
|
||||
slots[1] = Some(InvSlotView {
|
||||
duration_ms: None,
|
||||
already_bound: false,
|
||||
bar_placeable: true,
|
||||
durability: None,
|
||||
@@ -303,6 +304,7 @@ mod tests {
|
||||
enchants: Vec::new(),
|
||||
});
|
||||
slots[11] = Some(InvSlotView {
|
||||
duration_ms: None,
|
||||
already_bound: false,
|
||||
bar_placeable: true,
|
||||
durability: None,
|
||||
@@ -320,6 +322,7 @@ mod tests {
|
||||
enchants: Vec::new(),
|
||||
});
|
||||
slots[19] = Some(InvSlotView {
|
||||
duration_ms: None,
|
||||
already_bound: false,
|
||||
bar_placeable: true,
|
||||
durability: None,
|
||||
@@ -344,6 +347,7 @@ mod tests {
|
||||
slots.insert(
|
||||
1,
|
||||
ContainerSlot {
|
||||
duration_ms: None,
|
||||
petition: None,
|
||||
already_bound: false,
|
||||
bar_placeable: true,
|
||||
@@ -487,6 +491,7 @@ mod tests {
|
||||
});
|
||||
// An equipped bag in Bag0Slot (id 20): its equip_slots is the four bag slots.
|
||||
slots[20] = Some(InvSlotView {
|
||||
duration_ms: None,
|
||||
already_bound: false,
|
||||
bar_placeable: true,
|
||||
durability: None,
|
||||
@@ -526,6 +531,7 @@ mod tests {
|
||||
slots.insert(
|
||||
3,
|
||||
ContainerSlot {
|
||||
duration_ms: None,
|
||||
petition: None,
|
||||
already_bound: false,
|
||||
bar_placeable: true,
|
||||
|
||||
@@ -167,8 +167,8 @@ pub use char_stats::{
|
||||
pub use chat_send::ChatSend;
|
||||
pub use chat_window::ChatWindowLook;
|
||||
pub use container::{
|
||||
BagAutoStore, ContainerMove, ContainerSlot, ContainerState, EnchantView, PetitionSlotView,
|
||||
RandomPropertyView, UiCursorMode,
|
||||
BagAutoStore, ContainerMove, ContainerSlot, ContainerState, EnchantView, PendingWrap,
|
||||
PetitionSlotView, RandomPropertyView, UiCursorMode,
|
||||
};
|
||||
pub use craft::{CraftReagent, CraftRecipe, CraftState, CraftTooltip};
|
||||
pub use cursor::{
|
||||
|
||||
@@ -862,6 +862,18 @@ pub(crate) struct Model {
|
||||
/// stack) — the popup-confirmed destroy (decision 0216 §3), drained by the app into
|
||||
/// `CMSG_DESTROYITEM`.
|
||||
pub(crate) container_destroys: Vec<(i64, u32, u32)>,
|
||||
/// **The armed gift wrap** — the `(bag, slot)` of a piece of wrapping paper whose right-click
|
||||
/// took the reference's begin-wrap arm (`0x5edea0`: lock the paper, cursor mode 2, no packet).
|
||||
/// `None` = nothing armed, which is nearly always. Decision 1934.
|
||||
///
|
||||
/// Deliberately **not** a [`cursor::CursorPayload`]: the reference writes no payload global at
|
||||
/// all here, only the displayed-cursor mode, so `CursorHasItem()` stays nil and none of the
|
||||
/// thirteen stock `CursorHasItem()` gates open. Making it a payload would silently open every
|
||||
/// one of them — the same trap decision 1677's mode-5 note names for the vendor cursor.
|
||||
pub(crate) pending_wrap: Option<container::PendingWrap>,
|
||||
/// `(giftBag, giftSlot, itemBag, itemSlot)` quads a completed wrap queued, drained by the app
|
||||
/// into `CMSG_WRAP_ITEM`. The paper's pair leads, as the wire does.
|
||||
pub(crate) container_wraps: Vec<(i64, u32, i64, u32)>,
|
||||
/// The Lua-set **displayed-cursor override** (`ShowContainerSellCursor`/`ShowMerchantSellCursor`/
|
||||
/// `ShowBuybackSellCursor`/`ShowInspectCursor` set it, `ResetCursor` clears it; [`container`] +
|
||||
/// [`merchant`]) — the single "displayed mode" the real client keeps at `0xbe2c2c`, restored to
|
||||
@@ -1704,6 +1716,8 @@ impl Model {
|
||||
item_picks: Vec::new(),
|
||||
enchant_confirms: Vec::new(),
|
||||
container_destroys: Vec::new(),
|
||||
pending_wrap: None,
|
||||
container_wraps: Vec::new(),
|
||||
ui_cursor: None,
|
||||
ui_cursor_dirty: false,
|
||||
container_autoequips: Vec::new(),
|
||||
|
||||
@@ -51,9 +51,9 @@ pub(super) fn install(lua: &Lua, m: &Table) -> mlua::Result<()> {
|
||||
// call in FrameXML and in the addon corpus, and it only bites where a region already carries
|
||||
// alpha < 1 — where it would surface as art going permanently translucent, which is a LOOK and
|
||||
// therefore the director's call, not a quiet correction to fold into an unrelated slice. The
|
||||
// known live case is the action bar's grid ring, and `ActionBar.xml`'s
|
||||
// BenillaActionButton_SetRing is already written to be correct under BOTH readings (it passes
|
||||
// the fourth argument explicitly), so closing this cannot silently change that file.
|
||||
// known live case is the action bar's grid ring: stock `ActionButton_ShowGrid` passes the
|
||||
// fourth argument explicitly (`SetVertexColor(1.0, 1.0, 1.0, 0.5)`), so closing this cannot
|
||||
// silently change that file.
|
||||
m.set(
|
||||
"SetVertexColor",
|
||||
lua.create_function(
|
||||
|
||||
@@ -64,6 +64,7 @@ fn shift_compare_fires_seats_and_renders_the_compare_shape() {
|
||||
slots.insert(
|
||||
1,
|
||||
ContainerSlot {
|
||||
duration_ms: None,
|
||||
petition: None,
|
||||
already_bound: false,
|
||||
bar_placeable: true,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user