From 6da7ed3fcabbcd5ab78a3de4ea561a32f80ef44e Mon Sep 17 00:00:00 2001 From: Sam Date: Wed, 2 Sep 2026 16:57:55 +0700 Subject: [PATCH] the action bars' empty slots go ghost, and a rooted creature stays put MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - picking a spell up off the bar no longer blacks out every empty slot: `UI-Quickslot` is a FILLED 60%-black plate, not a hollow ring (decoded off the shipped BLPs — Quickslot2's interior is RGBA 0,0,0,0, Quickslot's 0,0,0,153), and the reference dims it to half alpha in `ActionButton_ShowGrid`, so an empty well now ghosts at 30% black where it read as a hole; the wow-re cross-check then found the reference never UN-dims it (a 3-argument `SetVertexColor` preserves the current alpha at `0x79abd0`, and `SetNormalTexture` reuses the region), so in the real client the first drag leaves every button's border at half alpha for the rest of the session — benilla ends the dim on purpose and records it as the divergence it is (1782) - the bag bar is the reference's own `MainMenuBarBagButtons.xml` (window 3 of the stock-first migration), and the swap found a real bug rather than a move: the reference's `ItemAnim_OnEvent` matches a push against the button's inventory-slot id (20..23) and benilla emitted its own container numbering (1..4), so all four bag-push cards would have silently never played against the stock body — `push_container` now emits the reference's selector (`bag + 1`), which is what an addon expects anyway; beside it, a readiness probe that asks the LOADER, not a grep, which of the 85 stock windows load off the player's chain today (58 do; the 27 that don't name their own blocker) - a rooted, hovering, water-walking or walking creature reached benilla as an unhandled opcode: the twelve `SMSG_SPLINE_MOVE_*` (the observer twin of the ack'd force family — bare packed guid, no ack, any unit) now decode into one MOVEMENTFLAGS word per unit, which benilla kept for our mover and for a relayed player and not at all for a creature, the one body this family is ever addressed to; ROOT wipes the direction bits so the dead-reckon stops walking a rooted body on for its next heartbeat (`0x7c7340`), HOVER clears a yard off the floor without climbing a body already inside that yard (`0x636e81`), WATERWALK elects the liquid surface against the terrain, SAFE_FALL's terminal is 7.0 not 60.148 (`0x7c5d23`), and SET_RUN_MODE's bool is inverted against the bit, folded at the parse; the inspect card names a unit's granted modes in words (`granted rooted+hover`) so a creature resting a yard off the floor reads as intended rather than broken Co-Authored-By: Claude Fable 5.1 --- crates/benilla-app/assets/ui/ActionBar.xml | 113 +- crates/benilla-app/assets/ui/BagFrame.xml | 994 ------------------ .../assets/ui/ContainerFrameAdapters.xml | 274 ++++- .../assets/ui/ItemSlotButtonTemplates.xml | 38 +- crates/benilla-app/assets/ui/benilla.toc | 50 +- .../benilla-app/src/creature_anim/driver.rs | 27 +- .../benilla-app/src/creature_anim/select.rs | 14 +- .../src/creature_anim/select/tests.rs | 72 +- crates/benilla-app/src/debug_panel/inspect.rs | 57 +- crates/benilla-app/src/net.rs | 2 +- crates/benilla-app/src/net/apply.rs | 26 +- crates/benilla-app/src/net/apply/objects.rs | 365 ++++++- crates/benilla-app/src/net/motion.rs | 3 + crates/benilla-app/src/net/motion/modes.rs | 80 ++ crates/benilla-app/src/net/motion/remote.rs | 105 +- crates/benilla-app/src/net/motion/spline.rs | 123 ++- crates/benilla-app/src/net/motion/tests.rs | 11 +- crates/benilla-app/src/player/wire_in.rs | 2 +- crates/benilla-app/src/ui_loot.rs | 39 +- .../src/ui_script/action_bar_tests.rs | 15 +- crates/benilla-app/src/ui_script/bag_tests.rs | 165 +-- .../src/ui_script/bottom_hud_tests.rs | 4 +- .../src/ui_script/dressup_tests.rs | 3 +- .../benilla-app/src/ui_script/escape_tests.rs | 10 +- .../src/ui_script/frame_flag_gate.rs | 42 +- .../src/ui_script/game_menu_tests.rs | 6 +- .../src/ui_script/merchant_tests.rs | 2 +- .../src/ui_script/multibar_stance_tests.rs | 127 ++- .../benilla-app/src/ui_script/panel_tests.rs | 1 - .../benilla-app/src/ui_script/reference_ui.rs | 131 +++ crates/benilla-app/src/ui_script/test_ui.rs | 38 +- .../src/ui_script/tooltip_compare_tests.rs | 5 +- crates/benilla-protocol/src/events.rs | 15 + crates/benilla-protocol/src/events/decode.rs | 3 + crates/benilla-protocol/src/lib.rs | 2 +- crates/benilla-protocol/src/messages/mod.rs | 2 +- .../benilla-protocol/src/messages/movement.rs | 51 + .../benilla-protocol/src/messages/opcode.rs | 51 + .../benilla-protocol/src/messages/packet.rs | 31 +- crates/benilla-protocol/src/messages/parse.rs | 40 + crates/benilla-protocol/tests/movement.rs | 124 +++ crates/benilla-ui/src/script/region/paint.rs | 16 + crates/benilla-world/src/world_point.rs | 35 +- 43 files changed, 2048 insertions(+), 1266 deletions(-) delete mode 100644 crates/benilla-app/assets/ui/BagFrame.xml create mode 100644 crates/benilla-app/src/net/motion/modes.rs diff --git a/crates/benilla-app/assets/ui/ActionBar.xml b/crates/benilla-app/assets/ui/ActionBar.xml index d6c434e2..fc65e56c 100644 --- a/crates/benilla-app/assets/ui/ActionBar.xml +++ b/crates/benilla-app/assets/ui/ActionBar.xml @@ -56,8 +56,14 @@ 1218/1219 fixed for MultiBarLeft. Unlike the ref (which HIDES an empty, non-grid slot entirely), our MAIN bar always shows all 12 wells; the grid only swaps an EMPTY slot's ring art between the occupied and empty - Quickslot textures. The ref's extra SetVertexColor(1,1,1,0.5) dim on a grid ring is NOT - ours — that look is unchanged since 0216 and is the director's to move. + Quickslot textures. The ref's SetVertexColor(1,1,1,0.5) dim on a grid ring IS ours as of + 1781 — this note said for three hundred decisions that the dim was "the director's to + move", and they moved it: an empty well under a held spell read as a black hole, because + UI-Quickslot is a FILLED plate (see BenillaActionButton_SetRing) and we were painting it + opaque. The dim now rides the ring's own setter, so art and tint cannot drift apart. + What we do NOT take is where the ref's dim ENDS: nothing in the real client ever clears + it, so one pickup fades every border for the session (byte-pinned; the setter's comment + has the addresses). Ours lets go with the payload — divergence, chosen, decision 1782. - Exhaustion tick + rest fill, XP text (hover/cvar-gated), MaxLevelBar: OUT. The bar's right-hand cluster is otherwise complete — the backpack toggle + four bag slots live in BagFrame.xml and the eight micro buttons in MicroMenu.xml (both anchored into @@ -291,8 +297,12 @@ end -- Losing the last key with the keyring open would strand an orphan window; the ref -- can't reach this state (its latch is one-way), so this is ours to decide. The window - -- is an ordinary ContainerFrame since 1751, so this is the reference's own CloseBag. - if not show then CloseBag(KEYRING_CONTAINER) end + -- is an ordinary ContainerFrame since 1751, so this is the reference's own CloseBag — + -- nil-guarded like every other reach out of this function, because the trigger above + -- lives on THIS file's art frame now (1751 window 3) and a dozen harnesses load the + -- bar with no bag stack behind it. With no ContainerFrame.lua there is no keyring + -- window to strand either, so skipping is the whole correct answer. + if not show and CloseBag then CloseBag(KEYRING_CONTAINER) end end -- ── The bonus-bar slide (decision 1524; ref BonusActionBarFrame.lua:1-98) ────────────── @@ -888,12 +898,64 @@ BenillaActionButton_Update(button) end + -- ── The ring: art and tint set TOGETHER ──────────────────────────────────────────────── + -- One well, one answer to "what does it look like". Setting the two apart is precisely how + -- the reference's own pair got out of step: ActionButton_ShowGrid (ActionButton.lua:247) + -- dims the ring and ActionButton_Update, the only thing that ever repaints it, never + -- restores the dim. + -- + -- `ghost` is that dim, the ref's own SetVertexColor(1.0, 1.0, 1.0, 0.5), and it is not + -- cosmetic trim. UI-Quickslot and UI-Quickslot2 are not two rings: Quickslot2 is a hollow + -- border (interior alpha 0), Quickslot is a FILLED plate — solid black at alpha 0.6 across + -- its whole middle (measured off the shipped BLPs). So the grid swap does not outline an + -- empty well, it fills it, and painting that plate opaque drops a 60%-black square into + -- every empty slot on every bar the moment a payload is picked up. The ref's half-alpha + -- takes it to 30%, where it reads as a drop-target hint rather than a hole. Ours skipped + -- the dim from 0216 to 1781 and the bars went dark under a held spell. + -- + -- **Only the EMPTY well ghosts, and that is a DIVERGENCE — a chosen one (1782).** The + -- reference fans SHOWGRID to every button and dims each one, occupied included, and then + -- nothing ever clears it: `SetNormalTexture(path)` REUSES the region and touches only the + -- texture handle, and `UpdateUsable`'s three-argument SetVertexColor is + -- `SetVertexColor(r, g, b, currentAlpha)` — the binding asks a second time whether the + -- alpha argument is present and, when it is not, copies the alpha it just read back over + -- the 1.0 default. Both byte-verified: wow-re `system/ui/scratch/ + -- button-state-texture-path-setter.md` §2/§7 (`0x778f10`, `0x770200`, `0x79adfe`). + -- + -- So in stock 1.12.1 the FIRST spell anyone picks up leaves every action button's border at + -- vertex alpha 0x80 for the rest of the session, occupied slots included. That is a stuck + -- state, not a look: it is keyed to nothing the player can see, it never comes back, and + -- reproducing it would fade the whole bar permanently on the first drag. We take the look + -- the dim was written for — the wells the grid opens read as drop targets — and let go of + -- it with the payload. 1782 is why, and what we would need to see to revisit it. + function BenillaActionButton_SetRing(button, texture, ghost) + button:SetNormalTexture(texture) + -- The derived name, exactly as UpdateUsable reaches the same region (and as the ref + -- reaches it); nil only for an anonymous foreign button, which has no wells of ours. + local name = button:GetName() + local normal = name and getglobal(name .. "NormalTexture") + if not normal then return end + if ghost then + normal:SetVertexColor(1.0, 1.0, 1.0, 0.5) + else + -- The fourth argument is written OUT, never left to the default. Our binding + -- currently reads an absent alpha as 1.0 and the reference reads it as the alpha + -- already there (above); the day that divergence is closed, a 3-argument call here + -- would preserve the 0.5 it is supposed to be clearing and the ghost would become + -- exactly the stuck state this function exists to avoid — silently, with no test + -- touching this file able to see it. Saying 1.0 out loud is correct under either. + normal:SetVertexColor(1.0, 1.0, 1.0, 1.0) + end + end + function BenillaActionButton_Update(button) local action = BenillaActionButton_Action(button) if HasAction(action) then button.icon:SetTexture(GetActionTexture(action) or BENILLA_FALLBACK_ICON) button.icon:Show() - button:SetNormalTexture("Interface\\Buttons\\UI-Quickslot2") + -- Full alpha here only to clear whatever the ring last wore; the real occupied + -- tint is UpdateUsable's, below. + BenillaActionButton_SetRing(button, "Interface\\Buttons\\UI-Quickslot2", false) button:Show() button.rangeTimer = -1 BenillaActionButton_UpdateCount(button) @@ -925,17 +987,23 @@ if not grid then button:Hide() else - button:SetNormalTexture("Interface\\Buttons\\UI-Quickslot") + BenillaActionButton_SetRing(button, "Interface\\Buttons\\UI-Quickslot", true) button:Show() end else -- While the grid is up, an empty slot shows the ref's own "no action" ring -- (Interface\Buttons\UI-Quickslot) so it reads as a drop target; otherwise the -- plain occupied-style ring, unchanged from before this slice (SCOPE note). + -- + -- Either way the tint is WRITTEN, not left: this arm used to touch the ring's + -- art and never its colour, so a slot emptied while out of power kept + -- UpdateUsable's blue-grey 0.5/0.5/1.0 on a well with nothing in it — 1108's + -- "a tint outlives the art" one region over. A ring that is only ever painted + -- as a function of the button's state cannot carry the last one's history. if grid then - button:SetNormalTexture("Interface\\Buttons\\UI-Quickslot") + BenillaActionButton_SetRing(button, "Interface\\Buttons\\UI-Quickslot", true) else - button:SetNormalTexture("Interface\\Buttons\\UI-Quickslot2") + BenillaActionButton_SetRing(button, "Interface\\Buttons\\UI-Quickslot2", false) end end end @@ -2036,6 +2104,35 @@ ActionBar_PageDown() PlaySound("UChatScrollButton") + + + + this:RegisterEvent("PLAYER_ENTERING_WORLD") + this:RegisterEvent("BAG_UPDATE") + + MainMenuBar_UpdateKeyRing() + - - - - - - - - - - - - - - - - - - - - - - - - - - - - BenillaItemPushAnim_OnLoad(self, 0) - BenillaItemPushAnim_OnEvent(self) - BenillaItemPushAnim_OnUpdate(self, arg1) - - - - - - - - - - self.icon = getglobal("MainMenuBarBackpackButtonIconTexture") - if BenillaActionBarArt_SeatAbove then BenillaActionBarArt_SeatAbove(self) end - - - - self:SetChecked(not self:GetChecked()) - if IsShiftKeyDown() then - OpenAllBags() - else - BenillaBagToggle_OnClick() - end - - BenillaBagToggle_OnEnter(self) - BenillaBagBarButton_OnLeave() - - - - - - - - - - - - - - - - - - - - - - BenillaItemPushAnim_OnLoad(self, 1) - BenillaItemPushAnim_OnEvent(self) - BenillaItemPushAnim_OnUpdate(self, arg1) - - - - - - - - - - - - - - - - - BenillaBagBarSlot_OnLoad(self, 1) - BenillaBagBarSlot_OnClick(self) - BenillaBagBarSlot_OnEvent(self) - BagSlotButton_OnEnter() - BenillaBagBarButton_OnLeave() - - BenillaBagBarSlot_Place(self) - BenillaBagBarSlot_Place(self) - - - - - - - - - - - - - - - - - - - - BenillaItemPushAnim_OnLoad(self, 2) - BenillaItemPushAnim_OnEvent(self) - BenillaItemPushAnim_OnUpdate(self, arg1) - - - - - - - - - - - - - - - - BenillaBagBarSlot_OnLoad(self, 2) - BenillaBagBarSlot_OnClick(self) - BenillaBagBarSlot_OnEvent(self) - BagSlotButton_OnEnter() - BenillaBagBarButton_OnLeave() - - BenillaBagBarSlot_Place(self) - BenillaBagBarSlot_Place(self) - - - - - - - - - - - - - - - - - - - - BenillaItemPushAnim_OnLoad(self, 3) - BenillaItemPushAnim_OnEvent(self) - BenillaItemPushAnim_OnUpdate(self, arg1) - - - - - - - - - - - - - - - - BenillaBagBarSlot_OnLoad(self, 3) - BenillaBagBarSlot_OnClick(self) - BenillaBagBarSlot_OnEvent(self) - BagSlotButton_OnEnter() - BenillaBagBarButton_OnLeave() - - BenillaBagBarSlot_Place(self) - BenillaBagBarSlot_Place(self) - - - - - - - - - - - - - - - - - - - - BenillaItemPushAnim_OnLoad(self, 4) - BenillaItemPushAnim_OnEvent(self) - BenillaItemPushAnim_OnUpdate(self, arg1) - - - - - - - - - - - - - - - - BenillaBagBarSlot_OnLoad(self, 4) - BenillaBagBarSlot_OnClick(self) - BenillaBagBarSlot_OnEvent(self) - BagSlotButton_OnEnter() - BenillaBagBarButton_OnLeave() - - BenillaBagBarSlot_Place(self) - BenillaBagBarSlot_Place(self) - - - - - - diff --git a/crates/benilla-app/assets/ui/ContainerFrameAdapters.xml b/crates/benilla-app/assets/ui/ContainerFrameAdapters.xml index 00e0e1f1..bc879b6b 100644 --- a/crates/benilla-app/assets/ui/ContainerFrameAdapters.xml +++ b/crates/benilla-app/assets/ui/ContainerFrameAdapters.xml @@ -21,7 +21,7 @@ Bongos_ActionBar, the CT_BarMod family) hook that global and call through; they now reach the real thing. - ── The one adapter, and why it is not a stub ──────────────────────────────────────────────── + ── The adapters, and why neither is a stub ────────────────────────────────────────────────── `ContainerFrameItemButton_OnEnter` opens with a keyring fork (ref l.615-620): if this:GetParent():GetID() == KEYRING_CONTAINER then @@ -118,5 +118,277 @@ OpenBackpack = BenillaOpenBackpack CloseBackpack = BenillaCloseBackpack CloseAllBags = BenillaCloseAllBags + + -- ══ THE ITEM-PUSH DROP CARD, over the reference's own bag bar (decision 0887) ═════════ + -- + -- The bar is `Interface\FrameXML\MainMenuBarBagButtons.xml` now (1751 window 3), and its + -- `ItemAnimTemplate` is a `` playing `Interface\ItemAnimations\ForcedBackpackItem.mdx`. + -- **This engine's `` holds the scene the Lua API reads and writes but draws no + -- pixels** (`script/modelframe.rs` — the pane is the app renderer's job and no renderer + -- claims a FrameXML model), so those six frames load, register, and animate nothing. + -- + -- That is a genuine engine difference, which is the one thing 1751 §2 keeps an adapter + -- for. So the reference's file is sourced unedited — its Models exist and are harmless — + -- and the card the player actually sees is built HERE, as a sibling Frame + Texture on the + -- same six buttons, driven by the model's OWN keyframes read out of the shipped .m2. The + -- bodies below moved verbatim from the deleted `BagFrame.xml`; every number in them is + -- byte-derived and the comments carry their provenance. + -- + -- ONE THING CHANGED IN THE MOVE, and it is the reason the swap needed thought at all. + -- `ItemAnim_OnEvent` matches on `this:GetParent():GetID()`, which for a bag button is its + -- INVENTORY-slot id (20..23, via `GetInventorySlotInfo`). benilla used to translate + -- `ITEM_PUSH`'s `arg1` into its own container vocabulary (1..4) because the old bar was + -- keyed that way, and against the reference's body that comparison is false for every bag + -- — four cards that would simply never have played, with nothing raising. The fix was not + -- to adapt the reference: `ui_loot.rs`'s `push_container` stopped translating and now emits + -- `0x491bb5`'s selector verbatim, which is what addons expect too. So the id a card matches + -- on is `button:GetID()`, exactly as the reference's own body reads it. + -- ── The item-push drop animation (decision 0887) ───────────────────────────────────────── + -- An item lands in a bag: its icon pops in above the bag bar, hangs there, then falls into + -- the button that took it and shrinks away. ref ItemAnim (MainMenuBarBagButtons.xml l.4-16 + + -- .lua l.68-80, .lua l.92-94): each bag button owns a hidden child that, on ITEM_PUSH + -- naming its own container, swaps in the pushed item's icon, rewinds to sequence time 0 and + -- shows itself; OnAnimFinished hides it again. A second push on the same button RESTARTS it + -- (SetSequenceTime(0,0)); pushes into different bags run independently. That is exactly the + -- machine below, with the reference's ITEM_PUSH `arg1` translated from its inventory-slot + -- vocabulary (20..23) into ours (bagId 1..4) app-side — ui_loot.rs `push_container` carries + -- the byte-verified selector. + -- + -- The reference's is a playing Interface\ItemAnimations\ForcedBackpackItem.mdx — a + -- single 4-vertex quad on one animated bone, i.e. a 2D icon card with keyframed scale, alpha + -- and offset. benilla has no Model widget (decision 0137's Cooldown took the same posture: + -- model the mechanism, let our own renderer draw it), so it is a Texture driven by the + -- model's OWN tracks, read out of the shipped .m2 with `benilla-extract m2anim`/`m2bones`: + -- + -- sequence 0, CLAMP (plays once), duration 1.000 s + -- texture-weight (alpha) 0.000→0.0 0.133→1.0 0.267→1.0 1.000→0.0 linear + -- bone 0 scale 0.000→1.0 0.133→1.2 0.267→1.0 1.000→0.014 linear + -- bone 0 translation 0.000→(0,0) 0.500→(0,0) 1.000→(+0.010,-0.040) + -- the quad itself: 0.03 x 0.03 model units, blend Alpha, unlit, no depth write + -- + -- So: fade in over the first 133 ms while swelling to 1.2x; settle back to full size by + -- 267 ms; from there dwindle and fade continuously for the remaining 733 ms — while the + -- POSITION holds until the half-second and only then falls. The three tracks deliberately + -- do not share a schedule: the card is already down to ~0.69 size and ~0.68 alpha when it + -- starts moving, so the drop reads as the icon being drawn into the bag rather than as a + -- solid tile sliding. Those numbers are the file's, not ours. + -- + -- THE MODEL-UNIT → PIXEL SCALE is no longer ours to calibrate: the widget's + -- projection is RE-recorded now (wow-re `system/ui/scratch/modelframe-animation-clock.md`, + -- dispatched over the pet bar's B228 and folded back with it). The ortho leg gives + -- + -- px per model unit = (5/3) * modelScale * layoutScale * screenHeightPx + -- + -- which in FrameXML units is **1280 * modelScale * layoutScale**, aspect- and + -- resolution-independent, with the model ORIGIN at the frame rect's bottom-left. The ref's + -- `ItemAnimTemplate` (MainMenuBarBagButtons.xml:4) carries **no `scale` attribute**, so + -- modelScale is 1 and the constant is a flat 1280. Decision 0887's named gap is closed. + -- + -- This REPLACES a back-fit, and the back-fit is worth naming because it looked like + -- evidence. The old 1200 came from assuming CooldownFrameTemplate's 0.04-unit sweep + -- *exactly* covers a 36 px action button at its scale="0.75" (⇒ K = 1200). The assumption + -- is the error: at the real projection that sweep is 0.04 · 1280 · 0.75 = **38.4 px**, so + -- it overhangs its button by 6.7% rather than fitting it — exactly as the autocast shine's + -- 30.72 px square overhangs its 30 px button. Solving against a wrong premise produced a + -- number that then "cross-checked" against a second model derived the same wrong way, and + -- both files cited the agreement as the reason to trust it. + -- + -- Everything below is the model's own ratios times this constant, so the correction lands + -- in one place and the animation stays in proportion. + BENILLA_ITEM_PUSH_PX_PER_UNIT = 1280 + BENILLA_ITEM_PUSH_DURATION = 1.0 + + -- ── The widget rect, and why the card is not placed against the button ───────────────── + -- A `` with no `` and no `setAllPoints` takes its rect from the **M2 header + -- bounding box** (`MD20+0xB4`, max−min per axis), consumed RAW as layout units — unlike + -- CSimpleTexture's texel fallback, there is no AbsDimension conversion. So the ref's + -- ItemAnim widget is **42.41 x 124.72**, nothing like the 36-square everyone assumes from + -- looking at the XML, and **model-space (0,0,0) lands on that rect's BOTTOM-LEFT corner**. + -- (wow-re `system/ui/scratch/modelframe-implicit-size-law.md`, §5-cross-checked: vtable + -- 0x81c5d8 +0x1c/+0x20 -> 0x76d080/0x76d0d0, bbox source 0x713640 at `713661 add esi,0xb4`. + -- The one worker who read `MD20+0xD0` — the VertexBox — got an all-zero box and concluded + -- the widget was degenerate; that wrong premise is exactly what the pair round exists to + -- catch, and it is why this looked unresolvable in 0887.) + -- + -- That rect is the whole reason this animation reads the way it does: it stands 124.72 tall + -- off the button's own bottom edge, so the card's travel band reaches **54 px above the + -- button's top**. Decision 0887 could not know that and placed the card against the + -- button's CENTER instead, which is what put it in the wrong place in x and gave it no + -- horizontal drift at all. + BENILLA_ITEM_PUSH_FRAME_W = 42.41 + BENILLA_ITEM_PUSH_FRAME_H = 124.72 + -- Where that rect hangs off the button: the ref anchors the widget's BOTTOMRIGHT to the + -- button's, 10 in from the right for the five bag buttons and flush (0) for the keyring — + -- MainMenuBarBagButtons.xml l.19-27 (BagSlotButtonTemplate) and l.152-160 (KeyRingButton). + -- Per-button, so it is an OnLoad argument, not a constant. + BENILLA_ITEM_PUSH_ANCHOR_X = -10 + + -- ── The model's own numbers, unrounded ──────────────────────────────────────────────── + -- Every one of these was rounded in 0887 and every rounding moved the card. The quad span + -- read as 0.03 (it is 0.0288 — a 4% size error), and the translation as (0.010, -0.040) + -- (which alone accounts for half a pixel of the landing point). + -- + -- The bone transform is `v' = pivot + T(t) + S(t) * (v - pivot)` — three wow-re workers + -- derived that independently in three matrix conventions and agreed. The pivot + -- (M2CompBone+0x60) is NOT the quad's centre: it sits 0.23 px right and 0.07 px above it, + -- so the card shrinks *almost* in place and drifts a fifth of a pixel doing it. Keeping + -- the pivot as its own term rather than folding it into the centre is what makes this + -- reproduce the reference exactly at all three sampled times instead of nearly. + -- Bone 0's ROTATION track is empty (nKeys = 0), so there is no roll to model. + BENILLA_ITEM_PUSH_PIVOT_X = 0.017580900341272354 + BENILLA_ITEM_PUSH_PIVOT_Y = 0.053958501666784286 + BENILLA_ITEM_PUSH_QUAD_X = 0.017400000942870975 + BENILLA_ITEM_PUSH_QUAD_Y = 0.053900001570582390 + BENILLA_ITEM_PUSH_QUAD_SPAN = 0.028800001833587885 + + -- {time, value} keyframes, linear between (M2 track interpolation 1) — the file's own keys, + -- at full precision. The three tracks keep DIFFERENT schedules and that is the whole feel: + -- the card fades up while swelling to 1.2x over 133 ms, settles back to full size at + -- 267 ms, then dwindles and fades *continuously* for the remaining 733 ms — while its + -- position holds until the half second and only then falls. So it is already at ~0.69 size + -- and ~0.68 alpha when it starts to move, and it is invisible by the time it lands. + BENILLA_ITEM_PUSH_ALPHA = { {0.000, 0.0}, {0.133, 1.0}, {0.267, 1.0}, {1.000, 0.0} } + BENILLA_ITEM_PUSH_SCALE = { {0.000, 1.0}, {0.133, 1.2000000476837158}, {0.267, 1.0}, {1.000, 0.01441549975425005} } + -- Translation is per-AXIS: the card falls 51.4 px and drifts 13.3 px RIGHT, so one shared + -- 0..1 "drop" curve (0887's shape) cannot express it — the old code had no horizontal term + -- at all and moved the card the wrong way in x when it inferred one. + BENILLA_ITEM_PUSH_TRANS_X = { {0.000, 0.0}, {0.500, 0.0}, {1.000, 0.010394600220024586} } + BENILLA_ITEM_PUSH_TRANS_Y = { {0.000, 0.0}, {0.500, 0.0}, {1.000, -0.040167398750782013} } + + -- Sample a {time, value} keyframe list at `t`, linear between keys, clamped at both ends + -- (the M2 sampler's own shape for a CLAMP sequence). + function BenillaItemPushAnim_Sample(track, t) + local last = table.getn(track) + if t <= track[1][1] then return track[1][2] end + for i = 2, last do + local key = track[i] + if t <= key[1] then + local prev = track[i - 1] + local span = key[1] - prev[1] + if span <= 0 then return key[2] end + return prev[2] + (key[2] - prev[2]) * (t - prev[1]) / span + end + end + return track[last][2] + end + + -- Place/size/fade the card for the current clock, from the model's own transform. + -- + -- The card's centre in MODEL space is `pivot + T(t) + S(t)*(quadCentre - pivot)`; times the + -- projection constant that is its offset in layout units from the model origin, which is + -- the widget rect's bottom-left corner. The rect's own BOTTOMRIGHT hangs off the button's, + -- so the card is placed against the BUTTON'S BOTTOMRIGHT — never its centre — and the + -- frame width is what converts between the two. + -- + -- On screen: a button-sized card appears with its bottom edge ~13.6 px ABOVE the button's + -- top, pops to 1.2x and back while fading in, hangs there through the first half second, + -- then falls down-and-right INTO the button, shrinking to a half-pixel point at its centre + -- as it fades out. Reproduces the reference at t = 0, 0.133 and 1.0 to the hundredth of a + -- pixel (`modelframe-implicit-size-law.md` §3's table). + -- + -- NOT modelled, deliberately, and both are named in the record: the reference's widget + -- CLIPS the card to its own rect, which shaves 1.94 px off the right edge at the 1.2x peak + -- **at 4:3 only** (16:10 and 16:9 clear it); and the widget clears the depth buffer inside + -- that 124.72-tall rect every paint. benilla draws this as a Texture, not a Model, so it + -- inherits neither — reproducing an aspect-conditional sliver would be hard-coding a + -- rendering artefact rather than modelling the mechanism that causes it. + function BenillaItemPushAnim_Paint(anim) + local t = anim.elapsed + local k = BENILLA_ITEM_PUSH_PX_PER_UNIT + local scale = BenillaItemPushAnim_Sample(BENILLA_ITEM_PUSH_SCALE, t) + local size = BENILLA_ITEM_PUSH_QUAD_SPAN * scale * k + local cx = (BENILLA_ITEM_PUSH_PIVOT_X + + BenillaItemPushAnim_Sample(BENILLA_ITEM_PUSH_TRANS_X, t) + + scale * (BENILLA_ITEM_PUSH_QUAD_X - BENILLA_ITEM_PUSH_PIVOT_X)) * k + local cy = (BENILLA_ITEM_PUSH_PIVOT_Y + + BenillaItemPushAnim_Sample(BENILLA_ITEM_PUSH_TRANS_Y, t) + + scale * (BENILLA_ITEM_PUSH_QUAD_Y - BENILLA_ITEM_PUSH_PIVOT_Y)) * k + anim:SetWidth(size) + anim:SetHeight(size) + anim:SetPoint("CENTER", anim:GetParent(), "BOTTOMRIGHT", + anim.anchorX - BENILLA_ITEM_PUSH_FRAME_W + cx, cy) + anim:SetAlpha(BenillaItemPushAnim_Sample(BENILLA_ITEM_PUSH_ALPHA, t)) + end + + -- ref ItemAnim_OnLoad (l.68-70): the card listens for ITEM_PUSH and nothing else. It is + -- authored hidden, so it costs no OnUpdate until a push shows it (events reach hidden + -- frames; OnUpdate does not — the reference's own split). + function BenillaItemPushAnim_OnLoad(anim, containerId, anchorX) + anim.containerId = containerId + -- The widget's own offset from the button's BOTTOMRIGHT: -10 for the five bag buttons, + -- flush 0 for the keyring (the ref's two different anchors — see the constant block). + anim.anchorX = anchorX or BENILLA_ITEM_PUSH_ANCHOR_X + anim.icon = anim.icon or getglobal(anim:GetName() .. "Icon") + anim.elapsed = 0 + anim:RegisterEvent("ITEM_PUSH") + end + + -- ref ItemAnim_OnEvent (l.72-80): `if this:GetParent():GetID() == arg1`, then + -- ReplaceIconTexture(arg2) / SetSequence(0) / SetSequenceTime(0,0) / Show. A push into a bag + -- with no button (a bank bag) matches nothing and animates nowhere, there and here. + function BenillaItemPushAnim_OnEvent(anim) + if arg1 ~= anim.containerId then return end + anim.icon:SetTexture(arg2) + anim.elapsed = 0 + -- Seat the card above its button rather than at load: the bag bar's own level is set in + -- the BUTTON's OnLoad (BenillaActionBarArt_SeatAbove), which the loader may run after + -- this child exists. Re-deriving it per push is one call and can never read it stale. + anim:SetFrameLevel(anim:GetParent():GetFrameLevel() + 1) + BenillaItemPushAnim_Paint(anim) + anim:Show() + end + + -- The CLAMP sequence's end is the ref's OnAnimFinished → this:Hide() (l.92-94). + function BenillaItemPushAnim_OnUpdate(anim, elapsed) + anim.elapsed = anim.elapsed + (elapsed or 0) + if anim.elapsed >= BENILLA_ITEM_PUSH_DURATION then + anim:Hide() + return + end + BenillaItemPushAnim_Paint(anim) + end + + -- The six buttons and the offset each one's card hangs at, read off the reference's own + -- anchors: BOTTOMRIGHT -10 for the five bag buttons (`BagSlotButtonTemplate` l.19-27 and + -- `MainMenuBarBackpackButton` l.69-77), flush 0 for the keyring (l.152-160). + BENILLA_ITEM_PUSH_BUTTONS = { + { "MainMenuBarBackpackButton", -10 }, + { "CharacterBag0Slot", -10 }, + { "CharacterBag1Slot", -10 }, + { "CharacterBag2Slot", -10 }, + { "CharacterBag3Slot", -10 }, + { "KeyRingButton", 0 }, + } + + -- Build one button's card. Authored hidden, so it costs no OnUpdate until a push shows it + -- — the reference's own split (events reach hidden frames, OnUpdate does not). + -- + -- Named `$parentBenillaItemPush` rather than `$parentItemAnim`: the reference's Model + -- already owns that name on every one of these buttons, and an addon that reaches for it + -- must find the reference's frame, not ours. + function BenillaItemPushAnim_Attach(button, anchorX) + local anim = CreateFrame("Frame", button:GetName() .. "BenillaItemPush", button) + anim:Hide() + anim:SetWidth(36) + anim:SetHeight(36) + anim:SetPoint("CENTER", button, "CENTER", 0, 0) + anim.icon = anim:CreateTexture(anim:GetName() .. "Icon", "OVERLAY") + anim.icon:SetAllPoints(anim) + BenillaItemPushAnim_OnLoad(anim, button:GetID(), anchorX) + anim:SetScript("OnEvent", function() BenillaItemPushAnim_OnEvent(this) end) + anim:SetScript("OnUpdate", function() BenillaItemPushAnim_OnUpdate(this, arg1) end) + return anim + end + + -- At load, because the bar is entry 48 and this file is entry 50 — every button exists and + -- has run its own OnLoad, so `GetID()` is already the reference's. A button that is absent + -- (a harness that loaded no bar) is skipped rather than raising: the card is a flourish, + -- and a missing bar is not this file's error to report. + for _, entry in ipairs(BENILLA_ITEM_PUSH_BUTTONS) do + local button = getglobal(entry[1]) + if button then + BenillaItemPushAnim_Attach(button, entry[2]) + end + end ]]> diff --git a/crates/benilla-app/assets/ui/ItemSlotButtonTemplates.xml b/crates/benilla-app/assets/ui/ItemSlotButtonTemplates.xml index dae01e53..fa8c45b1 100644 --- a/crates/benilla-app/assets/ui/ItemSlotButtonTemplates.xml +++ b/crates/benilla-app/assets/ui/ItemSlotButtonTemplates.xml @@ -67,36 +67,10 @@ - - - - - PaperDollItemSlotButton_OnLoad(); - this:RegisterEvent("BAG_UPDATE"); - this.isBag = 1; - -- OURS, and the one deviation in this file's scripts. The reference's three bag - -- handlers each recompute a `translatedID` from these same two ids at every call - -- site; our bodies read `button.bagId`, the field BenillaBagBarSlot_OnLoad sets for - -- the four hand-wired slots. Resolving it once here is what lets a template-built - -- button and a hand-wired one share one implementation instead of two. - this.bagId = this:GetID() - CharacterBag0Slot:GetID() + 1; - - - if ( IsShiftKeyDown() ) then - BagSlotButton_OnShiftClick(); - else - BagSlotButton_OnClick(); - end - - BagSlotButton_OnDrag(); - BagSlotButton_OnClick(); - BagSlotButton_OnEnter(); - - - + diff --git a/crates/benilla-app/assets/ui/benilla.toc b/crates/benilla-app/assets/ui/benilla.toc index f44b025c..71bbf7e3 100644 --- a/crates/benilla-app/assets/ui/benilla.toc +++ b/crates/benilla-app/assets/ui/benilla.toc @@ -297,13 +297,43 @@ ErrorsFrame.xml # (Cooldown.xml), `UIPanelCloseButton` (UIPanelTemplates.xml) — plus `parent="UIParent"`. It # cannot load "first" the way a Lua-only sourcing had to. Interface\FrameXML\ContainerFrame.xml -# The bag BAR: MainMenuBarBackpackButton / CharacterBag0..3Slot / KeyRingButton and the item-push -# card — this client's stand-in for the reference's own MainMenuBarBagButtons.xml, which cannot be -# sourced until `PutItemInBag`/`PickupBagFromSlot` are bound (the file says so at the site). After -# ContainerFrame.xml for legibility, not necessity: every verb it calls resolves at click time. It -# also owns `KEYRING_CONTAINER`, which is where the reference puts it and which the file above -# reads — again a global resolved at call time, so the order does not bind. -BagFrame.xml +# The reference's item-SLOT button base, `PaperDollItemSlotButtonTemplate` — published for addons +# (pfUI builds its bag bar from what inherits it) and, since window 3, a LOAD-TIME dependency of the +# entry directly below: stock `MainMenuBarBagButtons.xml` inherits it on all five bag buttons, and +# an `inherits=` is resolved at load. That requirement is what moved this file up from below +# CharacterFrame.xml, where it sat only because the chain crossed the old BagFrame -> CharacterFrame +# the wrong way. It needs ItemButtonTemplate.xml above it for the base and Cooldown.xml for the +# $parentCooldown the doll template carries — both far above. +# +# `BagSlotButtonTemplate` used to be declared here too, transcribed from MainMenuBarBagButtons.xml +# because that file could not be sourced. It is sourced now, one entry down, so the reference's own +# declaration is the live one and the copy is deleted — the whole point of the migration. +ItemSlotButtonTemplates.xml +# ══ THE BAG BAR — the reference's own, off the player's chain (decision 1751, window 3) ══ +# `MainMenuBarBackpackButton`, `CharacterBag0Slot`..`CharacterBag3Slot`, `KeyRingButton`, +# `BagSlotButtonTemplate` and the `BagSlotButton_*`/`BackpackButton_*`/`Disable_BagButtons` family +# — the real 1.12 file, with `MainMenuBarBagButtons.lua` arriving through its own `