From 348f55e37e635a529964a162d454be6e2b7fab5a Mon Sep 17 00:00:00 2001 From: Sam Date: Fri, 21 Aug 2026 18:12:19 +0700 Subject: [PATCH] four extra action bars as options, and the bottom band gets one owner MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - the four extra action bars become per-character options, off by default, on a new Action Bars options page — the toggles ride the server's own byte (`CMSG_SET_ACTIONBAR_TOGGLES` into `PLAYER_FIELD_BYTES`), so a fresh character's zero is the default for free; turning the bottom-left bar off gives the stance bar its previously unbuilt unraised look, end-caps and 64×64 rings - bag windows stop drawing straight through a raised bottom bar: the bottom band has ONE owner — the manage pass — the bag stack joins it through a listener seam, and a guard test walks all eight raisable-bar combinations across 24 bar/tenant pairs and refuses overlap - the item-push card takes the reference's real geometry, closing the gap 0887 measured - resizable addon windows stop at their bounds instead of pinning and walking across the screen: four per-frame f32s with 0.0 the disable sentinel, the clamp on the interactive drag only with the residual rebated out of the drag delta, `` in XML honoured — and `SetFontString`'s three raises land exactly as the bytes have them Co-Authored-By: Claude Fable 5 --- crates/benilla-app/assets/ui/ActionBar.xml | 151 +++-- crates/benilla-app/assets/ui/BagFrame.xml | 161 +++++- crates/benilla-app/assets/ui/GameTooltip.xml | 13 +- crates/benilla-app/assets/ui/MultiBars.xml | 267 +++++++-- crates/benilla-app/assets/ui/OptionsFrame.xml | 152 ++++- crates/benilla-app/assets/ui/StanceBar.xml | 108 +++- crates/benilla-app/assets/ui/UIParent.xml | 83 ++- crates/benilla-app/assets/ui/benilla.toc | 14 +- crates/benilla-app/src/net.rs | 15 + crates/benilla-app/src/net/io.rs | 3 + .../src/ui_script/action_bar_tests.rs | 33 +- crates/benilla-app/src/ui_script/bag_tests.rs | 110 +++- .../src/ui_script/bottom_hud_tests.rs | 464 +++++++++++++++ .../benilla-app/src/ui_script/cast_tests.rs | 6 +- .../benilla-app/src/ui_script/extract/mod.rs | 13 +- .../src/ui_script/group_loot_tests.rs | 6 +- crates/benilla-app/src/ui_script/mod.rs | 14 + .../src/ui_script/multibar_stance_tests.rs | 533 +++++++++++++++++- .../src/ui_script/options_tests.rs | 196 ++++++- .../src/ui_script/pet_bar_tests.rs | 11 +- .../src/ui_script/tooltip_anchor_tests.rs | 25 +- crates/benilla-app/src/ui_unit.rs | 59 ++ .../src/messages/action_bar.rs | 31 + crates/benilla-protocol/src/messages/mod.rs | 3 +- .../benilla-protocol/src/messages/opcode.rs | 10 + .../messages/update_object/fields/player.rs | 30 + .../messages/update_object/fields/tests.rs | 13 +- .../src/world/writer/action_bar.rs | 25 + crates/benilla-protocol/tests/spells.rs | 25 + crates/benilla-ui/src/loader/geometry.rs | 25 + .../src/script/action_bar_toggles.rs | 319 +++++++++++ crates/benilla-ui/src/script/binding_abi.rs | 141 +++++ crates/benilla-ui/src/script/button.rs | 132 ++++- crates/benilla-ui/src/script/mod.rs | 21 +- crates/benilla-ui/src/script/model.rs | 15 + .../benilla-ui/src/script/object/movable.rs | 186 ++++-- crates/benilla-ui/src/script/region.rs | 29 + .../benilla-ui/src/script/simplehtml/mod.rs | 22 +- crates/benilla-ui/src/script/tests/button.rs | 128 +++++ crates/benilla-ui/src/script/tests/mod.rs | 1 + .../src/script/tests/resize_bounds.rs | 316 +++++++++++ crates/benilla-ui/src/widget/mod.rs | 47 +- 42 files changed, 3620 insertions(+), 336 deletions(-) create mode 100644 crates/benilla-app/src/ui_script/bottom_hud_tests.rs create mode 100644 crates/benilla-ui/src/script/action_bar_toggles.rs create mode 100644 crates/benilla-ui/src/script/tests/resize_bounds.rs diff --git a/crates/benilla-app/assets/ui/ActionBar.xml b/crates/benilla-app/assets/ui/ActionBar.xml index ffb565b0..e17afe1e 100644 --- a/crates/benilla-app/assets/ui/ActionBar.xml +++ b/crates/benilla-app/assets/ui/ActionBar.xml @@ -39,11 +39,23 @@ a mount, which has no stack; the engine-side law is ItemInfo::is_consumable, byte-cited from 0x4e5250). - Empty-slot grid (ref ActionButton.lua:241-259's ShowGrid/HideGrid, a per-button COUNTER): - simplified to one bar-level BOOLEAN — the cursor is a single global payload, so no two - "shows" can ever nest, and a counter would just be a boolean with extra steps. Unlike the - ref (which HIDES an empty, non-grid slot entirely), our bar always shows all 12 wells; the - grid only swaps an EMPTY slot's ring art between the occupied and empty Quickslot textures - — the default (nothing held) look is unchanged from before this slice, no capture regression. + the counter is BACK, and the argument that retired it is exactly why. 0216 §7 collapsed it + to one global boolean because "the cursor is a single global payload, so no two shows can + ever nest" — true while the payload was the only thing that could ask for a grid. Decision + 1500 gives it a second asker (ALWAYS_SHOW_MULTIBARS, which holds the four extra bars' + empty wells open with no payload in hand), and with two independent askers the boolean is + wrong by its own reasoning: dropping a payload would close wells the option is still + asking for. So `button.showgrid` COUNTS, exactly as the reference's does, and + ActionButton_ShowGrid/_HideGrid below are its two verbs. + The count — rather than a flag — is also what the addon corpus already assumes: zBar + REPLACES both verbs and writes `button.showgrid = 1/0`, CT_BarMod writes 0/1/2 and + increments it on OUR ActionButton1-12, and Bartender2 HOOKS ActionButton_ShowGrid at + load — which died on a nil global here until now, the same session-start crash class + 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. - 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 @@ -125,10 +137,6 @@ end end - -- Whether a placeable cursor payload is held (decision 0216 §7's ACTIONBAR_SHOWGRID/ - -- _HIDEGRID pair, simplified to a boolean — see the SCOPE note above). - BENILLA_ACTIONBAR_GRID_SHOWN = false - -- Every live action button — the main bar's 12 plus the multibars' (MultiBars.xml joins -- at OnLoad). The fan-out roster for the bar-level events below: the ref registers the -- feedback events per-button; our bar-level fan needs the full set in one place. @@ -146,23 +154,17 @@ CURRENT_ACTIONBAR_PAGE = 1 -- "Table of actionbar pages and whether they're viewable or not" (the reference's own -- comment). The page walk below honours it rather than counting, which is what makes the - -- next two lines work at all. + -- whole thing work: a page an extra bar already displays must not ALSO be reachable on the + -- main bar, or paging up shows you the twelve actions already on screen beside it. -- - -- **Pages 5 and 6 are deliberately NOT viewable, and that is the reference's own rule - -- applied to our configuration rather than a deviation from it.** - -- `MultiActionBar_Update` (ref MultiActionBars.lua l.49-60) blanks - -- `VIEWABLE_ACTION_BAR_PAGES[BOTTOMLEFT_ACTIONBAR_PAGE]` whenever MultiBarBottomLeft is - -- SHOWN — a page a multibar already displays must not also be reachable on the main bar, - -- or paging up shows you the twelve actions already on screen below. benilla's two bottom - -- bars are always-on with no `SHOW_MULTI_ACTIONBAR_*` state (0270, the director's call), - -- and they own pages 6 and 5 (MultiBars.xml: BottomLeft = actions 61..72, BottomRight = - -- 49..60), so the two rows are blanked at declaration instead of by a toggle. - -- - -- MultiBarLeft/Right (pages 4 and 3) stay viewable because ours ship HIDDEN (1219), which - -- is the same state the reference is in with SHOW_MULTI_ACTIONBAR_3/4 unset. - VIEWABLE_ACTION_BAR_PAGES = {1, 1, 1, 1} - VIEWABLE_ACTION_BAR_PAGES[BOTTOMRIGHT_ACTIONBAR_PAGE] = nil - VIEWABLE_ACTION_BAR_PAGES[BOTTOMLEFT_ACTIONBAR_PAGE] = nil + -- **This is the shipped START state, not a constant: it FOLLOWS the four bar toggles.** + -- All six pages are viewable here because all four extra bars now ship hidden (decision + -- 1500 — they are player options, off by default), and `MultiActionBar_Update` + -- (MultiBars.xml) does the arithmetic from then on: a bar that comes up takes its page out + -- of the cycle, a bar that goes down puts it back. Blanking two rows at declaration was + -- only ever right because visibility was static (0270's always-on bottom pair), and the + -- moment the player can move it a declaration cannot be the answer. + VIEWABLE_ACTION_BAR_PAGES = {1, 1, 1, 1, 1, 1} -- `ChangeActionBarPage()` takes NO arguments — every reference caller sets -- CURRENT_ACTIONBAR_PAGE first and then calls this to apply it (l.79, l.100), and @@ -339,6 +341,9 @@ button.cooldown = getglobal(n .. "Cooldown") button.flashing = button.flashing or 0 button.flashtime = button.flashtime or 0 + -- The grid count, same posture: a foreign button reaching ActionButton_HideGrid with + -- no count would raise on the arithmetic, which is 1199's class of silent-wrong-shape. + button.showgrid = button.showgrid or 0 end function BenillaActionButton_Action(button) @@ -369,14 +374,21 @@ -- it; 0954 ended that and cvars.rs now registers the key (default "1", byte-read — the -- evidence lives on that row), so the fork is live and the stock look is unchanged. -- - -- The ref's LEFT set is {MultiBarBottomRight, MultiBarRight, MultiBarLeft}. Only the first - -- exists here — the two VERTICAL bars are deliberately not shipped (see the header of - -- MultiBars.xml) — so it is the whole test, and the day they land they join it. + -- The ref's LEFT set is {MultiBarBottomRight, MultiBarRight, MultiBarLeft} — the three bars + -- that sit at (or against) the right edge, so their plates have to open toward the centre. + -- All three exist here now: the two VERTICAL bars landed with 1219 and became player + -- options with 1500, and this comment said "only the first exists" for as long as that was + -- true. Membership is by FRAME, not by visibility — a hidden bar is never hovered anyway. function BenillaActionButton_OnEnter(button) BenillaActionButton_Adopt(button) if not GameTooltip then return end -- engine tests load file subsets if GetCVar("UberTooltips") == "0" then - if button:GetParent() == MultiBarBottomRight then + -- `parent and` first: an orphan button's nil parent would otherwise match a nil + -- MultiBarRight in a harness that loaded no multibars, and anchor the wrong way. + local parent = button:GetParent() + local edgeBar = parent and (parent == MultiBarBottomRight + or parent == MultiBarRight or parent == MultiBarLeft) + if edgeBar then GameTooltip:SetOwner(button, "ANCHOR_LEFT") else GameTooltip:SetOwner(button, "ANCHOR_RIGHT") @@ -400,6 +412,8 @@ button.flashing = 0 button.flashtime = 0 button.rangeTimer = nil + -- ref ActionButton_OnLoad l.104: the empty-well grid count starts at zero. + button.showgrid = 0 -- Every bar button carries its binding COMMAND (0997 main bar, 1008 multibars): -- the caller names the command family (MultiBars.xml passes MULTIACTIONBAR1/2BUTTON -- — the ref's own bar↔page mapping quoted there); nothing passed = the main bar's @@ -673,10 +687,16 @@ local button = this if not button then return end BenillaActionButton_Adopt(button) + -- Through the two verbs, ref-verbatim in shape (ActionButton_OnEvent l.317-324) and + -- no longer the 1/0 assignment this arm shipped with: `showgrid` is a COUNT now, and a + -- foreign button handed a bare 0 while the option still wanted its wells open would + -- close them. Each verb repaints, so these arms return rather than fall through. if event == "ACTIONBAR_SHOWGRID" then - button.showgrid = 1 + ActionButton_ShowGrid(button) + return elseif event == "ACTIONBAR_HIDEGRID" then - button.showgrid = 0 + ActionButton_HideGrid(button) + return elseif event == "UPDATE_BINDINGS" then BenillaActionButton_PaintHotkey(button) end @@ -704,6 +724,46 @@ BenillaActionButton_UpdateUsable(this) end + -- ── The empty-slot grid (ref ActionButton.lua l.243-260) ──────────────────────────────── + -- `button.showgrid` is a COUNT of how many things currently want this button's empty well + -- visible, and it is the reference's own field under its own name — see the SCOPE note for + -- why a count and not a flag, and for the three corpus addons that already write it. + -- + -- Today there are exactly two askers and they are independent: the cursor's placeable + -- payload (the ACTIONBAR_SHOWGRID/_HIDEGRID pair, fanned over the whole roster in + -- BenillaActionBar_OnEvent) and ALWAYS_SHOW_MULTIBARS (MultiBars.xml's + -- MultiActionBar_UpdateGridVisibility, over the four extra bars only). Either alone holds + -- the wells open; the count is what lets one let go without closing what the other holds. + -- + -- Divergence from the ref, deliberate: ours re-derive the whole button through + -- BenillaActionButton_Update rather than doing their own Show/Hide + vertex tint. There is + -- one paint path in this file and the grid is one of its inputs, so a verb that painted + -- directly would be a second, slightly different answer to "what does an empty well look + -- like" — which is how the two got out of step in the ref (its ShowGrid dims the ring, its + -- Update never restores the dim). + function BenillaActionButton_GridShown(button) + return button.showgrid ~= nil and button.showgrid > 0 + end + + function ActionButton_ShowGrid(button) + button = button or this + BenillaActionButton_Adopt(button) + button.showgrid = (button.showgrid or 0) + 1 + BenillaActionButton_Update(button) + end + + function ActionButton_HideGrid(button) + button = button or this + BenillaActionButton_Adopt(button) + -- Clamped at zero. The ref subtracts unconditionally and can go negative — a button + -- that joined the roster mid-payload takes one HIDEGRID it never took a SHOWGRID for, + -- and from then on its wells need TWO shows to open. Ours simply cannot owe. + local count = (button.showgrid or 0) - 1 + if count < 0 then count = 0 end + button.showgrid = count + BenillaActionButton_Update(button) + end + function BenillaActionButton_Update(button) local action = BenillaActionButton_Action(button) if HasAction(action) then @@ -730,24 +790,25 @@ button.hotkey:SetVertexColor(0.6, 0.6, 0.6) button:SetChecked(0) BenillaActionButton_StopFlash(button) + local grid = BenillaActionButton_GridShown(button) if button.base then -- An EMPTY multibar well is the ref's own default: hidden entirely, appearing - -- as a drop-target ring only while a payload is held (ActionButton_Update's - -- showgrid==0 Hide; MultiActionBar_ShowAllGrids). Only the MAIN bar keeps our - -- always-visible wells divergence (the SCOPE note) — its art has sockets; - -- floating rings over bare world would not read as a bar. + -- as a drop-target ring only while the grid is asked for (a held payload, or + -- ALWAYS_SHOW_MULTIBARS). Only the MAIN bar keeps our always-visible wells + -- divergence (the SCOPE note) — its art has sockets; floating rings over bare + -- world would not read as a bar. button.hotkey:SetText("") - if BENILLA_ACTIONBAR_GRID_SHOWN then + if grid then button:SetNormalTexture("Interface\\Buttons\\UI-Quickslot") button:Show() else button:Hide() end else - -- While a payload is held, an empty slot shows the ref's own "no action" ring + -- 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). - if BENILLA_ACTIONBAR_GRID_SHOWN then + if grid then button:SetNormalTexture("Interface\\Buttons\\UI-Quickslot") else button:SetNormalTexture("Interface\\Buttons\\UI-Quickslot2") @@ -863,8 +924,20 @@ end) return end + -- The payload edge, routed through the two PUBLISHED verbs rather than a private flag + -- — which is what makes Bartender2's `self:Hook("ActionButton_ShowGrid")` see a cursor + -- pick-up at all. The engine fires this pair strictly on the cursor's None↔Some edges + -- (`script/model.rs`), so the count it drives is balanced by construction. if event == "ACTIONBAR_SHOWGRID" or event == "ACTIONBAR_HIDEGRID" then - BENILLA_ACTIONBAR_GRID_SHOWN = (event == "ACTIONBAR_SHOWGRID") + local show = (event == "ACTIONBAR_SHOWGRID") + for i = 1, table.getn(BENILLA_ACTION_BUTTONS) do + if show then + ActionButton_ShowGrid(BENILLA_ACTION_BUTTONS[i]) + else + ActionButton_HideGrid(BENILLA_ACTION_BUTTONS[i]) + end + end + return -- each verb already repainted its own button end for i = 1, table.getn(BENILLA_ACTION_BUTTONS) do BenillaActionButton_Update(BENILLA_ACTION_BUTTONS[i]) diff --git a/crates/benilla-app/assets/ui/BagFrame.xml b/crates/benilla-app/assets/ui/BagFrame.xml index fdd4c8d1..081d1e84 100644 --- a/crates/benilla-app/assets/ui/BagFrame.xml +++ b/crates/benilla-app/assets/ui/BagFrame.xml @@ -728,15 +728,25 @@ -- ── The open-bag stack + layout law (ref ContainerFrame1.bags + -- updateContainerFrameAnchors, ContainerFrame.lua:472-552, transcribed shape) ───────────── - -- Open windows stack bottom-right UPWARD in show order: the first at the backpack's own - -- anchor (BOTTOMRIGHT, x=0/y=70 — CONTAINER_OFFSET_X/Y), each next one anchored above the - -- previous (TOPRIGHT → BOTTOMRIGHT, CONTAINER_SPACING=0 gap); when a stack would run off the - -- top of the screen, the NEXT window starts a fresh column one CONTAINER_WIDTH further left. + -- Open windows stack bottom-right UPWARD in show order: the first at the bottom-right + -- CLEARANCE CORNER, each next one anchored above the previous (TOPRIGHT → BOTTOMRIGHT, + -- CONTAINER_SPACING=0 gap); when a stack would run off the top of the screen, the NEXT + -- window starts a fresh column one CONTAINER_WIDTH further left. -- Out of scope (nothing here ever needs it): the ref's UI-scale/BankFrame-collision shrink -- branch (l.505-539) — benilla has no uiScale CVar and no bank window yet. + -- + -- The corner is `CONTAINER_OFFSET_X`/`CONTAINER_OFFSET_Y` — the two GLOBALS + -- `UIParent_ManageFramePositions()` recomputes every time the bottom of the screen fills up + -- or empties (UIParent.xml's var rows). Reading them live is the whole point and it is + -- load-bearing, not tidiness: this file used to keep its own `local + -- BENILLA_CONTAINER_OFFSET_X/Y = 0/70` shadow pair, which meant the stack sat at the + -- no-bars-raised corner FOREVER — with a bottom multibar up, the lowest bag window drew + -- straight through the bar's buttons (director-caught, decision 1499). A shadow copy of a + -- managed value cannot be kept in sync by anyone; the fix is to have no copy. + -- `BenillaBagStack_Relayout` is registered as a managed-position LISTENER at the end of this + -- file, so a bar raised while bags are open re-seats them on the same pass that moves the + -- cast bar and the chat frames. BENILLA_BAG_STACK = {} - local BENILLA_CONTAINER_OFFSET_X = 0 - local BENILLA_CONTAINER_OFFSET_Y = 70 local BENILLA_CONTAINER_WIDTH = 192 local BENILLA_CONTAINER_SPACING = 0 local BENILLA_VISIBLE_CONTAINER_SPACING = 3 @@ -757,19 +767,25 @@ end function BenillaBagStack_Relayout() + -- Read the clearance corner FRESH on every pass — never cached in a local that outlives + -- the call (see the law above). The `or` fallbacks cover the per-window test harnesses + -- that load this file without UIParent.xml: with no manage pass in the VM the globals + -- are nil, and the no-bars-raised corner is the right answer there by construction. + local offsetX = CONTAINER_OFFSET_X or 0 + local offsetY = CONTAINER_OFFSET_Y or 70 local screenHeight = GetScreenHeight() - local freeHeight = screenHeight - BENILLA_CONTAINER_OFFSET_Y + local freeHeight = screenHeight - offsetY local column = 0 for i = 1, table.getn(BENILLA_BAG_STACK) do local frame = BENILLA_BAG_STACK[i] if i == 1 then - frame:SetPoint("BOTTOMRIGHT", -BENILLA_CONTAINER_OFFSET_X, BENILLA_CONTAINER_OFFSET_Y) + frame:SetPoint("BOTTOMRIGHT", -offsetX, offsetY) elseif freeHeight < frame:GetHeight() then column = column + 1 - freeHeight = screenHeight - BENILLA_CONTAINER_OFFSET_Y + freeHeight = screenHeight - offsetY frame:SetPoint("BOTTOMRIGHT", - -(column * BENILLA_CONTAINER_WIDTH) - BENILLA_CONTAINER_OFFSET_X, - BENILLA_CONTAINER_OFFSET_Y) + -(column * BENILLA_CONTAINER_WIDTH) - offsetX, + offsetY) else frame:SetPoint("BOTTOMRIGHT", BENILLA_BAG_STACK[i - 1], "TOPRIGHT", 0, BENILLA_CONTAINER_SPACING) end @@ -777,6 +793,17 @@ end end + -- The stack is a CONSUMER of two managed vars, so it joins the pass's listener list: the + -- pass writes CONTAINER_OFFSET_X/Y and then calls everything registered here, which is how + -- an open bag stack gets out of the way of a bar raised underneath it. (The reference does + -- the same thing by hard-calling `updateContainerFrameAnchors()` from the end of its own + -- pass — UIParent.lua:1774; ours is that call generalized so the next var consumer does not + -- have to edit UIParent.xml to be noticed. Decision 1499.) Existence-guarded for the + -- harnesses that load this file alone. + if UIParent_RegisterManagedPositionListener then + UIParent_RegisterManagedPositionListener(BenillaBagStack_Relayout) + end + -- ── The bag bar (decision 0216 slice 2) — four 30×30 buttons left of MainMenuBarBackpackButton, -- mirroring the reference main-bar bag buttons (CharacterBag0..3Slot). No exact reference -- XML was available to transcribe (not in the session's extracted set) — size (30×30) and @@ -1113,16 +1140,63 @@ -- in one place and the animation stays in proportion. BENILLA_ITEM_PUSH_PX_PER_UNIT = 1280 BENILLA_ITEM_PUSH_DURATION = 1.0 - BENILLA_ITEM_PUSH_SIZE = 0.03 * BENILLA_ITEM_PUSH_PX_PER_UNIT -- 38.4 - BENILLA_ITEM_PUSH_FALL = 0.04 * BENILLA_ITEM_PUSH_PX_PER_UNIT -- 51.2 - BENILLA_ITEM_PUSH_DRIFT = 0.01 * BENILLA_ITEM_PUSH_PX_PER_UNIT -- 12.8 - -- {time, value} keyframes, linear between (M2 track interpolation 1) — the file's own keys. + + -- ── 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.2}, {0.267, 1.0}, {1.000, 0.014} } - -- The translation track as a 0..1 fraction of the fall (both components move together, so - -- one curve drives both axes — the file keys (0,0) at 0.000 and 0.500, the full offset at - -- 1.000, i.e. it hangs for the first half second and then drops). - BENILLA_ITEM_PUSH_DROP = { {0.000, 0.0}, {0.500, 0.0}, {1.000, 1.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). @@ -1141,27 +1215,52 @@ return track[last][2] end - -- Place/size/fade the card for the current clock. The card LANDS centred on its button and - -- STARTS a fall's height above it (and a drift's width to the left), so the icon reads as - -- floating clear above the bar and then dropping into the bag — the model's own relative - -- motion, anchored to the button rather than to the ref's unresolvable model origin. + -- 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 drop = BenillaItemPushAnim_Sample(BENILLA_ITEM_PUSH_DROP, t) - local size = BENILLA_ITEM_PUSH_SIZE * BenillaItemPushAnim_Sample(BENILLA_ITEM_PUSH_SCALE, t) + 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(), "CENTER", - BENILLA_ITEM_PUSH_DRIFT * (drop - 1), - BENILLA_ITEM_PUSH_FALL * (1 - drop)) + 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) + 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 = getglobal(anim:GetName() .. "Icon") anim.elapsed = 0 anim:RegisterEvent("ITEM_PUSH") @@ -2088,7 +2187,7 @@ - BenillaItemPushAnim_OnLoad(self, -2) + BenillaItemPushAnim_OnLoad(self, -2, 0) BenillaItemPushAnim_OnEvent(self) BenillaItemPushAnim_OnUpdate(self, arg1) diff --git a/crates/benilla-app/assets/ui/GameTooltip.xml b/crates/benilla-app/assets/ui/GameTooltip.xml index 1eadc34d..e1833092 100644 --- a/crates/benilla-app/assets/ui/GameTooltip.xml +++ b/crates/benilla-app/assets/ui/GameTooltip.xml @@ -31,8 +31,6 @@ -- GameTooltip_SetDefaultAnchor's position math (ref-GameTooltip.lua l.6,73-77): the -- bottom-right screen corner, -CONTAINER_OFFSET_X - 13 in x, CONTAINER_OFFSET_Y in y. DEFAULT_TOOLTIP_POSITION = -13 - CONTAINER_OFFSET_X = CONTAINER_OFFSET_X or 0 - CONTAINER_OFFSET_Y = CONTAINER_OFFSET_Y or 70 -- The shopping-compare pair count (ref GameTooltip.lua l.7; PaperDollFrame's listener -- bounds arg2 by it). NUM_SHOPPING_TOOLTIPS = 2 @@ -86,8 +84,17 @@ -- owner anchor (tooltip.rs SetOwner), so no ClearAllPoints - the ref has none either. function GameTooltip_SetDefaultAnchor(tooltip, parent) tooltip:SetOwner(parent, "ANCHOR_NONE") + -- Read the two managed globals FRESH, and fall back AT USE rather than assigning a + -- copy: this file used to open with `CONTAINER_OFFSET_X = CONTAINER_OFFSET_X or 0`, + -- which is a second written statement of numbers UIParent.xml owns. Harmless in the + -- shipped load order (UIParent.xml is manifest 42, this file later) but it is the same + -- shape as the shadow pair that froze the bag stack at the no-bars corner, so the law + -- of 1499 forbids it outright: nothing outside UIParent.xml assigns a managed offset. + -- The `or` defaults below cover the tooltip-only harnesses that load no UIParent. + local offsetX = CONTAINER_OFFSET_X or 0 + local offsetY = CONTAINER_OFFSET_Y or 70 tooltip:SetPoint("BOTTOMRIGHT", "UIParent", "BOTTOMRIGHT", - -CONTAINER_OFFSET_X + DEFAULT_TOOLTIP_POSITION, CONTAINER_OFFSET_Y) + -offsetX + DEFAULT_TOOLTIP_POSITION, offsetY) tooltip.default = 1 end diff --git a/crates/benilla-app/assets/ui/MultiBars.xml b/crates/benilla-app/assets/ui/MultiBars.xml index b03d3a0e..c56eb69c 100644 --- a/crates/benilla-app/assets/ui/MultiBars.xml +++ b/crates/benilla-app/assets/ui/MultiBars.xml @@ -1,9 +1,39 @@ + Geometry, anchor chain and TexCoords quoted whole: Left and Right are two crops of ONE + `ShapeshiftBarEnds` file (0..0.453125 and 0.453125..0.875), Middle its own, and Right + hangs off Middle's RIGHT exactly as the ref chains them — which is why hiding the + middle strip leaves its gap rather than closing it, the reference's own result. + + `hidden="true"` is the RAISED state, and it is only the pre-pass value now: + `ShapeshiftBar_UpdatePosition` (above) shows and hides all three from the bottom-left + multibar's state on every managed-position pass. Declared hidden rather than shown so a + harness that never runs the pass draws nothing new. -->