fbf01c486e3ce3d660d0bfc781f5a38e00d3b845
1723
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
fbf01c486e |
Eleven data goldens, and the gesture scan's order now matters
First batch of the benilla-formats goldens, all of them the realm's tables being bigger or retuned rather than anything misread. The safety check came first and is worth recording: **every `*_FIELDS` constant in the crate still matches its live DBC header** — Spell 173, AreaTable 25, CreatureFamily 18, Talent 21, Lock 33 and the rest — so no decoder is reading a moved layout, which is the only way one of these counts could have been a defect rather than data. Added `examples/dbc_shapes` to read that off the chain in one go. Counts: `AnimationData` 208 → 227, `AreaPOI` 339 → 483, `AreaTrigger` 432 → 530 (172/144 across the two continents, from 133/121), `AreaTable` 1081 → 1481, `GameTips` 74 → 95, cinematics 10 → 13 sequences and cameras both. Three that are more than a count: - **Rested experience is ×1.5 here, not ×2.** `Exhaustion.dbc` row 1 is where the client reads the multiplier it shows, and the realm retuned it. Every vanilla guide quotes 2.0. - **The help window offers two ticket categories, not ten** — "report someone's behavior" and "an issue with my character" replace 1.12's Stuck/Guild/Item/… list. - **Warlock minion pet icons are blank.** `CreatureFamily` rows 15-19, 23, 28, 29 carry no icon where 1.12 gave each one a path, so `GetPetIcon()` answers nil for a minion and an addon that concatenates it unguarded gets nothing. Not a column slip: every hunter family in the same column answers a real path. The realm also backported the TBC families (Dragonhawk, Ravager, Warp Stalker, Sporebat, Nether Ray, Serpent), added four of its own (Fox, Plagued, Horse, Moth) and filled two minion ids, Incubus 18 and Felguard 29 — 35 families where 5875 had 23. And `Emotes.dbc`'s gesture scan, which was pinned on a premise that has stopped holding: 1.12 had one row per gesture bit, so the walk order could not matter. The realm's file has three rows carrying `0x8` and a row (424) carrying `0x8|0x10|0x20`, so it matters now — and **our scan is already right**, which the disassembly settles rather than luck. `0x603a60` walks the table backwards (`imul $0x1c` to the end, `sub $0x1c` per step) and overwrites each slot, so the earliest record survives; and its bit test is an if/else-if chain, so one row fills at most the slot of its first set bit. Ours walks forward keeping the first, with `position` for the chain — the same table. The test now asserts that rule, re-derived from the file, instead of counting carriers. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn |
||
|
|
9df4b6abd8 |
Read the client's key enum out of WoW.exe — the fact KEY_DOWN was blocked on
`KEY_DOWN`/`KEY_UP` were the last two nampower events left unfired, and the blocker was never effort: it was the client's numeric `KEY_*` enum. The DLL's own strings publish four values (`KEY_NONE=-1`, `KEY_SHIFT=0`, `KEY_A=65`, `KEY_ESCAPE=512`, `KEY_F1=768`) — enough to see three banks and not enough to name a key past the first in any of them, and a guessed code is worse than no event, since an addon switching on `arg1` would silently never match. It is in the binary, in the name parser `SetBinding` resolves a key string through (`0x4886f0`), and the arithmetic is in the instructions rather than inferred: - **ASCII, bank 0** — a printable key is its own byte (`movsbl (%esi)`), which is why `KEY_A` is 65. - **Numpad, `0x100`** — the digit character plus `0xd0` (`add $0xd0,%edi` at `0x48875b`), so `NUMPAD0 = 0x100`..`NUMPAD9 = 0x109`, then `+ - * / .` at `0x10a`..`0x10e`, each reached by its own character compare. - **Specials, `0x200`** — contiguous from `ESCAPE = 0x200` through `PAGEDOWN = 0x20d`, then a hole, `NUMLOCK = 0x20f`, two more holes, `PRINTSCREEN = 0x212`. Every one is a `mov $imm,%edi` in the chain at `0x4888a0`..`0x488a20`; the holes are codes this parser assigns to no name. - **Function keys, `0x300`** — `0x2ff + n` (`add $0x2ff,%edi`), so `F1 = 768` as published, and `NUMPADEQUALS` sits past them at `0x30c`, from the parser's own `=` arm. Transcribed into `bindings::chord::key_code`, which composes with the existing `key_token`, with a test per bank and the three published values as its anchors. `CAPSLOCK`, `SPACE` and the mouse-wheel names are in the client's name table at `0x846c20` but take no code from this parser, so they answer `None` rather than a number nobody can check — and a second test walks every token `key_token` can produce and fails on any that has neither a code nor a written-down reason. No caller yet, and that is the honest state: what is left for the events is a **producer** on the keyboard path, which is nampower's hook and the same hook the press-interception queue uses, so it belongs with that decision rather than ahead of it. `docs/ADDON_COMPAT.md` now says so instead of naming the enum as the blocker. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn |
||
|
|
a9ed091ebe |
texture miss: '' was three setters storing an empty path
`Texture:SetTexture("")` already cleared, and so did a button's slot setters — the reference treats
nil and `""` alike on the path arm (`0x770200`). But `SetStatusBarTexture`, `SetThumbTexture` and
the two `ColorSelect` setters stored the empty string, so the region reached the extractor as
`path: Some("")`, and the renderer asked the patch chain for a nameless file.
What that cost is less than the warning suggested, and the comments say so rather than the opposite:
both the decode and the warning are cached per key, so it was one chain lock and two candidate
reads, once. The damage was the line itself — a defect-shaped warning with nothing under it, in a
client where a real `texture miss` is worth reading. pfUI blanks textures this way in a dozen
places, which is why it showed up at all.
Measured both ways: the new test reports `[""]` with any one guard removed, and three UI captures
(`ui-char`, `ui-options`, `ui-actionbar`) now log zero `texture miss` of any kind. His own
2026-10-28 log has the line once, at 20.7 s.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
|
||
|
|
266af6eb26 |
The realm's flight network is bigger than 1.12's, and a second capture point
All of today's update, measured rather than assumed. `TaxiNodes.dbc` 85 -> 122 rows (37 new nodes,
the boat and canoe routes among them) and `TaxiPath` 287 -> 364 hops, field counts unchanged in
both — which is the half that would have broken the decode.
What moved with them:
- **Stormwind's node is "Stormwind, Elwynn Forest"**, where stock clipped it to "Elwynn".
- **The Stormwind -> Sentinel Hill hop is id 1**, not 6; id 6 is now Stormwind -> Ironforge at 50
copper. The 110-copper fare did not move.
- **Path 302 is 23 nodes and stays on Eastern Kingdoms**, where 1.12's was 36 and crossed. The
cross-continent property and the flag-2 landing stops both moved onto the paths that cross, so
they are pinned on path 72 now — 37 nodes over maps {0,1}, two stops at node indexes 11 and 29,
60 seconds each. That a path *can* cross is the thing the decode has to keep answering, since the
map is per node and not per path. 302 and 285 are asserted as the straight runs they now are.
- **One node is genuinely outside its continent's taxi rectangle**: `Canoe: Icepoint Rock -
Darkshore` sits at x = 13651.5 where Kalimdor's `WorldMapContinent.dbc` box ends at 12470, so it
projects 4.85% below the map. `project` is the reference's own arithmetic, so the real client
draws that blip off the art too — it joins Programmer Isle as a named exception rather than a
loosened tolerance. The other 121 rows are inside.
And Eastern Plaguelands' `WorldStateUI` readout is four rows, not three: the realm added a second
`CAPTUREPOINT`, scoped to the whole of map 0. Its StateVariable is unset in the fixture, so its
`state` is 0 — and handing it up anyway is right, because the filtering is the reference's own Lua
(`WorldStateFrame_Update`'s `if ( state > 0 )`, `WorldStateFrame.lua:41`). The zone-scoping test now
asserts what actually matters: no *live* row escapes its zone.
All seven gates green.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
|
||
|
|
3a0882d4d0 |
The realm's group-finder keybinding, which arrived with today's update
`Bindings.xml:637-639` declares `TOGGLEGROUPFINDER`, `GlobalStrings.lua:442` labels it "Toggle Group Finder", and its body is a plain call into the realm's own group finder (`LFT_Toggle`, `LFT\LFT.lua:1097`) which `benilla.toc` already loads — so there was nothing engine-side to build, only the row to carry. Unbound by default, because the realm ships no default key for it. That is 229 live bindings where stock 1.12.1 has 228, and both halves of the binding gate now say so. It is also the one thing the realm-update checklist missed this morning: I counted toc rows, FrameXML digests, DBC shapes and census results, and never counted `Bindings.xml` rows. Worth a line on that checklist. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn |
||
|
|
792baf9859 |
The reader is pushable now, and a letter covers rather than evicts
The last two UI integration files, both resting on `UIPanelWindows` rows the realm changed.
`ItemTextFrame` is `{ area = "left", pushable = 5 }` where 1.12's row was `pushable = 0`. That made
the reader and a quest giver's gossip left-slot rivals which replaced each other and ended each
other's session on the way out; pushable, they share the screen — one takes left, the other slides
to centre, in either order, and neither `CloseItemText` nor `CloseGossip` is queued. The test is
renamed for what it now proves. Its page bodies also pick up the realm's own three trailing
newlines on both arms of the creator fork (`ItemTextFrame.lua:41-46`), where 1.12 wrote two.
`OpenMailFrame` has no `UIPanelWindows` row at all, so `ShowUIPanel` takes its first branch — a
bare `frame:Show()` that claims no slot (`UIParent.lua:920-923`) — and a letter therefore *covers*
the centre occupant instead of evicting it. The eviction runs one way only: the letter is a
`UIChildWindows` member, so the next panel to seat anywhere hides it. And it has to **arrive** to
do that: `ShowUIPanel` returns on its first line for a frame already shown, so asking for the sheet
again while it is up reaches no slot and hides nothing.
Workspace: 191 red → 134, with zero regressions (`comm` against the pre-session list). Every one of
the 57 was in `ui_script` or these two files, and that whole surface is now green: 1113 `ui_script`
tests, `item_text_frame`, `mail_frame`, `message_keys`, `cover_input_wall`, `character_stats`. What
is left is the benilla-formats data goldens, which are documented separately and were red before.
All seven gates green.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
|
||
|
|
3d6306f828 |
Five stock options checkboxes were inert, and GetCVar raised where it should answer
Two product fixes behind the last three red UI tests.
**`GetCVar`/`GetCVarDefault` no longer raise on a nameless ask.** The realm's own options window
depends on it: the checkbox loop calls `GetCVar(value.cvar)` for every row in its table
(`UIOptionsFrame.lua:265`), and the rows driven by a saved variable carry no `cvar` at all — so a
nil reaches the verb every time the window loads. The reference reads the argument with
`lua_tostring`, which answers NULL for anything but a string or a number, and a NULL name matches
no row, so the push is nil. `SetCVar` is deliberately left raising, and that is not a guess:
`0x488c10` checks `lua_isstring(1)` and falls into
`luaL_error("Usage: SetCVar(\"cvar\", value [, \"scriptCvar\")")`, with `0x6f4940` being `luaL_error`
itself (`where` + `pushvfstring` + `concat 2` + `lua_error`). A write with no name is a mistake; a
read with no name is a miss.
**The action-bar block now loads above the stock interface-options window**, which is the inverse
of 1.12's order and has to be. Stock patched the five multibar rows of `UIOptionsFrameCheckButtons`
from `MultiActionBarFrame_OnLoad`, under a comment calling itself a "Hack to get around load order
dependencies" — that hack is what let the window load first. The realm gutted the function
(`MultiActionBars.lua:7-12`) and wrote the readers into the table literal instead
(`UIOptionsFrame.lua:58-62`, `func = MultiBar1_IsVisible`), where they capture whatever the globals
are at the window's own load. Loaded above the bars, all five captured nil, those five checkboxes
read and wrote nothing, and `UIOptionsFrame_Save` fell through to `SetCVar(nil, ...)` and raised —
so pressing Okay abandoned the rest of the save. `reference_ui`'s shadowing gate is what kept this
honest: the first arrangement I tried let the stock window overwrite our own `UpdateNameplates`.
Also: the stock video-options window is gone as 1.12 knew it. The realm rewrote `OptionsFrame.xml`
into a searchable category panel — `OptionsFrameCategoryList*`, two scroll frames, a search box,
twenty result rows — keeping only the frame, its header and the three footer buttons. Gone with the
old page: `OptionsFrameDisplay`, `WorldAppearance`, `Brightness`, `PixelShaders`, `Miscellaneous`,
the three resolution/refresh/multisample dropdowns, `OptionsFrameSlider1..9` and
`OptionsFrameCheckButton1..18`. Worth stating rather than asserting around: **pfUI's options-video
skin walks those names and finds none of them**, so on this realm it skins nothing there.
And `MULTI_ACTION_BAR_STAND_INS` is retired — it existed to pre-create the rows that hack needed,
and nothing indexes the table before `UIOptionsFrame.lua` declares it.
All seven gates green. `ui_script` is 1113 green, 0 red.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
|
||
|
|
c5544a7ea3 |
A double load, a stand-in that won a race, and the unified ActionButtonUp
Three of these are the harness telling the truth about load order, which is worth more than the tests they unblocked: - **The load ledger now guards a file a kit names twice**, not just a prerequisite. A second load builds a second frame under the same name, the global keeps one of them, and the reference's own `parent == MultiBarRight` (`ActionButton.lua:363`) then reads false for a button whose parent really is the bar. The toc names each file once; a kit should too. - **`MultiActionBars.xml` belongs before `BonusActionBarFrame.xml`** (toc rows 48 and 84), and the order is load-bearing: `ShapeshiftBar_OnLoad` runs `UIParent_ManageFramePositions` at load, whose stand-in pass seats a hidden parentless frame for every managed name still absent — `MultiBarRight` among them — and a frame's publish to `_G` never overwrites (`0x701bd0`). The stand-in kept the global for good and the anchor fork read the wrong side. - **`paperdollframe.xml` must not name `UIDropDownMenu.lua`** beside its `.xml`. The XML brings its own `<Script file=>`; naming the chunk again **re-ran** it, and the file declares `UIDROPDOWNMENU_DEFAULT_TEXT_HEIGHT = nil` at scope while `DropDownList1`'s OnLoad is what fills it from Button1's font. The second run put the nil back, and then every dropdown row went to `SetFont(STANDARD_TEXT_FONT, nil)` — a usage refusal that took out a whole file's OnLoads, seven at a time. The ledger could not catch it: a `<Script file=>` chunk never passes through `load_entry`. `uidropdownmenu.xml` also gained `Fonts.xml` as a prerequisite for the same height. - **`ActionButtonUp` is one body now** (`ActionButton.lua:27-46`): it picks the bonus or the normal button and passes the caller's own `onSelf` through. Stock's separate BONUS branch handed `UseAction` a literal 0, so ALT-1 in a stance drove the bonus page without self-casting. Gone. - The bonus bar is driven by an offset plus `UPDATE_BONUS_ACTIONBAR`, not a bare `Show()`: the event is the only thing that runs `ActionButton_Update` on that row, and a button registers `ACTIONBAR_UPDATE_COOLDOWN` only inside its `HasAction` arm — while the event's own handler hides the bar again when the offset is 0, so both halves are needed. - And `tooltip_compare`'s player needed the name `CharacterFrame_OnShow` gsubs unguarded. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn |
||
|
|
9dde1a80e5 |
The keyring grows in rows, the rank is art, and two fixtures I left behind
- **Keyring**: `GetKeyRingSize` has no level term at all — it scans for the highest filled slot
and rounds up to a multiple of four (`ContainerFrame.lua:817-844`), expanding only when the key
count has caught up with that slot. So one key is one row of four, four keys in four slots grow
to eight rather than refusing a fifth, and full is all 32. The realm also **fixed** 1.12's silent
refusal: it renamed the message to `NO_EMPTY_KEYRING_SLOTS_ERROR` and defined it
("Your keyring is full."), where stock printed a name that did not exist.
- **Bag tooltip**: the item-button `OnUpdate` accumulates toward `TOOLTIP_UPDATE_TIME` instead of
counting down from it (`:694-704`), and reads the threshold before adding — so a landing template
repaints an open plate two frames after it crosses a fifth of a second, not on the next frame.
- **Raid grid**: the rank is a `$parentRankIcon` Texture now (`Blizzard_RaidUI.xml:49`), set to
`RAID_LEADER_ICON`/`RAID_ASSISTANT_ICON` (`:100`, `:155-161`). The `$parentRank` FontString that
held "(L)"/"(A)" is still declared and never written.
- **Ready check**: the realm moved it into its own group UI (`Turtle_GroupUI.lua:259`) and
`RaidFrameReadyCheckButton`'s OnClick calls `GroupFrame_InitiateReadyCheck` by name, so
`load_social_ui` seats that file; without it the button clicks into a nil.
And two fixtures that were mine to fix and I missed: `quest_tests`' detail and reward panels still
carried `item_id: 0`, which `3f2982445` taught the counters to take-while out of existence. Same
correction as the `benilla-ui` fixture in that commit, with the reason written where it is read.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
|
||
|
|
34097b6164 |
The panel registry, the map's own row, and a test that raced
`UIParent.lua`'s registry moved under three tests at once:
- `ItemTextFrame` is `pushable = 5`, so the reader shares the screen instead of replacing what is
there; `QuestLogFrame` is `area = "doublewide"` with `width = 690`, not a left-slot window, so a
`whileDead` open seats at the doublewide slot and leaves the left one empty; and
`UIChildWindows` has five rows, the realm having added `ReputationDetailFrame`.
- `SetUIPanel` runs `CloseChildWindows()` for **any** slot it seats (`UIParent.lua:792-796`), not
just the centre one, so a window merely pushed across also puts the open letter away. The test
that pinned the stock trigger now watches both halves go.
- `user_placed_layouts()` is not empty after a manifest load, and should not be: the realm's own
movable windows stamp themselves in their OnLoad — `PlayerFrame.xml:488-489` is
`SetMovable(1) SetUserPlaced(true)`, the same feature as the `MOVE`/`MOVE_RESET` rows in their
right-click menus — and `TargetFrame`, the Turtle shop, transmog and guild bank do likewise. The
map test now asserts about its own row rather than about the file being empty.
- The blip tooltip calls `UnitName("player")` (`WorldMapFrame.lua:660`), which reads the player
record; `load_default_ui` seats a placeholder one so the realm's files have a race and class at
load, so a test that wants a named player names the record, after the manifest.
And one real race, not a realm change: `a_binding_table_resolves_whatever_case_the_package_spelled_it_in`
moves `BENILLA_HOME` and `WOW_CAPTURE` without taking `ENV_LOCK`. It was the only env-moving test
in that module missing it, and the collision shows up as a *neighbour* reading the wrong roster —
which is how it surfaced here, two addons tests failing in the full run and passing alone.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
|
||
|
|
b9a58c9806 |
Seven more: the realm rewired what these windows actually do
- **Buff frame**: 32 helpful slots and 16 harmful (`BuffFrame.xml`), not 16 of each, so the first debuff is `BuffButton32`. `BuffButton16` is a helpful slot here and stays down with one buff up. - **Pet frame**: its `PetFrameDebuffN` row really is debuffs now — `RefreshBuffs(this, 0, "pet")` (`PetFrame.lua:38`, `:57`) picks `UnitDebuff`, where stock passed `1` and showed the pet's buffs under those names. The fixture seeds harmful auras. - **Pet paper doll**: the diet icon is the realm's own `PetStable-DietIcon` at 32x32, and it was moved out of the `<PlayerModel>` into a `frameLevel="5"` frame of its own (`PetPaperDollFrame.xml:245-258`), so it draws over the scene by level rather than by the model's ARTWORK callback draining last — the ordering this test used to pin. - **Merchant**: both mouse buttons share one shift arm now (`MerchantFrame.lua:304-338` and `:345-375` are the same body), and it opens the stack splitter for a bundle row too, stepping by the bundle size. The fork inside is only where `maxStack` comes from: `GetItemInfo` off the link for a bundle (so the row needs its template cached and a link to parse the id out of), `GetMerchantItemMaxStack` for a single. - **Trainer**: after a purchase the realm re-selects `ClassTrainerFrame.selectedService` (`Blizzard_TrainerUI.lua:75-91`) instead of leaving the pane blank, so the pane and the highlight follow the row that took the learned one's seat, and the engine's selection index comes back off the hidden tail. - **Game menu**: a `"full"` frame is exempt from both open gates (`UIParent.lua:927`, `:951` each spell out `frameArea ~= "full"`) and its arm runs `CloseAllWindows()` before taking the slot (`:962-966`), so asking for the map over the game menu closes the menu and opens the map. - A third prereq row, and a latent raise named: `WorldMapFrame.xml` needs `Overrides.lua`'s `CONTINENTS_LENGTH = 2` (toc row 13 against 85), because the continent and zone dropdown text helpers compare against it unguarded (`WorldMapFrame.lua:440`, `:457`). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn |
||
|
|
04a3f68009 |
Nine reds where the realm changed the data under a transcribed assertion
None of these were defects; each is a value, a row index or a semantic the realm moved and the test still asserted stock 1.12's: - `RAID_CLASS_COLORS.SHAMAN` is the later blue `|cff0070de` (`Fonts.xml:50`), not a copy of PALADIN's pink — 1.12 reused the row and the realm did not. - `ChatTypeInfo` has 106 keys, the extra being the realm's `HARDCORE`, and ten of them are sticky rather than five: it marks `CHANNEL`, `OFFICER`, `RAID_WARNING` and `WHISPER` too. Sticky is what the edit box keeps between sends, so that list is typing behaviour a player feels. - `ChatMenu`'s Macro row is eleventh: `HARDCORE_MESSAGE` went in fourth (`ChatFrame.lua:2371`). - A friend row is coloured three ways (`FriendsFrame.lua:186-197`) — name and class from `RAID_CLASS_COLORS[TW_CLASS_TOKEN[class]]`, level from `GetDifficultyColor`. `TW_CLASS_TOKEN` lives in the realm's `Globals.lua`, so that row joins `SOCIAL_UI`; without it the window draws uncoloured, which is a different window than the client paints. - A guild row goes offline by **alpha 0.55** on all four columns (`FriendsFrame.lua:526`), keeping its class and difficulty inks, where 1.12 wrote a flat grey. - The minimap clock plate is four lines — `Clock`, then a Zone/Server/Local time triple (`GameTime.lua:50-56`) — not one bare time. - `ERR_TAXINOPATHS` uses a straight ASCII apostrophe; the test looked for U+2019 and so never matched a line that was being drawn correctly all along. - `DropDownList1/2`'s OnHide **wipes** `OPEN_DROPDOWNMENUS[n]` instead of dropping it (`UIDropDownMenu.xml:17`, `:28`), so the row is emptied in place and an addon's reference to it stays live. Asserted both halves. - `CONFIRM_PET_UNLEARN` gained an `OnUpdate` that hides it unless `CheckPetUntrainerDist()` answers (`StaticPopup.lua:1290-1294`), so the dialog needs that verb seated before the first tick. - The spellbook shows its Spellbook tab with no pet at all (the realm sets tab 1 up on both arms of `hasPetSpells`), and it **fixed** both pet-paging quirks this test had pinned as "DO NOT FIX": `SpellBook_GetSpellID`'s pet arm carries the page term and Next writes the pet book's own page. Also corrected a test of mine from the last batch: `IsEnabled()` answers the number 0, which is truthy in Lua, so it is compared and not tested — as the stock UI itself does. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn |
||
|
|
d49f8daad8 |
Six more menu reds: the realm's UnitPopup tables are not 1.12's
Every one asserted stock 1.12's row set. The realm's `UnitPopup.lua:75-80` adds `REPORT` and `IGNORE` to PLAYER/PARTY/FRIEND, its own `XP` row to SELF, and `MOVE`/`MOVE_RESET` (its movable unit frames) to SELF, PARTY and PLAYER — and it **dropped the `RESET_INSTANCES` gate**: nothing in its 51 interface Lua rows so much as names `CanShowResetInstances` (scanned, file by file), so that row is always there, solo included. Stock left Cancel alone in a solo SELF menu, which is why the menu used not to open at all. The row lists are now asserted whole and in order rather than by count and spot index, which is what made these read as "7 != 5" instead of naming the two rows that arrived. The raid-marks row is found by label, since its seat has moved twice now. Also `status_bar_text_paints_the_player_numerals_but_not_the_targets`, which blamed the `TextStatusBar` template for a leave that never hides the plate. The realm overrides that bar's own `<OnEnter>`/`<OnLeave>` in `TargetFrame.xml:280-298`: the enter shows its two HP strings and then borrows the parent's plate by hand, because a child that takes the mouse means `TargetFrame` never gets an enter of its own — and the leave hides only those two strings. The plate standing after the cursor leaves is the realm's asymmetry, not ours. Measured by wrapping the handler and watching `GameTooltip:IsShown()` on both sides of it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn |
||
|
|
3a89f825bf |
The realm ships its own ArenaFrame, so our stand-in only shadowed it
`CharacterFrameAdapters.xml` existed on one premise: "There is no `ArenaFrame.lua` or `.xml`
anywhere in the player's chain". That is no longer true — the realm's FrameXML carries
`Turtle_ArenaUI\Turtle_ArenaFrame.xml` (toc rows 106-110, which `benilla.toc` already loads at
270-274), and it defines `ArenaFrame` with `enableMouse="true"` and `id="5"`, sharing the PvP tab
rather than inventing a sixth. Our empty stub loaded 100 rows earlier and was overwritten, and
wherever it loaded last it replaced the realm's real arena window with a frame that has no
content. `frame_flag_gate` had both halves of that pinned as divergences (mouse false, id 6).
Twelve more red UI tests, each one a file the realm rewrote under an assertion written against
stock 1.12:
- `inspect_tests` (4): all four died in `InspectArenaFrame.xml`'s own OnLoad, which indexes
`_G[this:GetName().."Text"]` — a child of `ArenaTeamTemplate`, which lives in FrameXML and so is
always there live. The rotate buttons are `InspectModelFrame…`, not stock's `InspectModel…`.
- `character_tests` (4): `HonorLevelText` keeps its authored placeholder because the realm
commented that `SetText` out and hides the string instead (`PaperDollFrame.lua:126-127`);
`PaperDollFrame.xml` builds 77 frames, not 75, the two being the realm's player-titles dropdown
and its template's `$parentButton` (read off `FrameXML_Debug(1)`'s own trace, which also proved
each of the 77 is created exactly once — a grown file, not a double load);
`CHARACTERFRAME_SUBFRAMES` has six names; and one sound at load is the reference's own —
`UIDropDownMenu_Initialize` runs its init function on the spot and `UnitPopup_ShowMenu` ends in
`PlaySound("igMainMenuOpen")`.
- `loot_tests` (4): same load-time blip, drained and named; and the master-loot menu is rewritten
end to end — the party arm drops any candidate it cannot resolve to a party unit, the raid arm
groups by class with `hasArrow` submenus instead of by subgroup, and the event is now a toggle
whose memory `DropDownList1`'s own OnHide clears. It also needs `MAX_RAID_MEMBERS`, so the loot
window's prereq row names `RaidFrame.lua`.
- `dressup_tests` (2): `CharacterFrame_OnShow` gsubs `UnitPVPName("player")` unguarded, so the
fixture's player needs the name a real one always has.
The one accepted flag difference is now written down with its reason: our options window keeps
1.12's `toplevel` where the realm's rewrite of `OptionsFrame.xml` took `movable` instead.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
|
||
|
|
89556c991a |
A chorded action button must run both halves, and now a test says so
grom's slam macro sits on SHIFT-1, which his bindings file resolves to MULTIACTIONBAR1BUTTON1, and pfUI points both `MultiActionButtonDown` and `MultiActionButtonUp` at the same `pfActionButton` (`modules/actionbar.lua` :1516-17), which calls `frame:Click()`. So the reference evaluates that macro twice per press — once on the press, once on the release — and he reported seeing it evaluate once. That second evaluation is a second roll at a 0.2 s no-clip window, so it is worth a test of its own. The test holds a modifier across both edges, as a hand holding shift does, and asserts the down half on the press and the up half on the release. It passes: the registry's `Kind::EdgeUpDown` does run both legs through a chord. The harness says the same one layer up — `MultiActionButtonDown` and `MultiActionButtonUp` each reach `UseAction` exactly once with pfUI loaded — so whatever is costing him the second evaluation is not in either of these two layers, and this test is here to keep it that way while the search goes on. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn |
||
|
|
3f29824458 |
A padded reward row is counted out by the reference, not painted — match it
grom opened the quest I had photographed on the reference client and counted two reward buttons where we paint three. Yesterday I told him the reference draws the same empty button, citing the DETAILS publisher. That reading was of the wrong layer: `0x500ef0` really does copy `count` entries in with no test on the item id (`500f41`-`500f58`) and zero-fill the rest — but the drop happens one layer up, in the count. `GetNumQuestChoices` (`0x501f00`) is a wrapper over `0x5016c0`, which walks the six 36-byte records at `0xbdf020`, tests the record's item-id dword and `je`s out of the loop on the first zero. It is a take-while, not a filter: a zero row hides every row behind it too, and `GetNumQuestRewards` (`0x501ec0` → `0x5016a0`) is the same loop on the reward triple's own first dword. The record layout is pinned off `0x501700`, the shared row reader, which string-compares the Lua type argument against 'reward' / 'choice' / 'required' and reads `+0x00/+0x04/+0x08`, `+0x0c/+0x10/+0x14` and `+0x18/+0x1c/+0x20` respectively; the first dword of each triple is the item id — it is what gets pushed into the item-cache query at `0x55ba30` — and the third is the count. So the publisher's pad is `(id 0, display 0, count 1)`, which is exactly the `item 0 x1, displayId 0` the instrument printed on his session. `GetNumQuestItems` keeps `.len()`: its counter (`0x5016e0`, from `0x501f40`) tests `+0x1c`, the *display* id, not the item id at `+0x18` — the one counter of the three that reads the wrong dword of its own triple — and QuestItemView carries no display id to reproduce that from. The instrument stays, narrowed to the case that is genuinely ours: a wire row that names a real item and still resolves to neither a name nor an icon. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn |
||
|
|
bb9eb08438 |
The effect cut must spare an addon's own PlaySoundFile — it silenced SoftSpoken outright
My last commit made the `MasterSoundEffects` 0→1 toggle stop every playing effect, as
the reference does. It stopped too much: grom's next session carries ten
`MasterSoundEffects re-enabled` lines and he heard **none** of the ten voice lines that
produced them.
The mechanism is not subtle once stated. Every 1.12 voice-over addon spends the toggle
*and then plays its own file* — SoftSpoken's `SoundQueue:PlaySound` is literally
if Version.IsLegacyVanilla then
self:MuteGameDialogue(channel, clip.cutsGameDialogue) -- SetCVar 0; SetCVar 1
end
local willPlay = SoundUtils:PlaySound(clip, channel) -- PlaySoundFile(clip.path)
so a cut that reaches file playback takes the very thing the toggle was spent to make
room for. `stopped_by_reenable` now excludes `kit == 0`, which is `play_file`'s own
marker for the `PlaySoundFile` path.
The two exclusions have different standing and the doc says so. Music and ambience are
**derived** — the reference's `flags & 0x4000008` class, muted rather than stopped
(`0x7a65a0`). `kit == 0` is **measured**: which flag class a streamed file falls in is
unreversed, so this is the player's own observation standing in for a fact I do not have.
What it costs is the other half of SoftSpoken's use of the toggle: `StopKeeping` spends it
to stop its own clip before replaying one, and that does nothing again, so the
overlapping-replay symptom may return. That is a worse-to-better trade, not a fix, and it
is written where it lives.
The stop now logs the kits it cut and how many file channels it spared, because a bare
count cannot separate "the bark died, as intended" from "we cut the line the addon was
about to speak" — which is exactly the distinction this commit got wrong.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
|
||
|
|
a811f50e86 |
Name a quest reward row that resolves to nothing, rather than painting an empty button
grom photographed "The Tower of Azora" (quest 41747) offering Scroll of Stamina, Scroll of Spirit, and a third button with no icon and no name — an empty bordered box. A reward row's two halves come from independent sources: the name from the item template (`Items::template`, which queries the server on a cache miss) and the icon from `ItemDisplayInfo.dbc` through the wire's own `display_id`. A merely late template still paints an icon, and the feed rebuilds its snapshot every frame the panel is open, so a template arriving a second later would repaint. **Neither half answering** means the triple itself carries nothing this client can resolve. Ruled out from here, so the next person does not re-run them: the decode is a count-prefixed run of triples (vmangos' own layout), so a mis-stride would have broken rows 1 and 2 as well; `ItemDisplayCatalog` is a `HashMap<u32, _>` keyed by row id, so the realm's custom 68xxx display range is not an indexing trap (its `ItemDisplayInfo.dbc` has 45,254 rows and a max id of 69,176, and 68793/68797 are both present); and the two scrolls that *did* paint carry vanilla display ids (1093, 2616) while the plausible third choices carry custom ones, which is suggestive but not evidence. This lands as an instrument rather than a fix because **the reference does not filter these either**. Its DETAILS publisher `0x500ef0` zeroes the six choice slots at `0xbdf028`, copies exactly `count` entries in with no test on the item id (`500f41`-`500f58`), then zero-fills the rest — so a server padding its reward list makes the reference draw the same empty button. A padded wire and a template we failed to fetch look identical from the painted frame and want opposite fixes, so guessing between them is how this becomes a fourth wrong verdict. `warn_unresolved_reward_once` prints the item id, count and displayId once per distinct pair (the snapshot rebuilds per frame, so unguarded it would print at frame rate) and says which of the two it is: an `item_id` of 0 is a padded wire and not ours, a real id is. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn |
||
|
|
f8e93c4484 |
MasterSoundEffects 0→1 is how a 1.12 addon stops a sound — make it stop sounds
grom: SoftSpoken "played a voice line over and over", and its **Silence NPC Voices**
option did nothing although it was switched on. Both are one deviation, and it is ours.
There is no Dialog channel to mute on 1.12, and SoftSpoken's own shim says so:
function SoundUtils:MuteChannel(channel, muted, fadeOut)
-- Intentionally unsupported on this client.
end
function SoundUtils:StopSound(clip, fadeMs)
SetCVar("MasterSoundEffects", 0)
SetCVar("MasterSoundEffects", 1)
end
That toggle is its whole repertoire, and it is not a hack the addon invented — the
reference makes it work on the **enabling** edge:
0x457500 the CVar callback: cl = !!value, tail-calls…
0x7a6570 …the flag store, edge-triggered (7a657f: je on an unchanged flag)
0x7a67a0 walks the live instance list; per node:
flags & 0x2 → skipped (the cinematic bypass)
flags & 0x4000008 → muted, not stopped (0x7a65a0)
otherwise, and ONLY when enabling (7a67eb: test cl,cl / je):
flags & 0x4 → 0x7a5950 stop
else → 0x7a4200 stop, then unlink (0x7604f0)
`0x7a5950` is unmistakable: it takes the node's stream handle (+0x34) and channel id
(+0x30) and calls the FMOD stop thunks. The *disabling* edge stops nothing at all —
sounds run on, gated silent at the pump, which is exactly what `category_amp` already
did. So we had half the CVar, and it was the half that does not matter. The comment on
`SoundConfig::enabled` claimed the two "sound the same"; they do for a player moving a
slider, and do not for an addon.
Both symptoms follow. `MuteGameDialogue` is gated on `AutoToggleDialog`, whose default is
`(Version.IsLegacyVanilla or …)` — true here, which is why his saved profile has no
`Audio` block and the option was on anyway — and on this client its body is nothing but
the toggle. The gate passed, the toggle ran, the NPC greeting played straight over the
line being read. The repeat is the same toggle spent on SoftSpoken's own clip:
`PlaySound` cuts *before* it plays ("On Vanilla, interrupt NPC audio BEFORE playback"),
and `StopKeeping` is documented as "silence the head but keep its place: it replays from
the start when Advance next reaches it, since the client has no seek". With the cut inert
the head never stopped, so the replay layered a second copy on a first still going.
`kit::cut_effects_on_reenable` now stops every `SoundCategory::Sfx` channel when the edge
is spent. Music and ambience are spared (the reference's mute-not-stop class) and
cinematic audio never enters `SoundOutput::channels` at all (the `flags & 0x2` bypass).
Two details are load-bearing, both pinned:
- **The edge is armed by the write, never sampled from the value.** The addon writes 0
and 1 inside one Lua call, so a system reading `SoundConfig::enabled` once a frame sees
`true` on both sides and no edge at all. `EffectCutArmed` is set in the CVar observer,
the one place that sees every write. It is its own resource rather than a `SoundConfig`
field because spending it needs `ResMut`, which as a field measured **43** new
ambiguous pairs against every `Res<SoundConfig>` reader in the frame.
- **The cut is spent between the Lua pass and the sound drain.** The reference stops
synchronously inside `SetCVar`, which is before the `PlaySoundFile` the addon calls
next. Applied after the drain it would stop the new clip instead of the old one — the
symptom inverted, not fixed.
The ceiling rises by 1, for the pair against the collider finisher: the same exclusive
`&mut World` shape `log_new_corpses` and `textinput::open_early` already record.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
|
||
|
|
08d574b09c |
GetCastTime applies spell-mod op 0xa itself — so a talented Slam is 2.0 s, not 2.5
grom's probe closed the slam hunt in one line:
mx=2.7 st=1.0255 u=2.7 c=2.5 w=7.4074
Both of my remaining candidates were wrong. The swing speed arrived correctly
(2.7, not the 2000 ms stand-in the last commit instrumented) and the swing timer
was running, so neither the feed nor the combat events were at fault. That leaves
one term: `c`, the cast time, and `w = (2.70 - 2.50) / 2.70` falls straight out
of it.
`SuperCleveRoidMacros` does not ask a verb for that number. `GetSlamCastTime()`
walks the spellbook for Slam, builds a nil-parent `GameTooltipTemplate` scanning
tooltip, calls `SetSpell`, and reads the `"%d+%.?%d* sec cast"` line — so the
whole conditional rests on one line of our spellbook tooltip.
`Spell_C::GetCastTime` (`0x6e3340`) applies `SPELLMOD_CASTING_TIME` **inside
itself**, after the row's minimum clamp:
6e33b0: cmp %esi,%eax ; row minimum (2500 for Slam)
6e33b4: mov %esi,-0x4(%ebp) ; ...clamped to it
6e33b7: push $0xa ; op 10 = SPELLMOD_CASTING_TIME
6e33be: call 0x6e6af0 ; value = (value + flat) * pct / 100
and the tooltip's cast line is a call to it, not a DBC read (`52eb48`/`52eb4b`/
`52eb4d: call 0x6e3340`). We read `SpellCastTimes.dbc` directly, so a
talent-shortened cast showed its base length — a deviation `ui_action/mod.rs` and
`cast_times.rs` both *documented* without anyone pricing it.
The realm's own data prices it. Slam (1464/8820/11604/11605) is `CastingTimeIndex`
20, row `{base 2500, perLevel 0, minimum 2500}`. Improved Slam is two ranks —
12862 and 12330, aura 107 / misc 10 / `basePoints` -251 and -501 with `dieSides`
1, a flat -250 or -500 ms on op 10 against `SpellFamilyFlags` bit 21, family 4
(warrior, `ChrClasses.dbc` field 15; the five-rank version is `zzOLD`). So a 2/2
warrior's Slam is 2.0 s, and on his 2.70 s main hand:
2.5 s (ours) 0.20 s window 7.4% of the swing
2.0 s (his client) 0.70 s window 25.9% of the swing
It is a size, not a sign. 0.20 s is narrower than human reaction time, so a
player pressing by hand overshoots it every press; 0.70 s he lands in every
swing. "477 Heroic Strikes, zero Slams" and "works just fine on the normal
client" are the same observation.
Note the order: the minimum clamp is 2500 and it runs *before* the modifier.
Applying op 0xa first would pin Slam at 2.5 s for ever and look identical to the
bug it was meant to fix.
`Spells::cast_time_ms` is now `GetCastTime` — row resolve, minimum clamp, op 0xa
— with `cast_time_signed_ms` for the reference's `allowNegative` form the
tooltip's "Instant cast" ladder needs (`0x52ebce`), and `cast_time_row_ms` kept
for the one question that wants the row alone: whether a spell has a cast time
*authored* at all, which is `opens_cast_bar`'s gate. The tooltip's cast line and
the four live readers pass the modifier tables through — including both
`PendingCast` arms, so nampower's local lockout on a talented cast was 500 ms too
long as well. The tooltip feed re-pushes on a level change now that the cast line
is level-scaled.
Still deviating, written where it lives: the final `UNIT_MOD_CAST_SPEED`
multiplier (`6e342c`, taken only when `Attributes & 0x30` is clear) is not
applied, because no site streams unit field 139. It is 1.0 without a casting-speed
aura, so it moves nothing a physical ability does.
Tests: `a_talent_shortened_cast_line_shows_its_talented_length` grades Slam on the
real DBCs untalented, at 2/2, and against a foreign class family (the gate that
stops a mage's table shortening a warrior's Slam). The harness test now drives
grom's own addon chain through both widths — `WINDOW_PCT` 7.4074074074074 and
`WINDOW_PCT_TALENTED` 25.925925925926 — with the addon's own
`RefreshSlamCastTime` between them, because its scan is cached for 2 s and a test
that re-pushed without it would measure the stale value and pass.
Both earlier answers are written down in `WINDOW_PCT`'s doc and in
docs/ADDON_COMPAT.md: I called this "not ours" twice, from two green tests, and
each was green because it asserted a number I had supplied.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
|
||
|
|
7c0a99c006 |
A substituted swing speed is invisible to everything downstream — say it once
grom reports the `[noslamclip]` macro still never picks Slam, and that the same
macro works on the normal client. That rules out my last answer: if the window
were the 7.4% the realm's numbers give (2.70 s weapon, 2.5 s Slam), he would
land Slam on about one press in fourteen. His log has **0 in 477**. A narrow
window is not what that is; a conditional pinned false is.
`GetSlamWindowPercent()` returns a hard `0` whenever `st_timerMax <= slamCastTime`,
and `st_timerMax` can only ever be `UnitAttackSpeed("player")`. That value is
either the streamed `UNIT_FIELD_BASEATTACKTIME` or exactly `VANILLA_SWING_MS`,
because `base_swing` substitutes 2000 for absent-or-zero. 2.0 < 2.5, so a
substituted swing makes `[noslamclip]` false for every press of the session,
silently — and his own swing interval, measured off consecutive `You hit` lines
in `WoWCombatLog.txt`, is 2.71 s.
So the question is whether that field reaches us, and **nothing downstream can
answer it**: `UnitAttackSpeed` returns a number either way, `char_stats`' stand-in
warning fires only when no sheet at all was pushed, and 2.00 s is a real weapon
speed, so a substituted sheet and a genuine 2.00 s weapon are indistinguishable
from Lua, from the log, and from every test — because every test seats a sheet
before it asks. That is the same blind spot that let me invent a cast time and
call the chain healthy.
`warn_once_if_main_swing_substituted` says it at the one place that still knows:
the feed, holding the store, before `base_swing` erases the difference. Once per
process, naming the field and pointing at the combat log as the cross-check.
This is an instrument, not a fix. It makes the next session's log answer the
question instead of another round of reasoning from code that reads correctly
under both hypotheses.
|
||
|
|
11c01d07ad |
The slam macro was never ours, and a fixture I invented is why I said otherwise
grom reports `/cast [noslamclip] Slam; [slamclip] Heroic Strike` never picking
Slam. Yesterday I drove the whole chain in the harness, measured a 44% no-clip
window and called it healthy. The 44% came from a cast time I supplied:
s.set_spell_tooltip(1464, … cast_time: Some("1.5 sec cast") …)
**OctoWoW's Slam is a 2.5 s cast.** `Spell.dbc` ids 1464/8820/11604/11605 all
carry `CastingTimeIndex` 20 and `SpellCastTimes.dbc` row 20 is 2500 ms, where
vanilla's Slam is 1500. SuperCleveRoidMacros' own
`DEFAULT_SLAM_CAST = 2.5 -- Turtle WoW` agrees independently. Against his 2.70 s
weapon the window is `(2.70 - 2.50) / 2.70` = 7.4%, 0.2 s wide — not something a
hand can hit, which is the whole report.
The same DBC gives every Slam rank `spellLevel` 54, and he is level 14. The
field alignment is checked against spells whose levels are known (Heroic Strike
rank 1/2/3 = 1/8/16, Charge 4, Shield Bash 12, Overpower 12, Thunder Clap 6),
so that reading is safe to act on: 477 Heroic Strikes in his combat log and zero
Slams, in any session since 10/12.
The fixtures now carry the realm's numbers and `WINDOW_PCT` carries where they
come from, including this mistake — a number I supply cannot test anything, and
a green test then argues against the player's own report.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
|
||
|
|
89dc99f4d1 |
What the second player actually saw: four panel entries and no nameplates
A second player's four `/errors` entries, all from one session, none of them a
fault in the two addons he was reporting:
1. `FloatingChatFrame.xml: SetScript("OnInputLanguageChanged") unsupported`.
Ours. The slot is real 1.12 and the *realm's own* FrameXML sets it on
`ChatFrameEditBox`, so every player met a red error on every login for a
line none of them wrote. `SCRIPT_KINDS` now accepts it and never fires it,
which is also what the reference does — a client with no IME never fires it
either. The rule the old test stated is unchanged and restated: a 1.12 slot
benilla neither fires nor finds in a shipped file still raises.
2. `bindings.xml: root element is <Bindings>, not <Ui>; tolerated`. Every
`Bindings.xml` in the corpus has that root and `bindings_xml` reads it
correctly. No longer said.
3. `bindings.xml: Unknown frame type: Binding`, counted 69. Faithful — the
reference logs the same line (`0x871124`) for every row while `0x51f400`
registers them separately — and ours registers them too:
AlternativeClassicActionBars' 69 bindings all answer `GetBinding`. Now a
trace, so the terminal keeps it and the panel does not.
4. `SetOwner: unknown anchor 'BOTTOMRIGHT' on owner 'WorldFrame'`, counted 292.
The hunt for that caller ended a while ago: it is `Shop_ProcessEntries` in
the realm's own `Turtle_ShopUI`, off the player's patch chain, and the
reference accepts an unknown anchorType silently (mode 0, ANCHOR_LEFT), as
we do. The diagnostic has done its job; it is now host-log-once
(`Model::warn_host_only_once`) and not retained.
The panel is where someone looks to find what is broken in *their* interface.
A line that is faithful, or names a file they cannot reach, crowds out the ones
that are theirs.
----
A second player sent a video: Options → Nameplates with all six boxes ticked,
the V binding set, and no plates at all — while the overhead *names* drew fine.
`VPlateMode::default().range_sq` was `0.0`, and the gate is
`distance_squared > range_sq`. Zero is not "no cull"; it is "cull everything, at
any distance". `NameplateRange` is a registered CVar with default `"80"`, and a
registered default is not written to `config.toml`, so no `CvarChanged` ever
fired and the field never left its derived value.
This is the trap the paragraph three fields above already documents, and which
`occlusion` and `spread` were fixed by writing their registered defaults into
`Default`; `range_sq` was left behind. It stayed hidden because
`NameConfig::default()` — the overhead names — already carried `80.0 * 80.0`, so
names drew and only plates were missing, which reads as a nameplate-addon
problem rather than an engine one. It could not be reproduced here either:
grom's own `config.toml` carries `NP_NameplateDistance = "62"`, which seeds the
same field through the other observer arm.
`the_plate_range_default_agrees_with_its_registered_cvar` now pins the default
to `registered_pairs()`'s row and pins the plate and the name culls to each
other, since a disagreement between those two is what made this read as
somebody else's bug.
It also answers the other half of his report. The gate `continue`s before the
plate is seated at all, so with `range_sq` at 0 no plate frame ever exists —
and UnitPlates works by adopting the plate frames under WorldFrame. One bug,
both symptoms: "nameplates will not turn on" and "UnitPlates does nothing" are
the same line.
And a packaging answer that comes before both, for UnitPlates specifically:
GitHub's "Download ZIP" unpacks `Redbu11dev/UnitPlates` as `UnitPlates-main`
with `UnitPlates.toc` inside it. The client looks for `<Folder>/<Folder>.toc`,
so the addon never loads — here or on the real client. The harness says it in
as many words: "no manifest under other-addons/UnitPlates-main — is that an
addon folder?". It has to be renamed `UnitPlates`.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
|
||
|
|
5751857890 |
The realm's client is not stock 1.12, and its Lua grammar is not 1.12's either
SoftSpoken (z1xel/SoftSpoken, a 1.12 narration port) failed to load: nine files across six addons would not compile, and the three that did then raised at runtime indexing namespaces the failed chunks never built. Every one of the nine is valid Lua 5.1 and invalid Lua 5.0 — `#`, `%`, `0x3040`, `local NAME = ...`. `third_party/lua-src` is a fork that deliberately 5.0-ifies the grammar: three arms deleted from `lparser.c` (`simpleexp`'s `TK_DOTS`, `getunopr`'s `'#'`, `getbinopr`'s `'%'`) and `read_numeral` replaced with 5.0's, which has no hex. Every deletion is byte-read out of `WoW.exe`'s own parser and every one is right about stock 1.12. No OctoWoW client runs stock 1.12. ClassicAPI.dll is in the realm's bundle and rewrites every chunk at the `luaL_loadbuffer` chokepoint — file scripts, `loadstring` and XML `OnLoad` alike — adding exactly five constructs: `#`, `%`, `...` as an expression, `0x` hex and `[=[ ]=]`. Confirmed three ways: the repo's own highlights line, PR #28, and the DLL's string pool, which twice says "Our 5.1 syntax backport makes the probe pass". `docs/ADDON_COMPAT.md`'s row for this said "free — benilla's VM is mlua/Lua 5.1 already". That was the error. So the target grammar is the union, and the fork now carries it: * the three parser arms restored, as stock 5.1 writes them; * `read_numeral` grown an explicit `0x` arm. It could not take 5.1's version back wholesale: 5.1 reached hex through a trailing `isalnum` slurp, and that slurp has to stay gone or `1then` breaks again (`AdvancedTradeSkillWindow2.lua:3878`, which the client compiles). The prefix arm gives both; * addon file chunks now called with `(addonName, addonPrivateTable)` (`UiScript::run_addon_chunk`, four call sites). Taking `...` without passing the arguments only moves the failure one line later, which is what ClassicAPI's own notes record; * `string.reverse` added to the stdlib backport — SoftSpoken calls it. 1.12's own dialect is untouched. `LUA_COMPAT_VARARG` withholds the `arg` table only from a function that *uses* `...` as a value, so the shipped FrameXML's `function f(...) … arg.n … end` still gets `arg`: the two dialects coexist one function at a time, which is what the client-plus-ClassicAPI does. The cost is the era probe. 170 corpus `loadstring` sites ask `loadstring("return function(...) return ... end")` and 92 library files in 24 folders branch on the answer; they now take the 5.1 branch, as they do on the real client. Two branch wrongly there and ClassicAPI ships a compensation for each — Compost-2.0 rev 17406+'s no-argument `Erase` stub (bundled by BigWigs and QuickHeal; it silently stops clearing in place) and Cartographer_Notes' `WorldMapDeathRelease`. Both are now carried in `lua51_probe_fallout.rs`, probed by behaviour rather than by revision and re-checked on every `ADDON_LOADED`, because AceLibrary can upgrade a library mid-session. Measured on grom's own AddOns folder: SoftSpoken, SoftSpoken_Stories, SoftSpoken_Places, SoftSpoken_Writings, SpokenZonesAudio, SpokenBooksAudio, SpokenQuestsAudio{Horde,Alliance,Gossip,Shared}, SSS_VOFallback and AI_VoiceOverData_Turtle all load with zero load errors and zero session errors, 98 `Spoken*` globals, `/softspoken` registered, and PLAYER_ENTERING_WORLD, QUEST_DETAIL, GOSSIP_SHOW and ITEM_TEXT_BEGIN reaching 108/108, 1/1, 6/6 and 2/2 handlers clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn |
||
|
|
7a3ab5e7bf |
A tooltip raise cost 6.6 ms because every frame it minted re-derived the layout graph
`WOW_UI_VERBS=1` over the hover bench answered it in one line: the rebuilt frame's cost was **`frame:GetLeft`, 3 calls at 2188 us each — 6.56 of the 6.6 ms** — inside `UIParent:OnUpdate`. `GetLeft` forces a resolve, and each of those resolves was a whole-graph derivation of ~19,000 frames and ~44,000 regions, because `Model::frame_id` gave up naming the graph on every mint. OctoPawn mints two frames on every `GameTooltip` OnShow purely to own a one-shot `OnUpdate` — the 1.12 way to write `C_Timer.After` — so a raise dirtied the graph twice and the three `GetLeft`s each paid for it. A just-minted frame is now **appended to the cached graph as a leaf** (`LayoutScope::append_frame_leaf`). Sound because the id was minted this instant, so no anchor anywhere can name it: nothing cached reads the new node and no cached edge moves. What it reads is its own anchors' targets, which the append is handed — a mint is not always a creation, since `frame_id` mints on the first *fetch* too, and a frame built in Rust (a nameplate, a chat bubble) is first fetched when Lua asks for its wrapper with its anchors long written. `last` stays `NO_NODE` so the incremental seed sees the hash move; `commit_incremental` counts the slot, which keeps the next full pass's `matched != last_count` node-death test honest. Refused when the frame already carries regions: a region is a graph node without needing a minted id — the derivation mints region ids inside its own walk — so only a derivation can add those rows. **Measured, same bench, same interface: the rebuild costs -0.08 ms/frame under forty churning plates and +0.14 ms with none up, from 6.63 and 6.78.** The rebuilt windows now report no derivation sites at all. Two things about how this was found, both in the bench's doc now: - Its own earlier conclusion was that the cost was "the plate's line walk and the hooks hanging off it, flat, and not the graph it dirties", because the crowded and the empty shape cost the same. They cost the same because **both** were paying the full derivation. A fair experiment that was blind to its own answer, and one env var would have named it. - `WOW_LAYOUT_VERIFY` over the real interface is red on this change — and it is the documented pre-existing 48-region shortfall, reproduced identically with the whole change stashed: `frames 13544 -> 13544, regions 29481 -> 29529` either way. All 2069 benilla-ui tests pass, and they run that verifier forced on after every resolve. 189 workspace failures, unchanged, no regressions. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn |
||
|
|
3bbd99ebaf |
The slam macro, end to end through the path the game takes
He re-tested and it still never casts Slam, so I drove the whole thing rather
than the conditional: `UseAction` on a macro slot → `run_macro` fires
`EXECUTE_CHAT_LINE` per line (`0x4f14e0`) → the stock ChatFrame sends it through
its edit box (`ChatFrame.lua:1343`) → `ChatEdit_ParseText` finds
`SlashCmdList["CAST"]` → CleveRoids' hook → `GetParsedMsg` → `DoWithConditionals`
→ `Keywords.noslamclip` → `ValidateNoSlamClip` → `CastSpellByName`. On his own
interface, with his own 2.70 s weapon and Battle Stance, the line casts **Slam**
at the top of a swing and **Heroic Strike** 1.6 s in — which is the assertion the
old test never made, and the reason "the conditional answers true" meant nothing.
Three things came out of the drive.
**The load-time latch is worse than I wrote this morning, and it is what he is
seeing.** `GetSlamWindowPercent` reads SP_SwingTimer's `st_timerMax` cache, not
`UnitAttackSpeed`, so the `ADDON_LOADED` latch decides the window for the whole
first fight: the stand-in gives (2.0 - 1.5)/2.0 = **25%** against his weapon's
44%, while `st_timer` is still 0 so the swing reads **100% elapsed** — and
`[noslamclip]` is false with nothing said about it. Before the stand-in it was
false *and* printed "requires SP_SwingTimer" once; the fix traded a visible
failure for a silent one. Both states are pinned in the test now.
So the sheet is **seeded before the addon walk**, beside the roster record and
unit snapshot that are already seeded there for the same reason
(`ui_char::seed_combat_stats`). A `/reload` — and anyone mid-fight — now latches
the real weapon. A fresh login still has no self store (the create block has not
arrived, which is why the record comes off the roster), so the stand-in stands
there and heals on the first swing.
**And the stand-in now says so in the log.** One line per VM per token, through
the channel `/errors` lists: a combat-stats verb answering for "player" before
anything pushed that unit's sheet. One such read is expected at load; what the
line is for is the case where it never stops, which no test of the verb can see
because every one of them seats the sheet first. `UnitAttackSpeed` reaching Lua
with the store's own `UNIT_FIELD_BASEATTACKTIME` is now asserted through the real
`feed_char`, which is the test that was missing underneath all of this.
The dual-wield road is pinned too, because it is the one shape that can strand
the main-hand timer: `CheckDamageSource` returns "MAIN" on line one for a
two-hander and never runs its arithmetic, but dual-wielding it weighs the hit
against `UnitDamage`'s ranges scaled by the target's armour — and
`UnitArmor("target")` is 0 on any 1.12 client, ours included, because another
unit's resistances are not public. Every hit then reads UNKNOWN and falls to
`ClosestSwing == true or HeroicStrikeQueued()`, whose first half compares against
a *function* and whose second is true whenever this very macro has queued a
Heroic Strike — and `ResetTimer(true)` refills the **off-hand** timer while
`st_timer` runs to 0, where the elapsed read is 100 for good. That loop is the
addon's own and reproduces on any client; what we must not do is enter it when
the player is two-handed, which is `has_offhand` alone, and his combat log says
two-handed: 2.70, 2.71, 2.72, 2.68 between consecutive hits, with no 0.2 s
off-hand pairing anywhere.
189 failures, unchanged, no regressions.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
|
||
|
|
d89192d962 |
The group-loot popups were never under test: a stand-in had their name
All five group-loot reds were one cause, and it was in the harness rather than
the client. `test_ui`'s `UIParent_ManageFramePositions` wrapper seated a bare
stand-in for every non-`isVar` row of `UIPARENT_MANAGED_FRAME_POSITIONS`, and
`GroupLootFrame1` is a row (`UIParent.lua:1909`) declared in `LootFrame.xml`,
which loads later. A frame's publish to `_G` is non-overwriting (`0x701bd0`), so
the stand-in kept the name and the real popups — built, parented, scripted —
were unreachable: no `<OnShow>`, so no name, no icon, no bind-on-pickup
decoration, and no `OnLoad`, so `CANCEL_LOOT_ROLL` reached no registered frame.
Every assertion was reading an empty frame that happened to answer `IsVisible`.
This is the third time that seat has cost tests — `ChatFrame1`/`ChatFrame2` 137,
the pet bar 9 — so the chat windows' exemption is now the rule: a managed row's
own key is never seated. The pass reads every row through
`frame = _G[index]; if ( frame )` (`UIParent.lua:1962`), so an absent one is
skipped and costs nothing; what *is* reached unguarded is the custom-positioning
tail, and every name that tail touches is already in the explicit list above,
where it can be read. Anchor targets still seat — those are a raise at l.1668.
`FramerateLabel` and `TutorialFrameParent` were the same shape waiting to
happen. No regressions anywhere in the workspace from dropping them.
With the real popups under test, three assertions turned out to be written
against stock 1.12 rather than the file on the chain. This realm's
`GroupLootFrame_OnShow` opens:
local texture, name, count, quality, bindOnPickUp = GetLootRollItemInfo(this.rollID);
if ( not name ) then this:Hide(); return; end
where stock painted straight through a nil name. So an unresolved roll shows
nothing at all and frees the slot it claimed, rather than opening blank — and
the quality-miss-tail protection (`0x4c31a3`'s `nil, nil, 1.0, 1.0, nil`) is
never reached through that paint, so it is now asserted at the verb, where the
stock file and any addon still index `ITEM_QUALITY_COLORS` with it unguarded.
`ui_loot_roll`'s own doc says what that costs it: its `TEMPLATE_HOLD_MAX_MS`
fallback opens a frame unresolved "because a frame that never opens cannot even
be passed on", and this file closes it again on the same frame. Kept and written
down rather than worked around — the hold is what actually saves the roll, and
the alternative is never announcing it.
189 failures from 195.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
|
||
|
|
40cbec55cd |
The hardcoded-sentence gate failed on its own corpus read, not on a violation
`reference_strings` demanded a sentence from each of the four string sources, because the two `Localize()` files wrap their assignments in a function and that shape has to be proven to parse. This realm's `GlueXML\GlueLocalization.lua` has its whole `Localize()` body commented out and tweaks frames instead, so the assertion fired on a chain where nothing is wrong — and took the policy half of the test, the part that actually reads every Rust source for a sentence the reference ships, down with it. Asserted over the pair now: a base table can never be empty, and at least one `Localize()` file must yield, which `FrameXML\Localization.lua`'s enGB overrides do. The policy half runs again and passes, so nothing is hardcoded. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn |
||
|
|
76e022ba78 |
Two real macro-window bugs, and three expectations the realm had moved on from
The macro window's four reds were four separate things, two of them bugs a player meets. **Every macro made through the icon chooser had a blank icon.** This realm rewrote `Blizzard_MacroUI` to keep bare upper-cased icon names in its own `MACRO_ICON_FILENAMES` — `UpdateMacroIconFilenames` strips `INTERFACE\ICONS\` off `GetMacroIconInfo` — and `MacroPopupOkayButton_OnClick` hands that name straight to `CreateMacro`. `icon_path` took any string as a finished path, so `GetMacroInfo` answered `ABILITY_AMBUSH` and `MacroFrame_Update`'s `macroIcon:SetTexture(texture)` resolved nothing. The engine composes the prefix itself: `Interface\Icons\` is at `0x44ca64`, inside `UIMacros.cpp`'s own string block between the `DeleteMacro`/`GetMacroInfo`/`GetNumMacros`/`CreateMacro` registration names and `MACRONODE`, which is what says the macro code owns it. A number is still a chooser index, and a string that already carries a separator is still a path. **Selecting a macro with no icon raised and took the whole details pane down.** `Blizzard_MacroUI.lua:84-85` feeds `GetMacroInfo`'s texture to `SetTexture` and to `strupper` with no guard, so a nil answered there is `bad argument #1 to 'strupper' (string expected, got nil)` inside `MacroFrame_Update` — no editor, no body box, no character counter. The reference cannot be answering nil, or its own UI would raise the same way: a macro with no icon of its own now reads as the client's own `INV_Misc_QuestionMark` (`0x447fe4`, same UI block), which is also the name the realm's chooser puts in row 1. The other three were stale readings of a file the realm has rewritten, each checked against the chain copy rather than guessed: - the character counter is `MacroFrameCharLimitText:SetText(this:GetNumLetters() .. "/" .. this:GetMaxLetters())` in the realm's own XML — "21/510", with no `MACROFRAME_CHAR_LIMIT` and no "Characters Used" tail; - the chooser shows `GetNumMacroIcons() + 1` rows, because `UpdateMacroIconFilenames` inserts `INV_MISC_QUESTIONMARK` at index 1 ahead of anything the engine offers, so every list index is one past the engine's; - both tabs call `PanelTemplates_TabResize(0)` with no cap and read `text="GENERAL"`/`text="CHARACTER"`, where stock passed -15, capped tab 2 at 150 and formatted the player's name into it. That test was *about* the name, so it now checks the row is independent of it instead. 195 failures from 203, no regressions (`comm -13` over the sorted names). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn |
||
|
|
48c27d45bf |
The three multibar reds were one duplicate load, not a loader defect
`_G["MultiBarBottomLeftButton1NormalTexture"]` really was a different object from that button's `GetNormalTexture()`, with both answering that same `GetName()`, so every tint written through the global — the realm's own `ActionButton_ShowGrid`, and pfUI constantly — landed on a region nothing drew. I wrote that up as a state-texture publish naming the wrong region at two levels of template inheritance and left three tests red on it. It was neither the publish nor the depth. **These tests loaded `MultiActionBars.xml` twice**: once through `load_action_bar`, where toc order puts it, and again in each test's own list, left behind when the first was added — 15 tests. Two loads build two sets of MultiBar buttons, and the two `_G` publishes disagree about a collision: a frame's is non-overwriting (`0x701bd0`), so `MultiBarBottomLeftButton1` stayed the *first* set's button, while the loader's state-texture publish is a plain `globals().set`, so the region name became the *second* set's texture. Every write through the global reached the hidden second set. What rules the loader out is now its own test: `expand`'s merge concatenates children, and only the base of the real chain declares a `<NormalTexture>` — `ActionBarButtonTemplate` and `MultiBar1ButtonTemplate` declare only `<Scripts>`, read off the chain — so there is exactly one declaration at any depth, and a synthetic three-level chain answers `named == live` at every level, top-level or child of a named parent. The other two reds in that file were a misread of the reference, in the same place twice: `ShapeshiftBar_Update`'s last line is `UIParent_ManageFramePositions()`, and the pass hides all three shelf strips under `if ( MultiBarBottomLeft:IsShown() )`. So a form learned while the bottom-left bar is up shows the middle strip and takes it down again inside one call — it is never seen across the row, where both tests asserted it would be "until the next pass". 17/17 in that module now. The publish asymmetry is left alone and written down where it was found: no single load can reach it, the reference's region-name path is not disassembled, and matching frames on a guess is not a fix. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn |
||
|
|
27c75776c6 |
The plates step needs the install, because the WorldFrame does
`test-no-install` caught it: a plate is parented to the `WorldFrame`, which comes from FrameXML off the patch chain, so with `WOW_DATA=` there is nothing to parent to and nothing for the fixture's own scan to walk. Skipped without the install like its render and use-probe siblings — which is also the honest statement about that VM, since a plate addon can do nothing in it either. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn |
||
|
|
f3fec804eb |
Two addons another player asked for, and the Linux-only gap that killed one
AlternativeClassicActionBars and UnitPlates, both now in `other-addons/`
(untracked — third-party source, not ours to vendor). Measured with the survey
and the read-back probe on a root holding only those two, so neither borrows the
other's globals. Both already loaded clean; three things were missing, and the
first was costing an entire addon's keybinding surface.
**A binding table spelled `bindings.xml`.** `ui_script::addons` asks for
`Bindings.xml` by the reference's own fixed name — `0x51f400` reads the name,
not a `.toc` entry — and the open was case-sensitive, so on Linux ACAB's 69
`<Binding>` rows never registered. Silently: an absent binding table is the
normal case, so nothing raised and nothing warned. Hover-bind, edit-mode escape
and every bar page key were dead. The texture and font side of the same file
namespace has always resolved case-insensitively (`loose_addon_file`, which
walks the path a component at a time and enumerates a directory when the spelling
does not match); the Lua and XML side now shares one resolver with it, and both
crates' private `read_under` copies delegate to it. Measured after: 69 bindings
listed by `GetBinding`, `SetBinding("CTRL-9", "ACABBIND1")` round-trips, and the
addon's drawn-frame count went 42 -> 175, because its login sequence had been
stopping short of its own bars.
**`PixelUtil`.** Called unguarded on ACAB's bar-geometry path at four sites,
three of them the same question — one screen pixel in UI units, which is what
every pixel-perfect border in that addon is drawn from. Not in ClassicAPI's
published list, but ACAB disables itself without ClassicAPI, so on this server's
kit it is present by implication. A Lua chunk beside `math_ext`, whose `Round` it
uses. One deviation, written down where it lives: upstream reads
`region:GetEffectiveScale()`, which a 1.12 texture does not answer, so the scale
read falls back to the parent and then `UIParent` — the same number in every case
that matters.
**`UnitXP("nop", "nop")` answering the boolean.** The presence probe.
SuperCleveRoidMacros keeps only `pcall`'s `ok`, but ACAB's `HasUnitXPSP3` wants
`ok and result == true`, and the fall-through handed it the stock XP getter's
`0` — truthy, so it passed the loose reading and failed the strict one. ACAB
reported UnitXP_SP3 absent on a client that answers the whole family.
Left alone with the reason in `docs/ADDON_COMPAT.md`: `IsStackableAction`
(post-1.12, guarded, and stock 1.12's own ActionButton.lua shows a count off
`IsConsumableAction` alone), `CursorHasMacro` (post-1.12 and redundant — ACAB
tests `GetCursorInfo() == "macro"` first), and five third-party tables that are
missing because those addons are not installed.
The harness grew a `--plates [n]` step, because a nameplate addon is the one kind
it could not see working: the subject is a `WorldFrame` child `vplates.rs` mints
per frame, so with none seated the addon's scan finds nothing, does nothing, and
reports a clean load over an empty screen. It seats n live plates and counts how
many an addon hung a frame on, which doubles as the contract check on the plate's
own shape — a plate that stopped being a `Button`, or stopped putting its border
texture first, would read as an addon adopting nothing. UnitPlates adopts every
plate and reads its name, health and level off ours.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
|
||
|
|
1b1a90cf13 |
A swing speed of 0 latched for the session, and [noslamclip] never fired
grom's own probe is the record of it:
st=0 max=0 pct=nil win=0 idx=1
Four readings of one cause. `UnitAttackSpeed("player")` answered `0` between the
VM's birth and the first `feed_char` push, and that window always contains the
addon walk — `load_ingame_ui_on_world_entry` mints the VM, seeds the player's
roster record and runs every addon's file scope and `ADDON_LOADED` before a
Bevy feed system gets a turn. SP_SwingTimer latches `st_timerMax` there behind
`if not st_timerMax`, and `0` is truthy in Lua, so the guard never re-fires:
the first write to that global in a session is always the pre-push answer.
With `0` latched, `CleveRoids.GetSwingPercentElapsed()` answers nil (its own
`attackSpeed > 0` gate), `GetSlamWindowPercent()` answers 0, and
`ValidateNoSlamClip()` takes its "requires SP_SwingTimer" branch and returns
false. So `[noslamclip]` is false and `[slamclip]` is true for the rest of the
session, and the macro picks Heroic Strike every time with no indication —
which is exactly what he reported. `idx=1` in the same probe says the
`stance:1/3` half was never the problem.
I had called this latch faithful, on the grounds that `reference/1.12-shapes.tsv`
has `UnitAttackSpeed` returning exactly 2 values and never none. That is about
the arity and says nothing about a `0`, which is a swing speed no 1.12 client
can answer: the player's UNIT_FIELD_BASEATTACKTIME is in the create block the
real client has before `UI_Init`, and a unit with no weapon still carries the
engine's 2000. `ui_char::base_swing` already wrote the rule down — "0 or absent
read as not yet streamed: the vanilla 2000, never zero, as the reference divides
damage by it" — and `UnitCombatStats::default()`, which *is* the pre-push
answer, broke it. Now both read `VANILLA_SWING_MS` from one place.
It was never only this macro: BetterCharacterStats' DPS rows divide by the same
number and read `inf` in that window.
The bench test that documented the latch as harmless now pins it as the defect,
and asserts what the stand-in buys: a drained swing timer measures 100% elapsed
rather than nil, `[noslamclip]` is false out of combat by the window math rather
than by the addon's own bail, and nothing tells him in chat to install the addon
he already has.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
|
||
|
|
9d5a974649 |
Say out loud that GetAuctionSellItemInfo answers more than 1.12
The shape gate caught the extra two returns, which is exactly its job. They are not a gap to close, so they do not belong on NOT_YET_ASSERTED, which is a list of things that are wrong and may only shrink. BEYOND_1_12_ARITY is the other category: a binding that answers more because the realm's own FrameXML reads more, named with the reference's arity, ours, and the reason. The gate asserts both numbers, so an entry cannot drift — a stale reference arity fails, and so does a different count than the one the reason covers. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn |
||
|
|
21ebe65d83 |
Three more kits loaded their files out of toc order, and one real defect found
Each of these was one cause behind a whole cluster, and none of them was our client being wrong — except the last, which is. The character sheet, six tests: the realm's PaperDollFrame_OnLoad builds a titles dropdown (UIDropDownMenu_Initialize at PaperDollFrame.lua:34, nothing stock 1.12 has), so without UIDropDownMenu.xml the OnLoad raises and the whole file fails to load. The trade-skill window, six tests: its two filter checkboxes index _G[this:GetName() .. "Text"] straight out of their OnLoad, and that FontString comes from OptionsCheckButtonTemplate, which the kit never loaded. The extra action bars, two of five: MultiActionBars.xml is toc row 48 and BonusActionBarFrame.xml is row 84, and the kit had them the other way round. The stance file's ShapeshiftBar_OnLoad runs the managed-positions pass, which seats bare stand-ins for MultiBarLeft/Right/BottomLeft; a publish to _G being non-overwriting, the real bars stayed shadowed by nameless empties — the bar frames answered, their buttons did not. Third kit in two sessions to trip on this exact thing. The three that remain are a genuine client defect, left red on purpose and written up where it fails. A MultiBar button's grid ring ignores its half-alpha tint because `_G["<name>NormalTexture"]` is a **different object** from `button:GetNormalTexture()` — while the live region's own GetName() is that very name. The fork is the depth of template inheritance: one level (ActionButton1) is fine, two (MultiBar1ButtonTemplate -> ActionBarButtonTemplate -> ActionButtonTemplate) is not, and neither outer template declares a <NormalTexture> of its own, so the duplicate comes from the loader's expansion. That reaches every _G[name .. "NormalTexture"] write in FrameXML and in addons, pfUI included, so it wants its own change in loader::widgets with its own tests. Workspace failures 257 -> 227, clippy clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn |
||
|
|
998ab3f2f0 |
Two one-cause clusters: the pet bar never loaded, the auction window never built
**The pet bar, nine tests.** No script errors, and the shelf simply never drew. PetActionBarFrame had no scripts attached at all: the kit seats a bare stand-in under that name from the managed-positions pass — which it must, since the pass does PetActionBarFrame:IsShown() (UIParent.lua:2065) — and the real frame could never take the name back, a publish to _G being non-overwriting (0x701bd0). So PetActionBar_OnLoad never ran, showgrid stayed nil instead of 0, and ShowPetActionBar's `showgrid == 0` gate refused forever. The kit was loading BonusActionBarFrame.xml, whose ShapeshiftBar_OnLoad calls that pass, *before* PetActionBarFrame.xml. The realm's FrameXML.toc has them at rows 83 and 84, the other way round. Loading them in toc order fixes eight; the ninth asserted stock 1.12's unguarded `Attack ()`, and the realm now wraps that append in `if ( key ~= "" )`, so the client was right. Same shape and the same seat list that cost 137 tests through ChatFrame1. **The auction window, fourteen tests.** The whole Blizzard_AuctionUI load raised: the realm's rewritten CalculateAuctionDeposit is `floor(price * (duration/120) * (1 + (maxStack - stackSize) * 0.05) * 0.025)`, and GetAuctionSellItemInfo answered the reference's six values — 1.12 returns exactly six — so maxStack was nil and the arithmetic raised out of AuctionFrameAuctions_OnLoad. The window never built. It answers eight now: maxStack from the template's stack size, and the link. Then the realm's own CalculateAuctionDeposit replaces the engine binding, as it is meant to, which is why that test's numbers move — its fixture also needed Linen Cloth's real stack of 20, since a missing one reads 1 and makes the factor shrink below 1 for a two-item stack. Also recorded, because it changes what every earlier count in this session meant: `cargo test --workspace` stops at the first failing binary, so the "96 failures" I had been tracking was benilla-app's lib target alone. With --no-fail-fast the real total was 257; these two clusters take it to 243. benilla-ui is fully green (2061), and 80 of the rest are the known benilla-formats data goldens. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn |
||
|
|
9d0b4d068a |
The stock chat bubble drew under pfUI's, because its art had no handle
His screenshot shows two plates: a rounded grey one with pfUI's square box laid across the middle of it. pfUI's bubble module blanks every Texture it finds in the bubble's regions and lays its own backdrop over the result (modules/bubbles.lua:30-48). Our bubble held its two art paths only in the Backdrop struct, so the loop found the tail and nothing else. The note in this file reasoned that pfUI's own box covers the plate anyway — it does not: pfUI anchors that box 10 px wider and 10 px *shorter* than the bubble and fills it at 75% alpha, so the stock plate goes on showing above and below, through a translucent square. That is exactly the photograph. The background and the border are now also Texture regions, listed first as the reference lists a frame's engine regions first. They are anchorless and draw nothing themselves; the backdrop still paints. drive() reads them back each frame, and a region that has been blanked or hidden clears that half of the backdrop — never restored, since a skin blanks a pool slot once and keeps managing that frame for the session. This is the reference's own shape rather than a deviation from it: a 1.12 backdrop's pieces are CSimpleTexture objects the frame owns, which GetRegions (0x773f60) walks. Keeping the geometry in one struct is our shortcut; these put the handles back. The transcribed-pfUI test now expects three textures instead of one, and goes on to assert the thing that actually matters — after the blank, the extract carries none of the bubble's own art. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn |
||
|
|
96e95a51ec |
Price the rest of the tooltip path, and name what a raise costs
He said the tooltip lag is not only signs, so the two hover benches now tally layout derives and the live frame count per window, which is what turned the sign case from a hypothesis into a number. What they say. A held hover is clean: 1.7 ms/frame under forty plates, 0.76 in a city, and no per-frame derive. A rebuild costs 6.1 ms and gives up naming the graph exactly once, always at frame_id — the mint of a frame the model has not seen, which is the one write that genuinely cannot be named, since a new node has to join the cached graph. The frame is not ours. OctoPawn hooks GameTooltip's OnShow and creates an anonymous frame on every show, purely to own a one-shot OnUpdate delay — twice over, from ScoringCore.lua:540 and Pdmo.lua:101, which is the ordinary 1.12 way to write C_Timer.After. So every tooltip raise on his interface mints frames and re-derives, on the reference client too. Live frames rise by far less than the number created (137 against 480), so Lua reclaims most of them: a cost per raise, not an unbounded leak. Not acted on, deliberately. The lever would be letting a brand-new leaf join the cached graph without a full derive, which is a layout-engine change and wants its own measurement, not a change tacked onto a bug session. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn |
||
|
|
fff48cd75c |
The sell-junk diagnostic sat behind the gate that fails
He hit sell junk again and the log said nothing — not because the sweep had nothing to report, but because the sweep never ran. pfUI's Sell Grey Items button is `if GetNumJunkItems() <= 0 then return end` and only then SellAllJunkItems (modules/autovendor.lua). That is the one path he clicks, so a zero count ends the click there. Last session's instrument lived inside SellAllJunkItems, downstream of the thing that was answering zero, and it could not be reached. Both now share one junk_survey of the carried bags, so the count and the sweep can never disagree about what they are looking at, and each says its half: GetNumJunkItems, when it answers zero while occupied slots have no resolved quality, says so and names them — a truthful zero stays silent; SellAllJunkItems says what it queued as well as what it could not judge; and ui_merchant's drain now warns, rather than debugs, when a queued slot resolves to no item guid. That was the last silent place the click could die, and debug does not reach his log. So whichever of the three branches is at fault, his next click answers it. Also checked and cleared on the way: the drain's slot.saturating_sub(1) is right — the feed keys container slots 1-based. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn |
||
|
|
7ebae975d9 |
A recipe never names the spell it teaches, so "Already known" never fired
He is still told he can learn recipes he already knows. The check was there; it was written against a shape no recipe in the game has. Read off his own item cache, the realm's Plans: Copper Chain Vest (3609) carries exactly one spell: 3349, at trigger 0. The rule looked for trigger 6 carrying the taught spell. Neither half matches — and 3349 is not the craft spell either, it is a learn *wrapper* whose single SPELL_EFFECT_LEARN_SPELL teaches 3321. Nothing in the item record names 3321 at all, so no check that reads only the item can ever answer this question. The app now makes the hop while it has the catalog in hand, through SpellCatalog::learned_spell, and hands the result over as ItemTemplateView::teaches. The tooltip reds ITEM_SPELL_KNOWN when any of those is in the known-spell set. The trigger-6 arm stays for an item that really does carry its taught spell. The old test passed throughout, which is the part worth keeping in mind: its fixture was invented — trigger 6, spell 3330 — and 3330 is Silvered Bronze Shoulders, nothing to do with this item. It now uses the realm's own record, and a second test pins the 3349 -> 3321 hop against the shipped Spell.dbc so the fixture can never drift from the data again. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn |
||
|
|
0da710810e |
An EditBox's engine quads answered nil, and ate the trade window's money row
His trade window has no money amounts: one empty box where the gold/silver/ copper row belongs. The cause is four regions that answer the wrong thing. pfUI skins that row by walking the gold EditBox's regions for the first Texture whose texture names "MoneyIcons", showing it, and re-anchoring it to the right of the box — then the silver box hangs off that icon and the copper box off the silver one (api/ui-widgets.lua:1225, api/api.lua:1693). Its scan reads a nil GetTexture() as "no name to match against, take this one". An EditBox's first four regions are the engine's own: the three selection highlights and the caret, which GetRegions lists ahead of the authored <Layers> ones. They paint host-side here, so they carried no texture and no fill and answered nil — and pfUI took the caret for the gold coin. Silver and copper then anchored off a zero-size region and the whole row collapsed. The reference builds those four as colour-set textures, not empty ones, so GetTexture answers them "Solid Texture" — the string is in WoW.exe, and pfUI's scan does filter that out. They answer it here now. Nothing renders differently: the host still draws the highlight and caret from sel_start / sel_end / caret_shown, and the fill stays unset. The new test runs pfUI's scan itself rather than asserting on our side of it, because what matters is which region that loop picks. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn |
||
|
|
e17d7dff04 |
The slam-clip macro chain is sound, and now has a test that says so
His rotation macro never picks Slam. Five of our verbs and feeds stand between `[noslamclip]` and an answer, and any one of them would read in play exactly as he describes, so this drives all five on his own 92-addon interface rather than reasoning about them: UnitAttackSpeed's (number, nil) shape, which is what isDualWield() asks; CHAT_MSG_COMBAT_SELF_HITS reaching a registered frame with arg1 as the composed sentence, which is the only thing SP_SwingTimer resets on; the GetSpellName spellbook walk; GameTooltip:SetSpell on a nil-parent scanning tooltip built from GameTooltipTemplate, read back through getglobal; and the <OnUpdate> delta arriving as seconds. All five answer correctly. Measured, the conditional is true at the top of a swing with a 44.4% window off a scanned 1.5 s cast time — so the cause is not in this chain, and the test is here to keep that true. The one asymmetry it does pin down is faithful: before any swing lands, SP_SwingTimer's ADDON_LOADED arm latches st_timerMax to 0, because UnitAttackSpeed answers 0 while the stats feed has not pushed and 0 is truthy in Lua, so its own `if not st_timerMax` guard never fires again. reference/1.12-shapes.tsv has UnitAttackSpeed returning exactly two values and never none, so answering 0 is right; the first swing heals it, which the second half of the test shows. The scanning-tooltip leg is worth naming on its own: without it the addon falls back to a 2.5 s constant, and against a 2.70 s swing that is a 7% window instead of 44% — which would read as "Slam never fires" too. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn |
||
|
|
0c698b8797 |
Hovering a sign re-derived the whole layout graph, every frame
grom reports a frame drop on hovering signs, and asked for that one first. A signpost is the only GameObject whose plate follows the cursor — GO_TYPE_GENERIC with floating_tooltip, the `cursor_seated` leg of drive_mouseover_tooltip — so every frame the hand on the mouse moves, GameTooltip is re-anchored. That re-anchor called the whole-graph Model::touch_layout for a write that moves two offsets and nothing else. Measured on his own interface, not guessed at: a new bench window prices a held plate against a drifting one against no plate at all. A drifting pointer cost +8.6 ms a frame over the baseline; a held one cost +0.0, so the plate is not the cost, the re-anchor is. It is now +0.2 ms. tooltip_unit::seat_on_cursor is the shared seat for both arms (the raise in world_tooltip_gameobject and the follow in world_tooltip_move). An offset-only move names the frame — touch_layout_frame, exactly as SetPoint's own write_anchor does for a dragged window — and only a roster or target change gives up naming. The bench asserts the ratio rather than the absolute, so a loaded machine scales every window together, and reads GetLeft at the end: a "fix" that stopped the plate moving would price identically, and this test would otherwise applaud it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn |
||
|
|
17ee517602 |
An unpainted loot-roll name is the reference's own placeholder, not nothing
`nothing_repaints_a_frame_that_opened_before_its_snapshot` asserted that a frame
which never painted has no name text. It has one: `LootFrame.xml` authors
`text="Item Name"` on `$parentName` as a design-time placeholder, and every copy
of that file carries it — the realm's `patch-4`, `patch.MPQ` and stock
`interface.MPQ` alike. So "stays blank" means **unchanged**, which is what the
test is actually about, and it now reads the untouched label once and compares
against that rather than against nil. The realm owns its own placeholder.
Leaves the five other `group_loot_tests` failures, and they are a different
thing worth stating: the realm has **rewritten `GroupLootFrame_OnShow`**. It now
opens with
local texture, name, ... = GetLootRollItemInfo(this.rollID)
if ( not name ) then this:Hide() return end
where the stock body had no such guard and called `SetText(name)` with a nil.
So on this realm an in-flight roll does not open at all until its name is known,
and `in_flight_roll_opens_without_a_name_or_icon` encodes the older behaviour in
its name as well as its assertions. That one needs a probe of what
`GetLootRollItemInfo` answers in that state before anything is rewritten — the
visibility assertion above it passes, which does not fit either reading yet.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
|
||
|
|
cb8eb7f01d |
The chat dock indexes the video window unguarded, so the kit owes it
`not OptionsFrame:IsShown()` at `FloatingChatFrame.lua:862`, inside the hover pass `UIParent`'s OnUpdate drives — an unguarded index of a global the kit only seated from `IsOptionFrameOpen` and `ToggleGameMenu`, neither of which that path calls. Every kit that drove a chat hover raised on it. Given as a prerequisite rather than a stand-in, because the realm's toc loads the stock video window 29 rows above the chat one (row 29 against 58): that is the toc's own order, and it hands over the real frame instead of something that would shadow it the way the `ChatFrame1` stand-in did. `chat_resize_tests` 7 → 0, `chat_options_tests` 7 → 0, `chat_tests` 6 → 1. 116 → 97, nothing newly failing. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn |
||
|
|
1aa3418fdc |
Read the test that reports errors first: 20 more, from four causes
The guild and friends clusters — 26 tests between them — were one nil function.
`guild_tests::driving_the_guild_windows_raises_no_script_errors` exists to name
its errors, and all four of them were the same line: `attempt to call global
'GetDifficultyColor' (a nil value)` at `FriendsFrame.lua:523`, reached from four
entry points. `GetDifficultyColor` is FrameXML's, not a client global
(`reference/1.12-globals.tsv` types it so), and `QuestLogFrame.lua` defines it —
ten toc rows above `FriendsFrame.xml`. The raise took the rest of the roster
update with it, including the `GuildPlayerStatusFrame:Show()` that ends that
branch, which is how it read as "the guild tab opens and the player pane is not
visible": an assertion about visibility with no hint of a nil function behind it.
One prerequisite row took guild 17 → 1 and friends 9 → 1.
The reputation, honor and skills pages (20 more) shared a fixture gap: their
player `UnitState` had a level, a race and a class and **no name**. The realm's
`CharacterFrame_OnShow` formats `gsub(UnitPVPName("player"), "\n.+", "")`, and
`UnitPVPName` answers nil for a nameless unit, so every one of those pages raised
`bad argument #1 to 'gsub'` on the way up and never finished showing.
The inspect page's arena tab raised three more: its team buttons inherit
`ArenaTeamTemplate`, which is a FrameXML row of the realm's own
(`Turtle_ArenaUI\Turtle_ArenaUITemplates.xml`) and not in the addon's templates
file, so the inherits was skipped, `$parentText` was never built, and the OnLoad's
`_G[this:GetName().."Text"]:SetText("(2v2)")` had nothing to index. That fixture
already said "Before the inspect addon, as `inherits=` resolves at load" — it was
simply missing this one.
And five expected a rank title without its trailing space. The realm's
`GlobalStrings.lua` gives every rank one — `PVP_RANK_8_1 = "Master Sergeant "`,
where stock 1.12's copy has none — because its own callers concatenate a name
after the title. The titles are now read off `PVP_RANK_<rank>_<team>` through one
helper, so the realm owns its own strings; that also turned up a wrong index
hiding behind the stale spelling (Grand Marshal is rank 18, not 14).
136 → 116, nothing newly failing.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
|
||
|
|
bae75b9d3f |
A WMO material with no extension lost its texture outright
`wmo.rs` built its submesh texture URLs by hand — backslashes folded,
lowercased, `mpq://` prefixed — which is exactly what `texture_url` does, minus
the one thing that matters: `texture_url` appends `.blp` to a name that has no
extension, as the reference's `CImageManager::CreateTexture` does. The M2 loader
has gone through it all along.
The realm's WMOs ship bare material names too, so that submesh drew untextured
and Bevy could not even pick a loader for it. From grom's 2026-10-28 log, the
only two lines in the whole session above WARN:
WARN Multiple AssetLoaders found for Asset … Extension: None
ERROR Path not found: textures/wetlands_grimbatol/mm_irnfg_trim_02flip
Repeat/repeat is passed, which is what the hand-rolled path was: `texture_url`
returns the plain `mpq://…` for that pair and marks the stem only for a clamped
mode, and a WMO submesh carries no wrap flags here to ask about. That one name is
now a case in `texture_url`'s own test.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
|
||
|
|
b5259bd608 |
The missing item values were eight absent id getters, not the tooltips
grom reported no item values on quest-reward tooltips. The value line is pfUI's, and `modules/sellvalue.lua` says plainly how it works: it hooks every `Set*Item` tooltip verb and takes the row's **id** — never its link — then asks `C_Item.GetItemSellPriceByID`. An absent getter answers nil and `AddVendorPrices` returns on its first line. So it was never about quests. This client answered exactly two members of that family, `C_Container.GetContainerItemID` and `GetInventoryItemID`, which is why his bag and equipped items had a price line and his quest rewards, loot rows, loot rolls, merchant rows, mail attachments and trade-skill rows did not. The other eight are here: `GetQuestItemID`, `GetQuestLogItemID`, `GetLootSlotItemID`, `GetLootRollItemID`, `GetMerchantItemID`, `GetInboxItemID`, `GetTradeSkillItemID`, `GetTradeSkillReagentItemID`. Each sits beside that row's own `*Link` getter, in that row's own module, reading state already held — no new feed, no new state. The id is also the **earlier** fact, which is the part worth keeping in mind. An item id rides the packet; a link is built only once the item's template has landed to give it a name and quality, and `ui_quest::resolve_item` says so in as many words. A quest reward is close to the worst case for that — it is usually an item the player has never seen, so its template is not in the 14,000-row cache and the first hover beats the answer home. These getters answer there, where the link beside them is still nil; the test asserts exactly that contrast. Also, from the same thread of play reports: the sell-junk sweep now says what it could not judge. `SellAllJunkItems` and `GetNumJunkItems` both gate on a *known* poor quality, and a carried slot's quality is `None` until its template arrives, so a grey sitting in such a slot is neither counted nor sold — which is what "not all grey items sell" looks like from outside. Whether that is in fact the cause is not something this end can tell, so the sweep records the slots it skipped for that reason, with their item ids, once per click. If the next log names some, the fix is to wait for those templates; if it names none, the cause is downstream in the drain or the server's refusal, and the line says that too. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn |
||
|
|
3b8050a91d |
Party members had no buffs because nothing ever wrote their auras
grom reported it from play: the party frames show no buffs. pfUI's group header
reads `UnitBuff("party1", i)` and the stock `PartyMemberFrame_UpdateAuras` reads
the same verb, and nothing was wrong with the verb — `ui_aura`'s feed pushed six
tokens (player, target, pet, targettarget and ClassicAPI's focus pair) and not
one party token, so `nth_aura` found no list at all for `party1` and every buff
row stopped on the first nil.
The four tokens now get their own lists off `GroupState::party_slots`, through
the same `other_list` builder and the same `buffs_visible_on` gate the target,
pet and focus tokens use — so a party member shows exactly what a targeted party
member shows, and an empty slot clears its token. Each gets its own `UNIT_AURA`
fire keyed on guid and projection, because a party member's auras change on their
own clock and a `"target"` fire tells a party frame nothing.
Not the raid tokens: `raid1`..`raid40` is ten times this work every frame, each
list walking a descriptor and re-running the visibility gate, and that cost has
not been measured. The party's four are bounded and are what was reported.
The guid index and the roster share one system parameter because bevy's
`IntoSystem` tops out at sixteen.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
|
||
|
|
b39d901257 |
Twelve more, all of them numbers the realm moved out from under
Both clusters left after the chat-window shadow were stale numbers written into tests rather than anything of ours, and both are stale because the realm's FrameXML moved — which is the thing to suspect first in this crate. Seven in `battlefield_minimap_tests`: the manifest was expected to warn exactly once, about `gxRefresh`. This client does not register that CVar because no target offers the exclusive mode-set a refresh rate needs, and the only reader was stock `OptionsFrame.lua`'s `OptionsFrameRefreshDropDown_OnLoad` — which the realm replaced with a rewrite that has no refresh-rate dropdown at all: three mentions of the CVar in `patch.MPQ`'s stock copy, none in `patch-4.mpq`'s. The list is now empty rather than gone, so a realm that restores the stock window brings the row back and that is where it shows. Five in `questlog_tests`, from three separate numbers in the one file: `"Quests: 8/20"` — the realm sets `MAX_QUESTLOG_QUESTS = 25`, and this file had already learned that once: another test reads the cap off the global with a comment saying why. This call site had been missed, so it is read the same way. A wheel notch was asserted to scroll the list, with eight fixture quests described as "two more than the list shows". The realm shows `QUESTS_DISPLAYED = 24` where 1.12 shows six, so eight fit with room to spare and the notch had nothing to move — the `assert_ne!` was passing only while the row count read as six, which is to say it stopped testing anything a while ago. The log is re-seated past the window for that leg, sized off the live global, and put back to the eight the rest of the test reads. And `frame_rect(quads, 300.0, 261.0)` / `(384.0, 512.0)` hunted the quad list for a bare frame of a given size, which is a number the realm can change and did. `named_rect` asks the frame where it is instead. Nothing any of those tests asserts was ever about the size: they clip reward plates and text against the pane and compare the window's own edges. 269 → 160, still nothing newly failing. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn |