the bank is the reference's own window, and far sight freezes your body
- the bank window is the reference's own `BankFrame.xml` run off the player's chain — our 890-line copy is deleted whole; putting bags into the bank's bag slots works (the two verbs its buttons are wired to now exist), the purse is a real `SmallMoneyFrameTemplate` splitting 987654321 copper into three coin buttons instead of one string, `BANKFRAME_OPENED` loses the argument the real client never fires, and the bank stops auto-opening your bags while the vendor and mailbox keep doing it; the empty-slot click still plays the bag sound, because the reference's own line plays it on every click the cursor did not consume — named as faithful rather than quietly fixed - underneath it, the bank's inventory band is a VIEW over the container feed, not a second copy — inventory ids 40..69 had answered nil because the doll array stops at 24, so the stock file swapped in would have drawn an empty bank; the six bank bag slots become real inventory slots (64..=69) fed off the player descriptor, the three bag verbs are carved from the binary (`PutItemInBackpack` IS `PutItemInBag(0xFF)` through a ten-byte thunk, the occupied-by-an-ordinary-item leg auto-stores with no destination slot at all, and the return is the Lua number 1 or nil — the nil being the whole reason a plain left-click opens a bag), and the swap caught `BankButtonIDToInvSlotID`'s bag arm six slots wrong: it took a bag number 1..6 where the reference passes the button's own id, which is the container id - far sight freezes your own body while the view is out — the fifth conjunct of the movement predicates' shared precondition, joining the health term so the walk, the turn and the follow teardown go down together; both halves are load-bearing: the `IsActivePlayer` half is what keeps a Mind Control victim walking (possession rides the same PLAYER_FARSIGHT field), and the latch half is the RESOLVED engage rather than the raw field — and the camera is untouched, you still look around freely, which is the whole point of the spell - the reset offer no longer outlives the character that earned it — log out after walking out of the Deadmines and your next character was offered a reset the first one had earned — and the one lockout leg no probe can ever reach (vmangos writes a last-instance map only from raid binds, which the client's handler discards as not-a-party-dungeon) is now covered end to end through the real system, its module doc saying plainly: tested and never observed Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,889 +0,0 @@
|
||||
<Ui>
|
||||
<!--
|
||||
benilla's bank window (decision 0604 phase 4): the vault is already streamed (the 24 bank
|
||||
slots + 6 bank bags ride the descriptor at login, exactly like the backpack — see the
|
||||
decision's "pinned mechanism"), so this file is purely the window over the existing
|
||||
container/cursor plumbing. Geometry/art below are QUOTED from the real `BankFrame.xml`
|
||||
(`WoW/_extracted_framexml/BankFrame.xml`/`.lua`, read this session — line-cited inline),
|
||||
turned onto OUR house idioms per decision 0068/0084: no `ItemButtonTemplate`/
|
||||
`SmallMoneyFrameTemplate`/`UIPanelButtonTemplate` ship, so every one of those is either a
|
||||
cross-file `inherits` of a benilla template that already reconstructs the real art
|
||||
(`BenillaBagSlotTemplate`, now in this file — see the kit banner below) or a local redraw
|
||||
(the Purchase button, `UI-Panel-Button-*`, the house convention every NPC window already
|
||||
uses — TrainerFrame.xml/QuestFrame.xml &c.).
|
||||
|
||||
MAXIMAL REUSE, per the phase-4 brief: the 24 generic slots (container `-1`) are 24
|
||||
`BenillaBagSlotTemplate` buttons — the SAME click/drag/tooltip/stack-split/lock/cooldown
|
||||
chain the bag windows use, just parented under a frame carrying `id="-1"` (this window
|
||||
itself) so `button:GetParent():GetID()` resolves to `BANK_CONTAINER`.
|
||||
|
||||
THE SIX BANK-BAG WINDOWS ARE NO LONGER HERE (decision 1751). They were
|
||||
`BenillaBankBagFrame1..6`, six copies of `BagFrame.xml`'s window template, and the reason
|
||||
given at the time was that "this engine has no `ToggleBag`/`CloseBag` globals (grepped;
|
||||
none exist)". It has them now — the reference's own, off the player's chain — and the bank
|
||||
bags are what decision 0604 already said they were in the real client: **ordinary
|
||||
ContainerFrames at ids NUM_BAG_SLOTS+1 .. +NUM_BANKBAGSLOTS**. So the 601 lines that copied
|
||||
a window six times are gone and the bag buttons call `ToggleBag`, exactly as
|
||||
`BankFrameItemButtonBag_OnClick` does. What this file still owes the reference's file is
|
||||
`NUM_BAG_SLOTS` and `UpdateBagButtonHighlight`, which live where BankFrame.lua puts them.
|
||||
|
||||
DEVIATIONS from the raw reference quote (each load-bearing, none silent):
|
||||
- The 24 generic slots reuse `BenillaBagSlot_Update`'s `physIndex` chain-resolution
|
||||
(`slot = size - physIndex + 1`) rather than a bespoke direct mapping, so slot i's
|
||||
button is loaded with `BenillaBagSlot_OnLoad(self, 25 - i)` — container `-1` is a FIXED
|
||||
24 slots (never resized, decision 0604), so this reversal is a stable constant, not a
|
||||
live recompute; it buys the exact same click/drag/split/lock/cooldown chain the bag
|
||||
windows get for free, at the cost of the physIndex arithmetic reading backwards from
|
||||
the reference's direct `id=i` (the reference's `BankFrameItem<i>` has no chain at all
|
||||
since a real ContainerFrame's `id~=0` slot count is genuinely fixed at generation time —
|
||||
our shared function always resolves dynamically, so the reversal is what makes button i
|
||||
show slot i).
|
||||
- The Purchase confirm popup (`CONFIRM_BUY_BANK_SLOT`) shows TEXT ONLY, no embedded cost —
|
||||
the house's `StaticPopup` engine (`UiPanels.xml`) has no money-frame variant (the
|
||||
`hasMoneyFrame` field the reference dialog carries is inert), the same accepted gap
|
||||
`MailFrame.xml`'s `SEND_MONEY`/`DELETE_MONEY` popups already live with (that file's own
|
||||
header note). The row's own Cost: coins (visible before you ever open the popup) carry
|
||||
the affordability information instead.
|
||||
- The close button is the house's `UI-Panel-MinimizeButton` at TOPRIGHT(-30,-8) — the
|
||||
Merchant/Trainer/Mail convention — rather than the reference's CENTER-relative offset
|
||||
against ITS OWN `UI-BankFrame` art's button recess (which our re-skin doesn't reproduce
|
||||
pixel-for-pixel).
|
||||
- ESC / `CloseAllWindows` closes the MAIN bank window (registered in `UiPanels.xml`'s
|
||||
`UIPanelWindows`, which drives the whole chain generically) and its `OnHide` explicitly
|
||||
closes every open bank-bag popout with it — but a popout is NOT independently in
|
||||
`CloseAllWindows`'s hardcoded bag list (`UiPanels.xml`), so it would survive an ESC
|
||||
press that DIDN'T close the bank frame itself (impossible today: nothing else occupies
|
||||
the left slot while the bank is open without closing it first, so this is a latent gap,
|
||||
not a reachable bug).
|
||||
|
||||
Money: reuses `BenillaMoney_Set`/`_SetColor` (`MerchantFrame.xml`, loaded first — the
|
||||
established cross-window reuse `TrainerFrame.xml`/`BagFrame.xml` already lean on).
|
||||
-->
|
||||
<Script><![CDATA[
|
||||
-- NUM_BAG_SLOTS is this file's in the reference too (BankFrame.lua:2), and it is read from
|
||||
-- outside it: the sourced `ContainerFrame.lua` (1751) branches on it in
|
||||
-- `ContainerFrame_OnShow` to tell an equipped bag from a bank bag, and `updateContainer
|
||||
-- FrameAnchors` measures against `BankFrame`. Lua resolves at CALL time, so this file
|
||||
-- sitting far below ContainerFrame.xml in the manifest is fine — but the ownership is the
|
||||
-- reference's, not a convenience.
|
||||
NUM_BAG_SLOTS = 4 -- (ref BankFrame.lua:2)
|
||||
NUM_BANKGENERIC_SLOTS = 24 -- (ref BankFrame.lua:3)
|
||||
NUM_BANKBAGSLOTS = 6 -- (ref BankFrame.lua:4)
|
||||
|
||||
-- ══ THE CONTAINER-SLOT KIT — moved here from BagFrame.xml with decision 1751's bag swap ══
|
||||
--
|
||||
-- `BenillaBagSlotTemplate` and the `BenillaBagSlot_*` bodies below were the bag WINDOWS'
|
||||
-- slot machinery, and this window borrowed them for its 24 vault slots. The bag windows are
|
||||
-- now the reference's own `ContainerFrame1..12`, executed off the player's chain (1751), and
|
||||
-- their slots are the reference's `ContainerFrameItemButtonTemplate`. That leaves this
|
||||
-- window as the kit's ONLY consumer, so the kit lives here — where the next migration
|
||||
-- (BankFrame's own stock files, whose `BankItemButtonTemplate`/`BankFrameItemButton_*`
|
||||
-- replace all of it) deletes it in one piece.
|
||||
--
|
||||
-- Names unchanged on purpose: 144 `inherits=` sites and ~145 `<OnLoad>` calls in this file
|
||||
-- write them, and a rename would be churn on code whose whole future is deletion.
|
||||
--
|
||||
-- Fully generic over `button:GetParent():GetID()`, which is why the move is a move and not
|
||||
-- a rewrite — this window carries `id="-1"` (BANK_CONTAINER) and its buttons resolve their
|
||||
-- container from it exactly as a bag window's did from theirs.
|
||||
|
||||
|
||||
-- BenillaBagSlot_OnLoad(button, physIndex): physIndex is the button's CHAIN position (1 =
|
||||
-- bottom-right, growing up-and-left — the anchor order below), not the game slot. The ref's
|
||||
-- own formula (index = size - j + 1, ContainerFrame.lua:426/446) maps chain position → game
|
||||
-- slot dynamically in BenillaBagSlot_Update, so slot 1 always reads top-left regardless of
|
||||
-- how many of the window's slots are actually occupied — recomputed every Update (the ref
|
||||
-- only computes it once, at window generation; recomputing is a strict robustness win for
|
||||
-- the rare live bag-swap case, not a behavior change for a size that never moves).
|
||||
function BenillaBagSlot_OnLoad(button, physIndex)
|
||||
button.physIndex = physIndex
|
||||
button.icon = getglobal(button:GetName() .. "Icon")
|
||||
button.count = getglobal(button:GetName() .. "Count")
|
||||
-- Both buttons must reach OnClick (the default set is LeftButtonUp only): LEFT-click is the
|
||||
-- pickup gesture (PickupContainerItem — pick up / place / swap on the cursor); RIGHT-click
|
||||
-- cancels a held cursor, else uses the slot (the C side sells it when a merchant is open —
|
||||
-- our container drain forks).
|
||||
button:RegisterForClicks("LeftButtonUp", "RightButtonUp")
|
||||
-- The drag trio (decision 0216 §3): click-carry and drag are ONE code path in the
|
||||
-- reference (ContainerFrame.xml:40-55 routes OnDragStart/OnReceiveDrag to the same
|
||||
-- OnClick("LeftButton", ignoreModifiers=1) the plain click uses) — see
|
||||
-- BenillaBagSlot_OnClick below for the identical routing (its own `ignoreModifiers`
|
||||
-- suppresses the shift-split fork, the reference's own contract for drag/receive).
|
||||
button:RegisterForDrag("LeftButton")
|
||||
BenillaBagSlot_Update(button)
|
||||
end
|
||||
|
||||
function BenillaBagSlot_OnClick(button, mouseButton, ignoreModifiers)
|
||||
if button.slot == nil then return end -- a hidden button (physIndex past the bag's size)
|
||||
local bagId = button:GetParent():GetID()
|
||||
if mouseButton == "LeftButton" then
|
||||
-- The reference's own five returns (ContainerFrame.lua:241) — decision 1199
|
||||
-- put this binding back on its 1.12 shape.
|
||||
local texture, itemCount, locked, quality, readable =
|
||||
GetContainerItemInfo(bagId, button.slot)
|
||||
-- The modifier fork, the reference's own order (ContainerFrame.lua:563-579;
|
||||
-- BankFrame.lua:141-152 for a bank slot, which is this same handler at container
|
||||
-- -1). `ignoreModifiers` (the drag-trio calls, both true) suppresses it whole —
|
||||
-- the reference's drag/receive contract.
|
||||
--
|
||||
-- CTRL: preview the item on your own character in the dressing room (decision 1060).
|
||||
if IsControlKeyDown() and not ignoreModifiers then
|
||||
DressUpItemLink(GetContainerItemLink(bagId, button.slot))
|
||||
return
|
||||
end
|
||||
-- SHIFT: post the item's link into the chat edit box if it's open, ELSE split the
|
||||
-- stack. The ref's own `else` (l.567-577) — and note it gates on `IsShown()` here
|
||||
-- where its siblings use `IsVisible()`; ours are the same thing (the edit box's only
|
||||
-- parent is UIParent), so the ref's own word is quoted at each site.
|
||||
if IsShiftKeyDown() and not ignoreModifiers then
|
||||
-- The visibility test is the REF'S fork, not the helper's: with chat open this
|
||||
-- arm never splits, even in the one benilla state where nothing can be posted
|
||||
-- (an unresolved template — `BenillaChatEdit_InsertLink`'s own note).
|
||||
if ChatFrameEditBox and ChatFrameEditBox:IsShown() then
|
||||
BenillaChatEdit_InsertLink(GetContainerItemLink(bagId, button.slot))
|
||||
return
|
||||
end
|
||||
if texture ~= nil and not locked and (itemCount or 1) >= 2 then
|
||||
button.SplitStack = function(btn, n)
|
||||
SplitContainerItem(bagId, btn.slot, n)
|
||||
BenillaBankFrame_RepaintItems()
|
||||
end
|
||||
-- Ref ContainerFrameItemButton_OnLoad/_OnClick (ContainerFrame.lua:558-560,
|
||||
-- 573-576): the owner's own SplitStack closure carries the bag/slot so
|
||||
-- StackSplitFrameOkay_Click's generic `owner.SplitStack(owner, split)` call
|
||||
-- needs no window-specific knowledge. SplitContainerItem lives in cursor.rs
|
||||
-- rather than container.rs — it is the one container-named verb whose
|
||||
-- payload is the cursor's, not a bag's — but like every container verb it is
|
||||
-- a plain top-level global (decision 1198).
|
||||
OpenStackSplitFrame(itemCount, button, "BOTTOMRIGHT", "TOPRIGHT")
|
||||
end
|
||||
return
|
||||
end
|
||||
-- A plain click hides any open split frame (ref ContainerFrame.lua:581) before the
|
||||
-- ordinary pickup/place proceeds.
|
||||
local split = getglobal("StackSplitFrame")
|
||||
if split then split:Hide() end
|
||||
-- PickupContainerItem is the single drag entry point (real client): an empty cursor
|
||||
-- picks this slot up; a held cursor places/swaps into it (same slot = cancel). Hide the
|
||||
-- hover tooltip (the item just left the slot for the cursor) and repaint so the picked
|
||||
-- slot dims / un-dims immediately — the swap itself settles when the server answers.
|
||||
PickupContainerItem(bagId, button.slot)
|
||||
GameTooltip:Hide()
|
||||
BenillaBankFrame_RepaintItems()
|
||||
elseif mouseButton == "RightButton" then
|
||||
-- A held cursor cancels (client-side, nothing sent); otherwise use/equip/sell the slot.
|
||||
if CursorHasItem() then
|
||||
ClearCursor()
|
||||
BenillaBankFrame_RepaintItems()
|
||||
else
|
||||
UseContainerItem(bagId, button.slot)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Hover: the era-true GameTooltip:SetBagItem(bag, slot) — the ENGINE renders the full
|
||||
-- item tooltip from the template store and owns the sell-price money law (real instance
|
||||
-- + merchant open + repair off ⇒ OnTooltipAddMoney(SellPrice × stack), or the
|
||||
-- ITEM_UNSELLABLE line — wow-re ui/scratch/tooltip-money.md's byte-verified gate,
|
||||
-- decision 0274 P1). The durability-adjusted damaged price is deferred until durability
|
||||
-- damage reaches the feed (a pristine item reduces to SellPrice × stack, the case we
|
||||
-- ship). Nothing shows while the template is in flight — the OnUpdate re-enter fills it.
|
||||
function BenillaBagSlot_OnEnter(button)
|
||||
if button.slot == nil then return end
|
||||
local bagId = button:GetParent():GetID()
|
||||
local texture, itemCount, locked, quality, readable =
|
||||
GetContainerItemInfo(bagId, button.slot)
|
||||
-- An emptied slot drops its tooltip (ref ContainerFrame_Update's texture-nil branch,
|
||||
-- ContainerFrame.lua:281-283) — without this the OnUpdate re-enter below would leave
|
||||
-- a stale tooltip up after the item is sold or moved out from under the hover.
|
||||
if not texture then
|
||||
if GameTooltip:IsOwned(button) then GameTooltip:Hide() end
|
||||
return
|
||||
end
|
||||
-- Ref ContainerFrameItemButton_OnEnter (ContainerFrame.lua:602-612) verbatim: a slot
|
||||
-- in the RIGHT half of the screen hangs its tooltip LEFT, so it never runs off the
|
||||
-- edge (the bag lives at the bottom-right — this is the ref's own screen-edge answer,
|
||||
-- a side-pick, not a clamp).
|
||||
if (button:GetRight() or 0) >= (GetScreenWidth() / 2) then
|
||||
GameTooltip:SetOwner(button, "ANCHOR_LEFT")
|
||||
else
|
||||
GameTooltip:SetOwner(button, "ANCHOR_RIGHT")
|
||||
end
|
||||
GameTooltip:SetBagItem(bagId, button.slot)
|
||||
-- The pouch/magnifier cursor (ref ContainerFrameItemButton_OnEnter tail,
|
||||
-- ContainerFrame.lua l.636-642): merchant open on the MERCHANT tab and not repair
|
||||
-- mode → the sell cursor over this slot; else a readable item (a mail letter copy —
|
||||
-- right-click opens the reader) → the Inspect magnifier; anything else restores the
|
||||
-- base cursor. The OnUpdate re-enter keeps it current while hovering (the ref's own
|
||||
-- loop). The ref's `IsControlKeyDown() and hasItem` magnifier arm (l.638) is the
|
||||
-- dress-up preview's own cursor tell — it landed with the dressing room (decision
|
||||
-- 1060); `info` is non-nil above, which is the ref's `hasItem`.
|
||||
if MerchantFrame and MerchantFrame:IsShown()
|
||||
and MerchantFrame.selectedTab == 1 and not InRepairMode() then
|
||||
ShowContainerSellCursor(bagId, button.slot)
|
||||
elseif readable or IsControlKeyDown() then
|
||||
ShowInspectCursor()
|
||||
else
|
||||
ResetCursor()
|
||||
end
|
||||
GameTooltip:Show()
|
||||
end
|
||||
|
||||
function BenillaBagSlot_OnLeave(button)
|
||||
GameTooltip:Hide()
|
||||
-- Ref ItemButton OnLeave (ContainerFrame.xml): the hover's sell cursor drops with it.
|
||||
ResetCursor()
|
||||
end
|
||||
|
||||
-- Ref ContainerFrameItemButton_OnUpdate (ContainerFrame.lua:645-660): while the tooltip
|
||||
-- is ours, re-run OnEnter every frame — the ref ships this loop UNTHROTTLED (its
|
||||
-- updateTooltip throttle is commented out in 1.12). It is the real client's own refresh
|
||||
-- mechanism: a tooltip set while the item template was still in flight fills itself in
|
||||
-- the moment the answer lands, no re-hover.
|
||||
function BenillaBagSlot_OnUpdate(button)
|
||||
if GameTooltip:IsOwned(button) then
|
||||
BenillaBagSlot_OnEnter(button)
|
||||
end
|
||||
end
|
||||
|
||||
-- The item's use-cooldown sweep (ref ContainerFrame_UpdateCooldown,
|
||||
-- ContainerFrame.lua:295-299, verbatim shape over our container dialect).
|
||||
function BenillaBagSlot_UpdateCooldown(button, bagId)
|
||||
local cooldown = getglobal(button:GetName() .. "Cooldown")
|
||||
local start, duration, enable = GetContainerItemCooldown(bagId, button.slot)
|
||||
CooldownFrame_SetTimer(cooldown, start, duration, enable)
|
||||
end
|
||||
|
||||
-- The physIndex → game-slot resolution (the module doc above) + the paint (ref
|
||||
-- ContainerFrame_Update, ContainerFrame.lua:234-293). Shared by every window's slot
|
||||
-- buttons. The occupied/empty fork drives the cooldown exactly as the ref does
|
||||
-- (l.248-253: occupied → UpdateCooldown, empty → Cooldown:Hide()).
|
||||
function BenillaBagSlot_Update(button)
|
||||
local bagId = button:GetParent():GetID()
|
||||
local size = GetContainerNumSlots(bagId)
|
||||
if button.physIndex > size then
|
||||
button.slot = nil
|
||||
button:Hide()
|
||||
return
|
||||
end
|
||||
button.slot = size - button.physIndex + 1
|
||||
button:Show()
|
||||
local texture, itemCount, locked = GetContainerItemInfo(bagId, button.slot)
|
||||
if texture ~= nil or itemCount ~= nil then
|
||||
BenillaBagSlot_UpdateCooldown(button, bagId)
|
||||
button.icon:SetTexture(texture or BENILLA_FALLBACK_ICON)
|
||||
-- A locked slot (its item is up on the cursor, or a send is still pending
|
||||
-- server-side, decision 0216 §4/0218 §3) dims, the real client's drag feedback.
|
||||
if locked then
|
||||
button.icon:SetVertexColor(0.4, 0.4, 0.4)
|
||||
else
|
||||
button.icon:SetVertexColor(1, 1, 1)
|
||||
end
|
||||
if (itemCount or 1) > 1 then
|
||||
button.count:SetText(itemCount)
|
||||
else
|
||||
button.count:SetText("")
|
||||
end
|
||||
else
|
||||
button.icon:SetTexture(nil)
|
||||
button.count:SetText("")
|
||||
getglobal(button:GetName() .. "Cooldown"):Hide()
|
||||
end
|
||||
end
|
||||
function BankFrame_OnLoad()
|
||||
local frame = this
|
||||
-- Named directly rather than through `$parent .. "Portrait"`: the reference's bank
|
||||
-- portrait is the UNPREFIXED global `BankPortraitTexture` (ref BankFrame.xml l.93),
|
||||
-- one of the reference's own naming inconsistencies, and it is the name addons write.
|
||||
-- `Bagnon_Core/Events.lua:41` does `SetPortraitTexture(BankPortraitTexture, "npc")`
|
||||
-- when its "show Bagnon's bank instead" option is off, and against a nil first argument
|
||||
-- that binding raises. Same standing lesson as `ToggleBackpack` / `CharacterBag0Slot`:
|
||||
-- the name an addon reaches for is the reference's, not ours.
|
||||
SetPortraitTexture(BankPortraitTexture, "npc")
|
||||
frame:RegisterEvent("BANKFRAME_OPENED")
|
||||
frame:RegisterEvent("BANKFRAME_CLOSED")
|
||||
frame:RegisterEvent("PLAYERBANKSLOTS_CHANGED")
|
||||
frame:RegisterEvent("PLAYERBANKBAGSLOTS_CHANGED")
|
||||
frame:RegisterEvent("PLAYER_MONEY")
|
||||
-- Container -1 is a container like any other: the same lock/cooldown maintenance
|
||||
-- events a bag window registers (the reference's `ContainerFrame_OnLoad`), handled
|
||||
-- below by BankFrame_OnEvent directly: this window's 24 slots are its own, painted by
|
||||
-- BenillaBankFrame_RepaintItems over the slot kit above.
|
||||
frame:RegisterEvent("ITEM_LOCK_CHANGED")
|
||||
frame:RegisterEvent("BAG_UPDATE_COOLDOWN")
|
||||
end
|
||||
|
||||
-- The banker's name arrives as arg1 of BANKFRAME_OPENED (the app resolves the banker guid
|
||||
-- through the NameCache — the merchant/trainer feed's own ask-once pattern); nil/empty
|
||||
-- until it lands, or if this ever fires with no name, falls back to "Banker".
|
||||
function BenillaBankFrame_SetTitle(name)
|
||||
if name == nil or name == "" then
|
||||
name = "Banker"
|
||||
end
|
||||
getglobal("BankFrameTitleText"):SetText(name)
|
||||
end
|
||||
|
||||
-- Repaint every one of the 24 generic slots (PLAYERBANKSLOTS_CHANGED names ONE slot; the
|
||||
-- spec allows repainting all 24 — cheap, and spares tracking a slot->button map).
|
||||
function BenillaBankFrame_RepaintItems()
|
||||
for i = 1, NUM_BANKGENERIC_SLOTS do
|
||||
local button = getglobal("BankItem" .. i)
|
||||
if button then BenillaBagSlot_Update(button) end
|
||||
end
|
||||
end
|
||||
|
||||
-- The bag row (ref UpdateBagSlotStatus, BankFrame.lua:81-117): each button's icon from the
|
||||
-- app-resolved BenillaGetBankBagTexture feed (falling back to the empty paper-doll bag
|
||||
-- slot art), tinted white when purchased / red when not; the purchase row's cost coins +
|
||||
-- afford color; the whole row hidden once GetNumBankSlots() reports `full`.
|
||||
function BenillaBankFrame_UpdateBagRow()
|
||||
local numSlots, full = GetNumBankSlots()
|
||||
for i = 1, NUM_BANKBAGSLOTS do
|
||||
local button = getglobal("BankBagButton" .. i)
|
||||
if button then
|
||||
local texture = BenillaGetBankBagTexture(i)
|
||||
button.icon:SetTexture(texture or "Interface\\PaperDoll\\UI-PaperDoll-Slot-Bag")
|
||||
if i <= numSlots then
|
||||
button.icon:SetVertexColor(1.0, 1.0, 1.0)
|
||||
button.tooltipText = "Bag Slot" -- BANK_BAG
|
||||
else
|
||||
button.icon:SetVertexColor(1.0, 0.1, 0.1)
|
||||
button.tooltipText = "Purchasable Bag Slot" -- BANK_BAG_PURCHASE
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local cost = GetBankSlotCost(numSlots)
|
||||
BankFrame.nextSlotCost = cost
|
||||
BenillaMoney_Set("BankFrameCostMoney", cost, true)
|
||||
if GetMoney() >= cost then
|
||||
BenillaMoney_SetColor("BankFrameCostMoney", 1.0, 1.0, 1.0)
|
||||
else
|
||||
BenillaMoney_SetColor("BankFrameCostMoney", 1.0, 0.1, 0.1)
|
||||
end
|
||||
|
||||
local purchaseFrame = getglobal("BankFramePurchaseInfo")
|
||||
if full then
|
||||
purchaseFrame:Hide()
|
||||
else
|
||||
purchaseFrame:Show()
|
||||
end
|
||||
end
|
||||
|
||||
-- `UpdateBagSlotStatus()` — THE REFERENCE'S NAME for the pass above, published for the same
|
||||
-- standing reason `ToggleBackpack` and `BagSlotButton_OnEnter` are: an addon calls the
|
||||
-- client's global, and it must exist and do the job. `Bagnon_Core/Events.lua:46` calls it
|
||||
-- from its own `ShowBlizBank()`, two lines after `SetPortraitTexture(BankPortraitTexture, …)`
|
||||
-- above — the path taken whenever a player turns Bagnon's "show Bagnon at the bank" option
|
||||
-- OFF and gets the stock window instead. It was nil, so that whole handler died there.
|
||||
--
|
||||
-- The reference's own body walks its `BankFrameBag1..6` and repaints
|
||||
-- `BankFrameDetailMoneyFrame`; ours is the same pass over our own bag row and cost frame,
|
||||
-- which is exactly what the reference's does for its window (it is not a shim: it is this
|
||||
-- window's real update, under the name the client publishes it as).
|
||||
function UpdateBagSlotStatus()
|
||||
BenillaBankFrame_UpdateBagRow()
|
||||
end
|
||||
|
||||
function BankFrame_OnEvent(event)
|
||||
local frame = this
|
||||
if event == "BANKFRAME_OPENED" then
|
||||
BenillaBankFrame_SetTitle(arg1)
|
||||
BenillaBankFrame_RepaintItems()
|
||||
BenillaBankFrame_UpdateBagRow()
|
||||
ShowUIPanel(frame)
|
||||
elseif event == "BANKFRAME_CLOSED" then
|
||||
HideUIPanel(frame)
|
||||
elseif event == "PLAYERBANKSLOTS_CHANGED" then
|
||||
BenillaBankFrame_RepaintItems()
|
||||
elseif event == "PLAYERBANKBAGSLOTS_CHANGED" then
|
||||
BenillaBankFrame_UpdateBagRow()
|
||||
elseif event == "PLAYER_MONEY" then
|
||||
BenillaMoney_Set("BankFrameMoney", GetMoney())
|
||||
BenillaBankFrame_UpdateBagRow() -- the cost coins' afford color can flip too
|
||||
elseif event == "ITEM_LOCK_CHANGED" then
|
||||
if arg1 == -1 then
|
||||
BenillaBankFrame_RepaintItems()
|
||||
end
|
||||
elseif event == "BAG_UPDATE_COOLDOWN" then
|
||||
-- No bag argument (ref l.243-253's own shape) — BenillaBagSlot_Update's occupied
|
||||
-- leg re-runs the cooldown sweep as part of the full 24-slot repaint.
|
||||
BenillaBankFrame_RepaintItems()
|
||||
end
|
||||
end
|
||||
|
||||
function BenillaBankFrame_CloseButton_OnClick()
|
||||
CloseBankFrame()
|
||||
HideUIPanel(getglobal("BankFrame"))
|
||||
end
|
||||
|
||||
-- CloseBankBagFrames (ref BankFrame.lua:119-123) — now VERBATIM, because the bank bags are
|
||||
-- ordinary ContainerFrames again (1751): ids NUM_BAG_SLOTS+1 .. +NUM_BANKBAGSLOTS, closed
|
||||
-- through the reference's own `CloseBag`. The button's lit ring follows from
|
||||
-- `ContainerFrame_OnHide`, which calls `UpdateBagButtonHighlight` for every id above
|
||||
-- NUM_BAG_SLOTS — so this no longer unchecks the buttons itself.
|
||||
function CloseBankBagFrames()
|
||||
for i = NUM_BAG_SLOTS + 1, NUM_BAG_SLOTS + NUM_BANKBAGSLOTS do
|
||||
CloseBag(i)
|
||||
end
|
||||
end
|
||||
|
||||
-- UpdateBagButtonHighlight(id) — ref BankFrame.lua:171-185, and the reference's own callers
|
||||
-- are the sourced `ContainerFrame_OnShow`/`_OnHide`: every container id above NUM_BAG_SLOTS
|
||||
-- routes its lit-state here instead of to a `CharacterBag<N>Slot`. Without it, opening a
|
||||
-- bank bag raises inside the reference's file.
|
||||
--
|
||||
-- ONE DIVERGENCE, in the art and not the rule: the reference lights a dedicated
|
||||
-- `BankFrameBag<n>HighlightFrameTexture` that its bag button carries; ours is a CheckButton
|
||||
-- whose CheckedTexture is that highlight (this file's own lit-ring convention, shared with
|
||||
-- the equipped-bag bar). So the same question — "is this bag's window open?" — writes
|
||||
-- `SetChecked` rather than showing a texture, and `IsBagOpen` asks it instead of the
|
||||
-- reference's open-coded ContainerFrame scan, which is the same scan under a name the
|
||||
-- reference itself publishes.
|
||||
function UpdateBagButtonHighlight(id)
|
||||
local button = getglobal("BankBagButton" .. (id - NUM_BAG_SLOTS))
|
||||
if not button then
|
||||
return
|
||||
end
|
||||
button:SetChecked(IsBagOpen(id) and 1 or nil)
|
||||
end
|
||||
|
||||
-- ── The 6 bag buttons (ref BankFrameItemButtonBag_OnClick/_OnShiftClick, BankFrame.lua:
|
||||
-- 171-204): NOT container slots (they hold a whole BAG item, not a stack) — the doll
|
||||
-- pickup idiom BagFrame.xml's BenillaBagBarSlot_* already established for the equipped
|
||||
-- bag slots, reused verbatim here over BankButtonIDToInvSlotID's bag arm (id 1..6 ->
|
||||
-- live 64..69). A held cursor item always PLACES; an empty cursor toggles the button's
|
||||
-- popout window instead (ref: PutItemInBag first, ToggleBag only if that failed). ──
|
||||
function BenillaBankBagButton_OnLoad(button, id)
|
||||
button:SetID(id)
|
||||
button.icon = getglobal(button:GetName() .. "Icon")
|
||||
-- Both buttons, ref-verbatim (BankItemButtonBagTemplate's OnLoad runs
|
||||
-- BankFrameBagButton_OnLoad -> BankFrameBaseButton_OnLoad, BankFrame.lua:12); the vault
|
||||
-- slots get the same pair through BenillaBagSlot_OnLoad. The OnClick below reads no
|
||||
-- button — right-click places/opens exactly as left does.
|
||||
button:RegisterForClicks("LeftButtonUp", "RightButtonUp")
|
||||
button:RegisterForDrag("LeftButton")
|
||||
end
|
||||
|
||||
function BenillaBankBagButton_Place(button)
|
||||
PickupInventoryItem(BankButtonIDToInvSlotID(button:GetID(), 1))
|
||||
PlaySound("BAGMENUBUTTONPRESS")
|
||||
end
|
||||
|
||||
function BenillaBankBagButton_OnClick(button)
|
||||
if CursorHasItem() then
|
||||
BenillaBankBagButton_Place(button)
|
||||
return
|
||||
end
|
||||
-- A bank bag's window is a ContainerFrame like any other now (1751), at container id
|
||||
-- NUM_BAG_SLOTS + <slot> — the reference's own arrangement, and the one decision 0604
|
||||
-- quoted ("the reference reuses ContainerFrames for bank bags identically") before this
|
||||
-- engine had `ToggleBag` to reuse them with. So the whole popout fork below is the
|
||||
-- reference's single call, and `ToggleBag`'s own `size > 0` guard is the "only a slot
|
||||
-- holding a bag opens" rule this file used to spell out.
|
||||
local id = NUM_BAG_SLOTS + button:GetID()
|
||||
PlaySound("BAGMENUBUTTONPRESS")
|
||||
ToggleBag(id)
|
||||
-- The lit ring: `ContainerFrame_OnShow`/`_OnHide` route here for a bank id, so this
|
||||
-- tail is the ref's own (BankFrameItemButtonBag_OnClick l.195) and covers the case
|
||||
-- ToggleBag declined — a bagless or unpurchased slot leaves the button unlit.
|
||||
UpdateBagButtonHighlight(id)
|
||||
end
|
||||
|
||||
function BenillaBankBagButton_OnEnter(button)
|
||||
GameTooltip:SetOwner(button, "ANCHOR_RIGHT")
|
||||
GameTooltip:SetText(button.tooltipText or "Bag Slot")
|
||||
GameTooltip:Show()
|
||||
end
|
||||
|
||||
-- The Purchase button (ref BankFramePurchaseButton OnClick, BankFrame.xml:478-482) + its
|
||||
-- confirm popup (ref StaticPopup.lua CONFIRM_BUY_BANK_SLOT, l.47-58 — text-only here, see
|
||||
-- the header's money-frame note). Accept queues PurchaseSlot(); the descriptor's byte-2
|
||||
-- delta is the only success signal (decision 0604) — PLAYERBANKBAGSLOTS_CHANGED repaints.
|
||||
function BenillaBankFramePurchaseButton_OnClick()
|
||||
PlaySound("igMainMenuOption")
|
||||
StaticPopup_Show("CONFIRM_BUY_BANK_SLOT")
|
||||
end
|
||||
|
||||
StaticPopupDialogs["CONFIRM_BUY_BANK_SLOT"] = {
|
||||
text = "Do you want to purchase a bank slot for:", -- CONFIRM_BUY_BANK_SLOT
|
||||
button1 = "Yes",
|
||||
button2 = "No",
|
||||
OnAccept = function() PurchaseSlot() end,
|
||||
timeout = 0,
|
||||
hideOnEscape = 1,
|
||||
}
|
||||
]]></Script>
|
||||
|
||||
<!-- The container-slot BUTTON — see the kit banner in the script block above. Moved here from
|
||||
BagFrame.xml with 1751's bag swap; this window's 24 vault slots are its only consumer. -->
|
||||
<Button name="BenillaBagSlotTemplate" virtual="true">
|
||||
<Size><AbsDimension x="37" y="37"/></Size>
|
||||
<Layers>
|
||||
<Layer level="BACKGROUND">
|
||||
<Texture name="$parentIcon"/>
|
||||
</Layer>
|
||||
<Layer level="OVERLAY">
|
||||
<FontString name="$parentCount" inherits="NumberFontNormal" justifyH="RIGHT">
|
||||
<Anchors>
|
||||
<Anchor point="BOTTOMRIGHT" relativePoint="BOTTOMRIGHT"><Offset><AbsDimension x="-5" y="2"/></Offset></Anchor>
|
||||
</Anchors>
|
||||
</FontString>
|
||||
</Layer>
|
||||
</Layers>
|
||||
<Frames>
|
||||
<!-- The item's use-cooldown sweep (ref ContainerFrame.xml:13 — $parentCooldown
|
||||
inherits CooldownFrameTemplate; ours is the native Cooldown widget, same as the
|
||||
action buttons'). Driven by BenillaBagSlot_Update / BAG_UPDATE_COOLDOWN via
|
||||
GetContainerItemCooldown + CooldownFrame_SetTimer (ActionBar.xml's transcription,
|
||||
loaded before this file). -->
|
||||
<Cooldown name="$parentCooldown" hidden="true">
|
||||
<Size><AbsDimension x="36" y="36"/></Size>
|
||||
<Anchors>
|
||||
<Anchor point="CENTER" relativePoint="CENTER"><Offset><AbsDimension x="0" y="-1"/></Offset></Anchor>
|
||||
</Anchors>
|
||||
</Cooldown>
|
||||
</Frames>
|
||||
<NormalTexture file="Interface\Buttons\UI-Quickslot2">
|
||||
<Size><AbsDimension x="64" y="64"/></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"/>
|
||||
<Scripts>
|
||||
<OnClick>BenillaBagSlot_OnClick(self, arg1)</OnClick>
|
||||
<OnEnter>BenillaBagSlot_OnEnter(self)</OnEnter>
|
||||
<OnLeave>BenillaBagSlot_OnLeave(self)</OnLeave>
|
||||
<OnUpdate>BenillaBagSlot_OnUpdate(self)</OnUpdate>
|
||||
<!-- The drag trio (decision 0216 §3): both route to the SAME left-click path the
|
||||
reference uses for both gestures (ContainerFrame.xml:40-55) — "LeftButton" hardcoded,
|
||||
exactly as the reference calls it, since RegisterForDrag above only ever arms Left —
|
||||
`true` (ignoreModifiers) suppresses the shift-split fork, matching the ref's own
|
||||
ignoreModifiers contract for a drag/receive. -->
|
||||
<OnDragStart>BenillaBagSlot_OnClick(self, "LeftButton", true)</OnDragStart>
|
||||
<OnReceiveDrag>BenillaBagSlot_OnClick(self, "LeftButton", true)</OnReceiveDrag>
|
||||
</Scripts>
|
||||
</Button>
|
||||
|
||||
<!--
|
||||
One bank-bag BUTTON (ref BankItemButtonBagTemplate, BankFrame.xml:46-74): the 37×37
|
||||
UI-Quickslot2 ring, a CheckButton so it lights (CheckedTexture) while its popout is open —
|
||||
the same lit-ring convention BagFrame.xml's BenillaBagBarSlot uses for the equipped bags.
|
||||
-->
|
||||
<CheckButton name="BenillaBankBagButtonTemplate" virtual="true">
|
||||
<Size><AbsDimension x="37" y="37"/></Size>
|
||||
<Layers>
|
||||
<Layer level="BACKGROUND">
|
||||
<Texture name="$parentIcon"/>
|
||||
</Layer>
|
||||
</Layers>
|
||||
<NormalTexture file="Interface\Buttons\UI-Quickslot2">
|
||||
<Size><AbsDimension x="64" y="64"/></Size>
|
||||
<Anchors><Anchor point="CENTER" relativePoint="CENTER"><Offset><AbsDimension x="0" y="-1"/></Offset></Anchor></Anchors>
|
||||
</NormalTexture>
|
||||
<HighlightTexture file="Interface\Buttons\ButtonHilight-Square" alphaMode="ADD"/>
|
||||
<CheckedTexture file="Interface\Buttons\CheckButtonHilight" alphaMode="ADD"/>
|
||||
<Scripts>
|
||||
<OnClick>BenillaBankBagButton_OnClick(self)</OnClick>
|
||||
<OnEnter>BenillaBankBagButton_OnEnter(self)</OnEnter>
|
||||
<OnLeave>GameTooltip:Hide()</OnLeave>
|
||||
<OnDragStart>BenillaBankBagButton_Place(self)</OnDragStart>
|
||||
<OnReceiveDrag>BenillaBankBagButton_Place(self)</OnReceiveDrag>
|
||||
</Scripts>
|
||||
</CheckButton>
|
||||
|
||||
<Frame name="BankFrame" enableMouse="true" toplevel="true" parent="UIParent" id="-1" hidden="true">
|
||||
<!-- The real 384×512 window (ref l.77-90), with the reference's own default anchor.
|
||||
ShowUIPanel/SetLeftFrame still SetPoints this to whichever slot it opens in (the NPC
|
||||
session convention Merchant/Trainer/Mail share), and TOPLEFT (0,-104) is exactly what
|
||||
the manager writes for the centre slot — so this changes nothing about where the window
|
||||
lands, and everything about the window while it is HIDDEN.
|
||||
|
||||
It was `No <Anchors>` until decision 1751, and that was a real divergence with a real
|
||||
consequence: an unanchored frame never resolves, so `BankFrame:GetRight()` answered nil
|
||||
forever. The reference's own `updateContainerFrameAnchors` reads exactly that, on every
|
||||
bag open and close (ContainerFrame.lua l.505), to decide whether the bags must shrink
|
||||
to clear the bank — so with the bags now running off the player's chain, a bank window
|
||||
with no rect took the whole bag layout down with it.
|
||||
id="-1" = BANK_CONTAINER: the 24 item buttons below read it via GetParent():GetID(), the
|
||||
SAME resolution the slot kit above uses for every other container. -->
|
||||
<Size><AbsDimension x="384" y="512"/></Size>
|
||||
<Anchors>
|
||||
<Anchor point="TOPLEFT"><Offset><AbsDimension x="0" y="-104"/></Offset></Anchor>
|
||||
</Anchors>
|
||||
<Layers>
|
||||
<Layer level="BACKGROUND">
|
||||
<!-- NPC portrait 60×60 TOPLEFT (7,-6) (ref l.93-104), bound in OnLoad via
|
||||
SetPortraitTexture(region, "npc"). The name is the REFERENCE'S, unprefixed —
|
||||
its bank frame breaks the $parentPortrait convention every other window here
|
||||
follows, and addons write the name the reference gave them. -->
|
||||
<Texture name="BankPortraitTexture">
|
||||
<Size><AbsDimension x="60" y="60"/></Size>
|
||||
<Anchors><Anchor point="TOPLEFT" relativePoint="TOPLEFT"><Offset><AbsDimension x="7" y="-6"/></Offset></Anchor></Anchors>
|
||||
</Texture>
|
||||
</Layer>
|
||||
<Layer level="ARTWORK">
|
||||
<!-- The whole window art, one 512×512 texture (ref l.107-114 — unlike Merchant/
|
||||
Trainer's 4-quadrant re-skin, the reference Bank window is a single slab). -->
|
||||
<Texture file="Interface\BankFrame\UI-BankFrame">
|
||||
<Size><AbsDimension x="512" y="512"/></Size>
|
||||
<Anchors><Anchor point="TOPLEFT" relativePoint="TOPLEFT"/></Anchors>
|
||||
</Texture>
|
||||
<!-- Title: the banker's name (ref BankFrameTitleText l.115-123, GameFontHighlight,
|
||||
CENTER (6,233)); "Banker" until BANKFRAME_OPENED's arg1 lands. -->
|
||||
<FontString name="BankFrameTitleText" inherits="GameFontHighlight" justifyH="CENTER" text="Banker">
|
||||
<Anchors><Anchor point="CENTER" relativePoint="CENTER"><Offset><AbsDimension x="6" y="233"/></Offset></Anchor></Anchors>
|
||||
</FontString>
|
||||
<!-- Section labels (ref l.124-141: ITEMSLOTTEXT/BAGSLOTTEXT, GameFontNormal, CENTER
|
||||
(-11,195) / (-11,-5)). -->
|
||||
<FontString inherits="GameFontNormal" text="Item Slots" justifyH="CENTER">
|
||||
<Anchors><Anchor point="CENTER" relativePoint="CENTER"><Offset><AbsDimension x="-11" y="195"/></Offset></Anchor></Anchors>
|
||||
</FontString>
|
||||
<FontString inherits="GameFontNormal" text="Bag Slots" justifyH="CENTER">
|
||||
<Anchors><Anchor point="CENTER" relativePoint="CENTER"><Offset><AbsDimension x="-11" y="-5"/></Offset></Anchor></Anchors>
|
||||
</FontString>
|
||||
</Layer>
|
||||
</Layers>
|
||||
<Frames>
|
||||
<!-- Close X: the house minimize-button convention (Merchant/Trainer/Mail), TOPRIGHT
|
||||
(-30,-8) — see the header's deviation note on the reference's own offset. -->
|
||||
<Button name="BankFrameCloseButton">
|
||||
<Size><AbsDimension x="32" y="32"/></Size>
|
||||
<Anchors><Anchor point="TOPRIGHT" relativePoint="TOPRIGHT"><Offset><AbsDimension x="-30" y="-8"/></Offset></Anchor></Anchors>
|
||||
<NormalTexture file="Interface\Buttons\UI-Panel-MinimizeButton-Up"/>
|
||||
<PushedTexture file="Interface\Buttons\UI-Panel-MinimizeButton-Down"/>
|
||||
<HighlightTexture file="Interface\Buttons\UI-Panel-MinimizeButton-Highlight" alphaMode="ADD"/>
|
||||
<Scripts><OnClick>BenillaBankFrame_CloseButton_OnClick()</OnClick></Scripts>
|
||||
</Button>
|
||||
|
||||
<!-- The 24 generic item slots (ref BankFrameItem1..24, l.154-369): 6 cols × 4 rows,
|
||||
TOPLEFT(40,-73), 12px column gap, -7px row gap — QUOTED verbatim. Each is a
|
||||
BenillaBagSlotTemplate button (BagFrame.xml) read against container -1; physIndex
|
||||
is `25 - i` so button i resolves to slot i (see the header's deviation note). -->
|
||||
<Button name="BankItem1" inherits="BenillaBagSlotTemplate">
|
||||
<Anchors><Anchor point="TOPLEFT" relativePoint="TOPLEFT"><Offset><AbsDimension x="40" y="-73"/></Offset></Anchor></Anchors>
|
||||
<Scripts><OnLoad>BenillaBagSlot_OnLoad(self, 24)</OnLoad></Scripts>
|
||||
</Button>
|
||||
<Button name="BankItem2" inherits="BenillaBagSlotTemplate">
|
||||
<Anchors><Anchor point="TOPLEFT" relativeTo="BankItem1" relativePoint="TOPRIGHT"><Offset><AbsDimension x="12" y="0"/></Offset></Anchor></Anchors>
|
||||
<Scripts><OnLoad>BenillaBagSlot_OnLoad(self, 23)</OnLoad></Scripts>
|
||||
</Button>
|
||||
<Button name="BankItem3" inherits="BenillaBagSlotTemplate">
|
||||
<Anchors><Anchor point="TOPLEFT" relativeTo="BankItem2" relativePoint="TOPRIGHT"><Offset><AbsDimension x="12" y="0"/></Offset></Anchor></Anchors>
|
||||
<Scripts><OnLoad>BenillaBagSlot_OnLoad(self, 22)</OnLoad></Scripts>
|
||||
</Button>
|
||||
<Button name="BankItem4" inherits="BenillaBagSlotTemplate">
|
||||
<Anchors><Anchor point="TOPLEFT" relativeTo="BankItem3" relativePoint="TOPRIGHT"><Offset><AbsDimension x="12" y="0"/></Offset></Anchor></Anchors>
|
||||
<Scripts><OnLoad>BenillaBagSlot_OnLoad(self, 21)</OnLoad></Scripts>
|
||||
</Button>
|
||||
<Button name="BankItem5" inherits="BenillaBagSlotTemplate">
|
||||
<Anchors><Anchor point="TOPLEFT" relativeTo="BankItem4" relativePoint="TOPRIGHT"><Offset><AbsDimension x="12" y="0"/></Offset></Anchor></Anchors>
|
||||
<Scripts><OnLoad>BenillaBagSlot_OnLoad(self, 20)</OnLoad></Scripts>
|
||||
</Button>
|
||||
<Button name="BankItem6" inherits="BenillaBagSlotTemplate">
|
||||
<Anchors><Anchor point="TOPLEFT" relativeTo="BankItem5" relativePoint="TOPRIGHT"><Offset><AbsDimension x="12" y="0"/></Offset></Anchor></Anchors>
|
||||
<Scripts><OnLoad>BenillaBagSlot_OnLoad(self, 19)</OnLoad></Scripts>
|
||||
</Button>
|
||||
<Button name="BankItem7" inherits="BenillaBagSlotTemplate">
|
||||
<Anchors><Anchor point="TOPLEFT" relativeTo="BankItem1" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-7"/></Offset></Anchor></Anchors>
|
||||
<Scripts><OnLoad>BenillaBagSlot_OnLoad(self, 18)</OnLoad></Scripts>
|
||||
</Button>
|
||||
<Button name="BankItem8" inherits="BenillaBagSlotTemplate">
|
||||
<Anchors><Anchor point="TOPLEFT" relativeTo="BankItem7" relativePoint="TOPRIGHT"><Offset><AbsDimension x="12" y="0"/></Offset></Anchor></Anchors>
|
||||
<Scripts><OnLoad>BenillaBagSlot_OnLoad(self, 17)</OnLoad></Scripts>
|
||||
</Button>
|
||||
<Button name="BankItem9" inherits="BenillaBagSlotTemplate">
|
||||
<Anchors><Anchor point="TOPLEFT" relativeTo="BankItem8" relativePoint="TOPRIGHT"><Offset><AbsDimension x="12" y="0"/></Offset></Anchor></Anchors>
|
||||
<Scripts><OnLoad>BenillaBagSlot_OnLoad(self, 16)</OnLoad></Scripts>
|
||||
</Button>
|
||||
<Button name="BankItem10" inherits="BenillaBagSlotTemplate">
|
||||
<Anchors><Anchor point="TOPLEFT" relativeTo="BankItem9" relativePoint="TOPRIGHT"><Offset><AbsDimension x="12" y="0"/></Offset></Anchor></Anchors>
|
||||
<Scripts><OnLoad>BenillaBagSlot_OnLoad(self, 15)</OnLoad></Scripts>
|
||||
</Button>
|
||||
<Button name="BankItem11" inherits="BenillaBagSlotTemplate">
|
||||
<Anchors><Anchor point="TOPLEFT" relativeTo="BankItem10" relativePoint="TOPRIGHT"><Offset><AbsDimension x="12" y="0"/></Offset></Anchor></Anchors>
|
||||
<Scripts><OnLoad>BenillaBagSlot_OnLoad(self, 14)</OnLoad></Scripts>
|
||||
</Button>
|
||||
<Button name="BankItem12" inherits="BenillaBagSlotTemplate">
|
||||
<Anchors><Anchor point="TOPLEFT" relativeTo="BankItem11" relativePoint="TOPRIGHT"><Offset><AbsDimension x="12" y="0"/></Offset></Anchor></Anchors>
|
||||
<Scripts><OnLoad>BenillaBagSlot_OnLoad(self, 13)</OnLoad></Scripts>
|
||||
</Button>
|
||||
<Button name="BankItem13" inherits="BenillaBagSlotTemplate">
|
||||
<Anchors><Anchor point="TOPLEFT" relativeTo="BankItem7" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-7"/></Offset></Anchor></Anchors>
|
||||
<Scripts><OnLoad>BenillaBagSlot_OnLoad(self, 12)</OnLoad></Scripts>
|
||||
</Button>
|
||||
<Button name="BankItem14" inherits="BenillaBagSlotTemplate">
|
||||
<Anchors><Anchor point="TOPLEFT" relativeTo="BankItem13" relativePoint="TOPRIGHT"><Offset><AbsDimension x="12" y="0"/></Offset></Anchor></Anchors>
|
||||
<Scripts><OnLoad>BenillaBagSlot_OnLoad(self, 11)</OnLoad></Scripts>
|
||||
</Button>
|
||||
<Button name="BankItem15" inherits="BenillaBagSlotTemplate">
|
||||
<Anchors><Anchor point="TOPLEFT" relativeTo="BankItem14" relativePoint="TOPRIGHT"><Offset><AbsDimension x="12" y="0"/></Offset></Anchor></Anchors>
|
||||
<Scripts><OnLoad>BenillaBagSlot_OnLoad(self, 10)</OnLoad></Scripts>
|
||||
</Button>
|
||||
<Button name="BankItem16" inherits="BenillaBagSlotTemplate">
|
||||
<Anchors><Anchor point="TOPLEFT" relativeTo="BankItem15" relativePoint="TOPRIGHT"><Offset><AbsDimension x="12" y="0"/></Offset></Anchor></Anchors>
|
||||
<Scripts><OnLoad>BenillaBagSlot_OnLoad(self, 9)</OnLoad></Scripts>
|
||||
</Button>
|
||||
<Button name="BankItem17" inherits="BenillaBagSlotTemplate">
|
||||
<Anchors><Anchor point="TOPLEFT" relativeTo="BankItem16" relativePoint="TOPRIGHT"><Offset><AbsDimension x="12" y="0"/></Offset></Anchor></Anchors>
|
||||
<Scripts><OnLoad>BenillaBagSlot_OnLoad(self, 8)</OnLoad></Scripts>
|
||||
</Button>
|
||||
<Button name="BankItem18" inherits="BenillaBagSlotTemplate">
|
||||
<Anchors><Anchor point="TOPLEFT" relativeTo="BankItem17" relativePoint="TOPRIGHT"><Offset><AbsDimension x="12" y="0"/></Offset></Anchor></Anchors>
|
||||
<Scripts><OnLoad>BenillaBagSlot_OnLoad(self, 7)</OnLoad></Scripts>
|
||||
</Button>
|
||||
<Button name="BankItem19" inherits="BenillaBagSlotTemplate">
|
||||
<Anchors><Anchor point="TOPLEFT" relativeTo="BankItem13" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-7"/></Offset></Anchor></Anchors>
|
||||
<Scripts><OnLoad>BenillaBagSlot_OnLoad(self, 6)</OnLoad></Scripts>
|
||||
</Button>
|
||||
<Button name="BankItem20" inherits="BenillaBagSlotTemplate">
|
||||
<Anchors><Anchor point="TOPLEFT" relativeTo="BankItem19" relativePoint="TOPRIGHT"><Offset><AbsDimension x="12" y="0"/></Offset></Anchor></Anchors>
|
||||
<Scripts><OnLoad>BenillaBagSlot_OnLoad(self, 5)</OnLoad></Scripts>
|
||||
</Button>
|
||||
<Button name="BankItem21" inherits="BenillaBagSlotTemplate">
|
||||
<Anchors><Anchor point="TOPLEFT" relativeTo="BankItem20" relativePoint="TOPRIGHT"><Offset><AbsDimension x="12" y="0"/></Offset></Anchor></Anchors>
|
||||
<Scripts><OnLoad>BenillaBagSlot_OnLoad(self, 4)</OnLoad></Scripts>
|
||||
</Button>
|
||||
<Button name="BankItem22" inherits="BenillaBagSlotTemplate">
|
||||
<Anchors><Anchor point="TOPLEFT" relativeTo="BankItem21" relativePoint="TOPRIGHT"><Offset><AbsDimension x="12" y="0"/></Offset></Anchor></Anchors>
|
||||
<Scripts><OnLoad>BenillaBagSlot_OnLoad(self, 3)</OnLoad></Scripts>
|
||||
</Button>
|
||||
<Button name="BankItem23" inherits="BenillaBagSlotTemplate">
|
||||
<Anchors><Anchor point="TOPLEFT" relativeTo="BankItem22" relativePoint="TOPRIGHT"><Offset><AbsDimension x="12" y="0"/></Offset></Anchor></Anchors>
|
||||
<Scripts><OnLoad>BenillaBagSlot_OnLoad(self, 2)</OnLoad></Scripts>
|
||||
</Button>
|
||||
<Button name="BankItem24" inherits="BenillaBagSlotTemplate">
|
||||
<Anchors><Anchor point="TOPLEFT" relativeTo="BankItem23" relativePoint="TOPRIGHT"><Offset><AbsDimension x="12" y="0"/></Offset></Anchor></Anchors>
|
||||
<Scripts><OnLoad>BenillaBagSlot_OnLoad(self, 1)</OnLoad></Scripts>
|
||||
</Button>
|
||||
|
||||
<!-- The 6 bag buttons (ref BankFrameBag1..6 id=5..10, l.372-425): under item19, 12px
|
||||
column gap. Button id = the bag-button number (1..6, what BankButtonIDToInvSlotID
|
||||
and BenillaGetBankBagTexture expect) — NOT the container id (5..10), which the
|
||||
popout windows below carry instead via their own `id=`. -->
|
||||
<CheckButton name="BankBagButton1" inherits="BenillaBankBagButtonTemplate">
|
||||
<Anchors><Anchor point="TOPLEFT" relativeTo="BankItem19" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-32"/></Offset></Anchor></Anchors>
|
||||
<Scripts><OnLoad>BenillaBankBagButton_OnLoad(self, 1)</OnLoad></Scripts>
|
||||
</CheckButton>
|
||||
<CheckButton name="BankBagButton2" inherits="BenillaBankBagButtonTemplate">
|
||||
<Anchors><Anchor point="TOPLEFT" relativeTo="BankBagButton1" relativePoint="TOPRIGHT"><Offset><AbsDimension x="12" y="0"/></Offset></Anchor></Anchors>
|
||||
<Scripts><OnLoad>BenillaBankBagButton_OnLoad(self, 2)</OnLoad></Scripts>
|
||||
</CheckButton>
|
||||
<CheckButton name="BankBagButton3" inherits="BenillaBankBagButtonTemplate">
|
||||
<Anchors><Anchor point="TOPLEFT" relativeTo="BankBagButton2" relativePoint="TOPRIGHT"><Offset><AbsDimension x="12" y="0"/></Offset></Anchor></Anchors>
|
||||
<Scripts><OnLoad>BenillaBankBagButton_OnLoad(self, 3)</OnLoad></Scripts>
|
||||
</CheckButton>
|
||||
<CheckButton name="BankBagButton4" inherits="BenillaBankBagButtonTemplate">
|
||||
<Anchors><Anchor point="TOPLEFT" relativeTo="BankBagButton3" relativePoint="TOPRIGHT"><Offset><AbsDimension x="12" y="0"/></Offset></Anchor></Anchors>
|
||||
<Scripts><OnLoad>BenillaBankBagButton_OnLoad(self, 4)</OnLoad></Scripts>
|
||||
</CheckButton>
|
||||
<CheckButton name="BankBagButton5" inherits="BenillaBankBagButtonTemplate">
|
||||
<Anchors><Anchor point="TOPLEFT" relativeTo="BankBagButton4" relativePoint="TOPRIGHT"><Offset><AbsDimension x="12" y="0"/></Offset></Anchor></Anchors>
|
||||
<Scripts><OnLoad>BenillaBankBagButton_OnLoad(self, 5)</OnLoad></Scripts>
|
||||
</CheckButton>
|
||||
<CheckButton name="BankBagButton6" inherits="BenillaBankBagButtonTemplate">
|
||||
<Anchors><Anchor point="TOPLEFT" relativeTo="BankBagButton5" relativePoint="TOPRIGHT"><Offset><AbsDimension x="12" y="0"/></Offset></Anchor></Anchors>
|
||||
<Scripts><OnLoad>BenillaBankBagButton_OnLoad(self, 6)</OnLoad></Scripts>
|
||||
</CheckButton>
|
||||
|
||||
<!-- The purchase row (ref BankFramePurchaseInfo, l.426-500): 300×72, CENTER of the
|
||||
window (-15,-105). House coin rig (BenillaMoney_Set) in place of the reference's
|
||||
SmallMoneyFrameTemplate; the Purchase button is a local UI-Panel-Button redraw (no
|
||||
UIPanelButtonTemplate ships — the house convention every NPC window uses). -->
|
||||
<Frame name="BankFramePurchaseInfo">
|
||||
<Size><AbsDimension x="300" y="72"/></Size>
|
||||
<Anchors><Anchor point="CENTER" relativeTo="BankFrame" relativePoint="CENTER"><Offset><AbsDimension x="-15" y="-105"/></Offset></Anchor></Anchors>
|
||||
<Layers>
|
||||
<Layer level="BACKGROUND">
|
||||
<FontString inherits="GameFontHighlight" text="Do you wish to purchase space for an additional bag?" justifyH="LEFT">
|
||||
<Size><AbsDimension x="280" y="0"/></Size>
|
||||
<Anchors><Anchor point="CENTER" relativePoint="CENTER"><Offset><AbsDimension x="10" y="20"/></Offset></Anchor></Anchors>
|
||||
</FontString>
|
||||
<FontString name="BankFrameCostLabel" inherits="GameFontNormal" text="Cost:" justifyH="LEFT">
|
||||
<Anchors><Anchor point="LEFT" relativePoint="LEFT"><Offset><AbsDimension x="30" y="-10"/></Offset></Anchor></Anchors>
|
||||
</FontString>
|
||||
</Layer>
|
||||
</Layers>
|
||||
<Frames>
|
||||
<Frame name="BankFrameCostMoneyCoin1">
|
||||
<Size><AbsDimension x="13" y="13"/></Size>
|
||||
<Anchors><Anchor point="LEFT" relativeTo="BankFrameCostLabel" relativePoint="RIGHT"><Offset><AbsDimension x="4" y="0"/></Offset></Anchor></Anchors>
|
||||
<Layers><Layer level="OVERLAY">
|
||||
<Texture name="$parentIcon" file="Interface\MoneyFrame\UI-MoneyIcons"><Size><AbsDimension x="13" y="13"/></Size><Anchors><Anchor point="RIGHT" relativePoint="RIGHT"/></Anchors><TexCoords left="0" right="0.25" top="0" bottom="1"/></Texture>
|
||||
<FontString name="$parentNum" inherits="NumberFontNormal" justifyH="RIGHT"><Size><AbsDimension x="20" y="13"/></Size><Anchors><Anchor point="RIGHT" relativePoint="RIGHT"><Offset><AbsDimension x="-13" y="0"/></Offset></Anchor></Anchors></FontString>
|
||||
</Layer></Layers>
|
||||
</Frame>
|
||||
<Frame name="BankFrameCostMoneyCoin2">
|
||||
<Size><AbsDimension x="13" y="13"/></Size>
|
||||
<Anchors><Anchor point="LEFT" relativeTo="BankFrameCostMoneyCoin1" relativePoint="RIGHT"><Offset><AbsDimension x="4" y="0"/></Offset></Anchor></Anchors>
|
||||
<Layers><Layer level="OVERLAY">
|
||||
<Texture name="$parentIcon" file="Interface\MoneyFrame\UI-MoneyIcons"><Size><AbsDimension x="13" y="13"/></Size><Anchors><Anchor point="RIGHT" relativePoint="RIGHT"/></Anchors><TexCoords left="0.25" right="0.5" top="0" bottom="1"/></Texture>
|
||||
<FontString name="$parentNum" inherits="NumberFontNormal" justifyH="RIGHT"><Size><AbsDimension x="20" y="13"/></Size><Anchors><Anchor point="RIGHT" relativePoint="RIGHT"><Offset><AbsDimension x="-13" y="0"/></Offset></Anchor></Anchors></FontString>
|
||||
</Layer></Layers>
|
||||
</Frame>
|
||||
<Frame name="BankFrameCostMoneyCoin3">
|
||||
<Size><AbsDimension x="13" y="13"/></Size>
|
||||
<Anchors><Anchor point="LEFT" relativeTo="BankFrameCostMoneyCoin2" relativePoint="RIGHT"><Offset><AbsDimension x="4" y="0"/></Offset></Anchor></Anchors>
|
||||
<Layers><Layer level="OVERLAY">
|
||||
<Texture name="$parentIcon" file="Interface\MoneyFrame\UI-MoneyIcons"><Size><AbsDimension x="13" y="13"/></Size><Anchors><Anchor point="RIGHT" relativePoint="RIGHT"/></Anchors><TexCoords left="0.5" right="0.75" top="0" bottom="1"/></Texture>
|
||||
<FontString name="$parentNum" inherits="NumberFontNormal" justifyH="RIGHT"><Size><AbsDimension x="20" y="13"/></Size><Anchors><Anchor point="RIGHT" relativePoint="RIGHT"><Offset><AbsDimension x="-13" y="0"/></Offset></Anchor></Anchors></FontString>
|
||||
</Layer></Layers>
|
||||
</Frame>
|
||||
<Button name="BankFramePurchaseButton" text="Purchase">
|
||||
<Size><AbsDimension x="124" y="21"/></Size>
|
||||
<Anchors><Anchor point="RIGHT" relativePoint="RIGHT"><Offset><AbsDimension x="-20" y="-10"/></Offset></Anchor></Anchors>
|
||||
<NormalFont inherits="GameFontNormal"/>
|
||||
<HighlightFont inherits="GameFontHighlight"/>
|
||||
<DisabledFont inherits="GameFontDisable"/>
|
||||
<NormalTexture file="Interface\Buttons\UI-Panel-Button-Up"><TexCoords left="0" right="0.625" top="0" bottom="0.6875"/></NormalTexture>
|
||||
<PushedTexture file="Interface\Buttons\UI-Panel-Button-Down"><TexCoords left="0" right="0.625" top="0" bottom="0.6875"/></PushedTexture>
|
||||
<DisabledTexture file="Interface\Buttons\UI-Panel-Button-Disabled"><TexCoords left="0" right="0.625" top="0" bottom="0.6875"/></DisabledTexture>
|
||||
<HighlightTexture file="Interface\Buttons\UI-Panel-Button-Highlight" alphaMode="ADD"><TexCoords left="0" right="0.625" top="0" bottom="0.6875"/></HighlightTexture>
|
||||
<Scripts><OnClick>BenillaBankFramePurchaseButton_OnClick()</OnClick></Scripts>
|
||||
</Button>
|
||||
</Frames>
|
||||
</Frame>
|
||||
|
||||
<!-- The player's own money (ref BankFrameMoneyFrame, l.501-509: BOTTOMRIGHT(-38,102)).
|
||||
Same right-to-left coin chain as MerchantFrame's purse — copper-first, right-flush
|
||||
off Coin1 (BenillaMoney_Set's own module doc, MerchantFrame.xml). -->
|
||||
<Frame name="BankFrameMoneyCoin1">
|
||||
<Size><AbsDimension x="13" y="13"/></Size>
|
||||
<Anchors><Anchor point="BOTTOMRIGHT" relativePoint="BOTTOMRIGHT"><Offset><AbsDimension x="-51" y="102"/></Offset></Anchor></Anchors>
|
||||
<Layers><Layer level="OVERLAY">
|
||||
<Texture name="$parentIcon" file="Interface\MoneyFrame\UI-MoneyIcons"><Size><AbsDimension x="13" y="13"/></Size><Anchors><Anchor point="RIGHT" relativePoint="RIGHT"/></Anchors><TexCoords left="0.5" right="0.75" top="0" bottom="1"/></Texture>
|
||||
<FontString name="$parentNum" inherits="NumberFontNormal" justifyH="RIGHT"><Size><AbsDimension x="20" y="13"/></Size><Anchors><Anchor point="RIGHT" relativePoint="RIGHT"><Offset><AbsDimension x="-13" y="0"/></Offset></Anchor></Anchors></FontString>
|
||||
</Layer></Layers>
|
||||
</Frame>
|
||||
<Frame name="BankFrameMoneyCoin2">
|
||||
<Size><AbsDimension x="13" y="13"/></Size>
|
||||
<Anchors><Anchor point="RIGHT" relativeTo="BankFrameMoneyCoin1" relativePoint="LEFT"><Offset><AbsDimension x="-4" y="0"/></Offset></Anchor></Anchors>
|
||||
<Layers><Layer level="OVERLAY">
|
||||
<Texture name="$parentIcon" file="Interface\MoneyFrame\UI-MoneyIcons"><Size><AbsDimension x="13" y="13"/></Size><Anchors><Anchor point="RIGHT" relativePoint="RIGHT"/></Anchors><TexCoords left="0.25" right="0.5" top="0" bottom="1"/></Texture>
|
||||
<FontString name="$parentNum" inherits="NumberFontNormal" justifyH="RIGHT"><Size><AbsDimension x="20" y="13"/></Size><Anchors><Anchor point="RIGHT" relativePoint="RIGHT"><Offset><AbsDimension x="-13" y="0"/></Offset></Anchor></Anchors></FontString>
|
||||
</Layer></Layers>
|
||||
</Frame>
|
||||
<Frame name="BankFrameMoneyCoin3">
|
||||
<Size><AbsDimension x="13" y="13"/></Size>
|
||||
<Anchors><Anchor point="RIGHT" relativeTo="BankFrameMoneyCoin2" relativePoint="LEFT"><Offset><AbsDimension x="-4" y="0"/></Offset></Anchor></Anchors>
|
||||
<Layers><Layer level="OVERLAY">
|
||||
<Texture name="$parentIcon" file="Interface\MoneyFrame\UI-MoneyIcons"><Size><AbsDimension x="13" y="13"/></Size><Anchors><Anchor point="RIGHT" relativePoint="RIGHT"/></Anchors><TexCoords left="0" right="0.25" top="0" bottom="1"/></Texture>
|
||||
<FontString name="$parentNum" inherits="NumberFontNormal" justifyH="RIGHT"><Size><AbsDimension x="20" y="13"/></Size><Anchors><Anchor point="RIGHT" relativePoint="RIGHT"><Offset><AbsDimension x="-13" y="0"/></Offset></Anchor></Anchors></FontString>
|
||||
</Layer></Layers>
|
||||
</Frame>
|
||||
</Frames>
|
||||
<Scripts>
|
||||
<OnLoad>BankFrame_OnLoad()</OnLoad>
|
||||
<OnEvent>BankFrame_OnEvent(event)</OnEvent>
|
||||
<!-- OnShow/OnHide (ref l.518-526): OpenBackpack/CloseBackpack per the 0561 contract
|
||||
(MerchantFrame.xml's own placement is the model); the reference OnHide order
|
||||
(CloseBankBagFrames(); CloseBankFrame(); PlaySound(...)) kept exactly, with
|
||||
CloseBackpack folded in at the Merchant convention's spot. -->
|
||||
<OnShow>OpenBackpack(); PlaySound("igMainMenuOpen")</OnShow>
|
||||
<OnHide>CloseBankBagFrames(); CloseBankFrame(); CloseBackpack(); PlaySound("igMainMenuClose")</OnHide>
|
||||
</Scripts>
|
||||
</Frame>
|
||||
|
||||
</Ui>
|
||||
@@ -1024,7 +1024,7 @@
|
||||
-- reference (PaperDollFrame.xml:20-25 routes OnDragStart/OnReceiveDrag to the same
|
||||
-- OnClick("LeftButton", ignoreModifiers=1) the plain click uses) — see
|
||||
-- BenillaPaperDollSlot_OnClick below for the identical routing, the bag slots' own
|
||||
-- precedent (BagFrame.xml's BenillaBagSlot_OnClick).
|
||||
-- precedent (the reference's ContainerFrameItemButton_OnClick, sourced since 1751).
|
||||
button:RegisterForDrag("LeftButton")
|
||||
else
|
||||
button:RegisterForClicks("LeftButtonUp")
|
||||
@@ -1095,7 +1095,7 @@
|
||||
button.broken = textureName and GetInventoryItemBroken("player", button.invSlotId)
|
||||
-- A locked slot (its item is up on the cursor, or a send is still pending
|
||||
-- server-side, decision 0216 §4/0218 §3) dims — the bag slots' own vertex-dim
|
||||
-- convention (BagFrame.xml's BenillaBagSlot_Update), applied here instead of the
|
||||
-- convention, applied here instead of the
|
||||
-- ref's separate SetItemButtonDesaturated call (no engine equivalent — header
|
||||
-- comment). The transient drag-dim outranks the broken red on the icon.
|
||||
if IsInventoryItemLocked(button.invSlotId) then
|
||||
@@ -1272,7 +1272,7 @@
|
||||
The shared equipment-slot button (ref PaperDollItemSlotButtonTemplate inherits
|
||||
ItemButtonTemplate, ref-PaperDollFrame.xml:4-38 — ItemButtonTemplate's own XML isn't part of
|
||||
this session's extraction, so its shape is taken from this codebase's own prior
|
||||
reconstruction of it, BagFrame.xml's BenillaBagSlotTemplate: 37x37, a BACKGROUND icon fill,
|
||||
reconstruction of it (BagFrame.xml's since-retired BenillaBagSlotTemplate): 37x37, a BACKGROUND icon fill,
|
||||
an OVERLAY count, the 64x64 UI-Quickslot2 ring centered (0,-1), UI-Quickslot-Depress pushed,
|
||||
ButtonHilight-Square ADD highlight — duplicated here rather than cross-file inherited so this
|
||||
window's slot behavior (UNIT_INVENTORY_CHANGED refresh, tooltip) stays self-contained in one
|
||||
|
||||
@@ -70,20 +70,23 @@
|
||||
never the behaviour; it was two XML declarations, because the reference ships its templates in
|
||||
`ContainerFrame.xml` and that file is not in the sourced set.
|
||||
|
||||
So the chain costs what it actually costs: this Button, and `ContainerFrameItemButtonTemplate`
|
||||
in ContainerFrameAdapters.xml beside the adapter that already wraps one of those very handlers.
|
||||
Our own bag windows keep `BenillaBagSlotTemplate` and are untouched — that is our design (0216),
|
||||
a thousand lines of it, and this template is the reference's PUBLISHED surface for addons that
|
||||
build their own slots. Two things that look alike because they do the same job; only one of them
|
||||
is an API.
|
||||
So the chain cost what it actually cost: this Button, and — at the time — a transcribed
|
||||
`ContainerFrameItemButtonTemplate` beside the adapter that already wrapped one of those very
|
||||
handlers. **Decision 1751 then took the last step and made both moot for the windows
|
||||
themselves**: `Interface\FrameXML\ContainerFrame.xml` is a manifest entry now, so the
|
||||
reference's OWN declaration of that template is the live one and the transcription is deleted.
|
||||
This Button stays, because it is the reference's PUBLISHED surface for addons that build their
|
||||
own slots and it has to exist whether or not any window of ours uses it.
|
||||
|
||||
**The bank arm is NOT here, and this is the omission, not an oversight** (1134 §4).
|
||||
`BankItemButtonGenericTemplate` inherits `BankItemButtonTemplate` (BankFrame.xml l.-), whose
|
||||
handlers — `BankFrameItemButton_OnLoad`, `_OnEvent`, `BankFrameItemButtonGeneric_OnClick` —
|
||||
live in `BankFrame.lua`, which is NOT sourced, and whose OnEnter calls `this:GetInventorySlot()`,
|
||||
a widget method we do not bind. That is the real unit of work the paragraph above thought it was
|
||||
describing. pfUI reaches it only for `bag == -1`, so its bag arm works and its bank arm raises
|
||||
where it raises today.
|
||||
**The bank arm resolves too, since 1751's second window.** `BankItemButtonGenericTemplate`
|
||||
inherits `BankItemButtonTemplate`, whose handlers live in `BankFrame.lua` — and that file
|
||||
arrives through `Interface\FrameXML\BankFrame.xml`'s own `<Script file=>`, off the chain, the
|
||||
same way. This paragraph used to say the arm was a named omission (1134 §4) whose real cost was
|
||||
the unsourced Lua plus `this:GetInventorySlot()`, a widget method we do not bind. Neither is
|
||||
true any more: the Lua is sourced, and `GetInventorySlot` is not a widget method at all — it is
|
||||
a plain field `BankFrameBaseButton_OnLoad` assigns (`this.GetInventorySlot =
|
||||
ButtonInventorySlot`). pfUI reaches the bank arm for `bag == -1`, and it now finds a real
|
||||
template.
|
||||
-->
|
||||
<Ui xmlns="http://www.blizzard.com/wow/ui/">
|
||||
<Script><![CDATA[
|
||||
|
||||
@@ -363,7 +363,7 @@
|
||||
end
|
||||
-- pin §2's $parentCheck: the per-row watch checkbox. SetTexture(path)/
|
||||
-- SetTexture(nil) as the show/hide toggle — the icon-slot blanking pattern
|
||||
-- (`BenillaBagSlot_Update` etc.). (Regions DO have Show/Hide these days —
|
||||
-- (the icon-clearing every slot repaint does). (Regions DO have Show/Hide these days —
|
||||
-- region.rs; this site just predates it and the pattern is equivalent.)
|
||||
if not isHeader and IsQuestWatched(index) then
|
||||
button.check:SetTexture("Interface\\Buttons\\UI-CheckBox-Check")
|
||||
@@ -1318,7 +1318,7 @@
|
||||
-- publish, state slots don't).
|
||||
self.foldHighlight = self:GetHighlightTexture()
|
||||
-- Both buttons must reach OnClick (the default set is LeftButtonUp only) — the same
|
||||
-- convention `BagFrame.xml`'s `BenillaBagSlot_OnLoad` uses for its own left/right split.
|
||||
-- convention the reference's own `BankFrameBaseButton_OnLoad` uses for its left/right split.
|
||||
self:RegisterForClicks("LeftButtonUp", "RightButtonUp")
|
||||
</OnLoad>
|
||||
<OnClick>BenillaQuestLogTitle_OnClick(self, arg1)</OnClick>
|
||||
|
||||
@@ -31,16 +31,16 @@
|
||||
- The ref's shift-click "insert the item link into the chat editbox" (StackSplitFrame isn't
|
||||
even opened in that branch — ContainerFrame.lua:568-569 diverts to
|
||||
`ChatFrameEditBox:Insert` instead of `OpenStackSplitFrame`) never triggers here: the
|
||||
shift-click gesture itself has no plumbing in this engine (BagFrame.xml's own comment on
|
||||
`BenillaBagSlot_OnClick` — no modifier-key global exists at all), so there is no path that
|
||||
shift-click gesture itself has no plumbing in this engine (no modifier-key global exists
|
||||
at all), so there is no path that
|
||||
reaches this branch to defer. `EditBox:Insert` DOES exist on our engine (editbox.rs) for
|
||||
when that gap closes.
|
||||
-->
|
||||
<Script><![CDATA[
|
||||
-- OpenStackSplitFrame(maxStack, owner, anchor, anchorTo) — ref StackSplitFrame.lua:2-24,
|
||||
-- verbatim shape. `owner` must already carry `owner.SplitStack = function(owner, n) ... end`
|
||||
-- (BagFrame.xml's `BenillaBagSlot_OnClick` sets it right before calling this, mirroring
|
||||
-- ContainerFrameItemButton_OnClick's own closure). maxStack < 2 hides instead of showing (a
|
||||
-- (the reference's own `ContainerFrameItemButton_OnClick` and `BankFrameItemButton_OnLoad`
|
||||
-- both set it, and both are sourced since 1751). maxStack < 2 hides instead of showing (a
|
||||
-- 1-stack has nothing to split) — the ref's own guard.
|
||||
function OpenStackSplitFrame(maxStack, owner, anchor, anchorTo)
|
||||
local frame = StackSplitFrame
|
||||
|
||||
@@ -1624,6 +1624,36 @@
|
||||
hideOnEscape = 1,
|
||||
}
|
||||
|
||||
-- The bank-slot purchase confirm (ref StaticPopup.lua l.47-60), transcribed verbatim.
|
||||
--
|
||||
-- **It is HERE because its window is the reference's own file now** (1751). Every other
|
||||
-- dialog in this client is declared by the window that shows it; the bank window is
|
||||
-- `Interface\FrameXML\BankFrame.xml`, executed off the player's chain, and the reference
|
||||
-- keeps this entry in `StaticPopup.lua` — the one file benilla replaces with the engine
|
||||
-- above. So the house's StaticPopup home carries it, and `BankFramePurchaseButton`'s
|
||||
-- `StaticPopup_Show("CONFIRM_BUY_BANK_SLOT")` finds it. Left out, the Purchase button
|
||||
-- would be a silent no-op.
|
||||
--
|
||||
-- The coin row is real (1580 made `hasMoneyFrame` live): `BankFrame.nextSlotCost` is set
|
||||
-- by the reference's own `UpdateBagSlotStatus` off `GetBankSlotCost`, and this OnShow is
|
||||
-- the reference's own line for reading it.
|
||||
CONFIRM_BUY_BANK_SLOT = CONFIRM_BUY_BANK_SLOT
|
||||
or "Do you want to purchase a bank slot for:"
|
||||
StaticPopupDialogs["CONFIRM_BUY_BANK_SLOT"] = {
|
||||
text = CONFIRM_BUY_BANK_SLOT,
|
||||
button1 = YES,
|
||||
button2 = NO,
|
||||
OnAccept = function()
|
||||
PurchaseSlot()
|
||||
end,
|
||||
OnShow = function()
|
||||
MoneyFrame_Update(this:GetName() .. "MoneyFrame", BankFrame.nextSlotCost)
|
||||
end,
|
||||
hasMoneyFrame = 1,
|
||||
timeout = 0,
|
||||
hideOnEscape = 1,
|
||||
}
|
||||
|
||||
-- ref UIParent.lua l.324-343 + l.356-360, the four arms on one driver. Each equip arm
|
||||
-- hides the OTHER dialog first — the two questions are mutually exclusive by construction
|
||||
-- (one item, one destination), and the hide is what cancels the record the new question
|
||||
|
||||
@@ -426,16 +426,33 @@ ItemTextFrame.xml
|
||||
# v1 uses plain-text costs, not MerchantFrame's coin helpers) and before the other NPC
|
||||
# session windows below.
|
||||
TrainerFrame.xml
|
||||
# The bank window (decision 0604 phase 4): the vault, purchase ladder, and the 6 bank-bag
|
||||
# popouts, over the container feed already carrying container -1/5..10 (ui_items.rs) +
|
||||
# the bank Lua surface (benilla-ui/src/script/bank.rs) + crate::ui_bank's feed/drain.
|
||||
# Loaded AFTER MerchantFrame.xml (reuses its BenillaMoney_* coin helpers for the purchase
|
||||
# row + purse) and AFTER BagFrame.xml, already above (its BenillaBagSlotTemplate/
|
||||
# BenillaBagWindowTemplate + the BenillaBagSlot_*/BenillaBagFrame_* function family are
|
||||
# reused verbatim — the 24 generic slots and the 6 bank-bag popouts are, respectively, a
|
||||
# container-slot grid and ordinary container windows, exactly like the bag arc's own).
|
||||
# Kept in the "NPC session windows grouped" cluster beside Trainer/Taxi.
|
||||
BankFrame.xml
|
||||
# ══ THE BANK WINDOW — the reference's own, off the player's chain (decision 1751, window 2) ══
|
||||
# `BankFrame`, `BankFrameItem1..24`, `BankFrameBag1..6`, `BankFramePurchaseInfo` and the
|
||||
# `BANK_CONTAINER`/`NUM_BAG_SLOTS`/`NUM_BANKGENERIC_SLOTS`/`NUM_BANKBAGSLOTS` constants — the real
|
||||
# 1.12 file, running as the file, with `BankFrame.lua` arriving through its own `<Script file=>`.
|
||||
# Our own 890-line BankFrame.xml is gone with it, and so are its six popout windows: a bank bag is
|
||||
# an ordinary `ContainerFrame` at container id 5..10, which is what the reference always did.
|
||||
#
|
||||
# THE POSITION IS THE REQUIREMENT, as it was for ContainerFrame.xml. This file's `inherits=` reach
|
||||
# four templates above it — `ItemButtonTemplate`, `UIPanelCloseButton` (UIPanelTemplates.xml),
|
||||
# `UIPanelButtonTemplate` (same), `SmallMoneyFrameTemplate` (MoneyFrame.xml) — plus
|
||||
# `parent="UIParent"`, and its `OnHide` calls `updateContainerFrameAnchors` out of
|
||||
# ContainerFrame.lua. All are loaded above.
|
||||
#
|
||||
# What it needed from this client, all built for it: `PutItemInBag`/`PickupBagFromSlot` (the two
|
||||
# verbs its bag buttons ARE), the bank-bag inventory band at live ids 64..=69 that gives those
|
||||
# verbs an item to move, `GetInventorySlotInfo("Bag1".."Bag6")` for the empty-slot art, and
|
||||
# `StaticPopupDialogs["CONFIRM_BUY_BANK_SLOT"]` re-homed into UiPanels.xml (the reference keeps it
|
||||
# in StaticPopup.lua, which this client replaces).
|
||||
#
|
||||
# One reference quirk carried rather than fixed: `BankFramePurchaseButton` is declared
|
||||
# `virtual="true"` INSIDE `<Frames>`. Only a top-level element is ever a template — nested nodes
|
||||
# are materialized regardless (`framexml.rs`) — so the button exists and works, which is what the
|
||||
# real client does too. Classic Era's Vanilla BankFrame.xml still carries the same attribute on
|
||||
# the same button, and that button works there.
|
||||
#
|
||||
# Kept in the "NPC session windows grouped" cluster beside Trainer/Taxi, and after MerchantFrame.
|
||||
Interface\FrameXML\BankFrame.xml
|
||||
# The taxi-map window (decision 0484 phase 2): the flight-master session, driven by the
|
||||
# taxi engine seam (`benilla-ui/src/script/taxi.rs`) over `crate::ui_taxi`'s feed/drain.
|
||||
# Loaded right after TrainerFrame per the same "NPC session windows grouped" posture — it
|
||||
|
||||
@@ -189,7 +189,7 @@ fn surface_report(deep: bool, dump_path: Option<String>) {
|
||||
//
|
||||
// Split again on the `Benilla` prefix, which is the difference between a name that CAN collide
|
||||
// with an addon's expectations and one that cannot. Nothing in the 1.12 corpus feature-tests
|
||||
// `BenillaBagSlot_OnClick`; our own namespace is safe by construction, and leaving 443 of those
|
||||
// `BenillaPaperDollSlot_OnClick`; our own namespace is safe by construction, and leaving 443 of those
|
||||
// in the list would bury the ones that are not.
|
||||
if let Some(fns) = extra_by_kind.get("function") {
|
||||
let (ours_ns, unprefixed): (Vec<&&str>, Vec<&&str>) = fns
|
||||
|
||||
@@ -341,18 +341,20 @@ pub(super) fn seed_ui_fixture(
|
||||
crate::net::SelfPlayer,
|
||||
crate::net::Guid(PLAYER_GUID),
|
||||
));
|
||||
// Open the session — `feed_bank` fires BANKFRAME_OPENED (title + portrait + the 0561
|
||||
// OpenBackpack contract) on the next frames exactly as a live SMSG_SHOW_BANK would.
|
||||
// Open the session — `feed_bank` fires BANKFRAME_OPENED (title + portrait) on the
|
||||
// next frames exactly as a live SMSG_SHOW_BANK would.
|
||||
bank.open(NPC_GUID);
|
||||
// One-shot popout: click bank bag 1 open as soon as its container feed lands (the
|
||||
// click path needs `GetContainerNumSlots(5) > 0`, which arrives over the settle
|
||||
// frames — the window's OnUpdate polls, flag-guarded, exactly once).
|
||||
// One-shot popout: open bank bag 1 as soon as its container feed lands (the click
|
||||
// path needs `GetContainerNumSlots(5) > 0`, which arrives over the settle frames —
|
||||
// the window's OnUpdate polls, flag-guarded, exactly once). `BankFrameBag1`'s own
|
||||
// `GetID()` is 5, the container id, which is what its handler passes to `ToggleBag`.
|
||||
if let Some(s) = script.as_mut() {
|
||||
if let Err(e) = s.run(
|
||||
"BankFrame:SetScript(\"OnUpdate\", function()\n\
|
||||
if not benillaBankPopoutDone and GetContainerNumSlots(5) > 0 then\n\
|
||||
benillaBankPopoutDone = 1\n\
|
||||
BenillaBankBagButton_OnClick(getglobal(\"BankBagButton1\"))\n\
|
||||
this = getglobal(\"BankFrameBag1\")\n\
|
||||
BankFrameItemButtonBag_OnClick(\"LeftButton\")\n\
|
||||
end\n\
|
||||
end)",
|
||||
) {
|
||||
@@ -742,11 +744,21 @@ pub(super) fn seed_ui_fixture(
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
// Force the tooltip open over the top-left bag button (BenillaBagSlot16 ⇒ game slot 1,
|
||||
// the green Small Shield) via the same OnEnter path a hover fires — deterministic, no
|
||||
// synthetic mouse. A top-left seat keeps the ANCHOR_RIGHT tooltip on-screen; the
|
||||
// engine's auto-size pass settles it over the capture window.
|
||||
if let Err(e) = script.run("BenillaBagSlot_OnEnter(getglobal(\"BenillaBagSlot16\"))") {
|
||||
// Force the tooltip open over the top-left bag button (`Item16` ⇒ game slot 1, the
|
||||
// green Small Shield — see `seed_bag_window` on the backwards numbering) via the same
|
||||
// OnEnter path a hover fires — deterministic, no synthetic mouse. A top-left seat keeps
|
||||
// the ANCHOR_RIGHT tooltip on-screen; the engine's auto-size pass settles it over the
|
||||
// capture window.
|
||||
//
|
||||
// **Which window the backpack landed in is ASKED, never assumed**: the reference
|
||||
// recycles twelve `ContainerFrame`s across every container, so the index depends on
|
||||
// what else is open. `IsBagOpen` is its own published scan.
|
||||
if let Err(e) = script.run(
|
||||
"local i = IsBagOpen(0)\n\
|
||||
if i then\n\
|
||||
ContainerFrameItemButton_OnEnter(getglobal(\"ContainerFrame\"..i..\"Item16\"))\n\
|
||||
end",
|
||||
) {
|
||||
warn!("capture: ui-tooltip seed failed to open the tooltip: {e}");
|
||||
}
|
||||
}
|
||||
@@ -1435,9 +1447,11 @@ fn seed_bag_window(
|
||||
};
|
||||
// ~5 items scattered across the 16 slots, a mix of stacked and unstacked, under their real
|
||||
// vanilla template ids (the Tooltip fixture seeds a full template view for the hovered one).
|
||||
// Game slot 1 (rendered top-left, button BenillaBagSlot16) carries the green-quality Small
|
||||
// Shield the Tooltip fixture hovers — a top-left seat keeps its ANCHOR_RIGHT tooltip
|
||||
// on-screen.
|
||||
// Game slot 1 carries the green-quality Small Shield the Tooltip fixture hovers, and it is
|
||||
// rendered TOP-LEFT: `ContainerFrame_GenerateFrame` numbers backwards (`index = size - j + 1`)
|
||||
// and anchors `Item1` at the window's BOTTOMRIGHT, filling leftwards then upwards — so for a
|
||||
// 16-slot bag the top-left button is `Item16` and it carries slot 1. A top-left seat keeps its
|
||||
// ANCHOR_RIGHT tooltip on-screen.
|
||||
let mut slots = std::collections::HashMap::new();
|
||||
slots.insert(1, slot(DISP_SHIELD, 1, 2362, "Small Shield", 2));
|
||||
slots.insert(3, slot(DISP_FOOD, 5, 117, "Tough Jerky", 1));
|
||||
@@ -1454,8 +1468,11 @@ fn seed_bag_window(
|
||||
);
|
||||
// Player money nonzero so the purse renders all three denominations (1g 23s 45c).
|
||||
script.set_money(12_345);
|
||||
// Open + paint the window (BenillaBagFrame_Update fills the slots + the purse).
|
||||
if let Err(e) = script.run("BenillaBagFrame_Update(); getglobal(\"BenillaBagFrame\"):Show()") {
|
||||
// Open the window with the reference's own verb — it builds and paints one in the same call
|
||||
// (`ToggleBag` → `OpenBag` → `ContainerFrame_GenerateFrame`), which is why nothing has to
|
||||
// repaint it afterwards. `OpenBag(0)` rather than `OpenBackpack()`: only the backpack is fed
|
||||
// here, and 0561 makes the latter open every equipped bag.
|
||||
if let Err(e) = script.run("OpenBag(0)") {
|
||||
warn!("capture: bag window seed failed: {e}");
|
||||
}
|
||||
|
||||
@@ -1591,9 +1608,9 @@ fn seed_equipped_bags(
|
||||
slots,
|
||||
}),
|
||||
);
|
||||
if let Err(e) = script.run(&format!(
|
||||
"local f = getglobal(\"BenillaBagFrame{bag_id}\"); BenillaBagWindow_Update(f); f:Show()"
|
||||
)) {
|
||||
// The reference's own verb, which builds the window as it opens it — and it must run
|
||||
// AFTER `set_container`, since `OpenBag` refuses a container with no slots.
|
||||
if let Err(e) = script.run(&format!("OpenBag({bag_id})")) {
|
||||
warn!("capture: equipped bag {bag_id} seed failed: {e}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -991,6 +991,16 @@ pub(crate) enum ClientCommand {
|
||||
src_bag: u8,
|
||||
src_slot: u8,
|
||||
},
|
||||
/// Auto-store an item into a bag (`CMSG_AUTOSTORE_BAG_ITEM`): take `(src_bag, src_slot)` and
|
||||
/// put it anywhere inside `dst_bag` — **the client names no destination slot** (wow-re
|
||||
/// `bag-verbs-law.md`: AUTOSTORE carries none). Sent by `PutItemInBag`'s third leg (an
|
||||
/// ordinary item dropped on an occupied bag button) and by all of `PutItemInBackpack`.
|
||||
/// Refusals surface as `InventoryFailure` events.
|
||||
AutoStoreBagItem {
|
||||
src_bag: u8,
|
||||
src_slot: u8,
|
||||
dst_bag: u8,
|
||||
},
|
||||
/// Carry a partial stack from one bag position to another (`CMSG_SPLIT_ITEM`, decision 0216
|
||||
/// §6) — either endpoint may be an equipped bag, unlike [`ClientCommand::SwapInvItem`]. The
|
||||
/// container-move drain sends this instead of a swap when the queued move carries a split
|
||||
|
||||
@@ -758,6 +758,11 @@ fn writer_loop(
|
||||
src_bag,
|
||||
src_slot,
|
||||
} => w.swap_item(dst_bag, dst_slot, src_bag, src_slot),
|
||||
ClientCommand::AutoStoreBagItem {
|
||||
src_bag,
|
||||
src_slot,
|
||||
dst_bag,
|
||||
} => w.auto_store_bag_item(src_bag, src_slot, dst_bag),
|
||||
ClientCommand::SplitItem {
|
||||
src_bag,
|
||||
src_slot,
|
||||
|
||||
@@ -225,9 +225,25 @@ pub(super) fn control(
|
||||
})
|
||||
})
|
||||
.unwrap_or((false, false));
|
||||
// The shared precondition `0x5144e0`, assembled once for this tick the way the reference
|
||||
// evaluates it once for the mover — health above, and the far-sight conjunct here.
|
||||
//
|
||||
// **Conjunct 5** is `!(IsActivePlayer(mover) && [mover+0x1c70] & 1)`: while your view is out on
|
||||
// a far-sight object you may not drive your own body. Both halves matter. The latch half is the
|
||||
// **resolved** subject and not the raw `PLAYER_FARSIGHT` field, because `0x5ee290` sets the
|
||||
// latch only on its post-resolve ENGAGE leg; the active-player half is `foreign_mover.is_none()`,
|
||||
// and it is what keeps Mind Control working — possession sets the very same field, so without
|
||||
// it the victim would be frozen (wow-re §6.2 and `farsight-and-client-control.md` §2.1).
|
||||
let mover = state::MoverInput {
|
||||
dead,
|
||||
view_is_out: state::view_is_out(
|
||||
player.foreign_mover.is_none(),
|
||||
view_subject.remote.is_some(),
|
||||
),
|
||||
};
|
||||
// `0x5145b0`, evaluated here because the first thing it suppresses is the mouse turn below. Its
|
||||
// translate sibling waits until after `apply_server_moves`, where the root edge it reads lands.
|
||||
let may_turn = state::may_turn(dead, stunned);
|
||||
let may_turn = mover.may_turn(stunned);
|
||||
// Drunkenness (B210): this frame's wobble angle, computed once — the facing veer and the
|
||||
// swim-pitch porpoise ([`swim::drive_step`]) both read it. Zero while sober (`wobble` early-outs on a 0.0
|
||||
// fraction), and zero whenever the turn predicate is down — the reference's wobble sits behind
|
||||
@@ -447,7 +463,7 @@ pub(super) fn control(
|
||||
// latch with its verified cancel set — all of it decoded once in [`input::move_axes`].
|
||||
// `0x514560`, evaluated now — after `apply_server_moves`, so a root edge that landed this
|
||||
// frame is already in `player.modes` and not read a frame late.
|
||||
let may_translate = state::may_translate(dead, player.modes.rooted);
|
||||
let may_translate = mover.may_translate(player.modes.rooted);
|
||||
let axes = input::move_axes(
|
||||
binds,
|
||||
&buttons,
|
||||
|
||||
@@ -259,9 +259,17 @@ pub(super) struct FollowInput<'w, 's> {
|
||||
/// which is deliberate and harmless: the cancel then lands on the frame *after* the look
|
||||
/// begins, versus the reference's same-frame commit.
|
||||
rig: Res<'w, CameraControl>,
|
||||
/// The **mover's** descriptor block, for the one term of the cancel set that is not an input:
|
||||
/// its health. See [`FollowInput::mover_dead`].
|
||||
/// The **mover's** descriptor block, for the terms of the cancel set that are not inputs:
|
||||
/// its health and its stun. See [`FollowInput::input_torn_down`].
|
||||
mover: Query<'w, 's, &'static crate::net::ObjectStore, With<crate::net::Embodied>>,
|
||||
/// What the camera orbits ([`super::view_subject`]) — the resolved half of the shared
|
||||
/// precondition's far-sight conjunct, which takes both predicates down and so reaches the
|
||||
/// teardown leg on its own (decision 1761).
|
||||
///
|
||||
/// Read possibly one frame behind, like `rig` above and for the same benign reason: the
|
||||
/// publisher and this system are both merely `.before(control)` and are unordered against each
|
||||
/// other, so a follow engaged and far-sighted on the very same frame ends on the next one.
|
||||
view_subject: Res<'w, super::view_subject::ViewSubject>,
|
||||
}
|
||||
|
||||
impl FollowInput<'_, '_> {
|
||||
@@ -284,7 +292,7 @@ impl FollowInput<'_, '_> {
|
||||
}
|
||||
|
||||
/// Has the input tick reached its **teardown leg** — the one that cancels click-to-move and the
|
||||
/// follow and fires `AUTOFOLLOW_END` ([`super::state::input_torn_down`])? Not an input, but the
|
||||
/// follow and fires `AUTOFOLLOW_END` ([`super::state::MoverInput::torn_down`])? Not an input, but the
|
||||
/// member of the cancel set that is a *state*, and the one benilla had wrong in both directions.
|
||||
///
|
||||
/// `rf86-autofollow-cancel-set.md` §5 named the mechanism as the health test at `0x5144f8`, and
|
||||
@@ -293,13 +301,20 @@ impl FollowInput<'_, '_> {
|
||||
/// so a pure ROOT — which benilla cancelled on — does **not** end a follow in the reference, and
|
||||
/// death — which benilla did not cancel on, so a follow taken into death kept steering the
|
||||
/// corpse's facing every frame — does. Ice Block, root and stun together, ends it as well.
|
||||
fn input_torn_down(&self, rooted: bool) -> bool {
|
||||
///
|
||||
/// **Far sight ends it too**, and for the same structural reason death does rather than as a
|
||||
/// case of its own: the far-sight conjunct sits in the precondition the two predicates *share*,
|
||||
/// so engaging it takes both down at once and lands on this leg.
|
||||
fn input_torn_down(&self, rooted: bool, driving_own_body: bool) -> bool {
|
||||
self.mover.single().is_ok_and(|s| {
|
||||
super::state::input_torn_down(
|
||||
s.0.unit_is_dead(),
|
||||
rooted,
|
||||
s.0.unit_flags() & super::UNIT_FLAG_STUNNED != 0,
|
||||
)
|
||||
super::state::MoverInput {
|
||||
dead: s.0.unit_is_dead(),
|
||||
view_is_out: super::state::view_is_out(
|
||||
driving_own_body,
|
||||
self.view_subject.remote.is_some(),
|
||||
),
|
||||
}
|
||||
.torn_down(rooted, s.0.unit_flags() & super::UNIT_FLAG_STUNNED != 0)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -341,7 +356,8 @@ pub(super) fn steer_follow(
|
||||
// is benilla's own and predates this: it is not one of `0x5144e0`'s conjuncts (conjunct 4 is
|
||||
// the Knockdown lockout, not a taxi test — wow-re §6.2), and nothing in this round bears on
|
||||
// it either way, so it is left exactly as it stood.
|
||||
input.input_torn_down(player.modes.rooted) || player.server_riding(),
|
||||
input.input_torn_down(player.modes.rooted, player.foreign_mover.is_none())
|
||||
|| player.server_riding(),
|
||||
) {
|
||||
info!("follow: cancelled by the player's own movement input");
|
||||
follow.stop();
|
||||
|
||||
@@ -473,93 +473,147 @@ impl MoveModes {
|
||||
/// `0x5144fd`. (The field identity is **VERIFIED** three independent ways in §6.1 — the
|
||||
/// descriptor-base arithmetic, the `UnitHealth` registrar pair `.data 0x850510 → 0x5174d0`, and
|
||||
/// the low-health warning's `fild [eax+0x40]; fidiv [eax+0x58]`. `rf79`'s INFERRED label is
|
||||
/// retired.)
|
||||
/// retired.) — the [`MoverInput::dead`] field;
|
||||
/// 3. the CMovement-aux gate `[[mover+0x118]+0xa4]` null-or-`&4` (`0x514516`);
|
||||
/// 4. `!0x60f5b0(obj)` — the **KNOCKDOWN animation lockout**, and *not* an on-taxi predicate: it
|
||||
/// returns `AnimationData.dbc` column 3 bit `0x80`, which the shipped table sets on exactly one
|
||||
/// of 208 rows, id 121 `Knockdown` (§6.2's census). It contributes nothing while dead;
|
||||
/// 5. `!(IsActivePlayer(mover) && [mover+0x1c70] & 1)` — the **far-sight ENGAGED latch**
|
||||
/// (`0x5ee3f6` sets it, `0x5ee4c8` clears it, both inside the far-sight machine `0x5ee290`), and
|
||||
/// *not* a charm bit: **while your view is out on a far-sight object you may not drive your own
|
||||
/// body** — though you may still drive a *possessed* one, since `IsActivePlayer` is then false.
|
||||
/// 5. `!(IsActivePlayer(mover) && [mover+0x1c70] & 1)` — the **far-sight ENGAGED latch**, the
|
||||
/// [`MoverInput::view_is_out`] field.
|
||||
///
|
||||
/// Conjuncts 4 and 5 are **not modelled here** — named, verified, and unbuilt (decision 1753's tail);
|
||||
/// they are their own behaviours with their own retests, not part of the death gate. Conjunct 1 is
|
||||
/// structural: [`super::controller::control`] returns before the axes on `control_lost` and on
|
||||
/// `reseat`, the frames where the mover would not resolve. Conjunct 3 has no benilla analog yet.
|
||||
/// **Conjuncts 1, 3 and 4 have no field here.** Conjunct 1 is structural:
|
||||
/// [`super::controller::control`] returns before the axes on `control_lost` and on `reseat`, the
|
||||
/// frames where the mover would not resolve. Conjunct 3 has no benilla analog yet. Conjunct 4 is
|
||||
/// named, verified and unbuilt — its own behaviour with its own retest — and this struct is where
|
||||
/// it lands when it is built.
|
||||
///
|
||||
/// **The health term is the one this function exists for.** benilla modelled the server's root on
|
||||
/// death (0308) and nothing else, and a root deliberately leaves turning live (0872), so a corpse on
|
||||
/// the ground could still be spun with the turn keys or a right-drag. That is decision 1753.
|
||||
///
|
||||
/// `dead` is health `== 0` off the **mover's** descriptor
|
||||
/// ([`benilla_protocol::messages::update_object::ObjectFields::unit_is_dead`]) — the mover's and
|
||||
/// not ours, because `0x5144e0` reads `[esi+0x110]` where `esi` is whatever we are driving
|
||||
/// (decision 1277's possessed creature). **A ghost is not dead by this test:** the server sets a
|
||||
/// released player's health to 1 (0308 §the release), so `0x5144fd`'s `jg` is taken and the ghost
|
||||
/// gets every input back. Nothing in the input path, the emitters or the send gates reads the ghost
|
||||
/// bit at all — a band census finds **zero** `PLAYER`-block reads in the whole InputControl and
|
||||
/// CMovement address ranges (§6.6). The ghost bit *is* read in the collision layer (it adds trace
|
||||
/// mask bit `0x8000` at `0x631658`), which is a separate, unbuilt finding.
|
||||
fn mover_input_ready(dead: bool) -> bool {
|
||||
!dead
|
||||
/// Carried as a **value** rather than as loose booleans threaded through each predicate because
|
||||
/// that is the reference's own shape (one precondition, evaluated once for the mover, handed to
|
||||
/// both), and because two same-typed terms passed positionally would transpose silently.
|
||||
#[derive(Clone, Copy, Debug, Default)]
|
||||
pub(crate) struct MoverInput {
|
||||
/// **Conjunct 2, the health term** — health `== 0` off the **mover's** descriptor
|
||||
/// ([`benilla_protocol::messages::update_object::ObjectFields::unit_is_dead`]). The mover's and
|
||||
/// not ours, because `0x5144e0` reads `[esi+0x110]` where `esi` is whatever we are driving
|
||||
/// (decision 1277's possessed creature).
|
||||
///
|
||||
/// **This is the term the struct exists for.** benilla modelled the server's root on death
|
||||
/// (0308) and nothing else, and a root deliberately leaves turning live (0872), so a corpse on
|
||||
/// the ground could still be spun with the turn keys or a right-drag — decision 1753.
|
||||
///
|
||||
/// **A ghost is not dead by this test:** the server puts a released player's health at 1
|
||||
/// (0308 §the release), so `0x5144fd`'s `jg` is taken and the ghost gets every input back.
|
||||
/// Nothing in the input path, the emitters or the send gates reads the ghost bit at all — a
|
||||
/// band census finds **zero** `PLAYER`-block reads across the whole InputControl and CMovement
|
||||
/// ranges (§6.6). The ghost bit *is* read in the collision layer, where it adds trace mask bit
|
||||
/// `0x8000` (`0x631658`) — a separate finding, and not this one.
|
||||
pub(crate) dead: bool,
|
||||
/// **Conjunct 5, the far-sight term** — `!(IsActivePlayer(mover) && [mover+0x1c70] & 1)`, held
|
||||
/// here in the affirmative: *my view is out on a far-sight object, and the body I would be
|
||||
/// driving is my own*. While that holds the reference refuses **both** predicates — you may
|
||||
/// neither walk nor turn your own body while you are looking through Mind Vision. (wow-re
|
||||
/// §6.2. Both of this conjunct's labels were CORRECTED on 2026-08-31: they previously read
|
||||
/// "charmed", and had never been derived.)
|
||||
///
|
||||
/// **Both halves are load-bearing, and `IsActivePlayer` is the one that would be easy to
|
||||
/// drop.** `0x514537 call 0x5fa6d0` tests the *mover* against the active player and returns the
|
||||
/// conjunct satisfied the moment they differ (`0x51453e je 0x514550`), so the latch is only
|
||||
/// ever read for our own body. Mind Control rides the same `PLAYER_FARSIGHT` field the camera
|
||||
/// does ([`super::view_subject`]), so during a possession the latch is engaged — and without
|
||||
/// this half the victim would freeze solid, which is the one body the whole spell exists to
|
||||
/// walk around. It is also why the latch is never read off a creature's descriptor, where
|
||||
/// `+0x1c70` is not a field at all.
|
||||
///
|
||||
/// **Engaged means the subject RESOLVED, not merely that the field arrived.** The far-sight
|
||||
/// machine `0x5ee290` sets the latch only on its ENGAGE leg (`0x5ee3f6 or ecx,1`), reached
|
||||
/// after `0x5ee2ef` resolves the guid; the *field non-zero but object unresolved* leg falls
|
||||
/// through to `0x5ee32d` — camera home, pending cast cancelled, `return 0` — and sets nothing
|
||||
/// (wow-re `object-layer/scratch/farsight-and-client-control.md` §2.1). So the benilla analog
|
||||
/// is the **resolved** [`super::view_subject::ViewSubject`] subject, not the raw
|
||||
/// `PLAYER_FARSIGHT` field: through the ~400 ms vmangos defers the set by, the body is still
|
||||
/// ours to drive.
|
||||
pub(crate) view_is_out: bool,
|
||||
}
|
||||
|
||||
/// **`0x514560` — "may this unit translate?"**, the ROOT predicate. Consumed by the input tick at
|
||||
/// `0x5146c1` (`test bl,bl`), the sole gate on the forward/back emitter `0x514da0` and the strafe
|
||||
/// emitter `0x514e80`. Its own terms past the shared precondition are `MOVEMENTFLAGS & 0x1200`
|
||||
/// (`0x51458c test dh,0x12` — our `rooted`) and stand state `!= 7` (`0x514591`), which vmangos
|
||||
/// never writes and which is therefore inert here.
|
||||
/// **Conjunct 5's two halves, composed** — `IsActivePlayer(mover) && [mover+0x1c70] & 1`, the
|
||||
/// value [`MoverInput::view_is_out`] carries and the field's doc explains in full.
|
||||
///
|
||||
/// Note the health term is tested **twice** — once inside `0x5144e0` and again at `0x51457c` on the
|
||||
/// predicate's own path. Death is not a corner of this gate; it is its first question.
|
||||
pub(crate) fn may_translate(dead: bool, rooted: bool) -> bool {
|
||||
mover_input_ready(dead) && !rooted
|
||||
/// A named function and not an inline `&&` at the one call site, because the half that is easy to
|
||||
/// lose is `driving_own_body`: drop it and Mind Control freezes its victim, which is a bug nothing
|
||||
/// about far sight would lead you to look for.
|
||||
///
|
||||
/// - `driving_own_body` — the mover is our own character, i.e. no possession is in force
|
||||
/// (`0x514537 call 0x5fa6d0`, the active-player test on the *mover*).
|
||||
/// - `far_sight_engaged` — our view is out on a far-sight object we have actually **resolved**
|
||||
/// (`[mover+0x1c70] & 1`, which the machine `0x5ee290` sets only on its post-resolve ENGAGE leg).
|
||||
pub(crate) fn view_is_out(driving_own_body: bool, far_sight_engaged: bool) -> bool {
|
||||
driving_own_body && far_sight_engaged
|
||||
}
|
||||
|
||||
/// **`0x5145b0` — "is this unit not stunned?"**, the STUN predicate. Consumed at `0x514755`
|
||||
/// (`je 0x51479c`), which skips the turn emitter `0x514f50` and the pitch emitter `0x515010`
|
||||
/// outright *and* force-stops either already in flight. Each emitter has exactly one caller,
|
||||
/// immediately behind that gate, and no data or vtable reference anywhere in the image — so while
|
||||
/// this predicate is false **no keyboard or mouse turn can reach `CMovement::StartTurn 0x7c6d90`
|
||||
/// by any route** (VERIFIED, wow-re `local-move-input-gate.md` §4).
|
||||
///
|
||||
/// Its own term past the shared precondition is `UNIT_FIELD_FLAGS & 0x40000` — a descriptor read,
|
||||
/// not an aura and not a movement flag (decision 0872). **And because the precondition is shared,
|
||||
/// a dead body is "stunned" as far as this predicate is concerned**: that single fact is why the
|
||||
/// reference refuses to turn a corpse.
|
||||
///
|
||||
/// **The MOUSE turn reaches the same answer down a different path** (§6.4, and this corrects what
|
||||
/// 1753 first wrote). The right-drag handler is `0x514400`, called from the mouse-MOVE handler
|
||||
/// `0x492c00`; its body hand-off `0x51447b call 0x5103e0` is gated at `0x514474` by a *third*
|
||||
/// predicate, **`0x5145e0`**, whose first act is to call this one — so the health term arrives
|
||||
/// through `0x5145b0` → `0x5144e0` and short-circuits before `0x5145e0` reaches its own remaining
|
||||
/// conjuncts (`[input+4] & 1`, and `GetStandState() == 0` via the CGPlayer vtable slot `0x5ed570`).
|
||||
/// The refusal is then **triple-redundant**: the two downstream commit gates `0x5151b0` (yaw, at
|
||||
/// `0x5151e6`) and `0x515250` (pitch, at `0x515283`) each carry their own health test. A closed
|
||||
/// census of all nine call sites of the body-facing setters `0x60de30`/`0x60de70` — neither of which
|
||||
/// has any dword reference image-wide — finds every one health-gated.
|
||||
///
|
||||
/// So one term in one place really does fix the keys and the mouse together, but not because they
|
||||
/// share `0x514755`: they share `0x5144e0`.
|
||||
pub(crate) fn may_turn(dead: bool, stunned: bool) -> bool {
|
||||
mover_input_ready(dead) && !stunned
|
||||
}
|
||||
impl MoverInput {
|
||||
/// `0x5144e0` itself — every conjunct we model, and the answer both predicates start from.
|
||||
fn ready(self) -> bool {
|
||||
!self.dead && !self.view_is_out
|
||||
}
|
||||
|
||||
/// **The input tick's teardown leg** — `0x5146d6 call 0x60fb60(0, 1)`, which cancels click-to-move
|
||||
/// and `/follow` and fires `AUTOFOLLOW_END` (event `0x170`).
|
||||
///
|
||||
/// It is reached only when **BOTH** predicates are down: `0x5146c3 jne` not taken (`bl == 0`) *and*
|
||||
/// `0x5146ce jne` not taken (`[ebp+0xf] == 0`). That is the correction wow-re's §6.3 makes to
|
||||
/// `rf86-autofollow-cancel-set.md` §5, which named the health test alone — necessary, but not enough
|
||||
/// to name the leg. **A pure ROOT does not cancel a follow** (translate down, turn still up), and
|
||||
/// neither does a pure stun; **death does**, because it takes both. Ice Block, which grants root and
|
||||
/// stun together, does too.
|
||||
///
|
||||
/// benilla had the root alone in this position and so cancelled on a Frost Nova, which the reference
|
||||
/// does not (decision 1753).
|
||||
pub(crate) fn input_torn_down(dead: bool, rooted: bool, stunned: bool) -> bool {
|
||||
!may_translate(dead, rooted) && !may_turn(dead, stunned)
|
||||
/// **`0x514560` — "may this unit translate?"**, the ROOT predicate. Consumed by the input tick
|
||||
/// at `0x5146c1` (`test bl,bl`), the sole gate on the forward/back emitter `0x514da0` and the
|
||||
/// strafe emitter `0x514e80`. Its own terms past the shared precondition are `MOVEMENTFLAGS &
|
||||
/// 0x1200` (`0x51458c test dh,0x12` — our `rooted`) and stand state `!= 7` (`0x514591`), which
|
||||
/// vmangos never writes and which is therefore inert here.
|
||||
///
|
||||
/// Note the health term is tested **twice** — once inside `0x5144e0` and again at `0x51457c` on
|
||||
/// the predicate's own path. Death is not a corner of this gate; it is its first question.
|
||||
pub(crate) fn may_translate(self, rooted: bool) -> bool {
|
||||
self.ready() && !rooted
|
||||
}
|
||||
|
||||
/// **`0x5145b0` — "is this unit not stunned?"**, the STUN predicate. Consumed at `0x514755`
|
||||
/// (`je 0x51479c`), which skips the turn emitter `0x514f50` and the pitch emitter `0x515010`
|
||||
/// outright *and* force-stops either already in flight. Each emitter has exactly one caller,
|
||||
/// immediately behind that gate, and no data or vtable reference anywhere in the image — so
|
||||
/// while this predicate is false **no keyboard or mouse turn can reach `CMovement::StartTurn
|
||||
/// 0x7c6d90` by any route** (VERIFIED, wow-re `local-move-input-gate.md` §4).
|
||||
///
|
||||
/// Its own term past the shared precondition is `UNIT_FIELD_FLAGS & 0x40000` — a descriptor
|
||||
/// read, not an aura and not a movement flag (decision 0872). **And because the precondition is
|
||||
/// shared, a dead body is "stunned" as far as this predicate is concerned**: that single fact is
|
||||
/// why the reference refuses to turn a corpse.
|
||||
///
|
||||
/// **The MOUSE turn reaches the same answer down a different path** (§6.4, and this corrects
|
||||
/// what 1753 first wrote). The right-drag handler is `0x514400`, called from the mouse-MOVE
|
||||
/// handler `0x492c00`; its body hand-off `0x51447b call 0x5103e0` is gated at `0x514474` by a
|
||||
/// *third* predicate, **`0x5145e0`**, whose first act is to call this one — so the health term
|
||||
/// arrives through `0x5145b0` → `0x5144e0` and short-circuits before `0x5145e0` reaches its own
|
||||
/// remaining conjuncts (`[input+4] & 1`, and `GetStandState() == 0` via the CGPlayer vtable slot
|
||||
/// `0x5ed570`). The refusal is then **triple-redundant**: the two downstream commit gates
|
||||
/// `0x5151b0` (yaw, at `0x5151e6`) and `0x515250` (pitch, at `0x515283`) each carry their own
|
||||
/// health test. A closed census of all nine call sites of the body-facing setters
|
||||
/// `0x60de30`/`0x60de70` — neither of which has any dword reference image-wide — finds every one
|
||||
/// health-gated.
|
||||
///
|
||||
/// So one term in one place really does fix the keys and the mouse together, but not because
|
||||
/// they share `0x514755`: they share `0x5144e0`.
|
||||
pub(crate) fn may_turn(self, stunned: bool) -> bool {
|
||||
self.ready() && !stunned
|
||||
}
|
||||
|
||||
/// **The input tick's teardown leg** — `0x5146d6 call 0x60fb60(0, 1)`, which cancels
|
||||
/// click-to-move and `/follow` and fires `AUTOFOLLOW_END` (event `0x170`).
|
||||
///
|
||||
/// It is reached only when **BOTH** predicates are down: `0x5146c3 jne` not taken (`bl == 0`)
|
||||
/// *and* `0x5146ce jne` not taken (`[ebp+0xf] == 0`). That is the correction wow-re's §6.3 makes
|
||||
/// to `rf86-autofollow-cancel-set.md` §5, which named the health test alone — necessary, but not
|
||||
/// enough to name the leg. **A pure ROOT does not cancel a follow** (translate down, turn still
|
||||
/// up), and neither does a pure stun; **death does**, because it takes both. Ice Block, which
|
||||
/// grants root and stun together, does too. So does the far-sight term, for the same reason
|
||||
/// death does — it is the other conjunct of the shared precondition.
|
||||
///
|
||||
/// benilla had the root alone in this position and so cancelled on a Frost Nova, which the
|
||||
/// reference does not (decision 1753).
|
||||
pub(crate) fn torn_down(self, rooted: bool, stunned: bool) -> bool {
|
||||
!self.may_translate(rooted) && !self.may_turn(stunned)
|
||||
}
|
||||
}
|
||||
|
||||
/// **The two incapacitate suppressions, applied to a freshly built move-flag word** (decision 0880)
|
||||
@@ -593,7 +647,7 @@ pub(crate) fn input_torn_down(dead: bool, rooted: bool, stunned: bool) -> bool {
|
||||
///
|
||||
/// The two arguments are therefore not "rooted" and "stunned" but **the two predicates being down**
|
||||
/// ([`may_translate`], [`may_turn`]) — a wider set by exactly one member: **death drops both**,
|
||||
/// through the precondition they share ([`mover_input_ready`], decision 1753). A corpse streams
|
||||
/// through the precondition they share ([`MoverInput`], decision 1753). A corpse streams
|
||||
/// neither a direction bit nor a turn bit.
|
||||
pub(crate) fn incapacitated_flags(flags: u32, translate_gated: bool, turn_gated: bool) -> u32 {
|
||||
use crate::creature_anim::move_flags as f;
|
||||
@@ -1498,8 +1552,8 @@ mod autorun_tests {
|
||||
#[cfg(test)]
|
||||
mod move_mode_tests {
|
||||
use super::{
|
||||
incapacitated_flags, input_torn_down, may_translate, may_turn, MoveModes, Player,
|
||||
FEATHER_TERMINAL_VELOCITY, GRAVITY, HOVER_CLIMB_RATE, HOVER_HEIGHT, TERMINAL_VELOCITY,
|
||||
incapacitated_flags, view_is_out, MoveModes, MoverInput, Player, FEATHER_TERMINAL_VELOCITY,
|
||||
GRAVITY, HOVER_CLIMB_RATE, HOVER_HEIGHT, TERMINAL_VELOCITY,
|
||||
};
|
||||
use crate::creature_anim::move_flags as f;
|
||||
use benilla_protocol::MoveMode;
|
||||
@@ -1739,7 +1793,13 @@ mod move_mode_tests {
|
||||
#[test]
|
||||
fn death_drops_both_movement_input_predicates() {
|
||||
// (dead, rooted, stunned) -> (may_translate, may_turn)
|
||||
let gate = |dead, rooted, stunned| (may_translate(dead, rooted), may_turn(dead, stunned));
|
||||
let gate = |dead, rooted, stunned| {
|
||||
let m = MoverInput {
|
||||
dead,
|
||||
view_is_out: false,
|
||||
};
|
||||
(m.may_translate(rooted), m.may_turn(stunned))
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
gate(false, false, false),
|
||||
@@ -1777,23 +1837,77 @@ mod move_mode_tests {
|
||||
#[test]
|
||||
fn only_both_predicates_down_tears_the_follow_down() {
|
||||
assert!(
|
||||
!input_torn_down(false, true, false),
|
||||
!MoverInput::default().torn_down(true, false),
|
||||
"a PURE root does NOT end a follow — translate is down, the turn is still up"
|
||||
);
|
||||
assert!(
|
||||
!input_torn_down(false, false, true),
|
||||
!MoverInput::default().torn_down(false, true),
|
||||
"and neither does a pure stun — the emitter never consults `0x5145b0`"
|
||||
);
|
||||
assert!(
|
||||
input_torn_down(true, false, false),
|
||||
MoverInput {
|
||||
dead: true,
|
||||
..Default::default()
|
||||
}
|
||||
.torn_down(false, false),
|
||||
"DEATH ends it, with nothing granted: it takes both predicates down by itself"
|
||||
);
|
||||
assert!(
|
||||
input_torn_down(false, true, true),
|
||||
MoverInput::default().torn_down(true, true),
|
||||
"and so does Ice Block, which is root and stun at once"
|
||||
);
|
||||
}
|
||||
|
||||
/// **Far sight freezes your own body — and never a possessed one** (conjunct 5 of
|
||||
/// `0x5144e0`). While your view is out on a far-sight object you may neither walk nor turn:
|
||||
/// the term sits in the *shared* precondition, so it takes both predicates exactly the way
|
||||
/// death does.
|
||||
///
|
||||
/// The second half is the one worth a test of its own. Mind Control rides the same
|
||||
/// `PLAYER_FARSIGHT` field the camera does, so during a possession the latch is engaged — and
|
||||
/// the reference reads it only after `0x514537`'s active-player test on the **mover** has
|
||||
/// already passed. Model the latch without that test and the victim freezes solid, which is the
|
||||
/// one body the whole spell exists to walk around.
|
||||
#[test]
|
||||
fn far_sight_freezes_your_own_body_but_never_a_possessed_one() {
|
||||
let driving = |driving_own_body, far_sight_engaged| {
|
||||
let m = MoverInput {
|
||||
dead: false,
|
||||
view_is_out: view_is_out(driving_own_body, far_sight_engaged),
|
||||
};
|
||||
(m.may_translate(false), m.may_turn(false))
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
driving(true, false),
|
||||
(true, true),
|
||||
"my own body, no far sight: the ordinary frame, both predicates pass"
|
||||
);
|
||||
assert_eq!(
|
||||
driving(true, true),
|
||||
(false, false),
|
||||
"MIND VISION — my view is out and the body is mine, so `0x5144e0` fails and takes the \
|
||||
walk and the turn together, exactly as death does"
|
||||
);
|
||||
assert_eq!(
|
||||
driving(false, true),
|
||||
(true, true),
|
||||
"MIND CONTROL — the latch is engaged (possession sets the same field), but the mover \
|
||||
is not the active player, so `0x51453e` returns the conjunct satisfied and the victim \
|
||||
stays drivable. Dropping the `IsActivePlayer` half freezes it."
|
||||
);
|
||||
assert!(
|
||||
MoverInput {
|
||||
dead: false,
|
||||
view_is_out: true,
|
||||
}
|
||||
.torn_down(false, false),
|
||||
"and it ends a follow, with neither a root nor a stun anywhere — `0x5146d6` needs both \
|
||||
predicates down, and a term in the precondition they SHARE is both by construction. \
|
||||
Engaging Mind Vision mid-follow stops you following, for the same reason dying does."
|
||||
);
|
||||
}
|
||||
|
||||
/// **The two incapacitate suppressions take exactly their own bits** (decision 0880) — the
|
||||
/// difference between a pure root and a stun, which is the whole distinction B179 was drawing,
|
||||
/// expressed on the one word that drives both the animation and the wire.
|
||||
|
||||
@@ -24,9 +24,6 @@ use bevy::prelude::*;
|
||||
|
||||
use benilla_ui::script::{BankState, ScriptValue, UiScript};
|
||||
|
||||
use crate::entities::ItemDisplays;
|
||||
use crate::items::Items;
|
||||
use crate::names::NameCache;
|
||||
use crate::net::{ClientCommand, NetCommands, ObjectStore, SelfPlayer};
|
||||
use crate::ui_script::UiInput;
|
||||
use crate::ui_session::{close_npc_session_out_of_range, npc_switched, NpcSession};
|
||||
@@ -118,20 +115,20 @@ fn bank_slot_error_text(result: u32) -> Option<String> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Push the purchase-row snapshot + the open/close/purchased-count events (module doc). The
|
||||
/// banker's name rides the `BANKFRAME_OPENED` event arg (the merchant feed's title pattern —
|
||||
/// ask-once through the [`NameCache`], `BANKFRAME_UPDATE` is not a real event, so a late name
|
||||
/// lands via a fresh `BANKFRAME_OPENED` only on re-open; the title shows "Banker" until then).
|
||||
/// Push the purchase-row snapshot + the open/close/purchased-count events (module doc).
|
||||
///
|
||||
/// **`BANKFRAME_OPENED` carries no arguments**, which is a change 1751's swap forced and a
|
||||
/// correction either way: the banker's name used to ride it as `arg1` (the merchant feed's title
|
||||
/// pattern), and the reference's own `BankFrame_OnEvent` reads no `arg1` at all — it titles the
|
||||
/// window with `UnitName("npc")`, off the interaction token [`crate::ui_session`] already points
|
||||
/// at the banker. Firing an argument the reference does not fire is a divergence an addon can
|
||||
/// see, and it bought nothing once the reference's file was the one reading the event.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn feed_bank(
|
||||
script: Option<NonSendMut<UiScript>>,
|
||||
open: Res<BankOpen>,
|
||||
self_q: Query<&ObjectStore, With<SelfPlayer>>,
|
||||
prices: Option<Res<BankPrices>>,
|
||||
mut items: ResMut<Items>,
|
||||
icons: Option<Res<ItemDisplays>>,
|
||||
commands: Res<NetCommands>,
|
||||
mut names: ResMut<NameCache>,
|
||||
mut errors: ResMut<BankErrors>,
|
||||
mut last: Local<crate::ui_script::VmMemo<Option<BankState>>>,
|
||||
mut last_banker: Local<crate::ui_script::VmMemo<Option<u64>>>,
|
||||
@@ -151,38 +148,13 @@ fn feed_bank(
|
||||
let purchased = store
|
||||
.and_then(|s| s.0.player_bank_bag_slots_purchased())
|
||||
.unwrap_or(0);
|
||||
let fresh = open.banker.map(|_| {
|
||||
// The bag buttons' icons: each held bank bag's item → template → icon, the container
|
||||
// feed's own resolution (ask-once; `None` while the answer is in flight or slot empty).
|
||||
let mut bag_textures: [Option<String>; 6] = Default::default();
|
||||
if let Some(store) = store {
|
||||
for (i, tex) in bag_textures.iter_mut().enumerate() {
|
||||
*tex = store
|
||||
.0
|
||||
.player_bank_bag_slot(i as u8)
|
||||
.filter(|g| *g != 0)
|
||||
.and_then(|guid| {
|
||||
let entry = items.object(guid)?.object_entry()?;
|
||||
let display_id = items.template(entry, guid, &commands)?.display_info_id;
|
||||
icons
|
||||
.as_deref()
|
||||
.and_then(|ic| ic.catalog.get(display_id))
|
||||
.and_then(|d| d.icon.clone())
|
||||
});
|
||||
}
|
||||
}
|
||||
BankState {
|
||||
num_purchased: u32::from(purchased),
|
||||
next_cost: prices
|
||||
.as_ref()
|
||||
.and_then(|p| p.0.next_slot_price(purchased))
|
||||
.unwrap_or(0),
|
||||
bag_textures,
|
||||
}
|
||||
let fresh = open.banker.map(|_| BankState {
|
||||
num_purchased: u32::from(purchased),
|
||||
next_cost: prices
|
||||
.as_ref()
|
||||
.and_then(|p| p.0.next_slot_price(purchased))
|
||||
.unwrap_or(0),
|
||||
});
|
||||
let banker_name = open
|
||||
.banker
|
||||
.and_then(|g| names.resolve(g, &commands).map(str::to_string));
|
||||
// A different banker while the window is already open is a real close+open (decision 0096 /
|
||||
// [`npc_switched`]) — both sounds, and the OnHide-queued close intent is consumed so the
|
||||
// drain doesn't clear the session we just re-opened.
|
||||
@@ -191,14 +163,13 @@ fn feed_bank(
|
||||
return;
|
||||
}
|
||||
script.set_bank(fresh.clone());
|
||||
let name_arg = || vec![ScriptValue::Str(banker_name.clone().unwrap_or_default())];
|
||||
if switched {
|
||||
script.fire_event("BANKFRAME_CLOSED", vec![]);
|
||||
script.fire_event("BANKFRAME_OPENED", name_arg());
|
||||
script.fire_event("BANKFRAME_OPENED", vec![]);
|
||||
let _ = script.take_bank_close();
|
||||
} else {
|
||||
match (&*last, &fresh) {
|
||||
(None, Some(_)) => script.fire_event("BANKFRAME_OPENED", name_arg()),
|
||||
(None, Some(_)) => script.fire_event("BANKFRAME_OPENED", vec![]),
|
||||
// The purchased count moved while open — the no-packet buy confirmation (decision
|
||||
// 0604): the reference event repaints the bag row + purchase frame.
|
||||
(Some(_), Some(_)) => script.fire_event("PLAYERBANKBAGSLOTS_CHANGED", vec![]),
|
||||
|
||||
@@ -42,8 +42,8 @@ use bevy::prelude::*;
|
||||
|
||||
use benilla_protocol::messages::PLAYER_SKILL_SLOTS;
|
||||
use benilla_ui::script::{
|
||||
weapon_subclass_skill, InvSlotView, InventorySlots, ScriptValue, SkillEntry, SkillsState,
|
||||
UiScript, UnitCombatStats, EQUIPMENT_BAG, SKILL_DEFENSE, SKILL_UNARMED,
|
||||
weapon_subclass_skill, BankBagSlots, InvSlotView, InventorySlots, ScriptValue, SkillEntry,
|
||||
SkillsState, UiScript, UnitCombatStats, EQUIPMENT_BAG, SKILL_DEFENSE, SKILL_UNARMED,
|
||||
};
|
||||
|
||||
use crate::entities::ItemDisplays;
|
||||
@@ -87,6 +87,7 @@ struct CharFeedState {
|
||||
struct CharFeedMemo {
|
||||
last_stats: Option<UnitCombatStats>,
|
||||
last_inv: Option<InventorySlots>,
|
||||
last_bank_bags: Option<BankBagSlots>,
|
||||
/// The gate's counter memories (1439) — the stores whose lazy resolves poison `is_changed`
|
||||
/// for this feed (the equipment templates' ask-once, the enchant-name creator lookups).
|
||||
items_objects: gate::Watch,
|
||||
@@ -538,21 +539,25 @@ fn combat_stats(store: &ObjectStore, items: &mut Items, commands: &NetCommands)
|
||||
/// equip_slots from the ask-once template (icon `None` while the answer is in flight — the
|
||||
/// empty-slot art shows and the landing template flips the view, which fires
|
||||
/// `UNIT_INVENTORY_CHANGED`), `locked` from the app's `PendingItemOps` (decision 0208 phase 1b —
|
||||
/// the same feed `ui_items::feed_containers` reads for a bag slot's own `.locked`). `slot0` is
|
||||
/// the wire `EQUIPMENT_SLOT_*`/`INV_SLOT` index (0..22 — equipment 0..18, the four equipped-bag
|
||||
/// icons 19..22); the live-API id `slot_view` reports through is `slot0 + 1` either way.
|
||||
/// the same feed `ui_items::feed_containers` reads for a bag slot's own `.locked`).
|
||||
///
|
||||
/// Takes the item's **guid and its live-API id** rather than a wire slot index, because the two
|
||||
/// bands that use it resolve their guid out of two different descriptor arrays: the doll's
|
||||
/// `PLAYER_FIELD_INV_SLOT_*` (live id = wire slot + 1, so equipment 1..=19 and the four
|
||||
/// equipped-bag icons 20..=23) and the bank bags' own `PLAYER_FIELD_BANK_BAG_SLOT_*` (live ids
|
||||
/// 64..=69). Everything past the guid — template, enchants, durability, the pending lock — is
|
||||
/// identical for both, which is the whole reason there is one function here and not two.
|
||||
#[allow(clippy::too_many_arguments)] // the slot resolve's full read set — the bag feed's twin
|
||||
fn slot_view(
|
||||
store: &ObjectStore,
|
||||
items: &mut Items,
|
||||
icons: Option<&ItemDisplays>,
|
||||
rolls: crate::items::RollCatalogs,
|
||||
commands: &NetCommands,
|
||||
pending: &PendingItemOps,
|
||||
names: &mut crate::names::NameCache,
|
||||
slot0: u8,
|
||||
guid: u64,
|
||||
live_id: u32,
|
||||
) -> Option<InvSlotView> {
|
||||
let guid = store.0.player_inv_slot(slot0)?;
|
||||
// The temp-enchant countdowns, read before the object borrow (both live on `Items`) — the bag
|
||||
// feed's twin.
|
||||
let enchant_ms: [Option<u64>; 7] =
|
||||
@@ -635,7 +640,6 @@ fn slot_view(
|
||||
let icon = icons
|
||||
.and_then(|i| i.catalog.get(display))
|
||||
.and_then(|d| d.icon.clone());
|
||||
let live_id = u32::from(slot0) + 1;
|
||||
Some(InvSlotView {
|
||||
item_id: entry,
|
||||
icon,
|
||||
@@ -738,36 +742,60 @@ fn inventory_slots(
|
||||
enchants: Vec::new(),
|
||||
});
|
||||
}
|
||||
for slot in 1..=19u8 {
|
||||
inv[usize::from(slot)] = slot_view(
|
||||
store,
|
||||
items,
|
||||
icons,
|
||||
rolls,
|
||||
commands,
|
||||
pending,
|
||||
names,
|
||||
slot - 1,
|
||||
);
|
||||
}
|
||||
// Bag0Slot..Bag3Slot (ids 20..23, PaperDollItemFrame.dbc-verified) — the bag bar's icon
|
||||
// source: the equipped bag ITEM at INV_SLOT 19..22 (BAG_SLOT_FIRST../SLOT_BAG_FIRST in the
|
||||
// container feed's own numbering), same `slot_view` resolution as any equipment slot.
|
||||
for (i, slot) in (20u8..=23).enumerate() {
|
||||
inv[usize::from(slot)] = slot_view(
|
||||
store,
|
||||
items,
|
||||
icons,
|
||||
rolls,
|
||||
commands,
|
||||
pending,
|
||||
names,
|
||||
19 + i as u8,
|
||||
);
|
||||
// Equipment 1..=19, then Bag0Slot..Bag3Slot (ids 20..23, PaperDollItemFrame.dbc-verified) —
|
||||
// the bag bar's icon source: the equipped bag ITEM at INV_SLOT 19..22 (BAG_SLOT_FIRST../
|
||||
// SLOT_BAG_FIRST in the container feed's own numbering). One band: the doll's live id is its
|
||||
// wire slot plus one across the whole range.
|
||||
for live in 1..=23u32 {
|
||||
let Some(guid) = store.0.player_inv_slot(live as u8 - 1).filter(|g| *g != 0) else {
|
||||
continue;
|
||||
};
|
||||
inv[live as usize] = slot_view(items, icons, rolls, commands, pending, names, guid, live);
|
||||
}
|
||||
inv
|
||||
}
|
||||
|
||||
/// The six bank-bag slots' views (live ids 64..=69) — the same [`slot_view`] resolution as any
|
||||
/// doll slot, off the descriptor's own `PLAYER_FIELD_BANK_BAG_SLOT_*` guids.
|
||||
///
|
||||
/// These are inventory slots, not bank-*window* state: the reference's bank paints and picks up a
|
||||
/// bank bag through `GetInventoryItemTexture`/`PickupBagFromSlot` at
|
||||
/// `BankButtonIDToInvSlotID(i, 1)` (BankFrame.lua:28-35, 194-198), and the guids stream in the
|
||||
/// player descriptor whether or not a banker is open. So they ride the inventory feed, beside the
|
||||
/// doll snapshot, rather than the `BankState` the bank window pushes.
|
||||
#[allow(clippy::too_many_arguments)] // [`slot_view`]'s read set, one band over
|
||||
fn bank_bag_slots(
|
||||
store: &ObjectStore,
|
||||
items: &mut Items,
|
||||
icons: Option<&ItemDisplays>,
|
||||
rolls: crate::items::RollCatalogs,
|
||||
commands: &NetCommands,
|
||||
pending: &PendingItemOps,
|
||||
names: &mut crate::names::NameCache,
|
||||
) -> BankBagSlots {
|
||||
let mut bags: BankBagSlots = Default::default();
|
||||
for (i, bag) in bags.iter_mut().enumerate() {
|
||||
let Some(guid) = store.0.player_bank_bag_slot(i as u8).filter(|g| *g != 0) else {
|
||||
continue;
|
||||
};
|
||||
*bag = slot_view(
|
||||
items,
|
||||
icons,
|
||||
rolls,
|
||||
commands,
|
||||
pending,
|
||||
names,
|
||||
guid,
|
||||
BANK_BAG_LIVE_FIRST + i as u32,
|
||||
);
|
||||
}
|
||||
bags
|
||||
}
|
||||
|
||||
/// `BankButtonIDToInvSlotID(1, isBag)` — the live-API id of the FIRST bank-bag slot. The band is
|
||||
/// six wide (`BANK_BAG_SLOT_COUNT`).
|
||||
const BANK_BAG_LIVE_FIRST: u32 = 64;
|
||||
|
||||
/// Fire the paper doll's per-group repaint events for one unit's snapshot transition, `arg1 =
|
||||
/// token` — the [`crate::ui_unit::fire_transitions`] shape, for the combat-stats surface.
|
||||
///
|
||||
@@ -1002,14 +1030,31 @@ fn feed_char(
|
||||
&pending,
|
||||
&mut names,
|
||||
);
|
||||
if memo.last_inv.as_ref() != Some(&inv) {
|
||||
let bank_bags = bank_bag_slots(
|
||||
store,
|
||||
&mut items,
|
||||
icons.as_deref(),
|
||||
crate::items::RollCatalogs {
|
||||
enchants: enchants.as_deref(),
|
||||
props: props.as_deref(),
|
||||
},
|
||||
&commands,
|
||||
&pending,
|
||||
&mut names,
|
||||
);
|
||||
// One transition for both bands: they are the same descriptor read at two offsets, and
|
||||
// `UNIT_INVENTORY_CHANGED` is the one repaint signal either has. Pushing them separately
|
||||
// would fire it twice for a single wire update.
|
||||
if memo.last_inv.as_ref() != Some(&inv) || memo.last_bank_bags.as_ref() != Some(&bank_bags) {
|
||||
gate.audit("feed_char", "the inventory snapshot");
|
||||
script.set_inventory_slots(inv.clone());
|
||||
script.set_bank_bag_slots(bank_bags.clone());
|
||||
script.fire_event(
|
||||
"UNIT_INVENTORY_CHANGED",
|
||||
vec![ScriptValue::Str("player".to_string())],
|
||||
);
|
||||
memo.last_inv = Some(inv);
|
||||
memo.last_bank_bags = Some(bank_bags);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -99,6 +99,13 @@
|
||||
//! that answers term 1. (1) is [`track_instance_state`]'s `CurrentMap` flip; (2) is
|
||||
//! [`apply::update_last_instance`]'s queue, drained through the same helper so the two cannot
|
||||
//! drift — [`LatchWriter`] is which of them is speaking.
|
||||
//!
|
||||
//! **(2) is therefore unreachable on our server by construction, and no live probe can cover it**
|
||||
//! — not with any rigging, because `PermBindAllPlayers` (vmangos's only path to a permanent bind)
|
||||
//! runs under `if (IsRaid())` and no GM command creates a bind. Its cover is
|
||||
//! `both_latch_writers_through_the_real_system`, which drives both legs through the real system
|
||||
//! against the real `Map.dbc`. That is the whole of the evidence behind (2): treat it as tested,
|
||||
//! never as *observed*.
|
||||
|
||||
use benilla_protocol::messages::{InstanceResetFailure, RaidInstanceMessage, RaidInstanceWarning};
|
||||
use benilla_ui::script::UiScript;
|
||||
@@ -128,10 +135,10 @@ pub(crate) struct InstanceState {
|
||||
/// benilla's own half of the same term: we watched the player walk out of a party dungeon, so
|
||||
/// we know they are bound to one without being told (decision 1754).
|
||||
///
|
||||
/// Raised by [`LatchWriter::WorldEntry`], cleared by `SMSG_INSTANCE_RESET`, and deliberately
|
||||
/// **not** cleared by an `owns_saved = false` packet — that packet is the wrong answer this
|
||||
/// field exists to work around, and vmangos sends one on the very teleport that takes us out
|
||||
/// of the dungeon.
|
||||
/// Raised by [`LatchWriter::WorldEntry`], cleared by `SMSG_INSTANCE_RESET` and by a **logout**
|
||||
/// ([`clear_witness_on_logout`]), and deliberately **not** cleared by an `owns_saved = false`
|
||||
/// packet — that packet is the wrong answer this field exists to work around, and vmangos
|
||||
/// sends one on the very teleport that takes us out of the dungeon.
|
||||
saw_own_dungeon: bool,
|
||||
/// `0xb4e374` — the `Map.dbc` id of the dungeon we were last inside, or `None` for "none
|
||||
/// recorded". Only a **party dungeon** ever lands here (`0x495d33`'s `cmp [rec+8],1`).
|
||||
@@ -226,6 +233,12 @@ impl InstanceState {
|
||||
self.last_dungeon_at = 0;
|
||||
// A reset landed, so whatever we witnessed ourselves is gone with it. The reference has
|
||||
// no equivalent line because its term 1 is the server's to retract.
|
||||
self.forget_witness();
|
||||
}
|
||||
|
||||
/// Drop benilla's own half of term 1 — see [`clear_witness_on_logout`]. Kept a method so the
|
||||
/// one piece of state no server will correct has exactly one place that clears it.
|
||||
pub(crate) fn forget_witness(&mut self) {
|
||||
self.saw_own_dungeon = false;
|
||||
}
|
||||
|
||||
@@ -598,6 +611,24 @@ fn drain_instance(script: Option<NonSendMut<UiScript>>, commands: Res<NetCommand
|
||||
|
||||
/// The lockout family: the four chat lines, the bookkeeping behind the SELF menu's reset row, and
|
||||
/// that row's one send.
|
||||
/// Drop the witness at logout — the next login is somebody else's character (`poi_marker`'s own
|
||||
/// shape, and the same reason).
|
||||
///
|
||||
/// Only [`InstanceState::saw_own_dungeon`] is cleared, not the reference's three globals: the
|
||||
/// reference does not clear those either (`0x495d00`'s only caller is the `SMSG_INSTANCE_RESET`
|
||||
/// handler), and it does not need to — its term 1 is the server's, re-advertised on the next
|
||||
/// character's world entry, so a stale latch cannot offer that character a reset on its own. Ours
|
||||
/// is the half no server will correct, so it gets the lifetime the packet would have given it
|
||||
/// (decision 1754).
|
||||
fn clear_witness_on_logout(
|
||||
mut state: ResMut<InstanceState>,
|
||||
mut logged_out: MessageReader<crate::net::LoggedOutMessage>,
|
||||
) {
|
||||
if logged_out.read().next().is_some() {
|
||||
state.forget_witness();
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct UiInstancePlugin;
|
||||
|
||||
impl Plugin for UiInstancePlugin {
|
||||
@@ -607,6 +638,7 @@ impl Plugin for UiInstancePlugin {
|
||||
(
|
||||
// The latch first: `feed_instance` publishes `CanShowResetInstances()` off it, and
|
||||
// a map change and its answer should not be a frame apart.
|
||||
clear_witness_on_logout.before(track_instance_state),
|
||||
track_instance_state.before(feed_instance),
|
||||
feed_instance.before(UiInput),
|
||||
drain_instance.after(UiInput),
|
||||
@@ -923,6 +955,106 @@ mod tests {
|
||||
assert!(!state.can_reset(Some(0), &dungeons, 100));
|
||||
}
|
||||
|
||||
/// The witness does not outlive the character that earned it (`clear_witness_on_logout`).
|
||||
///
|
||||
/// The reference can leave its stale latch lying about because term 1 is the server's and the
|
||||
/// next character's world entry re-answers it. Ours is the half nothing corrects, so a logout
|
||||
/// has to, or character B is offered a reset that character A walked out of.
|
||||
#[test]
|
||||
fn the_witness_does_not_survive_a_logout() {
|
||||
let dungeons = |m: u32| m == 36;
|
||||
let mut state = InstanceState::default();
|
||||
state.note_last_instance(36, Some(0), true, 100, LatchWriter::WorldEntry);
|
||||
assert!(state.can_reset(Some(0), &dungeons, 100));
|
||||
|
||||
state.forget_witness();
|
||||
assert!(!state.can_reset(Some(0), &dungeons, 100));
|
||||
// The reference's own three globals are untouched — we clear our addition, not its state.
|
||||
assert_eq!(state.last_dungeon, Some(36));
|
||||
assert_eq!(state.last_dungeon_at, 100);
|
||||
}
|
||||
|
||||
/// **Both latch writers, driven through the real system, against the real `Map.dbc`.**
|
||||
///
|
||||
/// This is the only cover writer 2 — `SMSG_UPDATE_LAST_INSTANCE` — can have. vmangos can
|
||||
/// never exercise it: `SendSavedInstances` names only `perm` binds, `DungeonMap::Add` takes a
|
||||
/// non-perm bind for a 5-man, and `PermBindAllPlayers` (the one path that makes a bind
|
||||
/// permanent) runs under `if (IsRaid())`, so the only map ids that packet can ever carry are
|
||||
/// raids — which `0x495d33`'s `cmp [rec+8],1` throws away. No GM command creates a bind
|
||||
/// either (`.instance` offers listbinds/unbind/groupunbind, nothing that binds). So the packet
|
||||
/// leg is unreachable on our server by construction, and a live probe cannot cover it however
|
||||
/// the character is rigged (decision 1754's open thread).
|
||||
///
|
||||
/// It runs both legs through `track_instance_state` itself rather than the methods, because
|
||||
/// the thing worth guarding is the wiring: that the queue drains against the map we ended up
|
||||
/// on rather than the one we left, and that the packet writer does **not** raise the witness
|
||||
/// (which would hand 1754's deviation to a server's say-so). Skips without client data.
|
||||
#[test]
|
||||
fn both_latch_writers_through_the_real_system() {
|
||||
let data = benilla_formats::wow_data_or_skip!();
|
||||
let mut chain = benilla_formats::open_chain(&data).expect("open chain");
|
||||
let maps = benilla_formats::load_map_catalog(&mut chain).expect("Map.dbc");
|
||||
assert!(maps.is_party_dungeon(36), "Deadmines is InstanceType 1");
|
||||
assert!(
|
||||
!maps.is_party_dungeon(409),
|
||||
"Molten Core is a raid, not a dungeon"
|
||||
);
|
||||
|
||||
// The system carries the last observed map in a `Local`, so it has to be the SAME system
|
||||
// across ticks — `run_system_once` hands out a fresh one each call and would never see a
|
||||
// transition at all.
|
||||
let mut app = App::new();
|
||||
app.init_resource::<InstanceState>()
|
||||
.init_resource::<Time<Real>>()
|
||||
.insert_resource(benilla_assets::MapCatalogRes(maps))
|
||||
.insert_resource(benilla_world::world_map::CurrentMap(36))
|
||||
.add_systems(Update, track_instance_state);
|
||||
|
||||
let state = |app: &App| {
|
||||
let s = app.world().resource::<InstanceState>();
|
||||
(s.last_dungeon, s.saw_own_dungeon)
|
||||
};
|
||||
let queue = |app: &mut App, map: u32| {
|
||||
apply::update_last_instance(&mut app.world_mut().resource_mut::<InstanceState>(), map);
|
||||
};
|
||||
|
||||
// Writer 1 — we walk out of the Deadmines ourselves. The first tick only OBSERVES map 36
|
||||
// (there is no map we left at login), so nothing is recorded until the flip.
|
||||
app.update();
|
||||
assert_eq!(state(&app), (None, false));
|
||||
|
||||
app.insert_resource(benilla_world::world_map::CurrentMap(0));
|
||||
app.update();
|
||||
assert_eq!(
|
||||
state(&app),
|
||||
(Some(36), true),
|
||||
"the flip records the dungeon AND our own eyes (1754's term 1)"
|
||||
);
|
||||
|
||||
// Writer 2 — the packet, on a fresh state. A raid id is dropped (the only thing vmangos
|
||||
// could ever send); a party dungeon lands, and lands WITHOUT the witness.
|
||||
app.insert_resource(InstanceState::default());
|
||||
queue(&mut app, 409);
|
||||
app.update();
|
||||
assert_eq!(state(&app), (None, false), "a raid is not a party dungeon");
|
||||
|
||||
queue(&mut app, 36);
|
||||
app.update();
|
||||
assert_eq!(
|
||||
state(&app),
|
||||
(Some(36), false),
|
||||
"the packet is the server's word, never our own eyes"
|
||||
);
|
||||
|
||||
// …and the packet's own early-out: the instance we are STANDING IN is not one we left.
|
||||
app.insert_resource(InstanceState::default());
|
||||
app.insert_resource(benilla_world::world_map::CurrentMap(36));
|
||||
app.update();
|
||||
queue(&mut app, 36);
|
||||
app.update();
|
||||
assert_eq!(state(&app), (None, false));
|
||||
}
|
||||
|
||||
/// `CanShowResetInstances()`'s four terms, one at a time.
|
||||
#[test]
|
||||
fn can_reset_needs_all_four_terms() {
|
||||
|
||||
@@ -124,6 +124,68 @@ pub(super) fn drain_container_autoequips(
|
||||
}
|
||||
}
|
||||
|
||||
/// Drain the auto-stores `PutItemInBag`/`PutItemInBackpack` queued (`benilla_ui`'s
|
||||
/// `cursor::bag_verbs`) and send them on the wire.
|
||||
///
|
||||
/// **The destination is a BAG, not a slot** — that is the finding this drain exists to carry
|
||||
/// (wow-re `bag-verbs-law.md`): `CMSG_AUTOSTORE_BAG_ITEM` names `(srcbag, srcslot, dstbag)` and
|
||||
/// the server picks where inside it the item lands, which is why an ordinary item dropped on a
|
||||
/// bag BUTTON goes in the bag rather than swapping with it. The destination bag byte is
|
||||
/// [`wire_pos`]'s own answer for that container's first slot (255 for the backpack, the bag's
|
||||
/// player-array slot 19..22 / 63..68 for an equipped or bank bag), so one map serves both ends.
|
||||
///
|
||||
/// A **split carry** takes `CMSG_SPLIT_ITEM` instead — the reference's own fork on `[0xb4b40c]`
|
||||
/// — with the literal `0xFF` where a destination slot would go, because that wire has a slot
|
||||
/// field and this one does not.
|
||||
///
|
||||
/// No pending-lock recording, matching [`drain_container_autoequips`]'s own precedent for the same
|
||||
/// class of send: the destination is not a slot, so there is no second end to lock.
|
||||
pub(super) fn drain_bag_autostores(
|
||||
script: Option<NonSendMut<UiScript>>,
|
||||
commands: Res<NetCommands>,
|
||||
) {
|
||||
let Some(mut script) = script else {
|
||||
return;
|
||||
};
|
||||
for a in script.take_bag_autostores() {
|
||||
let (Some((src_bag, src_slot)), Some((dst_bag, _))) =
|
||||
(wire_pos(a.src_bag, a.src_slot), wire_pos(a.dst_bag, 1))
|
||||
else {
|
||||
debug!("ui_items: bag autostore {a:?} out of range — ignored");
|
||||
continue;
|
||||
};
|
||||
match a.count {
|
||||
None => {
|
||||
debug!(
|
||||
"ui_items: autostore lua {}/{} → bag {} (wire {src_bag}/{src_slot} → {dst_bag})",
|
||||
a.src_bag, a.src_slot, a.dst_bag
|
||||
);
|
||||
let _ = commands.0.send(ClientCommand::AutoStoreBagItem {
|
||||
src_bag,
|
||||
src_slot,
|
||||
dst_bag,
|
||||
});
|
||||
}
|
||||
Some(n) => {
|
||||
let count = n.min(u32::from(u8::MAX)) as u8;
|
||||
debug!(
|
||||
"ui_items: autostore split lua {}/{} → bag {} × {count} (wire {src_bag}/{src_slot} → {dst_bag})",
|
||||
a.src_bag, a.src_slot, a.dst_bag
|
||||
);
|
||||
let _ = commands.0.send(ClientCommand::SplitItem {
|
||||
src_bag,
|
||||
src_slot,
|
||||
dst_bag,
|
||||
// `0xFF` — the reference's own literal for "no destination slot"; the split
|
||||
// wire has the field, the verb has no value for it.
|
||||
dst_slot: 0xFF,
|
||||
count,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Drain the inventory-slot ids `UseInventoryItem` queued (decision 0208 phase 1b: the doll
|
||||
/// slot's right-click) and route the equipped position (bag 255 plus the 0-based wire slot —
|
||||
/// `HandleUseItemOpcode` takes equipped positions the same as bag ones, vmangos `ItemHandler.cpp`)
|
||||
|
||||
@@ -53,8 +53,8 @@ pub(crate) mod feed;
|
||||
|
||||
pub(crate) use drain::send_auto_equip;
|
||||
use drain::{
|
||||
drain_container_autoequips, drain_container_destroys, drain_container_moves,
|
||||
drain_container_uses, drain_inventory_uses,
|
||||
drain_bag_autostores, drain_container_autoequips, drain_container_destroys,
|
||||
drain_container_moves, drain_container_uses, drain_inventory_uses,
|
||||
};
|
||||
use feed::{
|
||||
feed_containers, feed_item_sets, feed_item_stats, feed_player_req, feed_random_properties,
|
||||
@@ -1112,6 +1112,7 @@ impl Plugin for UiItemsPlugin {
|
||||
drain_container_destroys.after(UiInput),
|
||||
// AutoEquipCursorItem's queue (decision 0208 phase 1b) → CMSG_AUTOEQUIP_ITEM.
|
||||
drain_container_autoequips.after(UiInput),
|
||||
drain_bag_autostores.after(UiInput),
|
||||
// UseInventoryItem's queue (decision 0208 phase 1b) → CMSG_USE_ITEM against
|
||||
// the equipped position.
|
||||
drain_inventory_uses.after(UiInput),
|
||||
|
||||
@@ -778,7 +778,8 @@ fn readable_letter_hover_shows_the_inspect_magnifier() {
|
||||
}
|
||||
|
||||
/// The drag trio (decision 0216 §3): a real press-drag-release across two slot buttons routes
|
||||
/// through the SAME `BenillaBagSlot_OnClick("LeftButton")` path a two-click pickup/place does —
|
||||
/// through the SAME `ContainerFrameItemButton_OnClick("LeftButton")` path a two-click pickup/place
|
||||
/// does —
|
||||
/// unlike every other bag test here, which calls the Lua click handler directly, this one drives
|
||||
/// actual `mouse_button`/`mouse_move` so the `RegisterForDrag`/`OnDragStart`/`OnReceiveDrag` XML
|
||||
/// wiring itself is under test, not just the handler body.
|
||||
@@ -1330,7 +1331,7 @@ fn a_plain_click_hides_an_open_split_frame() {
|
||||
|
||||
/// Bag-slot item cooldowns through the shipped XML (decision 0263's deferral): a potion mid-
|
||||
/// cooldown pushes its triple with the container snapshot; opening the bag runs the ref's
|
||||
/// occupied-slot fork (`BenillaBagSlot_UpdateCooldown` → `GetContainerItemCooldown` →
|
||||
/// occupied-slot fork (`ContainerFrame_UpdateCooldown` → `GetContainerItemCooldown` →
|
||||
/// `CooldownFrame_SetTimer`) and the slot grows a live sweep; a `BAG_UPDATE_COOLDOWN` refresh
|
||||
/// with the cooldown gone hides it again.
|
||||
#[test]
|
||||
|
||||
@@ -1,24 +1,28 @@
|
||||
//! The shipped **bank window** driven end-to-end, engine-only (no Bevy): the real
|
||||
//! `assets/ui/BankFrame.xml` loaded behind the bag chain (the coin rig, our bag bar, and the
|
||||
//! reference's own `ContainerFrame.xml` off the player's install) — decision 0604 phase 4's
|
||||
//! machine checks. Split from `merchant_tests`/`bag_tests` along the folder's one-file-per-window
|
||||
//! convention.
|
||||
//! The **bank window** driven end-to-end, engine-only (no Bevy): the reference's own
|
||||
//! `Interface\FrameXML\BankFrame.xml`, executed off the player's patch chain behind the bag
|
||||
//! chain — decision 0604 phase 4's machine checks, re-pointed at the real file by 1751 (window 2).
|
||||
//! Split from `merchant_tests`/`bag_tests` along the folder's one-file-per-window convention.
|
||||
//!
|
||||
//! **What decision 1751 changed in here.** The bank used to own six popout windows of its own —
|
||||
//! `BenillaBankBagFrame1..6`, six copies of `BagFrame.xml`'s window template — and the tests drove
|
||||
//! them by name. They are gone: a bank bag is an ordinary `ContainerFrame` at container id
|
||||
//! `NUM_BAG_SLOTS + slot` (5..10), which is what the real client always did and what decision 0604
|
||||
//! already said it did. So the popout assertions below ask [`bag_open`] — the reference's own
|
||||
//! `IsBagOpen` scan — instead of naming a window. The twelve windows are RECYCLED across every
|
||||
//! container, so which `ContainerFrame<N>` a bank bag lands in is a coincidence and never an
|
||||
//! assertion.
|
||||
//! **What decision 1751 changed in here, in two passes.** First the bank lost six popout windows
|
||||
//! of its own — `BenillaBankBagFrame1..6`, six copies of `BagFrame.xml`'s window template — because
|
||||
//! a bank bag is an ordinary `ContainerFrame` at container id `NUM_BAG_SLOTS + slot` (5..10), which
|
||||
//! is what the real client always did and what 0604 already said it did. Then the window itself
|
||||
//! went, and every name below is the reference's: `BankFrameItem1..24`, `BankFrameBag1..6`,
|
||||
//! `BankFramePurchaseButton`, `BankFrameDetailMoneyFrame`.
|
||||
//!
|
||||
//! Two consequences worth stating, because both are places a test could quietly assert our old
|
||||
//! behaviour instead of the client's:
|
||||
//!
|
||||
//! · **The bag buttons' `GetID()` is the CONTAINER id, 5..10** — not a bag number. That is what
|
||||
//! `ButtonInventorySlot` hands `BankButtonIDToInvSlotID`, and what `ToggleBag` takes.
|
||||
//! · **Opening the bank does NOT open your bags.** Decision 0561 (a window that opens your bags
|
||||
//! opens every equipped one) is ours and stays for the vendor; the bank goes faithful, and the
|
||||
//! reference's `BankFrame` OnShow plays one sound and nothing else. The director's call.
|
||||
//!
|
||||
//! The knock-on for the fixtures: the reference generates a window only for a container that
|
||||
//! exists (`OpenBag`'s `size > 0` gate), where our own `BenillaBagFrame*` were static frames that
|
||||
//! `Show()` regardless. So a test that wants the backpack open has to feed a backpack, and one
|
||||
//! that wants a bank bag open has to feed a bag at id 5 — which is the honest fixture anyway: a
|
||||
//! player always has a 16-slot backpack, and an empty bank-bag slot has no window in the real
|
||||
//! client either.
|
||||
//! `Show()` regardless. So a test that wants a bank bag open has to feed a bag at id 5 — which is
|
||||
//! the honest fixture anyway: an empty bank-bag slot has no window in the real client either.
|
||||
|
||||
use benilla_ui::script::{
|
||||
BankState, ContainerSlot, ContainerState, ExtractedQuad, QuadContent, ScriptValue,
|
||||
@@ -39,10 +43,14 @@ fn empty_bag(name: &str, num_slots: u32) -> Option<ContainerState> {
|
||||
/// The bank's own dependency chain, in `benilla.toc` order: [`BAG_UI`] carries it whole — Fonts
|
||||
/// before anything with a font `inherits`, the four templates stock `ContainerFrame.xml` inherits,
|
||||
/// UiPanels before any UIPanel/StaticPopup use, GameTooltip before any slot's hover, the
|
||||
/// reference's own container file, our bag bar, and `BankFrame.xml` itself (which BAG_UI already
|
||||
/// needs, because `updateContainerFrameAnchors` measures every open bag against
|
||||
/// `BankFrame:GetRight()`). Then `MerchantFrame.xml` for the `BenillaMoney_*` coin rig the bank's
|
||||
/// purse and cost rows call — a global resolved at call time, so it may land after.
|
||||
/// reference's own container file, our bag bar, and the reference's own `BankFrame.xml` (which
|
||||
/// BAG_UI already needs, because `updateContainerFrameAnchors` measures every open bag against
|
||||
/// `BankFrame:GetRight()`).
|
||||
///
|
||||
/// `MerchantFrame.xml` used to load here too, for the `BenillaMoney_*` coin rig our own bank
|
||||
/// window's purse and cost rows called. The reference's rows are `SmallMoneyFrameTemplate`
|
||||
/// instances off `MoneyFrame.xml`, which BAG_UI already carries — so the merchant is not a bank
|
||||
/// dependency any more.
|
||||
///
|
||||
/// **Every caller needs client data**: `BAG_UI` names a chain entry, so each test below opens with
|
||||
/// `wow_data_or_skip!`.
|
||||
@@ -52,7 +60,6 @@ fn setup() -> UiScript {
|
||||
for file in BAG_UI {
|
||||
load_xml(&s, file);
|
||||
}
|
||||
load_xml(&s, "MerchantFrame.xml");
|
||||
s
|
||||
}
|
||||
|
||||
@@ -62,24 +69,31 @@ fn has_icon(quads: &[ExtractedQuad], needle: &str) -> bool {
|
||||
)
|
||||
}
|
||||
|
||||
/// Test 1 — BANKFRAME_OPENED shows the frame (through ShowUIPanel, landing at the left slot — the same
|
||||
/// NPC-session chain Merchant/Trainer use), sets the title to the event arg (falling back to
|
||||
/// "Banker" before any event ever fires), and opens the backpack window (the 0561 assertion
|
||||
/// pattern — `vendor_opens_and_closes_all_equipped_bags` in merchant_tests).
|
||||
/// Test 1 — BANKFRAME_OPENED shows the frame (through ShowUIPanel, landing at the left slot — the
|
||||
/// same NPC-session chain Merchant/Trainer use) and sets the title to the event arg.
|
||||
///
|
||||
/// The backpack it opens is the reference's own `ContainerFrame` now (1751), so the assertion is
|
||||
/// `IsBagOpen(0)` rather than a window name — and the backpack has to be FED, because the
|
||||
/// reference builds a window only for a container that exists.
|
||||
/// **It does NOT open your bags, and that is the change.** Decision 0561 — a window that opens
|
||||
/// your bags opens every equipped one — is this client's, and the vendor keeps it; the bank went
|
||||
/// faithful with 1751's swap (the director's call), and the reference's `BankFrame` OnShow plays
|
||||
/// `igMainMenuOpen` and nothing else. So the one sound is the whole sound list, which is a
|
||||
/// stronger assertion than it looks: a stray `OpenBackpack` anywhere on this path would put
|
||||
/// `igBackPackOpen` in front of it.
|
||||
///
|
||||
/// **And the title comes from `UnitName("npc")`, not from the event.** The reference's
|
||||
/// `BankFrame_OnEvent` reads no `arg1` (BankFrame.lua:125), so the banker's name has to arrive on
|
||||
/// the interaction token `crate::ui_session` points at the banker — which is why this test seats
|
||||
/// an `"npc"` unit and why `feed_bank` stopped firing a name argument the reference never fires.
|
||||
#[test]
|
||||
fn bankframe_opened_shows_sets_title_and_opens_backpack() {
|
||||
fn bankframe_opened_shows_and_sets_the_title() {
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
let mut s = setup();
|
||||
|
||||
// Pre-open default: the XML-authored placeholder.
|
||||
// The reference gives `BankFrameTitleText` no `text=` — it is empty until an event names the
|
||||
// banker (our own file used to author a "Banker" placeholder).
|
||||
assert_eq!(
|
||||
s.eval::<String>("return BankFrameTitleText:GetText()")
|
||||
s.eval::<String>("return BankFrameTitleText:GetText() or \"\"")
|
||||
.unwrap(),
|
||||
"Banker"
|
||||
""
|
||||
);
|
||||
assert!(!s.eval::<bool>("return BankFrame:IsVisible()").unwrap());
|
||||
|
||||
@@ -88,10 +102,14 @@ fn bankframe_opened_shows_sets_title_and_opens_backpack() {
|
||||
s.set_money(0);
|
||||
s.set_container(0, empty_bag("Backpack", 16));
|
||||
s.set_bank(Some(BankState::default()));
|
||||
s.fire_event(
|
||||
"BANKFRAME_OPENED",
|
||||
vec![ScriptValue::Str("Grumnus Steelshaper".into())],
|
||||
s.set_unit(
|
||||
"npc",
|
||||
Some(benilla_ui::script::UnitState {
|
||||
name: Some("Grumnus Steelshaper".into()),
|
||||
..Default::default()
|
||||
}),
|
||||
);
|
||||
s.fire_event("BANKFRAME_OPENED", vec![]);
|
||||
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
|
||||
|
||||
assert!(s.eval::<bool>("return BankFrame:IsVisible()").unwrap());
|
||||
@@ -101,8 +119,8 @@ fn bankframe_opened_shows_sets_title_and_opens_backpack() {
|
||||
"Grumnus Steelshaper"
|
||||
);
|
||||
assert!(
|
||||
bag_open(&s, 0),
|
||||
"opening the bank opens the backpack (the 0561 OpenBackpack contract)"
|
||||
!bag_open(&s, 0),
|
||||
"the reference's bank leaves your bags alone (0561 is the vendor's, not the bank's)"
|
||||
);
|
||||
assert!(
|
||||
s.eval::<bool>("return GetLeftFrame() == BankFrame")
|
||||
@@ -111,11 +129,8 @@ fn bankframe_opened_shows_sets_title_and_opens_backpack() {
|
||||
);
|
||||
assert_eq!(
|
||||
s.take_sounds(),
|
||||
vec![
|
||||
SoundRequest::KitName("igBackPackOpen".into()),
|
||||
SoundRequest::KitName("igMainMenuOpen".into()),
|
||||
],
|
||||
"the backpack kit leads (OpenBackpack shows it before the panel sound), then the bank's own OnShow kit"
|
||||
vec![SoundRequest::KitName("igMainMenuOpen".into())],
|
||||
"the bank's own OnShow kit, and only it"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -124,20 +139,18 @@ fn bankframe_opened_shows_sets_title_and_opens_backpack() {
|
||||
/// sound — the reference's own OnHide order (CloseBankBagFrames(); CloseBankFrame(); PlaySound).
|
||||
///
|
||||
/// **"Popout" is this test's own history, not a widget any more** (1751). It was
|
||||
/// `BenillaBankBagFrame1`, one of six copies of `BagFrame.xml`'s window template that this file
|
||||
/// owned, and the assertion was `:Show()` it then check `:IsShown()`. A bank bag is an ordinary
|
||||
/// container now — id `NUM_BAG_SLOTS + 1` = 5 — so it is OPENED with the reference's own `OpenBag`
|
||||
/// and asked for with `IsBagOpen`, and `CloseBankBagFrames` is the reference's verbatim
|
||||
/// `CloseBag(5..10)` loop. The property is unchanged and is the point of the test: closing the
|
||||
/// bank must close the bank BAGS, not merely play a sound.
|
||||
/// `BenillaBankBagFrame1`, one of six copies of `BagFrame.xml`'s window template that our own bank
|
||||
/// file owned, and the assertion was `:Show()` it then check `:IsShown()`. A bank bag is an
|
||||
/// ordinary container now — id `NUM_BAG_SLOTS + 1` = 5 — so it is OPENED with the reference's own
|
||||
/// `OpenBag` and asked for with `IsBagOpen`, and `CloseBankBagFrames` is now the reference's own
|
||||
/// `CloseBag(5..10)` loop rather than a transcription of it. The property is unchanged and is the
|
||||
/// point of the test: closing the bank must close the bank BAGS, not merely play a sound.
|
||||
#[test]
|
||||
fn bankframe_closed_queues_close_closes_open_popouts_and_plays_the_close_kit() {
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
let mut s = setup();
|
||||
s.set_money(0);
|
||||
// The backpack (the bank's own OnShow opens it) and a bag in the FIRST bank bag slot —
|
||||
// container 5, which is where `BenillaBankBagButton_OnClick` sends `BankBagButton1`.
|
||||
s.set_container(0, empty_bag("Backpack", 16));
|
||||
// A bag in the FIRST bank bag slot — container 5, which is `BankFrameBag1`'s own `GetID()`.
|
||||
s.set_container(5, empty_bag("Bank Bag", 8));
|
||||
s.set_bank(Some(BankState::default()));
|
||||
s.fire_event("BANKFRAME_OPENED", vec![ScriptValue::Str("Banker".into())]);
|
||||
@@ -245,21 +258,30 @@ fn item_slot_paints_from_container_minus_one_and_repaints_on_playerbankslots_cha
|
||||
);
|
||||
}
|
||||
|
||||
/// Test 4 — Bag buttons: with `set_bank` num_purchased=2, buttons 1-2 read normal (white) and 3-6 read
|
||||
/// the red tint (1.0, 0.1, 0.1 — the reference's exact tint); `BenillaGetBankBagTexture` drives
|
||||
/// button 1's icon.
|
||||
/// Test 4 — Bag buttons: with `set_bank` num_purchased=2, buttons 1-2 read normal (white) and 3-6
|
||||
/// read the red tint (1.0, 0.1, 0.1 — the reference's exact tint, `UpdateBagSlotStatus`
|
||||
/// BankFrame.lua:88-95); the bank-bag inventory band drives button 1's icon.
|
||||
#[test]
|
||||
fn bag_buttons_tint_by_purchase_count_and_texture_from_the_bank_bag_feed() {
|
||||
fn bag_buttons_tint_by_purchase_count_and_texture_from_the_bank_bag_band() {
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
let mut s = setup();
|
||||
s.set_money(0);
|
||||
let mut state = BankState {
|
||||
s.set_bank(Some(BankState {
|
||||
num_purchased: 2,
|
||||
next_cost: 100_000,
|
||||
}));
|
||||
// Bank bag slot 1 holds a bag: an inventory slot at live id 64, which is where the
|
||||
// reference's `BankFrameItemButton_OnUpdate` reads its icon from
|
||||
// (`GetInventoryItemTexture("player", ButtonInventorySlot())`).
|
||||
let mut bags: benilla_ui::script::BankBagSlots = Default::default();
|
||||
bags[0] = Some(benilla_ui::script::InvSlotView {
|
||||
item_id: 4500,
|
||||
icon: Some("Interface\\Icons\\INV_Misc_Bag_08".into()),
|
||||
count: 1,
|
||||
equip_slots: vec![20, 21, 22, 23],
|
||||
..Default::default()
|
||||
};
|
||||
state.bag_textures[0] = Some("Interface\\Icons\\INV_Misc_Bag_08".into());
|
||||
s.set_bank(Some(state));
|
||||
});
|
||||
s.set_bank_bag_slots(bags);
|
||||
s.fire_event("BANKFRAME_OPENED", vec![]);
|
||||
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
|
||||
s.resolve();
|
||||
@@ -294,43 +316,77 @@ fn bag_buttons_tint_by_purchase_count_and_texture_from_the_bank_bag_feed() {
|
||||
(c[0] - r).abs() < 0.01 && (c[1] - g).abs() < 0.01 && (c[2] - b).abs() < 0.15
|
||||
};
|
||||
|
||||
// Button 1: its own bag icon (BenillaGetBankBagTexture(1)), purchased -> white.
|
||||
let (x1, y1) = center("BankBagButton1");
|
||||
let c1 = color_at(x1, y1, "INV_Misc_Bag_08").expect("button 1 shows its fed bag icon");
|
||||
// Button 1: the live inventory slot's own bag icon, purchased -> white.
|
||||
let (x1, y1) = center("BankFrameBag1");
|
||||
let c1 = color_at(x1, y1, "INV_Misc_Bag_08").expect("button 1 shows the banked bag's icon");
|
||||
assert!(
|
||||
near(c1, 1.0, 1.0, 1.0),
|
||||
"purchased slot 1 stays white, got {c1:?}"
|
||||
"button 1 is purchased: white {c1:?}"
|
||||
);
|
||||
|
||||
// Button 2 (purchased, no bag fed): the fallback texture, still white.
|
||||
let (x2, y2) = center("BankBagButton2");
|
||||
let c2 =
|
||||
color_at(x2, y2, "PaperDoll-Slot-Bag").expect("button 2 shows the empty-slot fallback");
|
||||
// Button 2: purchased but empty -> the paper-doll empty bag art, still white. The art comes
|
||||
// from `GetInventorySlotInfo(strsub("BankFrameBag2", 10))` — the DBC's own `"Bag2"` row.
|
||||
let (x2, y2) = center("BankFrameBag2");
|
||||
let c2 = color_at(x2, y2, "UI-PaperDoll-Slot-Bag").expect("button 2 shows the empty-slot art");
|
||||
assert!(
|
||||
near(c2, 1.0, 1.0, 1.0),
|
||||
"purchased slot 2 stays white, got {c2:?}"
|
||||
"button 2 is purchased: white {c2:?}"
|
||||
);
|
||||
|
||||
// Buttons 3..6 (unpurchased): the fallback texture, tinted the reference's exact red.
|
||||
for name in [
|
||||
"BankBagButton3",
|
||||
"BankBagButton4",
|
||||
"BankBagButton5",
|
||||
"BankBagButton6",
|
||||
] {
|
||||
let (x, y) = center(name);
|
||||
let c = color_at(x, y, "PaperDoll-Slot-Bag")
|
||||
.unwrap_or_else(|| panic!("{name} shows the empty-slot fallback"));
|
||||
// Buttons 3..6: unpurchased -> the reference's red tint.
|
||||
for i in 3..=6 {
|
||||
let (x, y) = center(&format!("BankFrameBag{i}"));
|
||||
let c = color_at(x, y, "UI-PaperDoll-Slot-Bag")
|
||||
.unwrap_or_else(|| panic!("button {i} shows the empty-slot art"));
|
||||
assert!(
|
||||
near(c, 1.0, 0.1, 0.1),
|
||||
"{name} (unpurchased) tints red (1.0, 0.1, 0.1), got {c:?}"
|
||||
"button {i} is unpurchased: red {c:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Test 5 — The purchase flow: the Purchase button's click shows the `CONFIRM_BUY_BANK_SLOT` popup; the
|
||||
/// popup's accept queues `take_bank_purchase()`; the whole row hides once `num_purchased` reaches
|
||||
/// 6 (`GetNumBankSlots()`'s `full`).
|
||||
/// The purse row at the bottom of the window, and the director's "the gold at bottom you see
|
||||
/// 97…" report.
|
||||
///
|
||||
/// Our own window painted the purse with `BenillaMoney_Set` into a single FontString, which is
|
||||
/// what could run out of room. The reference's `BankFrameMoneyFrame` is an ordinary
|
||||
/// `SmallMoneyFrameTemplate` — the same three-button gold/silver/copper kit every other window in
|
||||
/// this client already uses — so a large amount splits across three buttons instead of
|
||||
/// overflowing one string. Asserted on the DIGITS in each button, not on the picture: whether it
|
||||
/// reads right on screen is the director's call, but "98765 / 43 / 21 in three separate buttons"
|
||||
/// is a fact a test can hold.
|
||||
#[test]
|
||||
fn the_purse_row_splits_a_large_amount_across_three_coin_buttons() {
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
let mut s = setup();
|
||||
s.set_bank(Some(BankState::default()));
|
||||
s.fire_event("BANKFRAME_OPENED", vec![]);
|
||||
// 98765g 43s 21c — wider than any single-string purse could hold.
|
||||
s.set_money(987_654_321);
|
||||
s.fire_event("PLAYER_MONEY", vec![]);
|
||||
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
|
||||
|
||||
for (coin, want) in [("Gold", "98765"), ("Silver", "43"), ("Copper", "21")] {
|
||||
assert_eq!(
|
||||
s.eval::<String>(&format!(
|
||||
"return BankFrameMoneyFrame{coin}ButtonText:GetText()"
|
||||
))
|
||||
.unwrap(),
|
||||
want,
|
||||
"the {coin} button's digits"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Test 5 — The purchase flow: the Purchase button's click shows the `CONFIRM_BUY_BANK_SLOT` popup;
|
||||
/// the popup's accept queues `take_bank_purchase()`; the whole row hides once `num_purchased`
|
||||
/// reaches 6 (`GetNumBankSlots()`'s `full`).
|
||||
///
|
||||
/// Two things this pins that only the swap made checkable. `BankFramePurchaseButton` is declared
|
||||
/// `virtual="true"` INSIDE `<Frames>` — a reference quirk Classic Era still carries — and only a
|
||||
/// TOP-LEVEL element is ever a template, so the button really exists and really clicks. And the
|
||||
/// dialog itself lives in `UiPanels.xml` now: the reference keeps it in `StaticPopup.lua`, which
|
||||
/// this client replaces, so without the re-home the button would be a silent no-op.
|
||||
#[test]
|
||||
fn purchase_flow_shows_popup_queues_the_intent_and_the_row_hides_when_full() {
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
@@ -339,7 +395,6 @@ fn purchase_flow_shows_popup_queues_the_intent_and_the_row_hides_when_full() {
|
||||
s.set_bank(Some(BankState {
|
||||
num_purchased: 2,
|
||||
next_cost: 100_000,
|
||||
..Default::default()
|
||||
}));
|
||||
s.fire_event("BANKFRAME_OPENED", vec![]);
|
||||
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
|
||||
@@ -353,12 +408,24 @@ fn purchase_flow_shows_popup_queues_the_intent_and_the_row_hides_when_full() {
|
||||
"no popup before the click"
|
||||
);
|
||||
|
||||
s.run("BenillaBankFramePurchaseButton_OnClick()").unwrap();
|
||||
s.run("BankFramePurchaseButton:Click()").unwrap();
|
||||
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
|
||||
assert!(
|
||||
s.eval::<bool>("return StaticPopup1:IsVisible()").unwrap(),
|
||||
"the click shows the confirm popup"
|
||||
);
|
||||
// The dialog carries the cost, which is why 1580's `hasMoneyFrame` had to be live before the
|
||||
// reference's own StaticPopup entry could be re-homed into UiPanels.xml: its OnShow reads
|
||||
// `BankFrame.nextSlotCost`, set by `UpdateBagSlotStatus` off `GetBankSlotCost`.
|
||||
assert!(
|
||||
s.eval::<bool>("return StaticPopup1MoneyFrame:IsShown()")
|
||||
.unwrap(),
|
||||
"the confirm shows the coin row"
|
||||
);
|
||||
assert_eq!(
|
||||
s.eval::<i64>("return BankFrame.nextSlotCost").unwrap(),
|
||||
100_000
|
||||
);
|
||||
assert!(!s.take_bank_purchase(), "nothing queued until accept");
|
||||
|
||||
s.run("StaticPopup1Button1:Click()").unwrap(); // Yes
|
||||
@@ -374,7 +441,6 @@ fn purchase_flow_shows_popup_queues_the_intent_and_the_row_hides_when_full() {
|
||||
s.set_bank(Some(BankState {
|
||||
num_purchased: 6,
|
||||
next_cost: 999_999_999,
|
||||
..Default::default()
|
||||
}));
|
||||
s.fire_event("PLAYERBANKBAGSLOTS_CHANGED", vec![]);
|
||||
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
|
||||
@@ -392,16 +458,20 @@ fn purchase_flow_shows_popup_queues_the_intent_and_the_row_hides_when_full() {
|
||||
/// nothing, so the widget default (`{"LeftButtonUp"}`) swallowed every right-click.
|
||||
///
|
||||
/// It was asserted on the click sound alone, because "the one observable that does not need a bag
|
||||
/// object fed into the slot" was all there was while the popout was a `BenillaBankBagFrame` this
|
||||
/// window drove by hand. 1751 gave the handler a real tail — `ToggleBag(NUM_BAG_SLOTS + id)` — so
|
||||
/// a bag IS fed here now and the right-click is followed all the way to the window it opens.
|
||||
/// That is strictly more of the same property: the right button reaches the whole OnClick body,
|
||||
/// not just its first line.
|
||||
/// object fed into the slot" was all there was while the popout was a `BenillaBankBagFrame` our
|
||||
/// own file drove by hand. The reference's handler has a real tail — `ToggleBag(this:GetID())` —
|
||||
/// so a bag IS fed here now and the right-click is followed all the way to the window it opens.
|
||||
///
|
||||
/// **The sound ORDER is the reference's, and it is the reverse of ours.** Its handler opens the
|
||||
/// bag FIRST and plays `BAGMENUBUTTONPRESS` after (BankFrame.lua:190-197), so the window's own
|
||||
/// `igBackPackOpen` leads. Ours played the press kit first. Nothing depends on it; it is here
|
||||
/// because a transcription that gets the order wrong is exactly the kind of thing the swap is
|
||||
/// meant to stop having to notice.
|
||||
#[test]
|
||||
fn a_bank_bag_button_answers_the_right_button_too() {
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
let mut s = setup();
|
||||
// A bag in bank slot 1 ⇒ container id NUM_BAG_SLOTS + 1 = 5, the id the button toggles.
|
||||
// A bag in bank slot 1 ⇒ container id 5, which IS `BankFrameBag1:GetID()`.
|
||||
s.set_container(5, empty_bag("Bank Bag", 8));
|
||||
s.set_bank(Some(BankState::default()));
|
||||
s.fire_event("BANKFRAME_OPENED", vec![]);
|
||||
@@ -410,8 +480,8 @@ fn a_bank_bag_button_answers_the_right_button_too() {
|
||||
|
||||
let (cx, cy): (f64, f64) = s
|
||||
.eval(
|
||||
"return (BankBagButton1:GetLeft() + BankBagButton1:GetRight()) / 2, \
|
||||
(BankBagButton1:GetTop() + BankBagButton1:GetBottom()) / 2",
|
||||
"return (BankFrameBag1:GetLeft() + BankFrameBag1:GetRight()) / 2, \
|
||||
(BankFrameBag1:GetTop() + BankFrameBag1:GetBottom()) / 2",
|
||||
)
|
||||
.unwrap();
|
||||
s.mouse_button(cx as f32, cy as f32, "RightButton", true);
|
||||
@@ -420,17 +490,17 @@ fn a_bank_bag_button_answers_the_right_button_too() {
|
||||
assert_eq!(
|
||||
s.take_sounds(),
|
||||
vec![
|
||||
benilla_ui::script::SoundRequest::KitName("BAGMENUBUTTONPRESS".into()),
|
||||
benilla_ui::script::SoundRequest::KitName("igBackPackOpen".into()),
|
||||
SoundRequest::KitName("igBackPackOpen".into()),
|
||||
SoundRequest::KitName("BAGMENUBUTTONPRESS".into()),
|
||||
],
|
||||
"a right-click runs the same OnClick a left-click does — the button's own press kit, \
|
||||
then the window the ToggleBag tail opened"
|
||||
"a right-click runs the same OnClick a left-click does — the window ToggleBag opened, \
|
||||
then the button's own press kit"
|
||||
);
|
||||
assert!(bag_open(&s, 5), "the right-click opened the bank bag");
|
||||
assert!(
|
||||
s.eval::<bool>("return BankBagButton1:GetChecked() and true or false")
|
||||
s.eval::<bool>("return BankFrameBag1HighlightFrameTexture:IsShown()")
|
||||
.unwrap(),
|
||||
"…and lit the button (UpdateBagButtonHighlight, this file's SetChecked divergence)"
|
||||
"…and lit the button (UpdateBagButtonHighlight's own texture)"
|
||||
);
|
||||
assert!(s.errors().is_empty(), "click errors: {:?}", s.errors());
|
||||
}
|
||||
@@ -472,8 +542,8 @@ fn clicking_item_slot_one_with_an_empty_cursor_queues_the_pickup() {
|
||||
|
||||
let (cx, cy): (f64, f64) = s
|
||||
.eval(
|
||||
"return (BankItem1:GetLeft() + BankItem1:GetRight()) / 2, \
|
||||
(BankItem1:GetTop() + BankItem1:GetBottom()) / 2",
|
||||
"return (BankFrameItem1:GetLeft() + BankFrameItem1:GetRight()) / 2, \
|
||||
(BankFrameItem1:GetTop() + BankFrameItem1:GetBottom()) / 2",
|
||||
)
|
||||
.unwrap();
|
||||
s.mouse_button(cx as f32, cy as f32, "LeftButton", true);
|
||||
|
||||
@@ -378,12 +378,6 @@ const KNOWN: &[Known] = &[
|
||||
flag: Flag::Id,
|
||||
why: "-2 is KEYRING_CONTAINER, which the reference also arrives at — from Lua, not XML",
|
||||
},
|
||||
Known {
|
||||
frame: "BankFrame",
|
||||
flag: Flag::Id,
|
||||
why: "id=\"-1\" is ours: the bank window's own sentinel for \"not a container slot\", \
|
||||
which the reference has no equivalent of",
|
||||
},
|
||||
// ── parent ─────────────────────────────────────────────────────────────────────────────────
|
||||
//
|
||||
// Three, and each is a seat inside the SAME tree the reference seats it in — which is the
|
||||
|
||||
@@ -3,27 +3,17 @@ use benilla_ui::script::{
|
||||
QuadContent, SoundRequest, UiScript,
|
||||
};
|
||||
|
||||
/// Load one shipped `assets/ui/<file>` into `s`, panicking on any loader error and returning the
|
||||
/// frame count it materialized — the panel tests all load `UiPanels.xml` (decision 0084 §2's slot
|
||||
/// manager) before the panel frame(s) under test, exactly as `ui_script.rs`'s own shipped-list
|
||||
/// order does, so `ShowUIPanel`/`HideUIPanel` already exist when a frame's OnLoad/OnEvent
|
||||
/// references them.
|
||||
fn load_xml(s: &UiScript, file: &str) -> usize {
|
||||
let text = std::fs::read_to_string(
|
||||
std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
|
||||
.join("assets/ui")
|
||||
.join(file),
|
||||
)
|
||||
.unwrap();
|
||||
let doc = benilla_ui::framexml::parse(&text).unwrap();
|
||||
let report = benilla_ui::loader::load(s, &doc, &|_| None);
|
||||
assert!(
|
||||
report.errors.is_empty(),
|
||||
"{file}: loader errors: {:?}",
|
||||
report.errors
|
||||
);
|
||||
report.frames
|
||||
}
|
||||
/// Load one manifest entry into `s`, panicking on any loader error and returning the frame count
|
||||
/// it materialized — the panel tests all load `UiPanels.xml` (decision 0084 §2's slot manager)
|
||||
/// before the panel frame(s) under test, exactly as `benilla.toc`'s own order does, so
|
||||
/// `ShowUIPanel`/`HideUIPanel` already exist when a frame's OnLoad/OnEvent references them.
|
||||
///
|
||||
/// This was a private disk-only reader until 1751's second window: `BankFrame.xml` is the
|
||||
/// reference's own file off the player's chain now, and a reader that only knows `assets/ui`
|
||||
/// cannot name it. [`super::test_ui::load_ui`] knows both stores by the manifest's own rule, so a
|
||||
/// caller that names a chain entry has to open with `wow_data_or_skip!` (only the bank test below
|
||||
/// does).
|
||||
use super::test_ui::load_ui as load_xml;
|
||||
|
||||
/// Find a bare frame's own rect via its `QuadContent::Frame` entry (every frame emits one, at its
|
||||
/// resolved rect, whether or not it paints anything itself — `UiScript::extract`'s doc). Used for
|
||||
@@ -729,25 +719,30 @@ fn shipped_panel_slot_pushable_promotes_to_center() {
|
||||
/// center-parked "left-area" occupant back — UIParent.lua l.777-782).
|
||||
#[test]
|
||||
fn gossip_bank_option_hands_the_left_slot_to_the_bank() {
|
||||
use benilla_ui::script::{BankState, ScriptValue};
|
||||
use benilla_ui::script::BankState;
|
||||
|
||||
// The bank window is the reference's own file off the player's chain (1751).
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
|
||||
// The bank window's own dependency chain (bank_tests::setup), plus the gossip window.
|
||||
let order_first = |bank_first: bool| {
|
||||
let mut s = UiScript::new().unwrap();
|
||||
s.set_screen_size(1024.0, 768.0);
|
||||
load_xml(&s, "Fonts.xml");
|
||||
load_xml(&s, "ItemButtonTemplate.xml"); // the reference bank's slot buttons inherit it
|
||||
load_xml(&s, "MoneyFrame.xml");
|
||||
load_xml(&s, "UiPanels.xml");
|
||||
load_xml(&s, "Cooldown.xml");
|
||||
load_xml(&s, "BagFrame.xml");
|
||||
load_xml(&s, "GameTooltip.xml");
|
||||
load_xml(&s, "MerchantFrame.xml");
|
||||
load_xml(&s, "BankFrame.xml");
|
||||
load_xml(&s, "ScrollTemplates.xml"); // the shared scroll kit the window rides
|
||||
// UIPanelScrollFrameTemplate lives here, and the gossip scroll frame inherits it. NOT
|
||||
// optional: a missing template is a loader *warning*, not an error, so an under-loaded
|
||||
// list passes load_xml and then loses the scrollbar silently.
|
||||
// Before BankFrame, not after: its close and purchase buttons inherit UIPanelCloseButton
|
||||
// and UIPanelButtonTemplate, and an `inherits=` is resolved at LOAD.
|
||||
load_xml(&s, "UIPanelTemplates.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\BankFrame.xml");
|
||||
load_xml(&s, "GossipFrame.xml");
|
||||
|
||||
// The gossip menu is open on the banker (its bank option showing).
|
||||
@@ -772,11 +767,11 @@ fn gossip_bank_option_hands_the_left_slot_to_the_bank() {
|
||||
s.set_bank(Some(BankState::default()));
|
||||
s.set_gossip(None);
|
||||
if bank_first {
|
||||
s.fire_event("BANKFRAME_OPENED", vec![ScriptValue::Str("Banker".into())]);
|
||||
s.fire_event("BANKFRAME_OPENED", vec![]);
|
||||
s.fire_event("GOSSIP_CLOSED", vec![]);
|
||||
} else {
|
||||
s.fire_event("GOSSIP_CLOSED", vec![]);
|
||||
s.fire_event("BANKFRAME_OPENED", vec![ScriptValue::Str("Banker".into())]);
|
||||
s.fire_event("BANKFRAME_OPENED", vec![]);
|
||||
}
|
||||
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
|
||||
|
||||
|
||||
@@ -102,8 +102,9 @@ pub(super) const BAG_UI: &[&str] = &[
|
||||
// `updateContainerFrameAnchors` measures every open bag against `BankFrame:GetRight()`
|
||||
// (ContainerFrame.lua l.505) on EVERY open and close, so the bank window is not optional
|
||||
// scenery for a bag test — it is a hard dependency of the reference's own layout pass. It is
|
||||
// one in the real client too; the manifest just satisfies it far below the bags.
|
||||
"BankFrame.xml",
|
||||
// one in the real client too; the manifest just satisfies it far below the bags. The
|
||||
// reference's own file since 1751's second window.
|
||||
"Interface\\FrameXML\\BankFrame.xml",
|
||||
];
|
||||
|
||||
/// The name of the `ContainerFrame` currently showing bag `id`, or `None` if it is not open.
|
||||
|
||||
@@ -103,6 +103,18 @@ impl WorldWriter {
|
||||
)
|
||||
}
|
||||
|
||||
/// Auto-store an item into a bag (`CMSG_AUTOSTORE_BAG_ITEM`, layout in
|
||||
/// [`messages::auto_store_bag_item`]): take `(src_bag, src_slot)` and put it anywhere inside
|
||||
/// `dst_bag` — **the client names no destination slot; the server picks it**. The wire for
|
||||
/// `PutItemInBag`'s auto-store leg and the whole of `PutItemInBackpack` (wow-re
|
||||
/// `bag-verbs-law.md`). Refusal answers `SMSG_INVENTORY_CHANGE_FAILURE`.
|
||||
pub fn auto_store_bag_item(&mut self, src_bag: u8, src_slot: u8, dst_bag: u8) -> Result<()> {
|
||||
self.send(
|
||||
opcode::CMSG_AUTOSTORE_BAG_ITEM,
|
||||
&messages::auto_store_bag_item(src_bag, src_slot, dst_bag),
|
||||
)
|
||||
}
|
||||
|
||||
/// Split a stack (`CMSG_SPLIT_ITEM`, layout in [`messages::split_item`]): carry `count` off
|
||||
/// `(src_bag, src_slot)` onto `(dst_bag, dst_slot)` — either endpoint may be an equipped bag
|
||||
/// (unlike [`Self::swap_inv_item`]). Success settles both slots via values deltas; refusal
|
||||
|
||||
@@ -10,6 +10,13 @@
|
||||
//! (`BankFrame.lua:1-4`), so the container verbs, the cursor drag-drop, and the stack split all
|
||||
//! work on bank slots with no bank-specific surface.
|
||||
//!
|
||||
//! Nor do the six bank BAGS — the bag items themselves, as opposed to what is inside them. Those
|
||||
//! are inventory slots at live ids 64..=69, fed beside the paper doll's
|
||||
//! ([`super::char_stats::BankBagSlots`]) and read through the ordinary `GetInventoryItem*` /
|
||||
//! `PickupBagFromSlot` surface, which is exactly how the reference's own bank reads them
|
||||
//! (`BankFrame.lua:28`, `ButtonInventorySlot`). They stream in the player descriptor whether or
|
||||
//! not a banker is open, so they are not part of the window's snapshot.
|
||||
//!
|
||||
//! ## The 5875 API shape (the reference `BankFrame.lua`, read as behavior spec this session)
|
||||
//!
|
||||
//! - `GetNumBankSlots()` → `numSlots, full` — purchased count 0..6, `full` as `1`/`nil`
|
||||
@@ -24,9 +31,10 @@
|
||||
//! - `CloseBankFrame()` — client-side close, **no packet exists** for it (decision 0604): flag the
|
||||
//! app to clear its session, the merchant/gossip pattern.
|
||||
//! - `BankButtonIDToInvSlotID(id, isBag)` — the pure button→live-inventory-slot map: item button
|
||||
//! `i` (1..24) → live `39 + i` (wire 39..62 + 1), bag button `j` (1..6) → live `63 + j`
|
||||
//! (wire 63..68 + 1) — the same "live id − 1 = wire slot" law as the doll
|
||||
//! (`crate::script::container`'s `EQUIPMENT_BAG` space).
|
||||
//! `i` (1..24) → live `39 + i` (wire 39..62 + 1), bag button — whose id is the **container id**
|
||||
//! 5..10, not a bag number — → live `59 + id` (wire 63..68 + 1); the same "live id − 1 = wire
|
||||
//! slot" law as the doll (`crate::script::container`'s `EQUIPMENT_BAG` space). See the binding
|
||||
//! for the four places the reference's own file pins the bag arm's numbering.
|
||||
|
||||
use mlua::{Lua, MultiValue, Value};
|
||||
|
||||
@@ -42,12 +50,6 @@ pub struct BankState {
|
||||
/// The NEXT slot's price in copper (`BankBagSlotPrices.dbc` row `num_purchased + 1`; the DBC's
|
||||
/// own 999999999 sentinel past slot 6). 0 only if the DBC row is genuinely absent.
|
||||
pub next_cost: u32,
|
||||
/// The bag buttons' icons: bank bag slot `i`'s held bag, resolved by the app to its icon path
|
||||
/// (`None` = the slot is empty or the template answer is in flight — the XML shows the empty
|
||||
/// slot texture). The reference reads these through the inventory-item API; benilla's doll
|
||||
/// array stops at the equipped bags, so the snapshot carries them instead
|
||||
/// (`BenillaGetBankBagTexture` below — benilla-named, like the merchant's stat feed).
|
||||
pub bag_textures: [Option<String>; 6],
|
||||
}
|
||||
|
||||
impl super::UiScript {
|
||||
@@ -107,24 +109,6 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
// BenillaGetBankBagTexture(i) → the bank bag slot's held-bag icon path | nil (empty slot, or
|
||||
// no bank open) — benilla-named (module doc: the snapshot carries what the reference read
|
||||
// through the inventory-item API).
|
||||
g.set(
|
||||
"BenillaGetBankBagTexture",
|
||||
lua.create_function(|lua, i: usize| {
|
||||
let texture = {
|
||||
let model = lua.app_data_ref::<Model>().expect("model app_data");
|
||||
model.bank.as_ref().and_then(|b| {
|
||||
i.checked_sub(1)
|
||||
.and_then(|n| b.bag_textures.get(n))
|
||||
.and_then(|t| t.clone())
|
||||
})
|
||||
};
|
||||
Ok(match texture {
|
||||
Some(t) => Value::String(lua.create_string(&t)?),
|
||||
None => Value::Nil,
|
||||
})
|
||||
})?,
|
||||
)?;
|
||||
|
||||
// PurchaseSlot() — queue the bank-slot buy intent (the CONFIRM_BUY_BANK_SLOT popup's accept).
|
||||
g.set(
|
||||
"PurchaseSlot",
|
||||
@@ -145,17 +129,28 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
})?,
|
||||
)?;
|
||||
|
||||
// BankButtonIDToInvSlotID(id, isBag) — the pure button→live-slot map (module doc): item
|
||||
// button 1..24 → 40..63, bag button 1..6 → 64..69. Out-of-range answers 0 (the reference
|
||||
// binding is total over its buttons; our XML never asks outside them — 0 is the visible
|
||||
// "wired wrong" tell rather than a silent misroute).
|
||||
// BankButtonIDToInvSlotID(id, isBag) — the pure button→live-slot map (module doc).
|
||||
//
|
||||
// **The bag arm takes the CONTAINER id 5..=10, not a bag number 1..=6**, and getting that
|
||||
// wrong reads six slots off the end of the band. The reference's own file is the oracle
|
||||
// (0675) and says so four times over: `BankFrame.xml` gives `BankFrameBag1..BankFrameBag6`
|
||||
// the ids **5..10** (the generic buttons `BankFrameItem1..24` take 1..24);
|
||||
// `ButtonInventorySlot` hands `this:GetID()` straight in; and both
|
||||
// `UpdateBagButtonHighlight(id)` (`"BankFrameBag"..(id - NUM_BAG_SLOTS)`) and
|
||||
// `BankFrameItemButton_UpdateLock` (`(this:GetID() - 4) > GetNumBankSlots()`) subtract 4 from
|
||||
// that same id to get back to 1..6. So the bag arm is `id + 59` — byte-identical to
|
||||
// `ContainerIDToInventoryID`'s carved second arm (`t = id - 1; t >= 4 → t + 60`,
|
||||
// `crate::script::container`), which is the same map under the other name.
|
||||
//
|
||||
// Out-of-range answers 0 (the reference binding is total over its buttons; our XML never asks
|
||||
// outside them — 0 is the visible "wired wrong" tell rather than a silent misroute).
|
||||
g.set(
|
||||
"BankButtonIDToInvSlotID",
|
||||
lua.create_function(|_, (id, is_bag): (u32, Option<Value>)| {
|
||||
let is_bag = is_bag.is_some_and(|v| v.as_boolean().unwrap_or(true));
|
||||
let live = match (is_bag, id) {
|
||||
(false, 1..=24) => 39 + id,
|
||||
(true, 1..=6) => 63 + id,
|
||||
(true, 5..=10) => 59 + id,
|
||||
_ => 0,
|
||||
};
|
||||
Ok(i64::from(live))
|
||||
@@ -183,7 +178,6 @@ mod tests {
|
||||
s.set_bank(Some(BankState {
|
||||
num_purchased: 2,
|
||||
next_cost: 100_000,
|
||||
..Default::default()
|
||||
}));
|
||||
assert!(s
|
||||
.eval::<bool>("local n, full = GetNumBankSlots()\nreturn n == 2 and full == nil")
|
||||
@@ -194,7 +188,6 @@ mod tests {
|
||||
s.set_bank(Some(BankState {
|
||||
num_purchased: 6,
|
||||
next_cost: 999_999_999,
|
||||
..Default::default()
|
||||
}));
|
||||
assert!(s
|
||||
.eval::<bool>("local n, full = GetNumBankSlots()\nreturn n == 6 and full == 1")
|
||||
@@ -207,26 +200,50 @@ mod tests {
|
||||
.unwrap());
|
||||
}
|
||||
|
||||
/// The bag-button icon feed: slot 1 resolved, slot 2 empty/in-flight (nil), out of range nil.
|
||||
/// The six bank BAGS are inventory slots at live ids 64..=69, read through the ordinary
|
||||
/// inventory API — the band the reference's own bank uses (`ButtonInventorySlot` →
|
||||
/// `BankButtonIDToInvSlotID(id, this.isBag)`, BankFrame.lua:28). Empty slots answer nil, and
|
||||
/// the band does not bleed into the equipment array below it.
|
||||
#[test]
|
||||
fn bank_bag_texture_reads() {
|
||||
fn bank_bag_slots_read_through_the_inventory_api() {
|
||||
let mut s = UiScript::new().unwrap();
|
||||
assert!(s
|
||||
.eval::<bool>("return BenillaGetBankBagTexture(1) == nil")
|
||||
.eval::<bool>("return GetInventoryItemTexture(\"player\", 64) == nil")
|
||||
.unwrap());
|
||||
let mut state = BankState::default();
|
||||
state.bag_textures[0] = Some("Interface\\Icons\\INV_Misc_Bag_08".into());
|
||||
s.set_bank(Some(state));
|
||||
|
||||
let mut bags: crate::script::BankBagSlots = Default::default();
|
||||
bags[0] = Some(crate::script::InvSlotView {
|
||||
item_id: 4500,
|
||||
icon: Some("Interface\\Icons\\INV_Misc_Bag_08".into()),
|
||||
count: 1,
|
||||
link: Some("|cffffffff|Hitem:4500:0:0:0|h[Traveler\'s Backpack]|h|r".into()),
|
||||
equip_slots: vec![20, 21, 22, 23],
|
||||
..Default::default()
|
||||
});
|
||||
s.set_bank_bag_slots(bags);
|
||||
|
||||
assert_eq!(
|
||||
s.eval::<String>("return BenillaGetBankBagTexture(1)")
|
||||
s.eval::<i64>("return BankButtonIDToInvSlotID(5, 1)")
|
||||
.unwrap(),
|
||||
64,
|
||||
"BankFrameBag1's own id is 5 — the container id, not a bag number"
|
||||
);
|
||||
assert_eq!(
|
||||
s.eval::<String>("return GetInventoryItemTexture(\"player\", 64)")
|
||||
.unwrap(),
|
||||
"Interface\\Icons\\INV_Misc_Bag_08"
|
||||
);
|
||||
assert_eq!(
|
||||
s.eval::<i64>("return GetInventoryItemID(\"player\", 64)")
|
||||
.unwrap(),
|
||||
4500
|
||||
);
|
||||
// Bag slot 2 is empty, and slot 70 is past the band — neither falls through to the doll.
|
||||
assert!(s
|
||||
.eval::<bool>("return BenillaGetBankBagTexture(2) == nil")
|
||||
.eval::<bool>("return GetInventoryItemTexture(\"player\", 65) == nil")
|
||||
.unwrap());
|
||||
assert!(s
|
||||
.eval::<bool>("return BenillaGetBankBagTexture(7) == nil")
|
||||
.eval::<bool>("return GetInventoryItemTexture(\"player\", 70) == nil")
|
||||
.unwrap());
|
||||
}
|
||||
|
||||
@@ -244,9 +261,77 @@ mod tests {
|
||||
assert!(!s.take_bank_close(), "drained");
|
||||
}
|
||||
|
||||
/// The button→live-slot map: items 1..24 → 40..63, bags 1..6 → 64..69 (live id − 1 = the wire
|
||||
/// slot: bank items 39..62, bank bags 63..68). The reference calls it with `this.isBag` = 1 or
|
||||
/// nil, so nil/false both mean "item button".
|
||||
/// **The bank's inventory BAND answers from the container feed** — the map decision 1751's
|
||||
/// bank swap turns on. The reference's bank paints every slot through the inventory API
|
||||
/// (`BankFrameItemButton_OnUpdate` → `GetInventoryItemTexture("player", BankButtonIDToInv
|
||||
/// SlotID(id))`, BankFrame.lua:35) while benilla feeds the same items as container `-1`. If
|
||||
/// those two ever disagree the bank draws empty, which is exactly the failure this pins.
|
||||
///
|
||||
/// Asserted through the live API rather than the model, because the API is what the
|
||||
/// reference's file calls, and the tooltip must agree with the icon under it.
|
||||
#[test]
|
||||
fn the_bank_band_reads_the_vault_through_the_inventory_api() {
|
||||
let mut s = UiScript::new().unwrap();
|
||||
let mut slots = std::collections::HashMap::new();
|
||||
slots.insert(
|
||||
3,
|
||||
super::super::ContainerSlot {
|
||||
item_id: 4496,
|
||||
count: 7,
|
||||
texture: Some("Interface\\Icons\\INV_Misc_Bag_08".into()),
|
||||
quality: Some(2),
|
||||
link: Some("|cff1eff00|Hitem:4496:0:0:0|h[Small Brown Pouch]|h|r".into()),
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
s.set_container(
|
||||
-1,
|
||||
Some(super::super::ContainerState {
|
||||
name: Some("Bank".into()),
|
||||
num_slots: 24,
|
||||
slots,
|
||||
}),
|
||||
);
|
||||
|
||||
// Vault slot 3 is live-API inventory id 42 (BankButtonIDToInvSlotID(3)).
|
||||
assert_eq!(
|
||||
s.eval::<i64>("return BankButtonIDToInvSlotID(3)").unwrap(),
|
||||
42
|
||||
);
|
||||
assert_eq!(
|
||||
s.eval::<String>("return GetInventoryItemTexture(\"player\", 42)")
|
||||
.unwrap(),
|
||||
"Interface\\Icons\\INV_Misc_Bag_08"
|
||||
);
|
||||
assert_eq!(
|
||||
s.eval::<i64>("return GetInventoryItemCount(\"player\", 42)")
|
||||
.unwrap(),
|
||||
7
|
||||
);
|
||||
assert_eq!(
|
||||
s.eval::<i64>("return GetInventoryItemID(\"player\", 42)")
|
||||
.unwrap(),
|
||||
4496
|
||||
);
|
||||
// …and an empty vault slot answers nil, not the equipment slot that shares no numbering
|
||||
// with it — the band must not fall through to `inventory_slots`.
|
||||
assert!(s
|
||||
.eval::<bool>("return GetInventoryItemTexture(\"player\", 43) == nil")
|
||||
.unwrap());
|
||||
// The doll is untouched either side of the band.
|
||||
assert!(s
|
||||
.eval::<bool>("return GetInventoryItemTexture(\"player\", 16) == nil")
|
||||
.unwrap());
|
||||
}
|
||||
|
||||
/// The button→live-slot map: item buttons 1..24 → 40..63; bag buttons — **whose ids are the
|
||||
/// container ids 5..10, not bag numbers** — → 64..69 (live id − 1 = the wire slot: bank items
|
||||
/// 39..62, bank bags 63..68). The reference calls it with `this.isBag` = 1 or nil, so
|
||||
/// nil/false both mean "item button".
|
||||
///
|
||||
/// The bag arm's numbering is the reference file's, checked here because getting it wrong
|
||||
/// reads six slots off the end of the band and draws an empty bag row: `BankFrame.xml` gives
|
||||
/// `BankFrameBag1` `id="5"`, and `ButtonInventorySlot` hands that id straight in.
|
||||
#[test]
|
||||
fn bank_button_to_inv_slot() {
|
||||
let s = UiScript::new().unwrap();
|
||||
@@ -259,15 +344,28 @@ mod tests {
|
||||
63
|
||||
);
|
||||
assert_eq!(
|
||||
s.eval::<i64>("return BankButtonIDToInvSlotID(1, 1)")
|
||||
s.eval::<i64>("return BankButtonIDToInvSlotID(5, 1)")
|
||||
.unwrap(),
|
||||
64
|
||||
64,
|
||||
"BankFrameBag1 carries id 5"
|
||||
);
|
||||
assert_eq!(
|
||||
s.eval::<i64>("return BankButtonIDToInvSlotID(6, 1)")
|
||||
s.eval::<i64>("return BankButtonIDToInvSlotID(10, 1)")
|
||||
.unwrap(),
|
||||
69
|
||||
69,
|
||||
"BankFrameBag6 carries id 10"
|
||||
);
|
||||
// It is the SAME map `ContainerIDToInventoryID` computes for a bank bag — one arithmetic
|
||||
// under two names, and a drift between them would put the bag row and the bag windows on
|
||||
// different slots.
|
||||
for id in 5..=10 {
|
||||
assert_eq!(
|
||||
s.eval::<i64>(&format!("return BankButtonIDToInvSlotID({id}, 1)"))
|
||||
.unwrap(),
|
||||
s.eval::<i64>(&format!("return ContainerIDToInventoryID({id})"))
|
||||
.unwrap()
|
||||
);
|
||||
}
|
||||
// false is "not a bag" (nil-or-false truthiness), out of range answers 0.
|
||||
assert_eq!(
|
||||
s.eval::<i64>("return BankButtonIDToInvSlotID(2, false)")
|
||||
@@ -279,9 +377,10 @@ mod tests {
|
||||
0
|
||||
);
|
||||
assert_eq!(
|
||||
s.eval::<i64>("return BankButtonIDToInvSlotID(7, 1)")
|
||||
s.eval::<i64>("return BankButtonIDToInvSlotID(4, 1)")
|
||||
.unwrap(),
|
||||
0
|
||||
0,
|
||||
"4 is an EQUIPPED bag, not a bank one"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -267,17 +267,31 @@ pub struct InvSlotView {
|
||||
pub const INVENTORY_SLOT_COUNT: usize = 24;
|
||||
pub type InventorySlots = [Option<InvSlotView>; INVENTORY_SLOT_COUNT];
|
||||
|
||||
/// The six BANK-BAG slots' snapshot, in button order (live-API ids 64..=69 —
|
||||
/// `BankButtonIDToInvSlotID(1..6, isBag)`). `None` = the slot is empty (or not purchased).
|
||||
///
|
||||
/// **A separate dense array rather than a wider [`InventorySlots`]**, because the live-id space
|
||||
/// between them is not ours to fill: 24..=39 is the backpack and 40..=63 the vault, and both of
|
||||
/// those the model already holds as CONTAINERS — [`Model::inv_slot`]'s bank band maps them rather
|
||||
/// than storing them twice. Growing the doll array to 70 would leave a 40-entry hole whose
|
||||
/// emptiness means "look somewhere else", which is exactly the kind of implicit second rule that
|
||||
/// lets a tooltip disagree with the icon under it. Three bands, three named sources.
|
||||
///
|
||||
/// Fed by the same system as the doll snapshot ([`super::UiScript::set_bank_bag_slots`]): these
|
||||
/// are player-descriptor inventory slots that exist whether or not the bank window is open, not
|
||||
/// part of the bank *window*'s state.
|
||||
pub const BANK_BAG_SLOT_COUNT: usize = 6;
|
||||
pub type BankBagSlots = [Option<InvSlotView>; BANK_BAG_SLOT_COUNT];
|
||||
|
||||
/// The client's own `PaperDollItemFrame.dbc` rows (re-verified against the 1.12.1 (build 5875)
|
||||
/// MPQ this session, byte-exact — 36 records, 3 `u32` fields each: `SlotName` offset,
|
||||
/// `SlotTexture` offset, `SlotID`): `(slotName, slotId, empty-slot art suffix)` for the 24 rows
|
||||
/// `GetInventorySlotInfo` actually names — the 20 equipment+ammo rows plus `Bag0Slot`..`Bag3Slot`
|
||||
/// (ids **20..23**, confirmed this session — every one points at the same
|
||||
/// `interface\paperdoll\UI-PaperDoll-Slot-Bag.blp`, matching the reference `CharacterBag0Slot`
|
||||
/// bag-bar buttons' `GetID()`s). The DBC's remaining 12 rows (`"Bag1".."Bag12"`, ids 64..75) are a
|
||||
/// different, unrelated numbering — not `GetInventorySlotInfo` names, out of scope here — left
|
||||
/// untranscribed. The oddballs among the 24: `BackSlot` shows the **Chest** art and `AmmoSlot` the
|
||||
/// **Ranged** art (both confirmed by their `SlotTexture` offset pointing at that other row's
|
||||
/// string, not a fresh one).
|
||||
/// `SlotTexture` offset, `SlotID`): `(slotName, slotId, empty-slot art suffix)` for all 36 rows —
|
||||
/// the 20 equipment+ammo rows, `Bag0Slot`..`Bag3Slot` (ids **20..23**, every one pointing at the
|
||||
/// same `interface\paperdoll\UI-PaperDoll-Slot-Bag.blp`, matching the reference
|
||||
/// `CharacterBag0Slot` bag-bar buttons' `GetID()`s), and `Bag1`..`Bag12` (ids 64..75 — the
|
||||
/// bank-bag band; see the note over those rows). The oddballs: `BackSlot` shows the **Chest** art
|
||||
/// and `AmmoSlot` the **Ranged** art (both confirmed by their `SlotTexture` offset pointing at
|
||||
/// that other row's string, not a fresh one).
|
||||
const SLOT_INFO: [(&str, i64, &str); 36] = [
|
||||
("AmmoSlot", 0, "Ranged"),
|
||||
("HeadSlot", 1, "Head"),
|
||||
@@ -418,6 +432,16 @@ impl super::UiScript {
|
||||
self.fire_event("UPDATE_INVENTORY_ALERTS", vec![]);
|
||||
}
|
||||
|
||||
/// Push the six bank-bag slots ([`BankBagSlots`] — live ids 64..=69). No event of its own:
|
||||
/// the app pushes it in the same step as [`Self::set_inventory_slots`], whose
|
||||
/// `UNIT_INVENTORY_CHANGED` is the repaint signal the reference's bank buttons already
|
||||
/// register for (`BankFrameBaseButton_OnLoad` takes `PLAYERBANKSLOTS_CHANGED` /
|
||||
/// `ITEM_LOCK_CHANGED` / `BANKFRAME_OPENED`, and `BankFrameItemButton_OnUpdate` re-reads
|
||||
/// every frame besides).
|
||||
pub fn set_bank_bag_slots(&mut self, slots: BankBagSlots) {
|
||||
self.model_mut().bank_bag_slots = slots;
|
||||
}
|
||||
|
||||
/// Drain the inventory-slot ids `UseInventoryItem` queued (decision 0208 phase 1b) — the app
|
||||
/// resolves each to the equipped item's guid and sends `CMSG_USE_ITEM` (bag 255 + the
|
||||
/// 0-based wire slot).
|
||||
@@ -491,20 +515,101 @@ fn player_inv_slot(lua: &Lua, token: &Option<String>, slot: i64) -> Option<InvSl
|
||||
let token = token.as_deref()?;
|
||||
let idx = usize::try_from(slot).ok()?;
|
||||
let model = lua.app_data_ref::<Model>().expect("model app_data");
|
||||
model.inv_slot(token, idx).cloned()
|
||||
model.inv_slot(token, idx)
|
||||
}
|
||||
|
||||
/// The live-API inventory ids the BANK occupies — `BankButtonIDToInvSlotID`'s own two bands
|
||||
/// (`super::bank`): the 24 vault slots at 40..=63, the six bank-bag slots at 64..=69.
|
||||
const BANK_INV_SLOTS: std::ops::RangeInclusive<usize> = 40..=63;
|
||||
const BANK_BAG_INV_SLOTS: std::ops::RangeInclusive<usize> = 64..=69;
|
||||
|
||||
/// The container id the vault's own slots live under — `ui_items`' constant of the same name,
|
||||
/// restated here because this file is the other end of the same map.
|
||||
const BANK_CONTAINER: i64 = -1;
|
||||
|
||||
impl Model {
|
||||
/// The equipment slot `token` exposes at live-API id `slot`, or `None`. The one place the
|
||||
/// two-source routing above is decided — shared by the `GetInventoryItem*` getters and
|
||||
/// source routing is decided — shared by the `GetInventoryItem*` getters and
|
||||
/// `GameTooltip:SetInventoryItem`, so a tooltip can never disagree with the icon under it.
|
||||
pub(super) fn inv_slot(&self, token: &str, slot: usize) -> Option<&InvSlotView> {
|
||||
let slots = if token.eq_ignore_ascii_case("player") {
|
||||
&self.inventory_slots
|
||||
} else {
|
||||
&self.inspect.as_ref().filter(|v| v.unit == token)?.slots
|
||||
};
|
||||
slots.get(slot)?.as_ref()
|
||||
///
|
||||
/// **THREE sources now, and the third is a VIEW, not a store** (decision 1751's bank swap).
|
||||
/// The reference's bank paints every one of its slots through the *inventory* API —
|
||||
/// `BankFrameItemButton_OnUpdate` reads `GetInventoryItemTexture("player", BankButtonIDToInv
|
||||
/// SlotID(id))` (BankFrame.lua:35) — while benilla feeds the very same items as *containers*
|
||||
/// (`-1` for the vault, `5..10` for the bank bags), which is how our own bank window read
|
||||
/// them. Those are two names for one set of descriptor fields, so the fix is a map, not a
|
||||
/// second copy: a read in the bank band is answered from the container snapshot. Duplicating
|
||||
/// the items into a wider `inventory_slots` array would put two truths in the model and let
|
||||
/// a tooltip disagree with the icon under it, which is the one thing this function exists to
|
||||
/// prevent.
|
||||
pub(super) fn inv_slot(&self, token: &str, slot: usize) -> Option<InvSlotView> {
|
||||
if token.eq_ignore_ascii_case("player") {
|
||||
if let Some(view) = self.bank_inv_slot(slot) {
|
||||
return Some(view);
|
||||
}
|
||||
return self.inventory_slots.get(slot)?.clone();
|
||||
}
|
||||
self.inspect
|
||||
.as_ref()
|
||||
.filter(|v| v.unit == token)?
|
||||
.slots
|
||||
.get(slot)?
|
||||
.clone()
|
||||
}
|
||||
|
||||
/// The bank band's answer, or `None` for any id outside it — see [`Model::inv_slot`].
|
||||
fn bank_inv_slot(&self, slot: usize) -> Option<InvSlotView> {
|
||||
if BANK_INV_SLOTS.contains(&slot) {
|
||||
let vault = self.containers.get(&BANK_CONTAINER)?;
|
||||
let n = (slot - BANK_INV_SLOTS.start() + 1) as u32;
|
||||
return vault.slots.get(&n).map(InvSlotView::from_container_slot);
|
||||
}
|
||||
if BANK_BAG_INV_SLOTS.contains(&slot) {
|
||||
// A bank BAG is not a slot in a container — it IS one, so there is no container slot
|
||||
// to map and this band is a real store ([`BankBagSlots`], fed off the player
|
||||
// descriptor's own `PLAYER_FIELD_BANK_BAG_SLOT_*` guids by the same system that feeds
|
||||
// the doll). It has to be the whole item, not just its icon: the reference picks a
|
||||
// bank bag up with `PickupBagFromSlot(BankButtonIDToInvSlotID(i, 1))` and describes it
|
||||
// with `GameTooltip:SetInventoryItem("player", …)`, both of which read this band.
|
||||
let i = slot - BANK_BAG_INV_SLOTS.start();
|
||||
return self.bank_bag_slots.get(i)?.clone();
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// The display name inside an item link — the text between `|h[` and `]|h`.
|
||||
fn link_item_name(link: &str) -> Option<String> {
|
||||
let (_, rest) = link.split_once("|h[")?;
|
||||
let (name, _) = rest.split_once("]|h")?;
|
||||
Some(name.to_string())
|
||||
}
|
||||
|
||||
impl InvSlotView {
|
||||
/// The same item, seen through the inventory API instead of the container API — the map
|
||||
/// [`Model::inv_slot`]'s bank band is built on. Every field is the same descriptor field under
|
||||
/// the other name; what a `ContainerSlot` has and this does not (`equip_slots`, `cooldown`,
|
||||
/// `readable`) has no inventory-API reader, and what this has and a container slot does not
|
||||
/// (`flags`) is not carried by the container feed.
|
||||
fn from_container_slot(slot: &super::container::ContainerSlot) -> Self {
|
||||
InvSlotView {
|
||||
item_id: slot.item_id,
|
||||
bar_placeable: slot.bar_placeable,
|
||||
icon: slot.texture.clone(),
|
||||
count: slot.count,
|
||||
quality: slot.quality.map_or(0, |q| q as i32),
|
||||
// A container slot carries no `name` of its own — the container API never asks for one
|
||||
// — but its `link` is composed FROM the name (`|Hitem:…|h[Name]|h`), so the one field
|
||||
// the inventory API adds is recoverable rather than absent.
|
||||
name: slot.link.as_deref().and_then(link_item_name),
|
||||
durability: slot.durability,
|
||||
already_bound: slot.already_bound,
|
||||
link: slot.link.clone(),
|
||||
locked: slot.locked,
|
||||
creator: slot.creator.clone(),
|
||||
enchants: slot.enchants.clone(),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -176,7 +176,30 @@ pub struct ContainerMove {
|
||||
pub count: Option<u32>,
|
||||
}
|
||||
|
||||
/// One queued **auto-store into a bag** — `PutItemInBag`'s third leg and the whole of
|
||||
/// `PutItemInBackpack` (`crate::script::cursor::bag_verbs`): take the item at `(src_bag, src_slot)`
|
||||
/// and put it anywhere inside container `dst_bag`.
|
||||
///
|
||||
/// **There is no destination slot, and that is the finding, not an omission**: the reference's
|
||||
/// `CMSG_AUTOSTORE_BAG_ITEM 0x10B` carries `(srcbag, srcslot, dstbag)` and nothing else, and its
|
||||
/// split sibling `CMSG_SPLIT_ITEM 0x10E` carries the literal `0xFF` where a slot would go. The
|
||||
/// server picks. `count`: `None` = the whole stack (AUTOSTORE); `Some(n)` = a split carry, which
|
||||
/// the drain sends as SPLIT instead — the same fork the reference makes on `[0xb4b40c]`.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct BagAutoStore {
|
||||
pub src_bag: i64,
|
||||
pub src_slot: u32,
|
||||
/// Live-API container id (0 = backpack, 1..=4 an equipped bag, 5..=10 a bank bag).
|
||||
pub dst_bag: i64,
|
||||
pub count: Option<u32>,
|
||||
}
|
||||
|
||||
impl super::UiScript {
|
||||
/// Drain the auto-stores `PutItemInBag`/`PutItemInBackpack` queued since the last call.
|
||||
pub fn take_bag_autostores(&mut self) -> Vec<BagAutoStore> {
|
||||
std::mem::take(&mut self.model_mut().bag_autostores)
|
||||
}
|
||||
|
||||
/// Push (or remove, with `None`) one bag's snapshot, keyed by live-API bag id (0 = backpack).
|
||||
/// Each slot's cooldown arrives with its absolute start already on the `GetTime` clock (ms) —
|
||||
/// storing is a pure unit conversion, the same seam shape as
|
||||
|
||||
@@ -15,6 +15,7 @@ use mlua::{Lua, Value};
|
||||
|
||||
use super::{Model, ScriptValue};
|
||||
|
||||
mod bag_verbs;
|
||||
mod bar;
|
||||
mod doll;
|
||||
mod drag;
|
||||
@@ -639,6 +640,7 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
)?;
|
||||
|
||||
doll::install(lua)?;
|
||||
bag_verbs::install(lua)?;
|
||||
bar::install(lua)?;
|
||||
pet::install(lua)?;
|
||||
|
||||
|
||||
@@ -0,0 +1,589 @@
|
||||
//! The three BAG-SLOT verbs — `PutItemInBag`, `PutItemInBackpack`, `PickupBagFromSlot`.
|
||||
//!
|
||||
//! CARVED, not inferred (wow-re `system/ui/ui.md`, "The three bag verbs — one worker, a three-way
|
||||
//! fork, and a return value that is not 'did it place'"; `scratch/bag-verbs-law.md`, §5 trio +
|
||||
//! orchestrator byte arbitration 2026-08-11). The three findings that shape everything below:
|
||||
//!
|
||||
//! 1 · **`PutItemInBackpack()` IS `PutItemInBag(0xFF)`.** `0x4c7ed0` is a ten-byte thunk —
|
||||
//! `mov ecx,0xff` / `jmp 0x4c7c00` — so there is one worker here too, and only the destination
|
||||
//! differs. The `0xFF` path is *not* "bag 0": the destination guid is the player's own, which
|
||||
//! makes the empty-slot leg unreachable and `0x4c7d3e je 0x4c7e0c` skip the swap fork outright,
|
||||
//! so a bag dropped on the backpack button is auto-stored INTO it, never swapped with it.
|
||||
//!
|
||||
//! 2 · **`PutItemInBag` forks three ways**, on the state of the target slot and the held object:
|
||||
//! empty slot → the paper-doll handler equips the held bag ([`super::doll::pickup_inventory_item`],
|
||||
//! which is `0x4c7300` — the same function the byte path calls); occupied + the held object is a
|
||||
//! CONTAINER → a bag↔bag swap; occupied + an ordinary item → auto-store into that container's
|
||||
//! contents, with the client picking no destination slot at all.
|
||||
//!
|
||||
//! 3 · **The return value answers "was this click consumed as a placement against an existing
|
||||
//! container?", not "did something get placed".** It is the Lua **number `1`**, never the boolean
|
||||
//! `true` — and it is `nil` on the empty-slot leg that really does equip the bag, and whenever
|
||||
//! the cursor is empty. That last one is the whole reason a plain left-click opens a bag:
|
||||
//! `BagSlotButton_OnClick`'s `if ( not hadItem ) then ToggleBag(...)`.
|
||||
//!
|
||||
//! `PickupBagFromSlot` has its own, much shorter worker (`0x4c7b00`) and is a *destructive*
|
||||
//! pickup: it drops whatever the cursor already held before it looks at the slot.
|
||||
|
||||
use mlua::{Lua, MultiValue, Value};
|
||||
|
||||
use crate::script::container::ContainerMove;
|
||||
use crate::script::Model;
|
||||
|
||||
use super::{clear_cursor, queue_cursor_update, queue_lock_changed, CursorItem, CursorPayload};
|
||||
use super::{doll::pickup_inventory_item, EQUIPMENT_BAG};
|
||||
|
||||
/// The live-API inventory ids the two verbs' shared argument reader (`0x4c8520`) accepts, minus
|
||||
/// what each binding's own floor gate (`cmp ecx,0x13; jl` on the 0-based value — `0x4c8f1f`,
|
||||
/// `0x4c8fbf`) then cuts off. In the space a Lua caller writes:
|
||||
///
|
||||
/// | band | what it is |
|
||||
/// |---|---|
|
||||
/// | 20..=23 | the four EQUIPPED bag slots (`Bag0Slot`..`Bag3Slot`) |
|
||||
/// | 40..=63 | the bank's 24 generic slots |
|
||||
/// | 64..=69 | the six BANK BAG slots |
|
||||
/// | 82..=113 | the keyring |
|
||||
///
|
||||
/// The reader rejects the backpack's own item slots (24..=39) and buyback (70..=81); the floor
|
||||
/// cuts equipment (1..=19) and ammo. **So a macro may legally hand either verb a bank item slot
|
||||
/// or a keyring slot, and nothing downstream catches it** — the reference sends a packet the
|
||||
/// server refuses. See [`autostore_container`] for the one place benilla is tighter, and why.
|
||||
fn accepts(live: u32) -> bool {
|
||||
(20..=23).contains(&live)
|
||||
|| (40..=63).contains(&live)
|
||||
|| (64..=69).contains(&live)
|
||||
|| (82..=113).contains(&live)
|
||||
}
|
||||
|
||||
/// The live-API **container** id a bag slot's own inventory id names — the inverse of
|
||||
/// `ContainerIDToInventoryID` (`crate::script::container`'s own table): 20..=23 → 1..=4, 64..=69 →
|
||||
/// 5..=10. `None` for an inventory id that is not a bag slot.
|
||||
///
|
||||
/// **This is the one place benilla's gate is tighter than the engine's**, and it is deliberate.
|
||||
/// [`accepts`] admits the bank's generic slots and the keyring because the reference's argument
|
||||
/// reader does; the reference then auto-stores *into* them, sending
|
||||
/// `CMSG_AUTOSTORE_BAG_ITEM(…, dstbag = live − 1)` for a `dstbag` that names no container, which
|
||||
/// the server refuses (`EQUIP_ERR_ITEM_DOESNT_GO_TO_SLOT`). benilla has no container id to name
|
||||
/// there, so [`put_item_in_bag`]'s auto-store leg answers `nil` and sends nothing. No shipped
|
||||
/// window produces the call — the reference's own two callers are `BagSlotButton_OnClick` and
|
||||
/// `BankFrameItemButtonBag_OnClick`, both of which pass a bag slot — and a refusal the player
|
||||
/// never sees is not worth a packet.
|
||||
fn autostore_container(live: u32) -> Option<i64> {
|
||||
match live {
|
||||
20..=23 => Some(i64::from(live) - 19),
|
||||
64..=69 => Some(i64::from(live) - 59),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// `Bag0Slot` — the first of the four EQUIPPED bag slots, and the tell that an item is a
|
||||
/// container. See [`is_container`].
|
||||
const BAG0_SLOT: u8 = 20;
|
||||
|
||||
/// The six BANK BAG slots' live-API ids (`BankButtonIDToInvSlotID(1..6, isBag)`).
|
||||
pub(super) const BANK_BAG_INV_SLOTS: std::ops::RangeInclusive<u32> = 64..=69;
|
||||
|
||||
/// Whether the held item is a CONTAINER — the fork the reference reads straight off the object
|
||||
/// (`0x4c7dc4 shr eax,2; test al,1`, `OBJECT_FIELD_TYPE`'s `TYPEMASK_CONTAINER` bit).
|
||||
///
|
||||
/// benilla has no object-type field on a cursor payload, and asks the equivalent question of the
|
||||
/// data it does carry: `equip_slots` is the server's own `Player::FindEquipSlot`, and
|
||||
/// `INVTYPE_BAG` is the ONLY inventory type in that table that yields the bag slots
|
||||
/// (`ui_items::find_equip_slot`, arm 18). So "this item names Bag0Slot as a place it could be
|
||||
/// worn" and "this item is a container" are the same set.
|
||||
fn is_container(item: &CursorItem) -> bool {
|
||||
item.equip_slots.contains(&BAG0_SLOT)
|
||||
}
|
||||
|
||||
/// Whether the held item may be dropped on doll slot `id` — the fit rule, one band wider than
|
||||
/// `equip_slots` alone.
|
||||
///
|
||||
/// The four EQUIPPED bag slots are in `equip_slots` because the server's `FindEquipSlot` names
|
||||
/// them; the six BANK BAG slots never are, because `FindEquipSlot` does not know about the bank
|
||||
/// (it is `CanBankItem`'s business there). The client's own predicate `CursorCanGoInSlot`
|
||||
/// (`0x5ea720`) is 0216 §5's unresolved residual, so the rule here is stated from the observable:
|
||||
/// a bank bag slot takes a container and nothing else, and the server's `CanBankItem` referees
|
||||
/// either way through `SMSG_INVENTORY_CHANGE_FAILURE`.
|
||||
pub(super) fn fits_slot(item: &CursorItem, id: u32) -> bool {
|
||||
if BANK_BAG_INV_SLOTS.contains(&id) {
|
||||
return is_container(item);
|
||||
}
|
||||
u8::try_from(id).is_ok_and(|id| item.equip_slots.contains(&id))
|
||||
}
|
||||
|
||||
/// `PutItemInBag(inventorySlot)` — module doc, finding 2. Returns the reference's own `1`/`nil`
|
||||
/// as a bool the binding maps ("was this click consumed as a placement against an existing
|
||||
/// container?", NOT "did something get placed").
|
||||
pub(super) fn put_item_in_bag(model: &mut Model, live: u32) -> bool {
|
||||
if !accepts(live) {
|
||||
return false;
|
||||
}
|
||||
// `0x4c7ce4 je 0x4c7eaf` — an empty cursor is the FIRST thing tested, and it answers `nil`
|
||||
// with nothing sent. A Spell/Action/Macro/PetAction/StablePet payload is not the client's
|
||||
// item cursor either, and takes the same exit: this is what leaves a plain click free to open
|
||||
// the bag.
|
||||
let Some(CursorPayload::Item(held)) = model.cursor.clone() else {
|
||||
return false;
|
||||
};
|
||||
let occupied = usize::try_from(live)
|
||||
.ok()
|
||||
.and_then(|slot| model.inv_slot("player", slot))
|
||||
.is_some();
|
||||
|
||||
if !occupied {
|
||||
// Leg 1 — the EMPTY slot. The byte path calls `0x4c7300`, the paper-doll handler, which
|
||||
// is `pickup_inventory_item`'s place arm here: the fit predicate, the queued move, the
|
||||
// clear and both events, all of it already the doll's. `0x4c7cae xor eax,eax` is
|
||||
// unconditional, so this answers `nil` whether or not the bag actually went in.
|
||||
pickup_inventory_item(model, live);
|
||||
return false;
|
||||
}
|
||||
|
||||
// A pure cancel: the held item IS this bag (put back where it came from). Sends nothing, and
|
||||
// answers `1` — the click WAS consumed.
|
||||
if held.bag == EQUIPMENT_BAG && held.slot == live {
|
||||
model.cursor = None;
|
||||
queue_cursor_update(model);
|
||||
queue_lock_changed(model, held.bag, held.slot);
|
||||
return true;
|
||||
}
|
||||
|
||||
if is_container(&held) || held.bag == EQUIPMENT_BAG {
|
||||
// Leg 2 — bag↔bag SWAP (`0x5e0c40`, `CMSG_SWAP_ITEM`/`CMSG_SWAP_INV_ITEM`). "Came from a
|
||||
// bag equipment slot" is the reference's second entry to this fork, hence the `||`. The
|
||||
// existing move drain already picks the opcode off the two endpoints.
|
||||
//
|
||||
// A split carry cannot swap: half a stack is not a bag. The reference reaches its SPLIT
|
||||
// sender only from the auto-store leg, so this falls through to a refusal, kept.
|
||||
if held.count.is_none() {
|
||||
model.cursor = None;
|
||||
model.container_moves.push(ContainerMove {
|
||||
src_bag: held.bag,
|
||||
src_slot: held.slot,
|
||||
dst_bag: EQUIPMENT_BAG,
|
||||
dst_slot: live,
|
||||
count: None,
|
||||
});
|
||||
queue_cursor_update(model);
|
||||
queue_lock_changed(model, held.bag, held.slot);
|
||||
return true;
|
||||
}
|
||||
model.cursor = Some(CursorPayload::Item(held));
|
||||
return true;
|
||||
}
|
||||
|
||||
// Leg 3 — AUTO-STORE into the container's contents (`CMSG_AUTOSTORE_BAG_ITEM 0x10B`, or
|
||||
// `CMSG_SPLIT_ITEM 0x10E` for a split carry). **The client never picks a destination slot**:
|
||||
// AUTOSTORE carries none and SPLIT carries the literal `0xFF`.
|
||||
let Some(dst_bag) = autostore_container(live) else {
|
||||
return false;
|
||||
};
|
||||
model.cursor = None;
|
||||
model.bag_autostores.push(crate::script::BagAutoStore {
|
||||
src_bag: held.bag,
|
||||
src_slot: held.slot,
|
||||
dst_bag,
|
||||
count: held.count,
|
||||
});
|
||||
queue_cursor_update(model);
|
||||
queue_lock_changed(model, held.bag, held.slot);
|
||||
true
|
||||
}
|
||||
|
||||
/// `PutItemInBackpack()` — module doc, finding 1: the same worker with the destination fixed to
|
||||
/// the player's own guid, which makes the empty-slot and swap legs unreachable. Everything that
|
||||
/// lands here auto-stores into the backpack.
|
||||
pub(super) fn put_item_in_backpack(model: &mut Model) -> bool {
|
||||
let Some(CursorPayload::Item(held)) = model.cursor.clone() else {
|
||||
return false;
|
||||
};
|
||||
model.cursor = None;
|
||||
model.bag_autostores.push(crate::script::BagAutoStore {
|
||||
src_bag: held.bag,
|
||||
src_slot: held.slot,
|
||||
dst_bag: 0,
|
||||
count: held.count,
|
||||
});
|
||||
queue_cursor_update(model);
|
||||
queue_lock_changed(model, held.bag, held.slot);
|
||||
true
|
||||
}
|
||||
|
||||
/// `PickupBagFromSlot(inventorySlot)` — a **destructive** pickup, not a restricted
|
||||
/// `PickupInventoryItem` (`0x4c7b00`). Returns nothing.
|
||||
///
|
||||
/// Three things it does that the doll pickup does not:
|
||||
///
|
||||
/// · **`ClearCursor(1, 1)` unconditionally, before it looks at anything** (`0x4c7b79`) — whatever
|
||||
/// was held is dropped and unlocked, even if the slot then turns out to be empty.
|
||||
/// · Resolves the occupant with **`TYPEMASK_CONTAINER`** (`0x4c7bb3 mov ecx,4`, where
|
||||
/// `PickupInventoryItem` uses `ecx = 2`). This is what makes it silently no-op at the bank item
|
||||
/// slots and the keyring that its floor-only gate still admits: those hold ordinary items.
|
||||
/// · Has **no place/swap path at all**, and none of `0x4c7300`'s targeting reroute, repair-cursor
|
||||
/// leg or slot-`−1` ammo leg.
|
||||
///
|
||||
/// There is **no client-side "a bag must be empty to be moved" rule** — both workers were read end
|
||||
/// to end and neither inspects container occupancy. That refusal is the server's
|
||||
/// `Player::CanUnequipItem` (`EQUIP_ERR_CAN_ONLY_DO_WITH_EMPTY_BAGS`), arriving as
|
||||
/// `SMSG_INVENTORY_CHANGE_FAILURE`.
|
||||
pub(super) fn pickup_bag_from_slot(model: &mut Model, live: u32) {
|
||||
if !accepts(live) {
|
||||
return;
|
||||
}
|
||||
clear_cursor(model);
|
||||
let picked = usize::try_from(live)
|
||||
.ok()
|
||||
.and_then(|slot| model.inv_slot("player", slot))
|
||||
.filter(|s| s.item_id != 0 && !s.locked)
|
||||
.map(|s| CursorItem {
|
||||
bag: EQUIPMENT_BAG,
|
||||
slot: live,
|
||||
item_id: s.item_id,
|
||||
texture: s.icon.clone(),
|
||||
link: s.link.clone(),
|
||||
quality: Some(u32::try_from(s.quality).unwrap_or(0)),
|
||||
count: None,
|
||||
bar_placeable: s.bar_placeable,
|
||||
equip_slots: s.equip_slots.clone(),
|
||||
})
|
||||
// TYPEMASK_CONTAINER, applied after the view is built so the predicate reads off the same
|
||||
// `equip_slots` every other bag decision here does ([`is_container`]).
|
||||
.filter(is_container);
|
||||
if let Some(item) = picked {
|
||||
model.cursor = Some(CursorPayload::Item(item));
|
||||
queue_cursor_update(model);
|
||||
queue_lock_changed(model, EQUIPMENT_BAG, live);
|
||||
}
|
||||
}
|
||||
|
||||
/// Register the three verbs — top-level globals, like every other cursor binding.
|
||||
pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
let g = lua.globals();
|
||||
|
||||
// The number `1` or nil — `0x6f3810` writes Lua tag 3 (number) with the IEEE-754 double
|
||||
// `0x3FF0000000000000`; tag 1 (boolean) appears in neither body. FrameXML only ever tests it
|
||||
// for truthiness, but an addon that compares against `true` must see what the reference gives
|
||||
// it.
|
||||
fn consumed(yes: bool) -> mlua::Result<MultiValue> {
|
||||
Ok(MultiValue::from_vec(vec![if yes {
|
||||
Value::Number(1.0)
|
||||
} else {
|
||||
Value::Nil
|
||||
}]))
|
||||
}
|
||||
|
||||
g.set(
|
||||
"PutItemInBag",
|
||||
lua.create_function(|lua, live: u32| {
|
||||
let yes = {
|
||||
let mut model = lua.app_data_mut::<Model>().expect("model app_data");
|
||||
put_item_in_bag(&mut model, live)
|
||||
};
|
||||
consumed(yes)
|
||||
})?,
|
||||
)?;
|
||||
g.set(
|
||||
"PutItemInBackpack",
|
||||
lua.create_function(|lua, ()| {
|
||||
let yes = {
|
||||
let mut model = lua.app_data_mut::<Model>().expect("model app_data");
|
||||
put_item_in_backpack(&mut model)
|
||||
};
|
||||
consumed(yes)
|
||||
})?,
|
||||
)?;
|
||||
g.set(
|
||||
"PickupBagFromSlot",
|
||||
lua.create_function(|lua, live: u32| {
|
||||
let mut model = lua.app_data_mut::<Model>().expect("model app_data");
|
||||
pickup_bag_from_slot(&mut model, live);
|
||||
Ok(())
|
||||
})?,
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::script::cursor::EQUIPMENT_BAG;
|
||||
use crate::script::{
|
||||
BagAutoStore, BankBagSlots, ContainerMove, ContainerSlot, ContainerState, InvSlotView,
|
||||
InventorySlots, UiScript,
|
||||
};
|
||||
|
||||
/// An item that IS a bag — `equip_slots` is `find_equip_slot(INVTYPE_BAG)`, the only inventory
|
||||
/// type in that table that names Bag0Slot, which is how [`super::is_container`] reads it.
|
||||
fn bag_slot_view(item_id: u32) -> InvSlotView {
|
||||
InvSlotView {
|
||||
item_id,
|
||||
icon: Some("Interface\\Icons\\INV_Misc_Bag_08".into()),
|
||||
count: 1,
|
||||
equip_slots: vec![20, 21, 22, 23],
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// A backpack holding one bag in slot 1 and one ordinary (head-slot) item in slot 2.
|
||||
fn backpack() -> ContainerState {
|
||||
let mut slots = std::collections::HashMap::new();
|
||||
slots.insert(
|
||||
1,
|
||||
ContainerSlot {
|
||||
item_id: 4500,
|
||||
count: 1,
|
||||
texture: Some("Interface\\Icons\\INV_Misc_Bag_08".into()),
|
||||
equip_slots: vec![20, 21, 22, 23],
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
slots.insert(
|
||||
2,
|
||||
ContainerSlot {
|
||||
item_id: 1234,
|
||||
count: 1,
|
||||
texture: Some("Interface\\Icons\\INV_Helmet_01".into()),
|
||||
equip_slots: vec![1],
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
ContainerState {
|
||||
name: Some("Backpack".into()),
|
||||
num_slots: 16,
|
||||
slots,
|
||||
}
|
||||
}
|
||||
|
||||
/// Bank bag slot 1 (live id 64) holds a bag; the equipped bag slots stay empty.
|
||||
fn one_bank_bag() -> BankBagSlots {
|
||||
let mut bags: BankBagSlots = Default::default();
|
||||
bags[0] = Some(bag_slot_view(4500));
|
||||
bags
|
||||
}
|
||||
|
||||
/// Finding 3, the one every caller depends on: an empty cursor answers **nil** and sends
|
||||
/// nothing, which is what leaves `BagSlotButton_OnClick`'s `if ( not hadItem ) then
|
||||
/// ToggleBag(...)` free to open the bag.
|
||||
#[test]
|
||||
fn an_empty_cursor_answers_nil_and_sends_nothing() {
|
||||
let mut s = UiScript::new().unwrap();
|
||||
s.set_bank_bag_slots(one_bank_bag());
|
||||
assert!(s.eval::<bool>("return PutItemInBag(64) == nil").unwrap());
|
||||
assert!(s.eval::<bool>("return PutItemInBag(20) == nil").unwrap());
|
||||
assert!(s.eval::<bool>("return PutItemInBackpack() == nil").unwrap());
|
||||
assert!(s.take_container_moves().is_empty());
|
||||
assert!(s.take_bag_autostores().is_empty());
|
||||
}
|
||||
|
||||
/// Leg 1 — the EMPTY slot equips the held bag, and answers **nil** while doing it
|
||||
/// (`0x4c7cae xor eax,eax` is unconditional). That pairing is the reference's, not a slip:
|
||||
/// the click both puts the bag in and opens the window it just created.
|
||||
#[test]
|
||||
fn an_empty_bag_slot_equips_the_held_bag_and_still_answers_nil() {
|
||||
let mut s = UiScript::new().unwrap();
|
||||
s.set_container(0, Some(backpack()));
|
||||
s.set_bank_bag_slots(Default::default());
|
||||
s.run("PickupContainerItem(0, 1)").unwrap();
|
||||
|
||||
assert!(s.eval::<bool>("return PutItemInBag(64) == nil").unwrap());
|
||||
assert!(s.cursor_item().is_none(), "placed, not kept");
|
||||
assert_eq!(
|
||||
s.take_container_moves(),
|
||||
vec![ContainerMove {
|
||||
src_bag: 0,
|
||||
src_slot: 1,
|
||||
dst_bag: EQUIPMENT_BAG,
|
||||
dst_slot: 64,
|
||||
count: None,
|
||||
}]
|
||||
);
|
||||
assert!(s.take_bag_autostores().is_empty(), "an equip, not a store");
|
||||
}
|
||||
|
||||
/// Leg 1's fit rule: a bank bag slot takes a CONTAINER and nothing else. An ordinary item
|
||||
/// dropped on an empty bank bag slot is refused and kept — the server's `CanBankItem` would
|
||||
/// refuse it anyway, and there is nothing to auto-store into.
|
||||
#[test]
|
||||
fn an_empty_bag_slot_refuses_an_ordinary_item() {
|
||||
let mut s = UiScript::new().unwrap();
|
||||
s.set_container(0, Some(backpack()));
|
||||
s.run("PickupContainerItem(0, 2)").unwrap();
|
||||
|
||||
assert!(s.eval::<bool>("return PutItemInBag(64) == nil").unwrap());
|
||||
assert!(s.cursor_item().is_some(), "refused, kept");
|
||||
assert!(s.take_container_moves().is_empty());
|
||||
assert!(s.take_bag_autostores().is_empty());
|
||||
}
|
||||
|
||||
/// Leg 2 — an OCCUPIED slot plus a held container is a bag↔bag swap, and answers `1`.
|
||||
#[test]
|
||||
fn an_occupied_bag_slot_swaps_with_a_held_bag_and_answers_one() {
|
||||
let mut s = UiScript::new().unwrap();
|
||||
s.set_container(0, Some(backpack()));
|
||||
s.set_bank_bag_slots(one_bank_bag());
|
||||
s.run("PickupContainerItem(0, 1)").unwrap();
|
||||
|
||||
assert_eq!(s.eval::<i64>("return PutItemInBag(64)").unwrap(), 1);
|
||||
assert!(s.cursor_item().is_none());
|
||||
assert_eq!(
|
||||
s.take_container_moves(),
|
||||
vec![ContainerMove {
|
||||
src_bag: 0,
|
||||
src_slot: 1,
|
||||
dst_bag: EQUIPMENT_BAG,
|
||||
dst_slot: 64,
|
||||
count: None,
|
||||
}]
|
||||
);
|
||||
}
|
||||
|
||||
/// Leg 3 — an OCCUPIED slot plus an ORDINARY item auto-stores INTO that bag. The intent
|
||||
/// carries no destination slot, because the wire has none.
|
||||
#[test]
|
||||
fn an_occupied_bag_slot_auto_stores_an_ordinary_item() {
|
||||
let mut s = UiScript::new().unwrap();
|
||||
s.set_container(0, Some(backpack()));
|
||||
s.set_bank_bag_slots(one_bank_bag());
|
||||
s.run("PickupContainerItem(0, 2)").unwrap();
|
||||
|
||||
assert_eq!(s.eval::<i64>("return PutItemInBag(64)").unwrap(), 1);
|
||||
assert!(s.cursor_item().is_none());
|
||||
assert!(s.take_container_moves().is_empty(), "a store, not a swap");
|
||||
assert_eq!(
|
||||
s.take_bag_autostores(),
|
||||
vec![BagAutoStore {
|
||||
src_bag: 0,
|
||||
src_slot: 2,
|
||||
// Live container id 5 — bank bag slot 1 (`ContainerIDToInventoryID(5) == 64`).
|
||||
dst_bag: 5,
|
||||
count: None,
|
||||
}]
|
||||
);
|
||||
}
|
||||
|
||||
/// Finding 1 — `PutItemInBackpack()` skips both the empty-slot and the swap legs outright: a
|
||||
/// held BAG dropped on the backpack button is auto-stored, never swapped with it.
|
||||
#[test]
|
||||
fn the_backpack_button_always_auto_stores_even_a_bag() {
|
||||
let mut s = UiScript::new().unwrap();
|
||||
s.set_container(0, Some(backpack()));
|
||||
s.run("PickupContainerItem(0, 1)").unwrap();
|
||||
|
||||
assert_eq!(s.eval::<i64>("return PutItemInBackpack()").unwrap(), 1);
|
||||
assert!(s.take_container_moves().is_empty());
|
||||
assert_eq!(
|
||||
s.take_bag_autostores(),
|
||||
vec![BagAutoStore {
|
||||
src_bag: 0,
|
||||
src_slot: 1,
|
||||
dst_bag: 0,
|
||||
count: None,
|
||||
}]
|
||||
);
|
||||
}
|
||||
|
||||
/// Finding 3's literal shape: the reference pushes the Lua **number** `1` (`0x6f3810` writes
|
||||
/// tag 3 with the IEEE-754 double `0x3FF0…`), never the boolean `true`. FrameXML only tests
|
||||
/// truthiness; an addon comparing against `true` must see what the reference gives it.
|
||||
#[test]
|
||||
fn the_answer_is_the_number_one_not_the_boolean_true() {
|
||||
let mut s = UiScript::new().unwrap();
|
||||
s.set_container(0, Some(backpack()));
|
||||
s.set_bank_bag_slots(one_bank_bag());
|
||||
s.run("PickupContainerItem(0, 2)").unwrap();
|
||||
assert!(s
|
||||
.eval::<bool>("return PutItemInBag(64) == 1 and type(PutItemInBackpack()) ~= 'boolean'")
|
||||
.unwrap());
|
||||
}
|
||||
|
||||
/// The floor gate (`cmp ecx,0x13; jl`): equipment slots and ammo are below it, so the verb
|
||||
/// never reaches the paper-doll handler through them.
|
||||
#[test]
|
||||
fn equipment_slots_are_below_the_floor() {
|
||||
let mut s = UiScript::new().unwrap();
|
||||
s.set_container(0, Some(backpack()));
|
||||
let mut doll: InventorySlots = Default::default();
|
||||
doll[16] = Some(bag_slot_view(9999));
|
||||
s.set_inventory_slots(doll);
|
||||
s.run("PickupContainerItem(0, 1)").unwrap();
|
||||
|
||||
assert!(s.eval::<bool>("return PutItemInBag(16) == nil").unwrap());
|
||||
assert!(s.eval::<bool>("return PutItemInBag(0) == nil").unwrap());
|
||||
assert!(s.cursor_item().is_some(), "untouched");
|
||||
assert!(s.take_container_moves().is_empty());
|
||||
assert!(s.take_bag_autostores().is_empty());
|
||||
}
|
||||
|
||||
/// `PickupBagFromSlot` picks a bank bag up, returns NOTHING, and locks the slot.
|
||||
#[test]
|
||||
fn pickup_bag_from_slot_takes_the_bag_and_returns_nothing() {
|
||||
let mut s = UiScript::new().unwrap();
|
||||
s.set_bank_bag_slots(one_bank_bag());
|
||||
assert_eq!(
|
||||
s.eval::<i64>("return select('#', PickupBagFromSlot(64))")
|
||||
.unwrap(),
|
||||
0,
|
||||
"the delegate's eax is never tested — no return values at all"
|
||||
);
|
||||
let held = s.cursor_item().expect("picked up");
|
||||
assert_eq!(
|
||||
(held.bag, held.slot, held.item_id),
|
||||
(EQUIPMENT_BAG, 64, 4500)
|
||||
);
|
||||
assert!(s.eval::<bool>("return IsInventoryItemLocked(64)").unwrap());
|
||||
}
|
||||
|
||||
/// **Destructive, and that is the finding**: `ClearCursor(1,1)` runs unconditionally before
|
||||
/// the slot is even looked at (`0x4c7b79`), so a held payload is dropped even when the slot
|
||||
/// turns out to hold nothing to pick up.
|
||||
#[test]
|
||||
fn pickup_bag_from_slot_drops_whatever_was_held_first() {
|
||||
let mut s = UiScript::new().unwrap();
|
||||
s.set_container(0, Some(backpack()));
|
||||
s.set_bank_bag_slots(Default::default());
|
||||
s.run("PickupContainerItem(0, 1)").unwrap();
|
||||
assert!(s.cursor_item().is_some());
|
||||
|
||||
s.run("PickupBagFromSlot(65)").unwrap();
|
||||
assert!(s.cursor_item().is_none(), "dropped, and nothing picked up");
|
||||
assert!(s.take_container_moves().is_empty(), "no place path exists");
|
||||
}
|
||||
|
||||
/// The `TYPEMASK_CONTAINER` resolve (`mov ecx,4`, where `PickupInventoryItem` uses 2): the
|
||||
/// bank's own item slots are inside the gate's range and hold ordinary items, so the verb
|
||||
/// silently no-ops there. A locked bag is refused too.
|
||||
#[test]
|
||||
fn pickup_bag_from_slot_only_takes_containers_and_never_a_locked_one() {
|
||||
let mut s = UiScript::new().unwrap();
|
||||
// Bank vault slot 3 → live id 42, an ordinary item: inside the gate, not a container.
|
||||
let mut vault = std::collections::HashMap::new();
|
||||
vault.insert(
|
||||
3,
|
||||
ContainerSlot {
|
||||
item_id: 1234,
|
||||
count: 1,
|
||||
equip_slots: vec![1],
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
s.set_container(
|
||||
-1,
|
||||
Some(ContainerState {
|
||||
name: Some("Bank".into()),
|
||||
num_slots: 24,
|
||||
slots: vault,
|
||||
}),
|
||||
);
|
||||
s.run("PickupBagFromSlot(42)").unwrap();
|
||||
assert!(s.cursor_item().is_none(), "not a container — silent no-op");
|
||||
|
||||
let mut bags = one_bank_bag();
|
||||
bags[0] = Some(InvSlotView {
|
||||
locked: true,
|
||||
..bag_slot_view(4500)
|
||||
});
|
||||
s.set_bank_bag_slots(bags);
|
||||
s.run("PickupBagFromSlot(64)").unwrap();
|
||||
assert!(s.cursor_item().is_none(), "locked — refused");
|
||||
}
|
||||
}
|
||||
@@ -22,6 +22,11 @@ use super::{queue_cursor_update, queue_lock_changed, CursorItem, CursorPayload,
|
||||
/// `wire_pos`'s `EQUIPMENT_BAG` arm maps live id 20..23 → the wire's equipped-bag inventory slots
|
||||
/// 19..22); ammo (0) stays a named deferral — refused outright, cursor untouched.
|
||||
///
|
||||
/// **The bank-bag band (64..=69) rides this too**, and only through one door: `PutItemInBag`'s
|
||||
/// empty-slot leg IS this function in the reference (`0x4c7c00` calls `0x4c7300` there), which is
|
||||
/// how a held bag reaches a bank bag slot at all. Its fit rule is one band wider than
|
||||
/// `equip_slots` alone — see [`super::bag_verbs::fits_slot`].
|
||||
///
|
||||
/// - empty cursor + an occupied, UNLOCKED doll slot → picks it up (payload `Item { bag:
|
||||
/// EQUIPMENT_BAG, slot: id, … }`), the same lock/event pair as a bag pickup.
|
||||
/// - holding + the SAME slot → cancel (mirrors `pickup_container_item`'s own same-slot branch).
|
||||
@@ -34,8 +39,9 @@ use super::{queue_cursor_update, queue_lock_changed, CursorItem, CursorPayload,
|
||||
///
|
||||
/// Returns whether the caller should repaint (the source lock, or the held payload, changed).
|
||||
pub(super) fn pickup_inventory_item(model: &mut Model, id: u32) -> bool {
|
||||
// Equipment slots (1..=19) + the four equipped-bag icons (20..=23); ammo (0) is out of scope.
|
||||
if !(1..=23).contains(&id) {
|
||||
// Equipment slots (1..=19) + the four equipped-bag icons (20..=23) + the six BANK bag slots
|
||||
// (64..=69, reached only through `PutItemInBag`); ammo (0) is out of scope.
|
||||
if !(1..=23).contains(&id) && !super::bag_verbs::BANK_BAG_INV_SLOTS.contains(&id) {
|
||||
return false;
|
||||
}
|
||||
// The targeting cursor's item half, on the doll (decision 0923). The reference's doll pickup
|
||||
@@ -52,9 +58,7 @@ pub(super) fn pickup_inventory_item(model: &mut Model, id: u32) -> bool {
|
||||
match model.cursor.take() {
|
||||
None => {
|
||||
let picked = model
|
||||
.inventory_slots
|
||||
.get(id as usize)
|
||||
.and_then(|s| s.as_ref())
|
||||
.inv_slot("player", id as usize)
|
||||
.filter(|s| s.item_id != 0 && !s.locked)
|
||||
.map(|s| CursorItem {
|
||||
bag: EQUIPMENT_BAG,
|
||||
@@ -86,7 +90,7 @@ pub(super) fn pickup_inventory_item(model: &mut Model, id: u32) -> bool {
|
||||
// A split carry can't equip a partial stack, and a non-fitting item has nowhere to
|
||||
// land on this slot — both refuse, kept exactly as picked up (no event: nothing
|
||||
// transitioned).
|
||||
if held.count.is_some() || !held.equip_slots.contains(&(id as u8)) {
|
||||
if held.count.is_some() || !super::bag_verbs::fits_slot(&held, id) {
|
||||
model.cursor = Some(CursorPayload::Item(held));
|
||||
return false;
|
||||
}
|
||||
@@ -139,9 +143,7 @@ pub(super) fn equip_cursor_item(model: &mut Model, id: u32) -> bool {
|
||||
/// future pin disagrees with the client's own table.
|
||||
pub(super) fn cursor_can_go_in_slot(model: &Model, id: u32) -> bool {
|
||||
match &model.cursor {
|
||||
Some(CursorPayload::Item(item)) => {
|
||||
u8::try_from(id).is_ok_and(|id| item.equip_slots.contains(&id))
|
||||
}
|
||||
Some(CursorPayload::Item(item)) => super::bag_verbs::fits_slot(item, id),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
@@ -186,12 +188,16 @@ pub(super) fn use_inventory_item(model: &mut Model, id: u32) {
|
||||
/// picked-up slot dims immediately, no server round-trip — the doll twin of
|
||||
/// `GetContainerItemInfo`'s `held_here` derivation) OR the app's fed `InvSlotView.locked` says so
|
||||
/// (an outstanding pending op the app's `PendingItemOps` tracks, decision 0216 §4/0218 §3).
|
||||
///
|
||||
/// Reads through [`Model::inv_slot`], not the doll array, so it answers over the WHOLE live-id
|
||||
/// space that binding covers — the reference's `BankFrameItemButton_UpdateLock` calls it with
|
||||
/// `BankButtonIDToInvSlotID(...)` for every bank slot, and a lock the vault feed reports has to
|
||||
/// dim the button the same as an equipped one.
|
||||
pub(super) fn is_inventory_item_locked(model: &Model, id: u32) -> bool {
|
||||
let held_here = matches!(&model.cursor, Some(CursorPayload::Item(c)) if c.bag == EQUIPMENT_BAG && c.slot == id);
|
||||
let fed = usize::try_from(id)
|
||||
.ok()
|
||||
.and_then(|i| model.inventory_slots.get(i))
|
||||
.and_then(|s| s.as_ref())
|
||||
.and_then(|i| model.inv_slot("player", i))
|
||||
.is_some_and(|s| s.locked);
|
||||
held_here || fed
|
||||
}
|
||||
|
||||
@@ -160,13 +160,13 @@ pub use bank::BankState;
|
||||
pub use bind_confirm::PendingEquipAnswer;
|
||||
pub use camera_view::{CameraViewRequest, CAMERA_VIEW_COUNT};
|
||||
pub use char_stats::{
|
||||
weapon_subclass_skill, InvSlotView, InventorySlots, UnitCombatStats, INVENTORY_SLOT_COUNT,
|
||||
SKILL_DEFENSE, SKILL_UNARMED,
|
||||
weapon_subclass_skill, BankBagSlots, InvSlotView, InventorySlots, UnitCombatStats,
|
||||
BANK_BAG_SLOT_COUNT, INVENTORY_SLOT_COUNT, SKILL_DEFENSE, SKILL_UNARMED,
|
||||
};
|
||||
pub use chat_send::ChatSend;
|
||||
pub use chat_window::ChatWindowLook;
|
||||
pub use container::{
|
||||
ContainerMove, ContainerSlot, ContainerState, EnchantView, PetitionSlotView,
|
||||
BagAutoStore, ContainerMove, ContainerSlot, ContainerState, EnchantView, PetitionSlotView,
|
||||
RandomPropertyView, UiCursorMode,
|
||||
};
|
||||
pub use craft::{CraftReagent, CraftRecipe, CraftState, CraftTooltip};
|
||||
|
||||
@@ -849,6 +849,9 @@ pub(crate) struct Model {
|
||||
/// `(bag, slot)` sources queued by `AutoEquipCursorItem` (decision 0208 phase 1b, `cursor`'s
|
||||
/// `doll` submodule) — drained by the app into `CMSG_AUTOEQUIP_ITEM`.
|
||||
pub(crate) container_autoequips: Vec<(i64, u32)>,
|
||||
/// Auto-stores queued by `PutItemInBag`/`PutItemInBackpack` (`cursor`'s `bag_verbs` submodule) —
|
||||
/// drained by the app into `CMSG_AUTOSTORE_BAG_ITEM` (or `CMSG_SPLIT_ITEM` for a split carry).
|
||||
pub(crate) bag_autostores: Vec<container::BagAutoStore>,
|
||||
|
||||
/// Per-frame drag-button registrations (`RegisterForDrag`) — kind-independent (any Frame, not
|
||||
/// just a Button), so it lives beside [`Model::mouse_down_on`] rather than inside a per-kind
|
||||
@@ -1253,6 +1256,11 @@ pub(crate) struct Model {
|
||||
/// binding's route is a fork between them (the [`Self::pet_book`] precedent).
|
||||
pub(crate) pet_combat_stats: Option<char_stats::UnitCombatStats>,
|
||||
pub(crate) inventory_slots: char_stats::InventorySlots,
|
||||
/// The six bank-bag slots (live ids 64..=69) — the third band of
|
||||
/// [`char_stats::Model::inv_slot`], and a store rather than a view because a bank bag has no
|
||||
/// container slot to be read out of: it IS the container. Fed beside
|
||||
/// [`Self::inventory_slots`] off the player descriptor's own guids.
|
||||
pub(crate) bank_bag_slots: char_stats::BankBagSlots,
|
||||
/// The 12 alert-region statuses (`GetInventoryAlertStatus`, DurabilityFrame's armor-guy
|
||||
/// feed) in the client's own `0x806eb8` table order (11 equipment regions + the low-ammo
|
||||
/// 12th) — recomputed on every inventory push, which fires `UPDATE_INVENTORY_ALERTS`
|
||||
@@ -1646,6 +1654,7 @@ impl Model {
|
||||
ui_cursor: None,
|
||||
ui_cursor_dirty: false,
|
||||
container_autoequips: Vec::new(),
|
||||
bag_autostores: Vec::new(),
|
||||
drag_registered: HashMap::new(),
|
||||
drag: None,
|
||||
moving: None,
|
||||
@@ -1802,6 +1811,7 @@ impl Model {
|
||||
player_combat_stats: None,
|
||||
pet_combat_stats: None,
|
||||
inventory_slots: Default::default(),
|
||||
bank_bag_slots: Default::default(),
|
||||
inventory_alerts: [0; 12],
|
||||
paperdoll_yaw: 0.0,
|
||||
inventory_uses: Vec::new(),
|
||||
|
||||
@@ -168,7 +168,7 @@ fn hiding_a_frame_does_not_reopen_the_gate() {
|
||||
///
|
||||
/// `ContainerFrameItemButton_OnUpdate` re-runs `OnEnter` every frame while the tooltip is the
|
||||
/// button's own — faithful, and UNTHROTTLED in 1.12 (its `updateTooltip` throttle is commented
|
||||
/// out; `BagFrame.xml`'s `BenillaBagSlot_OnUpdate` ships the same loop). So a bag hover clears and
|
||||
/// out; the reference's `ContainerFrameItemButton_OnUpdate` ships the same loop). So a bag hover clears and
|
||||
/// rebuilds the SAME tooltip 60×/sec, and the engine must absorb that: identical content re-derives
|
||||
/// an identical model, so the measure cache re-validates on its content key and the gate stays shut.
|
||||
///
|
||||
|
||||
@@ -301,7 +301,7 @@ pub(super) fn clear_content(model: &mut Model, h: FrameHandle) {
|
||||
// it here invalidates nothing the key would not — while destroying the one thing that
|
||||
// makes the hover re-enter loop affordable. That loop
|
||||
// (`ContainerFrameItemButton_OnUpdate`, unthrottled in 1.12 and shipped verbatim as
|
||||
// `BagFrame.xml`'s `BenillaBagSlot_OnUpdate`) clears and rebuilds the SAME content
|
||||
// the reference's `ContainerFrameItemButton_OnUpdate`) clears and rebuilds the SAME content
|
||||
// every frame: keeping the cache lets each rebuilt line re-validate against its own
|
||||
// key, so nothing re-shapes, the measure list stays empty, its forced second resolve
|
||||
// never happens, and the layout gate's fingerprint — which exists precisely to
|
||||
|
||||
Reference in New Issue
Block a user