From 766395116cd3b3efb09011d6b762dfcd36616cf1 Mon Sep 17 00:00:00 2001 From: Sam Date: Fri, 4 Sep 2026 20:49:33 +0700 Subject: [PATCH] a disarmed fighter punches, and the quest log tags its elite rows MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - a disarmed fighter swings bare-fisted: `UNIT_FLAG_DISARMED` was read nowhere but a dev banner, and the reference applies it inside `GetWeapon` on the `visFlag == 0` path, so the swing lands on AttackUnarmed; the RE cross-check (nine workers converging byte-for-byte) corrected the first reading twice — disarm hides exactly ONE weapon, main hand first (`5ec28d je 0x5ec2aa`), so a disarmed dual-wielder keeps swinging its dagger; and the weapon comes OFF the hand: the field-change reflex `0x5ff580` detaches a drawn main-hand weapon when the bit goes up, while a stowed one stays frozen on the back; one pure ladder feeds every unarmed-view site of the 82-site `visFlag` census — parry, the sheath machine, the melee sounds (a disarmed punch does not clang like a sword), Heroic Strike greying out, and the Attack button showing `Spell-Reset` with the weapon still equipped (1863) - a quest-log row reads `(Elite)`, `(Dungeon)`, `(Raid)` or `(PvP)`: `QuestInfo.dbc` was never parsed and `GetQuestLogTitle`'s third return was a literal nil; the table is seven rows read out of `patch-2.MPQ`, SPARSE (1 Elite · 21 Life · 41 PvP · 62 Raid · 81 Dungeon · 82 World Event · 83 Legendary — vanilla's 1 is "Elite", not TBC's "Group"), and the base `dbc.MPQ` copy has only four, so a chain stopping short would lose Dungeon, World Event and Legendary; the row gains the reference's own right-flush tag string and its precedence — a finished quest's FAILED/COMPLETE word OVERWRITES the tag — replacing the " (Complete)" we appended to the title; the RE round confirmed the tag path and found the arity bug: six values ALWAYS (`mov eax,6`), the level a NUMBER 0 even on a header, where we gave one nil; the reference's title shrink stays out: an explicit width is a WRAP width here (1873) - the death thud is silent indoors, and that is the content: BOTH the footstep and the thud lookups key on `TerrainType.SoundID`, and TerrainType 10 "None" has SoundID 0 — the footstep table has a row there for 17 classes and `DeathThudLookups` has none, so the floor that creaks underfoot swallows the body that lands on it; 10 075 of 10 299 WMO materials (97.8%) are TerrainType 10 and 694 of 815 WMO roots have no surface that can thud, every inn and tavern among them, the Kharanos inn included — the code was always right and the prose was not; the RE round also confirmed the strict `depth > 2.0` sense and the size-class column, pinned a sound id of 0 as silence, and placed `0x6005d0` as the loot sparkle, not a second sound; `thudcensus` prints all 121 buildings that do thud rather than a head, because a truncated list cannot answer "does THIS one" (1870) - a questgiver's overhead marker follows its rider on and off the mount: the reference re-runs the attach from the `UNIT_FIELD_MOUNTDISPLAYID` watch handler, and benilla picked the slot once at build, relying on a teardown the reseat rework no longer does (B199), so the seat rode at the WRONG slot; `overhead_slot` is now the one definition of the pick (`0x6074c0`) and the marker rebuilds when it moves (1871); and the chat menu's kit was ours overwriting the reference's — `UIMenu.xml` loads earlier on the chain and our `ChatFrame.xml` carried the same kit under the SAME ten names, faithful enough that deleting it changes nothing; nothing ever opened the chat menu, and now a test does; an event census holds the gap between registered and fired events at exactly eighteen, failing on a surprise AND on a quietly gained producer — features not yet built, none a split pair (1869) Co-Authored-By: Claude Fable 5.1 --- crates/benilla-app/assets/ui/ChatFrame.xml | 111 +------ .../benilla-app/assets/ui/QuestLogFrame.xml | 89 ++++-- crates/benilla-app/assets/ui/benilla.toc | 9 +- crates/benilla-app/src/capture/fixtures.rs | 7 +- crates/benilla-app/src/creature_anim.rs | 192 ++++++++++++ .../benilla-app/src/creature_anim/driver.rs | 25 +- .../src/creature_anim/driver/mode.rs | 5 +- .../src/creature_anim/driver/tests.rs | 284 ++++++++++++++++++ .../benilla-app/src/creature_anim/select.rs | 19 ++ .../benilla-app/src/creature_anim/sheath.rs | 11 +- crates/benilla-app/src/entities.rs | 77 ++++- .../benilla-app/src/entities/equipment/mod.rs | 51 ++++ .../src/entities/equipment/resolve.rs | 281 ++++++++++++++++- crates/benilla-app/src/player/posture.rs | 8 +- crates/benilla-app/src/quest_markers/mod.rs | 169 ++++++++--- crates/benilla-app/src/sound/combat.rs | 26 +- crates/benilla-app/src/sound/death_thud.rs | 70 ++++- crates/benilla-app/src/ui_action/usable.rs | 88 +++++- .../benilla-app/src/ui_action/weapon_icon.rs | 159 +++++++++- crates/benilla-app/src/ui_quest_log.rs | 43 ++- .../benilla-app/src/ui_script/chat_tests.rs | 63 ++++ .../src/ui_script/questlog_tests.rs | 78 +++++ .../benilla-app/src/ui_script/reference_ui.rs | 158 +++++++++- .../src/bin/benilla-extract/scan/mod.rs | 2 +- .../src/bin/benilla-extract/thudcensus.rs | 87 ++++++ crates/benilla-formats/src/dbc.rs | 41 +++ crates/benilla-formats/src/lib.rs | 2 + crates/benilla-formats/src/quest_headers.rs | 40 +-- crates/benilla-formats/src/quest_info.rs | 84 ++++++ .../benilla-formats/tests/quest_tag_names.rs | 42 +++ crates/benilla-ui/src/script/quest_log.rs | 108 ++++++- 31 files changed, 2146 insertions(+), 283 deletions(-) create mode 100644 crates/benilla-formats/src/quest_info.rs create mode 100644 crates/benilla-formats/tests/quest_tag_names.rs diff --git a/crates/benilla-app/assets/ui/ChatFrame.xml b/crates/benilla-app/assets/ui/ChatFrame.xml index dbc4139a..feb5d139 100644 --- a/crates/benilla-app/assets/ui/ChatFrame.xml +++ b/crates/benilla-app/assets/ui/ChatFrame.xml @@ -1204,112 +1204,11 @@ end end - -- ── BenillaUIMenu: the 1.12 UIMenu kit, re-expressed (ref UIMenu.lua — the chat menu's - -- row engine: fixed 16px rows in a tooltip-backdrop box, a 2 s no-hover timeout that - -- walks up parentMenu chains, nested submenus opened on row hover). Constants quoted. - -- Scoped here because the chat menu is its only consumer; a second consumer graduates - -- it to its own file. - BENILLA_UIMENU_BUTTON_HEIGHT = 16 - BENILLA_UIMENU_BUTTON_WIDTH = 104 - BENILLA_UIMENU_BORDER_HEIGHT = 12 - BENILLA_UIMENU_BORDER_WIDTH = 12 - BENILLA_UIMENU_NUMBUTTONS = 32 - BENILLA_UIMENU_TIMEOUT = 2.0 - - function UIMenu_Initialize() - local menu = this - menu.numButtons = 0 - menu.subMenu = "" - local name = menu:GetName() - for i = 1, BENILLA_UIMENU_NUMBUTTONS do - local button = getglobal(name .. "Button" .. i) - button:SetWidth(BENILLA_UIMENU_BUTTON_WIDTH) - button:SetHeight(BENILLA_UIMENU_BUTTON_HEIGHT) - button:Hide() - getglobal(button:GetName() .. "ShortcutText"):Hide() - end - menu:SetWidth(BENILLA_UIMENU_BUTTON_WIDTH + BENILLA_UIMENU_BORDER_WIDTH * 2) - menu:SetHeight(BENILLA_UIMENU_BORDER_HEIGHT * 2) - end - - -- Rows past the button pool are dropped (the ref message()s; nothing we author overflows). - function UIMenu_AddButton(text, shortcut, func, nested) - local menu = this - local id = menu.numButtons + 1 - if id > BENILLA_UIMENU_NUMBUTTONS then return end - menu.numButtons = id - local button = getglobal(menu:GetName() .. "Button" .. id) - if text then button:SetText(text) end - button.func = func - button.nested = nested - button:Show() - if shortcut then - local s = getglobal(button:GetName() .. "ShortcutText") - s:SetText(shortcut) - s:Show() - end - menu:SetHeight(id * BENILLA_UIMENU_BUTTON_HEIGHT + BENILLA_UIMENU_BORDER_HEIGHT * 2) - end - - function UIMenu_OnShow() - this.timeleft = BENILLA_UIMENU_TIMEOUT - this.counting = 1 - end - - function UIMenu_OnUpdate(elapsed) - if this.counting == 1 then - local timeleft = (this.timeleft or BENILLA_UIMENU_TIMEOUT) - elapsed - if timeleft <= 0 then - this:Hide() - return - end - this.timeleft = timeleft - end - end - - function UIMenu_StartCounting(menu) - menu.counting = 1 - if menu.parentMenu then UIMenu_StartCounting(getglobal(menu.parentMenu)) end - end - - function UIMenu_StopCounting(menu) - menu.counting = 0 - menu.timeleft = BENILLA_UIMENU_TIMEOUT - if menu.parentMenu then UIMenu_StopCounting(getglobal(menu.parentMenu)) end - end - - function UIMenuButton_OnLoad() - this:SetPoint("TOP", this:GetParent():GetName(), "TOP", 0, - -((this:GetID() - 1) * BENILLA_UIMENU_BUTTON_HEIGHT) - BENILLA_UIMENU_BORDER_HEIGHT) - end - - function UIMenuButton_OnClick() - if this.func then this.func() end - this:GetParent():Hide() - PlaySound("UChatScrollButton") - end - - function UIMenuButton_OnEnter() - local nested = this.nested - if nested then - local menu = getglobal(nested) - if not menu:IsVisible() then - local oldMenu = getglobal(this:GetParent().subMenu or "") - if oldMenu then oldMenu:Hide() end - this:GetParent().subMenu = nested - menu:SetPoint("BOTTOMLEFT", this:GetName(), "BOTTOMRIGHT", 10, -12) - menu:Show() - end - UIMenu_StopCounting(menu) - else - UIMenu_StopCounting(this:GetParent()) - end - end - - function UIMenuButton_OnLeave() - UIMenu_StartCounting(this:GetParent()) - if this.nested then UIMenu_StartCounting(getglobal(this.nested)) end - end + -- The UIMenu kit — the chat menu's row engine — is the REFERENCE's, off the player's + -- chain: `Interface\FrameXML\UIMenu.xml` at manifest line 194 sources `UIMenu.lua` at its + -- own line 3, and this file loads at 939. Our transcription kept the reference's names, so + -- it OVERWROTE the chain's ten verbs from the day that entry landed — the manifest header + -- claimed the swap "retires the transcription", and only the XML half had been (1869). -- ── The chat menu rows (ref ChatMenu_* in ChatFrame.lua, adapted): every type row -- prefixes the box with the type's slash and lets the LIVE PARSE convert — the ref's diff --git a/crates/benilla-app/assets/ui/QuestLogFrame.xml b/crates/benilla-app/assets/ui/QuestLogFrame.xml index 3f73ffd1..5ac0a115 100644 --- a/crates/benilla-app/assets/ui/QuestLogFrame.xml +++ b/crates/benilla-app/assets/ui/QuestLogFrame.xml @@ -205,12 +205,10 @@ -- -- The insert (l.480) is `gsub(this:GetText(), " *(.*)", "%1")` — the row LABEL with its -- leading indent trimmed, i.e. the bare quest name; note it is plain text, NOT a - -- hyperlink (1.12 has no |Hquest: link). We read `GetQuestLogTitle(index)` instead of - -- the label, because our label is not the ref's: this window appends the Complete/Failed - -- tag onto the ONE title string (the v1 simplification in BenillaQuestLogList_Update, - -- where the ref keeps a separate right-justified $parentTag FontString), so the ref's - -- gsub over our label would post "Quest 1 (Complete)". The API title is exactly the - -- string the ref's own trim yields — same result, different expression. `ChatFrameEditBox + -- hyperlink (1.12 has no |Hquest: link). We read `GetQuestLogTitle(index)` rather than + -- gsub'ing the label: the API title IS the string the ref's trim yields (our label is + -- that title behind the same two-space indent, and in both windows the tag lives on its + -- own FontString, not on the label) — same result, one fewer expression. `ChatFrameEditBox -- and` guards a window test that hasn't loaded ChatFrame.xml (BenillaPaperDollSlot_OnClick's -- precedent). -- @@ -356,24 +354,52 @@ text = " " .. text button:SetNormalTexture("") button.foldHighlight:SetTexture("") - if isComplete == 1 then - text = text .. " (Complete)" - elseif isComplete == -1 then - text = text .. " (Failed)" - end end - -- pin §2: the ref recolors via a SEPARATE right-side tag FontString; one combined - -- string (title + tag suffix, above) is our v1 simplification — see this widget's - -- own template comment. + -- The TAG, ref l.187-195 verbatim, INCLUDING the precedence: a finished quest's + -- Failed/Complete word OVERWRITES the engine's tag — one row never shows both, + -- and the state word wins. `questTag` is the bare word from the engine + -- (`Elite`, `Dungeon`, `Raid`, `PvP`, `Life`, `World Event`, `Legendary` — + -- QuestInfo.dbc keyed by the quest template's Type); the parentheses are the + -- ref's, added here. Headers arrive with a nil tag and a nil isComplete, so + -- this clears their string without a branch of its own (ref l.187 is likewise + -- outside the isHeader arm). + -- + -- KNOWN DEVIATION, and the only one left on this row: the ref also SHRINKS the + -- title to keep it out from under the tag (l.196-203, `275 - 15 - tagWidth`, + -- re-seating the watch check off the capped width at l.205-213). We don't cap: + -- an explicit width on a FontString is a WRAP width here + -- (`RegionData::measure_key`), so capping a 33+ character title would wrap it + -- onto a second line inside a 16px row — worse than the overlap it prevents. + -- Our check needs no reseat either: it anchors off the title's own ink + -- (template comment), which is what the ref's dummy-measure arithmetic + -- reconstructs. + if isComplete and isComplete < 0 then + tag = FAILED + elseif isComplete and isComplete > 0 then + tag = COMPLETE + end + if tag then + button.tag:SetText("(" .. tag .. ")") + else + button.tag:SetText("") + end button.text:SetText(text) button.questIndex = index + -- Ref l.239-246: the tag takes the SAME difficulty colour as the title (the + -- ref colours title, tag and the group-mate count together). What we don't do + -- is the ref's SELECTED-row recolour (l.253-256/l.336: highlight-white on both + -- title and tag) — our title can't take it either, since the ref gets it from + -- LockHighlight on a ButtonText and ours is a plain FontString. Whole-row, not + -- tag-only, so the two stay in step; the selection already reads off + -- QuestLogHighlightFrame. + local c if isHeader then - local c = QuestDifficultyColor["header"] - button.text:SetTextColor(c.r, c.g, c.b) + c = QuestDifficultyColor["header"] else - local c = GetDifficultyColor(level) - button.text:SetTextColor(c.r, c.g, c.b) + c = GetDifficultyColor(level) end + button.text:SetTextColor(c.r, c.g, c.b) + button.tag:SetTextColor(c.r, c.g, c.b) -- pin §2's $parentCheck: the per-row watch checkbox. SetTexture(path)/ -- SetTexture(nil) as the show/hide toggle — the icon-slot blanking pattern -- (the icon-clearing every slot repaint does). (Regions DO have Show/Hide these days — @@ -1310,10 +1336,9 @@ One list row (pin §2's QuestLogTitleButtonTemplate, QuestLogFrame.xml:4-107, ~300×16). Text lives on a NAMED `$parentText` FontString (not the auto ButtonText slot — same convention `GossipFrame.xml`'s `GossipTitleButtonTemplate` uses for its `$parentLabel`), so it can carry - an explicit LEFT justify. v1 simplification: the ref's Complete/Failed tag rides a SEPARATE - right-justified `$parentTag` FontString the client shrinks the title to make room for; we - append the tag straight onto the one title string instead (`BenillaQuestLogList_Update`) — - simpler, no dynamic width math, costs only the tag not being right-flush. + an explicit LEFT justify. The tag rides its own right-flush `$parentTag` string, as the + ref's does — see that FontString's own comment, and `BenillaQuestLogList_Update` for the + one thing the ref does with it that we don't (the title width cap). State art is now ref-verbatim (QuestLogTitleButtonTemplate, ref QuestLogFrame.xml:68-107): the 16x16 plus/minus fold icon slot at LEFT+3 is the button's NormalTexture (the Lua swaps Plus/Minus per header fold state and CLEARS it on quest rows — SetNormalTexture("")), the @@ -1333,10 +1358,10 @@ and with the ref's text inset of 20 that nets to `ink end + 4`. We get the same geometry from the anchor graph instead of the Lua dummy-measure hack: the title FontString is width-less (its resolved rect hugs its host-measured ink — the measure round-trip), and - the check anchors LEFT → the title's RIGHT at +4. Known deviation, unchanged: the ref - shrinks the title width to fit the right-justified tag (its `textWidth` cap); our combined - title+tag string means a "(Complete)" row seats the check after the tag, not between - title and tag. + the check anchors LEFT → the title's RIGHT at +4. That IS the ref's seat: its + `textWidth+24` off the row's left, with `textWidth` the un-capped title ink, is ink-end + 4 + given the 20-unit text inset — the ref only computes it because its own `GetWidth` returns + whatever width it last capped the title to. -->