From 1f06d032dd647c5ad0d6b104ebc7525a55af9ada Mon Sep 17 00:00:00 2001 From: Sam Date: Fri, 7 Aug 2026 16:01:27 +0700 Subject: [PATCH] the spellbook grows its pet tab, and the pet bar gets its keys - the spellbook's pet tab lists what the pet knows, with autocast toggled right on the page - pet ability cooldowns sweep on the pet bar, and the bar binds to 1.12's Secondary Action Button keys - the options window scrolls its pages in a real trough with seated arrows, nothing overflows the frame anymore - Esc un-queues a queued strike, and a hostile unit's frame stops showing buffs the real client hides Co-Authored-By: Claude Fable 5 --- crates/benilla-app/assets/ui/OptionsFrame.xml | 1171 ++++++++++------- crates/benilla-app/assets/ui/PetActionBar.xml | 34 + .../benilla-app/assets/ui/ScrollTemplates.xml | 147 ++- .../benilla-app/assets/ui/SpellBookFrame.xml | 318 ++++- crates/benilla-app/src/assets/mod.rs | 39 +- crates/benilla-app/src/bindings.rs | 34 + crates/benilla-app/src/bindings/commands.rs | 143 +- crates/benilla-app/src/capture/probes.rs | 5 + crates/benilla-app/src/creature_anim.rs | 130 ++ .../src/creature_anim/driver/tests.rs | 56 +- crates/benilla-app/src/debug_panel/mod.rs | 30 +- crates/benilla-app/src/debug_panel/state.rs | 2 +- crates/benilla-app/src/interact.rs | 2 +- crates/benilla-app/src/lib.rs | 11 +- crates/benilla-app/src/net.rs | 13 + crates/benilla-app/src/net/apply.rs | 1 + crates/benilla-app/src/net/apply/pet.rs | 21 + crates/benilla-app/src/net/apply/spells.rs | 260 +++- crates/benilla-app/src/net/io.rs | 5 + crates/benilla-app/src/player.rs | 55 +- crates/benilla-app/src/player/land.rs | 5 +- crates/benilla-app/src/player/state.rs | 3 +- crates/benilla-app/src/player/wire_in.rs | 5 +- crates/benilla-app/src/schedule.rs | 91 +- crates/benilla-app/src/target/by_name.rs | 8 +- crates/benilla-app/src/target/click.rs | 73 +- crates/benilla-app/src/target/flash.rs | 2 +- crates/benilla-app/src/target/mod.rs | 4 +- crates/benilla-app/src/target/relations.rs | 101 ++ crates/benilla-app/src/target/ring.rs | 8 +- crates/benilla-app/src/target/scan.rs | 247 ++-- crates/benilla-app/src/ui_action/cast_send.rs | 31 +- crates/benilla-app/src/ui_action/drain.rs | 70 +- crates/benilla-app/src/ui_aura.rs | 215 ++- crates/benilla-app/src/ui_cast.rs | 301 ++++- crates/benilla-app/src/ui_pet.rs | 3 +- crates/benilla-app/src/ui_pet_book.rs | 434 ++++++ .../benilla-app/src/ui_script/chat_tests.rs | 1 + .../src/ui_script/keybindings_tests.rs | 125 ++ crates/benilla-app/src/ui_script/manifest.rs | 369 ++++++ crates/benilla-app/src/ui_script/mod.rs | 375 +----- .../src/ui_script/options_tests.rs | 237 ++++ .../src/ui_script/pet_bar_tests.rs | 68 + .../src/ui_script/shipped_xml_tests.rs | 159 +++ .../src/ui_script/spellbook_tests.rs | 233 +++- crates/benilla-app/src/ui_spellbook.rs | 20 +- .../examples/spell_interrupt.rs | 26 + crates/benilla-formats/examples/spell_row.rs | 27 + crates/benilla-formats/src/lib.rs | 2 + crates/benilla-formats/src/pet_name_token.rs | 125 ++ crates/benilla-formats/src/spells/display.rs | 18 + crates/benilla-protocol/src/messages/mod.rs | 8 +- crates/benilla-protocol/src/messages/pet.rs | 22 + .../benilla-protocol/src/world/writer/pet.rs | 20 + crates/benilla-ui/src/script/mod.rs | 4 +- crates/benilla-ui/src/script/model.rs | 14 + crates/benilla-ui/src/script/scrollframe.rs | 23 +- crates/benilla-ui/src/script/spellbook.rs | 539 +++++++- .../src/script/tests/scrollframe.rs | 50 + .../src/script/tests/tooltip_spell.rs | 62 + crates/benilla-ui/src/script/tooltip_spell.rs | 26 +- 61 files changed, 5299 insertions(+), 1332 deletions(-) create mode 100644 crates/benilla-app/src/target/relations.rs create mode 100644 crates/benilla-app/src/ui_pet_book.rs create mode 100644 crates/benilla-app/src/ui_script/manifest.rs create mode 100644 crates/benilla-formats/examples/spell_interrupt.rs create mode 100644 crates/benilla-formats/examples/spell_row.rs create mode 100644 crates/benilla-formats/src/pet_name_token.rs diff --git a/crates/benilla-app/assets/ui/OptionsFrame.xml b/crates/benilla-app/assets/ui/OptionsFrame.xml index 644fa37c..b1a4a83b 100644 --- a/crates/benilla-app/assets/ui/OptionsFrame.xml +++ b/crates/benilla-app/assets/ui/OptionsFrame.xml @@ -116,13 +116,21 @@ rows, reflowed — values, writes and dependencies all work mid-search. The two strings ("Search Results", "No results found.") are ours: the era's SETTINGS_SEARCH_* globals live in the client binary, not the dump. - - **No scroll bars on the CVar pages**: the static category tree (9 rows, 2 headers) fits - its 199px column outright, and the deepest CVar page (Audio, 7 rows) fits its area. - MinimalScrollBar arrives when one outgrows the space. The KEYBINDINGS page (decision - 1008 — the 1.15 Settings panel's own Keybindings category, module + provenance in - KeyBindingsPage.xml) is the one page that can't fit and rides the shared faux-scroll - kit; its search results reflow live binding rows under a Keybindings group head whose - click jumps to the page (the era redirect law), capped at the static pool's size. + - **The PAGE AREA scrolls; the category tree does not.** The static tree (9 rows, 2 + headers) fits its 199px column outright, and every CVar page fits its area, so nothing + on a settled page has a bar. The page area is a real ScrollFrame all the same (B217 — + the era's SettingsList is a ScrollBox with a MinimalScrollBar, and results and pages + ride the same one): a SEARCH chains group heads and matched rows from every category + into one column, which for a broad query outran the page and drew out through the + window onto the world. The body is the scroll child, sized to the visible rect until + the content outgrows it (OptionsScroll_Fit). The KEYBINDINGS page (decision 1008 — the + 1.15 Settings panel's own Keybindings category, module + provenance in + KeyBindingsPage.xml) keeps its OWN faux-scroll list inside that body — it fills the + body exactly, so the outer scroll's range on it is 0 and the two bars are never live + together; its search results reflow live binding rows under a Keybindings group head + whose click jumps to the page (the era redirect law), capped at the static pool's size. + Both bars wear the shared kit's trough (the recessed channel — 1.12 authors that art + per window, so it is opt-in there and declared beside each bar here). - **Setting-page cuts (0957 Audio, 0959 Graphics)**: setting-row tooltips (the era OPTION_TOOLTIP_* hovers) are not wired; Defaults resets the VISIBLE page only (the era confirm dialog is not modeled); era rows with no 1.12 CVar + @@ -375,6 +383,67 @@ return order, groups end + -- ── The page scroll (B217) ────────────────────────────────────────────────────────── + -- The body is the scroll child (the ScrollFrame above it), so what these two do is + -- decide how TALL it is: the content's own extent, or the visible page rect, whichever + -- is larger. Below the rect nothing changes at all — range 0, no bar, every page seated + -- exactly where its XML puts it — and above it the overflow becomes a scroll instead of + -- a spill. + -- + -- The measured set is every element that takes a seat in the ONE layout law's chain: + -- each page's rows, the search group heads, and the keybinding result pool. All of them + -- hang off the body's TOP, so their extent is independent of the height being computed — + -- which is what makes this converge. Nothing anchored to the body's BOTTOM may join them + -- (the Keybindings page's output line is the live example): growing the body would move + -- it down, which would grow the body. IsVisible, not IsShown: a hidden page's rows keep + -- their own shown flag, and only what the player can actually see may set the height. + function OptionsScroll_ContentBottom() + local bottom = nil + local function take(name) + local f = getglobal(name) + if f and f:IsVisible() then + local b = f:GetBottom() + if b and (not bottom or b < bottom) then bottom = b end + end + end + for _, key in ipairs(OPTIONS_CATEGORY_KEYS) do + if OPTIONS_PAGE_ROWS[key] then + take("OptionsFrameContainerBodySearchHead" .. key) + for _, rkey in ipairs(OPTIONS_PAGE_ROWS[key]) do + take("OptionsFrameContainerBody" .. key .. rkey) + end + end + end + take("OptionsFrameContainerBodySearchHeadKeybindings") + for i = 1, KEYBINDINGS_SEARCH_ROWS do + take("OptionsFrameContainerBodyKeybindSearch" .. i) + end + return bottom + end + + -- Size the body to that, from the scroll frame's OnUpdate. Frame-late by nature (rects + -- resolve after Lua runs, so a reflow answers a frame later — the OptionsTab_FitWidth + -- idiom), and write-gated: SetWidth/SetHeight invalidate layout, so an every-frame write + -- would fight the resolver. UpdateScrollChildRect re-drives the bar through + -- OnScrollRangeChanged, which is what shows and hides it (and its trough). + function OptionsScroll_Fit() + local sf = OptionsFrameContainerScroll + local body = OptionsFrameContainerBody + local fw, fh = sf:GetWidth(), sf:GetHeight() + if not fw or fw <= 0 or not fh or fh <= 0 then return end + if math.abs(body:GetWidth() - fw) >= 0.5 then body:SetWidth(fw) end + local h = fh + local top, bottom = body:GetTop(), OptionsScroll_ContentBottom() + if top and bottom then + local content = (top - bottom) + BENILLA_SCROLL_PAD + if content > h then h = content end + end + if math.abs(body:GetHeight() - h) >= 0.5 then + body:SetHeight(h) + sf:UpdateScrollChildRect() + end + end + -- THE one layout law (era SettingsList: verticalPad 10, padLeft 25/padRight 0 → -- first element at (10,-12) off the body, spacing 9 → chain at (0,-9)). The rows' -- authored XML anchors state exactly this chain as the initial layout; search @@ -412,6 +481,8 @@ -- search holds the page (l.684-688), and an empty result shows the no-results line -- (l.680-682). function OptionsSearch_Display(text) + -- A new result set starts at the top (the era's own ScrollBox reset on a search). + OptionsFrameContainerScroll:SetVerticalScroll(0) for k in pairs(OPTIONS_PAGE_ROWS) do getglobal("OptionsFrameContainerBody" .. k):Show() for _, rkey in ipairs(OPTIONS_PAGE_ROWS[k]) do @@ -490,6 +561,7 @@ -- selection re-applied — SelectCategory re-shows the one body, retitles, and brings -- Defaults back (the era's no-text arm, l.655-658). function OptionsSearch_Restore() + OptionsFrameContainerScroll:SetVerticalScroll(0) for k in pairs(OPTIONS_PAGE_ROWS) do OptionsPage_Layout(k) getglobal("OptionsFrameContainerBodySearchHead" .. k):Hide() @@ -1446,515 +1518,600 @@ - - + + - - - - - - + + + self:SetScrollChild(OptionsFrameContainerBody) + OptionsScroll_Fit() + BenillaScroll_OnMouseWheel(this, arg1) + BenillaScroll_SyncBar(this) + BenillaScroll_SyncBar(this) + - - - - - - - - - - - - - - OptionsRow_OnLoad(self, "deselectOnClick", "Sticky Targeting") - self.invert = 1 - - - - - - - - OptionsRow_OnLoad(self, "autoLootDefault", "Auto Loot") - - - - - - - OptionsRow_OnLoad(self, "mouseInvertPitch", "Invert Mouse") - - + + + + + + + + + + + + + + + + + + + + + + + + + + OptionsRow_OnLoad(self, "deselectOnClick", "Sticky Targeting") + self.invert = 1 + + + + + + + + OptionsRow_OnLoad(self, "autoLootDefault", "Auto Loot") + + + + + + + OptionsRow_OnLoad(self, "mouseInvertPitch", "Invert Mouse") + + + + + + + + + + + + + + + + + OptionsRow_OnLoad(self, "MasterSoundEffects", "Enable All Sound") + self.soundQuirk = 1 + + + + + + + + OptionsRow_OnLoad(self, "MasterVolume", "Master Volume") + + + + + + + + OptionsRow_OnLoad(self, "SoundVolume", "Sound Volume", 1) + -- era parentInitializer (search pulls Master in + -- above a matched child, SettingsPanel.lua l.665) + self.parentRow = "RowMaster" + + + + + + + + + OptionsRow_OnLoad(self, "MusicVolume", "Music Volume", 1) + self.parentRow = "RowMaster" + + + + + + + + + OptionsRow_OnLoad(self, "AmbienceVolume", "Ambience Volume", 1) + self.parentRow = "RowMaster" + + + + + + + + + OptionsRow_OnLoad(self, "EnableMusic", "Enable Music") + self.soundQuirk = 1 + + + + + + + + + OptionsRow_OnLoad(self, "EnableAmbience", "Enable Ambience") + self.soundQuirk = 1 + + + + + + + + + + + + + + + + + + OptionsRow_OnLoad(self, "uiScale", "UI Scale") + OptionsSliderRow_Setup(self, 0.64, 1.0, 0.01) + -- era registers its uiscale row CommitFlag.Apply + -- (Graphics.lua l.684): the one row whose live + -- write would rescale the very slider mid-drag — + -- it stages and commits through the Apply button. + self.deferred = 1 + + + + + + + + + OptionsRow_OnLoad(self, "WorldDetail", "Environment Detail") + -- The stop names are the era preset ladder's (its + -- quality dropdowns say Low/Medium/High); 1.12's + -- slider had no stop labels to quote. + OptionsDropdownRow_Setup(self, { + { text = "Low", value = "0" }, + { text = "Medium", value = "1" }, + { text = "High", value = "2" }, + }) + + + + + + + + + + + + + + + + + OptionsRow_OnLoad(self, "UnitNamePlayer", "Player Names") + + + + + + + OptionsRow_OnLoad(self, "UnitNameNPC", "NPC Names") + + + + + + + OptionsRow_OnLoad(self, "UnitNameOwn", "Show Own Name") + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + KeyBindings_CharacterToggle(this) + + GameTooltip:SetOwner(this, "ANCHOR_RIGHT") + GameTooltip:SetText(KeyBindings_String("CHARACTER_SPECIFIC_KEYBINDING_TOOLTIP", + "Click this to toggle between general key bindings and key bindings specific to this character."), nil, nil, nil, nil, 1) + + GameTooltip:Hide() + + + + + + getglobal(this:GetName() .. "Label"):SetText( + KeyBindings_String("CHARACTER_SPECIFIC_KEYBINDINGS", "Character Specific Key Bindings")) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + BenillaFauxScrollFrame_OnLoad(this) + this.updateFunc = KeyBindingsPage_Update + + + + + + + + if not KeyBindingsPage.selected then + BenillaFauxScrollFrame_OnMouseWheel(getglobal(this:GetName() .. "ScrollFrame"), arg1) + end + + + + + + + + + + + + + + + + + + + - - + + - + diff --git a/crates/benilla-app/assets/ui/PetActionBar.xml b/crates/benilla-app/assets/ui/PetActionBar.xml index b4be12ba..4800bc58 100644 --- a/crates/benilla-app/assets/ui/PetActionBar.xml +++ b/crates/benilla-app/assets/ui/PetActionBar.xml @@ -44,6 +44,15 @@ them off the same cursor transition that drives ACTIONBAR_SHOWGRID/HIDEGRID but on a disjoint predicate, so a held spell lights the action bar's empty slots and a held pet action lights this bar's — never both, because neither payload can land on the other bar. + - NO HotKey corner, and none is missing (1052). The ref's button inherits + ActionButtonTemplate's $parentHotKey and repaints it on UPDATE_BINDINGS — but + PetActionButton_SetHotkeys (l.311-319) writes the label only when + `bindingSuffix == this:GetID()`, a STRING against a NUMBER, which Lua never equates. So + the real 1.12 pet bar renders an empty hotkey on every slot, always, and the reporter's + own reference shot for B218 shows exactly that: CTRL-1..CTRL-5 bound in the window, no + glyph on any pet button. An empty corner is the faithful render; a filled one would be + the divergence. (Our own action bar's corners are real — ActionBar.xml — because the + ref's ActionButton_UpdateHotkeys has no such bug.) - The autocast sparkle is TEXTURES, not the ref's . benilla has no Model widget (the Cooldown and the bag ItemAnim took the same posture): model the mechanism, let our own renderer draw it. The numbers below are the shipped model's own — see BENILLA_PET_ @@ -228,6 +237,31 @@ end end + -- The KEYBIND pair (decision 1052) — ref PetActionButtonDown/Up l.218-231, which is where + -- the BONUSACTIONBUTTONn bindings land: their bodies call BonusActionButtonDown/Up, and + -- those are one-liners onto these (BonusActionBarFrame.lua:106-112). The ref's own + -- runOnUp shape: DOWN only shows the pushed art, UP fires. + -- + -- Note what the key path does NOT do — it is a bare CastPetAction, with none of the click + -- path's forks: no shift pick-up, no right-button autocast, and no IsPetAttackActive + -- call-off. That is the reference's split, not a simplification of ours: the fork lives in + -- PetActionButton_OnClick, which a key press never reaches. So CTRL-1 on a pet already + -- attacking re-issues Attack where a left click would call it off. + function BenillaPetActionButtonDown(id) + local button = getglobal("BenillaPetActionButton" .. id) + if ( button:GetButtonState() == "NORMAL" ) then + button:SetButtonState("PUSHED") + end + end + + function BenillaPetActionButtonUp(id) + local button = getglobal("BenillaPetActionButton" .. id) + if ( button:GetButtonState() == "PUSHED" ) then + button:SetButtonState("NORMAL") + CastPetAction(id) + end + end + -- Both drag ends are the SAME verb (ref l.269-283, byte-verified at `0x4be180`): with an -- empty cursor it picks the slot up, with a pet payload already held it drops onto this -- slot. There is no separate "place" binding on this bar, which is also why a spell or an diff --git a/crates/benilla-app/assets/ui/ScrollTemplates.xml b/crates/benilla-app/assets/ui/ScrollTemplates.xml index 14458f89..1a023928 100644 --- a/crates/benilla-app/assets/ui/ScrollTemplates.xml +++ b/crates/benilla-app/assets/ui/ScrollTemplates.xml @@ -1,7 +1,10 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +