Commit Graph
25 Commits
Author SHA1 Message Date
gromandClaude Opus 5 78e4e02cfe docs: the registered list's buckets add up to 62
`StartItemExport` was in two buckets. It writes a file of its own, so it
belongs with the withheld — not with the names measured out for want of
a consumer. 45 built + 9 withheld + 4 not ours + 4 measured out = 62,
and the `comm -23` already in that section is the check: the seventeen
it prints are exactly the last three rows.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
2026-10-24 10:26:08 -06:00
gromandClaude Opus 5 f87ef148b8 app+ui: the trinket cooldown — and a stock verb that always said "cold"
Built: GetTrinketCooldown, UseTrinket, GetSpellRangeData,
SetLocalRaidTargetIndex, GetQuestLogQuestIds, GetQuestDialogQuestId,
CombatLogFlush.

The find on the way through is not a nampower verb.
**`GetInventoryItemCooldown` is a stock 1.12 verb and it answered
`(0, 0, 1)` unconditionally** — right shape, always "cold", so nothing
errored and no worn trinket ever drew its cooldown swirl on the
character pane. Stock PaperDollFrame.lua:691 calls it on every slot
update. Feeding an equipped slot's cooldown for GetTrinketCooldown fixes
that one off the same column, and the test runs
`CooldownFrame_SetTimer`'s own arithmetic against it.

The feed keys its query on the (use spell, item entry) pair exactly as
the bag feed does, so one trinket reads the same cooldown from the doll
and from a bag.

**The risk was the gate, and my first test for it was vacuous.**
`feed_char` sits behind a change gate and no descriptor moves when a
cooldown is armed, so `Cooldowns::feed_epoch` had to become a gate
input. The first test passed with that input deleted: `run_system_once`
builds a fresh system each call, so the gate's `Local` memo resets and
`vm_reset` opens the gate every time. A test for a gate input has to run
the system the way the schedule does. Rewritten with `add_systems` +
`update`, it fails with the input removed — verified by probe, and the
reason is written at the test.

`feed_char` also hit Bevy's sixteen-parameter ceiling, which was a fair
signal its parameters wanted grouping by what they are for:
`ItemCooldownInputs` and `RepairInputs` replace six.

Two decisions recorded at the code:

- `GetSpellRangeData(rangeIndex)` answers three values, not a table.
  Every table-returning verb in this DLL has its key names in the string
  pool and this row's are not there. `flags` bit 0 is the melee family,
  where the engine takes the combat-reach sum instead of the authored
  pair — a caller ignoring the bit reads 0..5 for every melee ability.

- `CombatLogFlush()` is a deliberate no-op. Upstream buffers its
  combat-log writes and this empties the buffer, so what an addon asks
  for is "my lines are on disk now" — and `ui_chat/logging.rs` writes
  each line to an unbuffered File as the window shows it, so that holds
  at all times. Defining it is the honest answer: the capability is
  present, and a missing global makes `HasFeature` report it absent.

Also fed, both one wire column each: `QuestState::quest_id` (the wire
names the quest on all three giver panels and 1.12's Lua names it
nowhere — a title is not a key) and `SpellDisplay::stack_amount`'s
neighbour work from the previous commit.

Five names stay out, each now with the field list that makes it a
decision rather than a shrug — `GetUnitData` (80 raw descriptor columns,
read off the pool, zero call sites, and a partial answer is worse than
none), `GetRaidTargets` (the reference keeps a board; this client feeds
the mark per unit, and a scan over named tokens would silently omit a
marked unit), `DisenchantAll` (a server-paced sequencer, which is the
queue), `GetSpellPower`, `StartItemExport` (writes a file — it belongs
with the withheld seven).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
2026-10-24 10:21:31 -06:00
gromandClaude Opus 5 f06caa9968 ui+app+formats: the Spell.dbc row, and seven columns that answered nil
`GetSpellRecField` knew three names. Counting what the corpus actually
asks it for: `name` (4 sites), `school` (3), `stackAmount` (2),
`spellIconID` (2), then one each of `rank`, `rangeIndex`, `mechanic`,
`manaCost`, `effectMechanic`, `effectApplyAuraName`. Seven of those ten
answered nil, and each was a feature quietly off rather than an error —
DoiteAuras caps its stack display on `stackAmount`
(DoitePlayerAuras.lua:539) and SuperCleveRoidMacros classifies a
crowd-control effect off the two arrays (Utility.lua:7776-7794).

The eighty field names are the DLL's own, read off its pool in Spell.dbc
column order. The mapping to `SpellDisplay` lives in
`ui_script/spell_rec.rs`, beside the struct it reads — a copy on the VM
side would be free to drift — and `GetSpellRec(spellId)` is that same
field list walked through that same per-field reader, so the two verbs
cannot answer differently for one name. A test asserts exactly that,
key by key, over one row.

The array columns are a 1-based table of three, which is measured, not
guessed: SuperCleveRoidMacros walks `effectMechanic` as
`for i = 1, 3 do local em = effectMechanics[i]`. `totem`, `reagent` and
`reagentCount` take the same shape, two and eight long.

One new DBC column — `StackAmount`, 39, bracketed by two constants this
tree already had (38 ModalNextSpell, 40 Totem[0]). It is the only one
added: fifteen further names upstream answers are left nil rather than
putting fifteen unused u32s on a struct the spell catalog holds one of
per row. They are listed at `FIELDS` with that reason, so the gap is a
decision rather than an omission.

`GetSpellRecField`'s three string columns keep the spellbook fallback
the verb had before the probe, so a VM with a book and no catalog — a
capture, or the boot VM before the catalog loads — still answers a
learnt spell's name and rank. Every other column is a DBC row and has
nowhere else to come from, which is why the fallback is exactly three
names wide and says so.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
2026-10-24 09:28:05 -06:00
gromandClaude Opus 5 cf0e157f34 ui+app: the bag walk, the modifier fold, and two shapes the DLL's pool corrected
Built: `GetBagItems`, `GetSpellModifiers`, `GetSpellIdCooldown` — plus
fixes to two verbs that already existed and had the wrong *shape*,
found the same way `GetItemIconTexture`'s wrong argument was: by
comparing what we answer against nampower's own string pool.

`GetBagItems()` hands back every occupied bag slot as
`{ [bagIndex] = { [slot] = row } }`. DoiteAuras asks seven times over
and walks both levels with `pairs`, reading `itemId` and `stackCount` —
it is how that addon answers "do I have this item" without 100
`GetContainerItemInfo` calls per condition evaluation. Empty slots and
empty bags are absent, because a nil value in a Lua table *is* absence
and `pairs` is how both levels are read.

`GetSpellModifiers(spellId, modifierType)` answers
`flat, percent, hasModifier`, where percent is the final multiplier (90
means 90% of the original) — the consumer says so in its own words and
multiplies by `percent / 100`. The 26 modifier types come from the
DLL's usage string. The fold is `spell/mods.rs`' existing reader,
reached through a new `spell_modifier_probe` the lifecycle reinstalls
whenever the wire moves the tables: a VM-side reimplementation of a
four-gate 64-bit fold would be free to drift from the mana cost the
action bar greys a button on. The third return is the point — the
consumer tests it before touching either number, and the duration path
multiplies, so `0, 0` for an unmodified spell would zero every debuff
timer that asks.

`GetSpellIdCooldown(spellId)` answers the twenty-key table, from a new
`Cooldowns::legs`: `info`'s three legs kept apart, same walk and same
arms, so the folded number a swirl draws and the legs an addon reads
cannot describe different timers.

**`GetBagItem`'s row had the wrong key names** — `bag`, `slot`,
`count`, `quality`, `link`, `locked`, this client's own
`GetContainerItemInfo` spellings. nampower's row is contiguous at
`0xa8a68` and is the block `GetEquippedItem` already used, so a caller
reading `.stackCount` got nil and no error. One `bag_slot_table` now
serves both, with three columns absent rather than zeroed: this client
resolves a bag slot's enchants to names at the app boundary and keeps
no random-properties id for a container slot, and a zeroed enchant id
would be a claim.

**`GetItemIdCooldown` returned the wrong kind of value** — the stock
`start, duration, enable` triple. At the version we report it is a
table, and the addon layer settles it: `API.GetItemCooldownInfo` and
`API.GetSpellCooldownInfo` return these calls' results *directly* and
their callers index them. `cd.gcdCategoryRemainingMs` is the primary
path of `API.GetGCDRemainingMs`, which the `[gcd]`/`[nogcd]` macro
conditionals divide by 1000 — against a triple that is
`(0.0).gcdCategoryRemainingMs`, which raises. Latent rather than live
(nothing calls those wrappers today), still wrong.

Two things the schedule gates caught and improved:

- The probe refresher started as an exclusive system, which is
  ambiguous against everything in Update it is not ordered against:
  +274 pairs over the ceiling. `NonSendMut` is all it needs.
- Its memo started as a bare `Local`, which `session.rs`' own gate
  refuses — a memo about what this VM has been told must die with the
  VM. As a `VmMemo` it now reinstalls on the first frame after a
  `/reload`, which is better than the reasoning I had written for the
  `Local`: a fresh VM has no closure at all until something installs
  one.

One count the earlier sweep got wrong, corrected in the doc:
`GetSpellPower`'s "7 call sites" are BetterCharacterStats' own method
`BCS:GetSpellPower(school)`, not nampower's global. Nothing in the
corpus calls the global at all. A bare name match is not a call site.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
2026-10-24 09:00:17 -06:00
gromandClaude Opus 5 dc59594389 app+protocol: nampower's ten cast events, off the queue that already had them
All ten ride `ui_cast_events.rs`'s existing `CastEventFeed` rows — the
same rows `UNIT_CASTEVENT` and the cast windows are folded from — with a
new `NpCastColumns` carrying the fields nampower's shapes want and
SuperWoW's one word has no place for. One row per packet, folded once,
which is this file's own rule: an addon woken by one of these and then
asking a getter must not be able to get a contradicting answer, and a
parallel queue over the same packets is exactly how that happens.

`NP_EnableSpellStartEvents` and `NP_EnableSpellGoEvents` go from
stored-only to LIVE. Their rows said the shapes wanted
"itemId/spellType/corpseOwnerGuid columns this client does not carry";
two of those three turned out to be a Spell.dbc lookup and a guid hop
the item-cooldown path already made. `itemId` is the cast item's entry
through `item_entry` (now shared with `item_cooldown`, which had the
same two hops inline) — `SessionEvent::SpellStart` was dropping the
guid the GO always carried, so it carries it now. `spellType` is
`AttributesEx & 0x44` for a channel and the auto-repeat bit for a shot.
`channel_ms` is `SpellDuration.dbc`. The GO's two counts are the
packet's own lists, capped at 255 because the server writes both as
`u8`s. `SPELL_FAILED_SELF`'s result byte is the failing
`SMSG_CAST_RESULT`'s reason — the number 1.12 turns into a red line and
then discards, so no addon on that surface can see why a cast failed.

`corpseOwnerGuid` is the one column that really is absent, and it is
not sent at all: a trailing nil in Lua is indistinguishable from an
absent return, and upstream sends nil for every cast without a corpse
target. For the handful that have one the gap is bounded and written
down — the targets block reads the corpse guid and drops it, and the
owner is a further hop through that corpse's own CORPSE_FIELD_OWNER.

Two subtleties, both pinned by tests:

- A channel's START is queued although its wire cast time is 0, and
  `UNIT_CASTEVENT`'s START is still suppressed for it. Without the row,
  `SPELL_START_SELF` would never fire for Mind Flay or Blizzard — pfUI's
  reader expects exactly that row and falls back to arg7 "only for true
  channels like Blizzard where arg6 is 0/nil". With the word, every
  cast-tracking addon on this realm would get a SuperWoW START whose
  arg5 is 0, which is a cast bar told to run for no time. So the
  suppression moved from the producer to the fire, and SuperWoW's word
  behaves exactly as it did before the row existed.

- The `_SELF`/`_OTHER` split is by caster here and by *target* for the
  aura family next door. Neither is a guess: upstream's wording for the
  aura pair is "casts that land on the active player", and for this one
  pfUI handles both names in a single arm and then reads arg3 to learn
  the caster, which only makes sense if the name already settled it.

A gate hole closed on the way past: `AURA_CAST_ON_SELF`/`_OTHER` were
fired through one `&str` variable, and `reference_ui.rs`'s beyond-1.12
event gate reads the string literal *at the call* — so it had been blind
to that pair for as long as they had existed. Both are literals now,
with their rows, and the probe confirms it: renaming either row makes
the gate name the event.

Tests read pfUI's own `local` bindings back out rather than a list of
values in push order, as the aura-cast block beside them does. A
positional shape got backwards here fails in silence — the handler binds
arg3 to casterGuid whatever arg3 happens to be — and this family is
where that would be most expensive, since pfUI's swing timer resets off
the START and its whole debuff bookkeeping gates on the GO's two counts.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
2026-10-24 08:00:19 -06:00
gromandClaude Opus 5 ecee994ab6 app+ui: the player's own cast state, and SPELL_CAST_EVENT off the commit
`GetCurrentCastingInfo` and `GetCastInfo` are the two most-called
nampower functions in grom's corpus — 25 and 20 call sites outside
`NampowerAPI.lua` — and one of them calls with no existence guard at
all: `local _,_,_,_,_,_,autoattack = GetCurrentCastingInfo()`
(SuperCleveRoidMacros/Core.lua:94), in the frame that keeps clearing the
target while an autoattack is queued. On this client that line raised.

The earlier doc row said the client had "a spell id, a deadline and a
guard bit — no start instant, no channel state, no GCD end". That was
true of `PendingCast` and wrong about the tree: `ui_cast_events.rs`
already folds a cast window per caster off the same packets, with a
start, an end, a channel bit and the accumulated pushback, behind
`C_Spell.UnitCastingInfo`. What was actually missing was the caster's
own machinery — the reference's cast globals, which have no descriptor
field anywhere — and the GCD, which `spell/cooldowns.rs` has run
correctly all along and never published.

So `GetCastInfo` folds two halves: its timing columns come from the
player's own window, its identity and GCD columns from a new per-tick
`SelfCast` snapshot. The feed declares itself after the window drain, so
the two halves can never describe different casts. Added:

- `SelfCast` (benilla-ui) — the inflight spell `0xceca88`, the bar's
  spell `0xceac48`, the auto-repeat key `0xceac30`, casting/channeling/
  onswing/autoattack, the cast counter, the cast type, the target, and
  the GCD's start and end on the GetTime clock.
- `ui_self_cast.rs` (benilla-app) — the push, plus `SPELL_CAST_EVENT`.
- `Cooldowns::gcd` — the running global cooldown as one timer. `info`
  still answers the per-category question every cooldown swirl asks;
  this answers "when does the GCD end", which has no spell to key on
  because its consumers divide it (`API.GetGCDRemainingMs`) or compare
  it against seconds (`[gcd:<1]`).
- `PendingCast`'s cast counter, in the one place every class of commit
  already passes through.

Three decisions where the tempting answer was the wrong one:

- `GetCastInfo` is **nil** when nothing is casting, and an instant's GCD
  does not count. Answering a table whenever the GCD is up would make
  the consumer's fast path always hit — and `API.IsCasting()` *is*
  `GetCastInfo() ~= nil`, so `[casting]` would read true for a second
  and a half after every instant. The wrapper's own comment says what to
  do instead ("GetSpellIdCooldown … works for instant casts").

- `castId` is two different numbers under one upstream name: a counter
  in `GetCastInfo`'s table, beside a separate `spellId`; a **spell id**
  in `GetCurrentCastingInfo`'s returns, which is how both of its readers
  treat it (Init.lua:89 stores it as castingSpellId, Core.lua:5427 hands
  it to C_Spell.GetSpellName). Each is implemented as what its own
  consumer reads, and both are spelled out at the binding.

- `SPELL_CAST_EVENT` fires for commits, not refusals. Upstream's
  `success` can be 0; this client's local refusals funnel through
  `CastErrors`, which carries a spell id and a reason byte and neither a
  cast type nor a target — a row from there would invent two of five
  fields. Nothing in the corpus reads the failing branch
  (Core.lua:5177 is one `if success == 1 then` with no else). Written
  down at the type rather than left to be found.

Tests run the consumers' own bodies transcribed, as `cast_info.rs` next
door does: `API.GetCastInfo` with its `GetCurrentCastingInfo` fallback
arm, `API.IsCasting`, `API.GetGCDRemainingMs`, `API.GetCastRemainingMs`
and `CleveRoids.UpdateCastingState`. A positional seven-return shape and
a ten-key table are only correct relative to the code that reads them.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
2026-10-24 07:16:30 -06:00
gromandClaude Opus 5 aab2130c62 ui+docs: nampower's own registration table, and the icon id it takes
`scripts/dll-surface.py` answers "what might this DLL publish". For
nampower.dll there is a better question with an exact answer: the names
it registers sit contiguously in the string pool, in registration order,
bracketed by the DLL's own log lines — `Registering Custom Lua
functions` immediately before the first, `Registering Glue Lua
functions` before the last two. That is 60 functions plus 2 glue ones,
with no inference at all, where the pointer filter gives a candidate
list a reader still has to judge. The event table (59 names) and the
CVar names sit the same way, just before it.

A compiler emits one string literal per `lua_register(L, "<name>", fn)`
and pools them in source order, so one registration function's names
land in one run, and the log line that opens it is the literal printed
immediately before the first call. Look for the log line, then read
forward until the strings stop looking like identifiers.

Two things fell out of the same pool that corrected code already
written:

- The 44 `Usage:` strings are the argument contract, printed verbatim
  when a call's arguments are wrong. `GetSpellRangeData(rangeIndex)`
  takes a SpellRange.dbc row index, not a spell id, which this doc had
  wrong; `GetSpellModifiers(spellId, modifierType)` enumerates all 26
  modifier constants.

- `GetItemIconTexture(displayInfoId)` takes an ItemDisplayInfo row id,
  the exact sibling of `GetSpellIconTexture(spellIconId)`. Ours took an
  *item* id and resolved the display id itself, which is a different
  function: upstream's addon layer keeps that hop in a wrapper of its
  own (`NampowerAPI.lua:2648` is the pass-through, `:2665` is
  `GetItemIconTextureByItemId`). So a caller handing us a real display
  id got "Item not found in DBCache", and one handing an item id got an
  icon the real DLL would not have answered. Fixed, and the test now
  pins that an item id answers nothing — it populates the item cache on
  purpose so the wrong reading would be visible if it came back.

With the table read, the surface is no longer a count of what a
heuristic flagged: 62 names, each in exactly one bucket (32 built, 8
withheld pending a decision that is not mine, 4 not ours to add, 18 to
build), with the `comm -23` that reproduces it. The two at the head of
the last bucket are the most-called nampower functions in the whole
corpus, and they are coupled: SuperCleveRoidMacros registers
SPELL_CAST_EVENT gated on `GetCurrentCastingInfo` existing, so defining
the function without firing the event would move it from never
registered to registered and silent.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
2026-10-24 06:26:11 -06:00
gromandClaude Opus 5 801bc09c05 ui+docs: SuperWoW's quest-log getters, and the DLL work list worked name by name
`GetQuestIDForLogIndex(index)` and `GetQuestLinkForLogIndex(index)`:
1.12's `GetQuestLogTitle` answers the title and never the id, so neither
the id nor a chat link for a log row is derivable from the stock API. The
id reads the same `quest_log.entries` ClassicAPI's
`C_QuestLog.GetQuestIDForLogIndex` does, so an addon that
feature-detects one and falls back to the other gets one answer; a header
row is id `0` and **no** link, rather than a link to quest 0.

`docs/ADDON_COMPAT.md` now carries the whole work list with a status per
name, and the most useful finding in it is not a missing function:
`SuperCleveRoidMacros/NampowerAPI.lua` is a capability registry whose
`HasFeature` checks the version **and** `_G[name] ~= nil`, so nothing on
the list was broken and raising `GetNampowerVersion` would not have
helped. Defining the global is the only honest way to make one live.

Counts, so the list cannot be read as more alarming than it is: 16 done
across this tranche and the two before it; 12 that were already present
and only looked absent because the script compares against `dump_globals`
on a bare VM (and because an *event* is never a global); 6 that need a
feed, each with the specific thing missing; 3 that are the spell queue
this client deliberately does not have; and 7 withheld.

The seven withheld are `ExportFile`, `ImportFile`, `WriteCustomFile`,
`ReadCustomFile`, `CustomFileExists`, `ExecuteCustomLuaFile` and
`EncryptPassword`. Six of those give addon Lua read, write and **execute**
on files under the client folder and the seventh takes a password. They
are real in nampower and used for config export, but they are a different
kind of surface from a getter and the right default for a client that did
not have them yesterday is not to grow them quietly.

`GetCastInfo` and `GetCurrentCastingInfo` are left out for a reason worth
keeping: their consumers divide by the cast and GCD timings, and this
client has a spell id, a deadline and a guard bit — no start instant, no
channel flag, no GCD end. A table of three real fields and seven zeros is
worse than the nil the addon's own fallback already handles correctly.

269 data-gated failures, identical to the baseline. All nine gates green.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
2026-10-23 21:32:37 -06:00
gromandClaude Opus 5 ada08720bb ui+docs: the GUID token's real spelling, 40-yard interact, and the whole patch map
Working down the 29 `.tdata` hooks from the realm's patched `WoW.exe`.
Most turned out to be already ported; two were wrong here, and one of
those was wrong in a way that mattered.

**The GUID token was the wrong string.** `unit::guid_token` wrote 16
lowercase hex digits and no prefix. The realm writes `0x` + 16 upper-case
digits: its formatter at `0xd06500` — which `UnitExists`'s second return
goes through — lays down `0x30 0x78` then sixteen nibbles each `+0x37`
above 9, and its parser at `0xd06650` demands the `0x` and takes upper
case only. grom's own addons agree from the other end: BigWigs decides a
string is a GUID with `string.sub(guidOrName, 1, 2) == "0x"`
(`MageTools.lua:963`), and AI_VoiceOver reads fields from position 3 and
mints its own with `format("0x%04X%06X%06X", …)`.

Same number, wrong string, and wrong where it mattered most:
`UNIT_CASTEVENT` and the aura events carry these tokens, so every such
test read our GUIDs as unit *names* and every `sub(3, …)` field read came
out two characters off. Now written the realm's way; both spellings and
either case are accepted on the way in, since refusing one buys nothing.
Pinned literally rather than by round trip — a round trip held fine
through the whole bug.

**`CheckInteractDistance` type 4 is 40 yards, not 30.** `0x48bafd` jumps
to `0xd0686c`, which tests the type against 4 and compares against the
float the section carries at `0xd06868`: `1600.0`, squared thresholds.
Stock 1.12's `900.0` is still what the other three types use.

`docs/ADDON_COMPAT.md` now carries all 39 hook sites of both sections
with a status each: 10 in `.octoql` (one feature, the quest log, ported),
and 29 in `.tdata` — the aura ids, the master looter, `UnitExists`'s
GUID, the char-create mask and `CHAT_MSG_HARDCORE` already done; the two
challenge-mode functions needing nothing, because the realm's own
`ChallengeList.lua` defines them in Lua for exactly our case; four read
but unported with the reason (unnamed struct fields, no consumer in the
corpus); and three that cannot apply to a client that loads no DLLs.

Also `scripts/dll-surface.py`, which asks the injected DLLs the same
question. Their export tables are one `DllMain` each, but a DLL that
registers a Lua function must point at the name: collecting NUL-delimited
identifier strings the image holds a pointer to cuts `ClassicAPI.dll`
from 2872 CamelCase candidates to 706, and subtracting the 1.12 census,
Lua's own names and benilla's real surface leaves a candidate list the
addon corpus can rank. It is a list to read, not a specification —
statically linked libraries contribute their own strings, and
`UnitXP_SP3.dll` yields nothing. It found 38 names in `nampower.dll` that
grom's addons call and this file did not know about; they are written
down rather than guessed at.

269 data-gated failures, identical to the baseline, all nine gates green.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
2026-10-23 19:57:26 -06:00
gromandClaude Opus 5 ce8992bc3e Draw flight masters: C_TaxiMap.GetTaxiNodesForMap over TaxiNodes.dbc
grom: "can't see flight master location marks, even tho I have them on."

Nothing was broken — the layer was silently empty. pfQuest-octo has deleted its
shipped pfDB.meta.flight table in favour of this call (db/meta.lua:181 is a stub
whose comment says the data "always comes live from the client (ClassicAPI)"),
and SearchFlightNodes opens with

    if not (C_TaxiMap and C_TaxiMap.GetTaxiNodesForMap) then return maps end

so with the namespace absent the option stays ticked, no error is raised, and no
icon is ever drawn. Same silent-by-construction shape as C_Map.GetAreas, same
consumer.

The contract is half read off the bundled ClassicAPI.dll's string pool
(C_TaxiMap, GetTaxiNodesForMap, nodeID, faction, reachable, state and the
FlightPathFaction/FlightPathState enum names) and half off the caller: the
areaID/mapX/mapY triple is not separately visible there, since the pool holds
one copy of each string and C_Map.GetAreaTriggerInfo already uses all three, so
for those SearchFlightNodes itself is the authority. It reads six fields, gates
on `node.areaID and node.mapX and node.mapY`, and feeds the pair into
pfMap:AddNode as meta.x/meta.y, which UpdateNodes divides by 100 — so they are
percentages of the zone rect. Both files say which half is which.

Faction comes from the two MountCreatureID columns, the reference's own gate
(0x4a2650 asks for the mount of the player's half): one column filled is that
faction, both is neutral, and neither means the row is not a flight master at
all — on this install those are exactly the eight Transport/Ferry/zeppelin
endpoints, and passing them on would put "Flight Master" pins on the Menethil
docks.

Attributing a node to a zone needed the file's own names. Geometry over
WorldMapArea boxes proposes — these are axis-aligned boxes around irregular
zones, so a node near a border sits inside two or three of them — but no purely
geometric rule works: "smallest box" misfiled a dozen of ~70 rows (Crossroads
under Mulgore, Aerie Peak under Alterac Mountains), and "most central box",
much better and what zones_at now sorts by, still loses Talonbranch Glade to
Darkshore by about one percent of inset. TaxiNodes.dbc names its rows
"<place>, <zone>" and a city row leads with the city, which is itself a zone, so
trying each comma-separated part against the candidates gets Ironforge over Dun
Morogh *and* Felwood over Darkshore from one rule. zones_at also reaches the
instance-map list, so Alterac Valley's two nodes place on the battleground map
instead of nowhere.

Seeded before the interface loads, after the map catalog it reads the rects
from. That deadline is tighter than file scope: pfQuest replays its saved
tracking selections once on PLAYER_ENTERING_WORLD and then unregisters every
event (database.lua:131-151), so a push one frame later is a push after the only
read.

Verified against the real addon offline, not just by unit test:
SearchFlightNodes now adds 69 nodes through the probe harness. Three tests pin
the rule against the DBC itself — the capitals and the border nodes by hand, and
every named row held to the zone its own name claims, which is the test that
rejected both earlier geometric rules. It also pins the four rows that name no
zone, three of which are this realm's own additions (Northshire Abbey and two
Darkshore canoe routes), so the next custom node shows up as a failing
assertion rather than a pin in the wrong zone.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
2026-10-22 07:42:22 -06:00
gromandClaude Opus 5 02265cc224 compat: nampower's equipment and item-cache getters, for the character panel
"then we gotta fix bettercharacterpanel, that one is driving me nuts."

BetterCharacterPanel is built on five nampower getters and had none of them.
`GetEquippedItem` was 8 raises in one session of his log; the other four failed
inside the addon's own `pcall`s, so its item-level column, quality colours,
missing-enchant warning and two-hand test were all dead at once. DoiteAuras
reads the same getter in six places.

The spec is the installed `nampower.dll` itself rather than inference, which is
what unblocked this — the earlier note here said to find the upstream spec
first, and the DLL *is* the spec:

  - its usage strings give the signatures,
  - "Slot must be a number between 1 and 19" gives the band, so the slots are
    1.12's own `GetInventoryItemLink` numbering and `NampowerAPI.lua`'s
    `slot - 1` in `IsItemInSlot` is that shim's bug, not the contract,
  - its string pool gives the key names in order, and
  - "Item not found in DBCache" is a raise, which is exactly why the addon
    `pcall`s these three and calls `GetEquippedItem` bare.

`itemLevel` is the one field 1.12 cannot reach at all: `GetItemInfo`'s fourth
value is the *required* level, and later clients are the ones that insert
`itemLevel` there — so the panel's item-level column had no possible source
before this. The cache row now carries it, with `buyPrice` and `displayInfoID`
beside it, and an equipped item carries the raw descriptor numbers the tooltip's
resolved enchant lines could not answer (an id whose `SpellItemEnchantment` row
hides its name is still an id).

Fields left absent rather than guessed are listed in code and in
`docs/ADDON_COMPAT.md`; nothing installed reads any of them.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
2026-10-17 18:57:12 -06:00
gromandClaude Opus 5 13989fb09b messageframe: a scroll that moves the view fires OnMessageScrollChanged
WIM sets this script on every window it builds — the history frame at load, then
one per correspondent (six whisper windows in one session of his log) — and we
refused the name by raise, which killed the rest of the builder function each
time, not just the handler.

It was on the deliberate "real 1.12 kinds nothing fires raise rather than
silently accept" list. That rule is right and this was the wrong side of it: the
slot now fires, from the ScrollingMessageFrame's own scroll verbs when a scroll
actually moves the view, which is what a chat addon hangs its scroll-down arrow
on. A scroll refused at either end moves nothing and says nothing; whether the
reference fires one there is untraced.

The raise list keeps its rule and its test — `OnInputLanguageChanged` and the
movie frame's are still raises, and a name leaves that list only when something
fires it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
2026-10-17 18:14:17 -06:00
gromandClaude Opus 5 15f069667e saved: a widget writes as its name, so a saved frame comes back a frame
Decursive raised `attempt to call method 'AddMessage' (a nil value)` on every
login (`Decursive.lua:1393`). It keeps its output window in its own saved table
and guards the reload with `if Dcr_Saved.Dcr_OutputWindow == nil or not ...`
(`:1245`) — which a non-nil *data* table passes.

Our writer walked the frame like any other table: his `Decursive.lua` came back
holding ChatFrame1's own fields (`buttonSide`, `channelList`, `editBox`, …),
twice the size of the real client's file and with no methods on it at all.

The reference tests the value's `T[0]` identity before its fields —
`lua_rawgeti(v, 0)` then `lua_touserdata`, `0x704613`-`0x704628`, reading the
lightuserdata `0x701bd0` wrote — and never walks a widget. A named one writes as
its **bare global name**, a Lua expression, so the reloaded chunk's own lookup
hands the live object back; an anonymous one as `nil --[[ unnamed object ]]`
(`0x872e0c`). His own real-client file is the proof:

    ["Dcr_OutputWindow"] = ChatFrame1,

Deviation: a name that is no Lua identifier writes as an anonymous object, where
the reference writes it raw and would leave a file that will not parse.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
2026-10-17 18:14:02 -06:00
gromandClaude Opus 5 68138334ed spell_fx: an effect on a host with no attachment points seats at its box top
Reported from play: "ore vein is still not sparkling" — so the switch fix
landed and the sparkle still was not there.

Reproduced offline in the `fxview` GameObject lane (`WOW_CAPTURE=fxview
WOW_FX_GO=310 WOW_FX_GO_TYPE=3`, a copper mining node), which runs the real
chain. Everything upstream works: the gate fires, `attach_spell_fx` spawns the
instance (`kit spawn … Particles\LootFX.mdl`), and `WOW_PARTICLE_CENSUS` finds
all four of LootFX's emitters at the node's position, live, ungated and drawn —
while the captured frame is byte-identical to one taken with the sparkle gated
off. Nothing was missing; nothing was visible.

The attach cascade's last rung is the host's origin, and for a unit that is
right: `0x13` is attachment 19, Base, which creature models author at the ground
point under the feet, and the loot sparkle is a ground twinkle. A 1.12
GameObject model authors no attachment points at all (`copper_Miningnode_01.m2`:
0 — `benilla-m2`'s `dump_attach`), so the cascade fell through to the node's
pivot, which is inside the rock: that model's own 397 vertices run y ∈ [-3.83,
+2.73], and `Particles\LootFX.m2` is a ~1 yd twinkle (four emitters, 0.06–0.14
yd particles, ~0.9 yd reach) with nowhere to go.

So a host with no attachment point seats the effect at the top centre of its own
box. Not its middle — a node is modelled around its pivot, not above it, and
this one's box centre is 0.55 yd underground, which capture-tested as invisible
too, as did half the box height. At the box top the capture differs from the
gated-off one; a peacebloom, whose model is small, already showed at the pivot.

The placement is ours and says so: SuperWoW publishes no source and the
reference has no object arm to copy. It stays subtle by design — row 14 of
`SpellVisualEffectName` scales 1.0 — so it reads as a few twinkles over the node,
not a beacon. If that is not what the real client shows him, the next lever is
the effect's scale, and that one wants his eyes, not mine.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
2026-10-17 16:06:29 -06:00
gromandClaude Opus 5 ed92574cea tooltip: SetOwner hides the plate, so a rebuild gives the addon its OnShow back
Reported from play with a video: hovering a bag item draws the enhanced tooltip
— MasterTradeSkills' recipe block, AdvancedTradeSkillWindow2's Bags/Bank/Total
— for about nine frames of sixty, then the plate falls back to the bare one and
stays bare while the cursor sits still.

The loop is stock. `ContainerFrameItemButton_OnUpdate` re-enters `..._OnEnter`
every `TOOLTIP_UPDATE_TIME` (`ContainerFrame.lua:693` in his own patch-4.mpq;
pfUI's bag slots are `ContainerFrameItemButtonTemplate`, so they run it too),
and each turn is `SetOwner` + `SetBagItem` — a clear that wipes every line an
addon appended. What puts them back in the reference is a hide/show pair, and
we had neither half: the `SetOwner` binding hides the plate before it even reads
the owner argument (`0x531179` zeroes the content gate at `+0xd0`, `0x531183`
calls `[vtable+0x84]` = `0x530a60`, which is the `SetOwner(NULL)` core plus the
base hide `0x76ad50`), and the content `Set*` re-shows it through the tooltip's
own Show override (`0x530a80`). So every rebuild raises `OnHide` then `OnShow`.
Ours cleared in place, and the base show early-outs on an already-visible frame
(`0x76ae3b`), so the plate never hid and the `OnShow` never came.

That is why exactly two addons lost their lines while others kept theirs:
`LibItemTooltip-1.0`, the tooltip contract of MasterTradeSkills and
Gromissingcrafts, hangs entirely on a scratch child frame of `GameTooltip`
listening for `OnShow`, where pfUI's ItemID and sell-price lines hook the `Set*`
methods themselves and so re-run on every refresh.

The reference also clears twice per `SetOwner` (once inside the hide, once in
the core) and so fires `OnTooltipCleared` twice; that falls out of calling the
same hide, and the existing lifecycle test already allowed for it.

Addresses read off the player's own `WoW.exe` with objdump, not from memory.
The new test is the hover loop in miniature — three turns of `SetOwner` + a
content `Set*` under a `LibItemTooltip`-shaped child probe — and fails on the
old behaviour with one `OnShow` instead of three.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
2026-10-17 15:38:36 -06:00
gromandClaude Opus 5 1287c328e0 creature_anim: the loot sparkle's switch starts where its CVar does
Reported from play: no sparkle on a mining vein, while lootable mobs sparkled
fine. That split is the diagnosis — only the GameObject arm reads the switch.

`LootSparkleMode` derived `Default`, so it booted `false` while its row
registered `"1"`. A boot fires `CvarChanged` only for rows `config.toml`
actually carries, and writing `"1"` over a value already `"1"` is a no-op that
fires nothing either, so nothing could ever tell the resource its real default —
not the boot, and not the player toggling the CVar. The feature was inert for
everyone who had never written that row.

`vplates.rs`'s `VPlateMode::default` carries this warning in as many words,
from `NameplateOcclusion` shipping inert the same way, and this is the same
mistake one feature later. So: a hand-written `Default`, and the twin of that
module's test, reading the registered row rather than restating it. Swept the
rest — of the 32 resources a `CvarChanged` observer writes, 6 derive `Default`
and every one of their rows registers `"0"`, so this was the only one.

The doc row kept two stale claims and both are corrected: the test has had two
arms (the activate bit *or* `GAMEOBJECT_TYPE_CHEST`) since it was widened, and
`FxStage` is immaterial here — `Particles\LootFX.m2`, the `"HARDCODED Loot Art"`
row's own model, holds one 2400 ms sequence with `flags = 0`, and bit 0 clear
loops, so `FxAnimLife::arm` repeats it whatever the stage says.

Still unverified in world: whether veins, herbs and chests sparkle, and whether
anything sparkles that should not.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
2026-10-17 13:41:47 -06:00
gromandClaude Opus 5 aeba81ec2e creature_anim: SuperWoW's LootSparkle, the sparkle on lootable treasure
1.12 sparkles a lootable corpse and nothing else — a chest, a herb, a vein or a
quest object is found by eye. Sparkling those arrived in 2.3.0 ("interactable
game objects sparkle and display their names") and SuperWoW backports it as
`LootSparkle`, which upstream describes as "the sparkling effect on lootable
treasure". So on this install the sparkle appears on mobs and not on objects,
which is what was reported.

`arm_loot_fx` grows a GameObject arm beside its unit and corpse ones, hanging the
same "HARDCODED Loot Art" row (0x13) at the same tag, driven by the same field
edges. The three-way decision is a named predicate with a test, because a corpse
falling into the unit arm would read its CORPSE_* bytes as a live unit's, and
because the switch being off has to answer *exactly* 1.12 rather than
approximately it.

The object test has two arms, because the mod's own phrase is "lootable treasure"
and neither arm alone is that:

- `GO_DYNFLAG_LO_ACTIVATE`, the server's per-viewer bit, documented on its own
  field reader as "the sparkle". But vmangos raises it through
  `GameObject::ActivateToQuest`, so on a chest it means *quest* loot: an ordinary
  chest, herb or vein need never carry it, and keying on it alone risks lighting
  up nothing but quest objects.
- `GAMEOBJECT_TYPE_CHEST`, which is exactly chests, herbs and mining nodes —
  treasure by construction, and a type the client reads without the server's
  help. Excluded when `GO_FLAG_IN_USE` or `NO_INTERACT` is set, so an emptied or
  disabled chest stops sparkling.

Both arms are a judgement about what the mod does rather than a trace of it —
there is no source and no published default — so they are written to be read and
corrected, and the predicate's own doc says which symptom indicts which arm: only
quest objects lighting up means the first is doing all the work, doors and signs
lighting up means the second is too wide.

Defaulted on: upstream publishes no default, and the sparkle is present on this
install with SuperWoW loaded, which is what "as if its own default config loaded"
means for this compat layer. `SetCVar("LootSparkle", "0")` is stock 1.12.

**Not verified in world.** The harness has no scene, so the test covers the
decision and not the rendering.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
2026-10-17 09:04:37 -06:00
gromandClaude Opus 5 0515274afc tests: four stale expectations this window's own commits left behind
Four benilla-ui tests were red and had been since the commits that changed what
they pin. They were missed because the round's gate was run as `cargo test -p
benilla-app -p benilla-ui` through a grep that only showed benilla-app's results;
scripts/check.sh is the gate that scopes this correctly.

- SetRaidTarget's third argument became a flag rather than the literal word
  "local" (7098d642), so the test asserting the word is the only form now
  states the contract it replaced: any truthy value is the local mark, which is
  what SoloRaidTargetIcons passes.
- GetStringHeight is installed deliberately (75f04b4f), so the regions surface
  check that asserted its absence had to flip.
- The LOCALIZED_CLASS_NAMES pair (0a907955) was published only once class data
  was fed, leaving the globals absent on a bare VM — which the
  allowed-beyond-1.12 list said we exposed and the creature_info test asserted
  we did not, while its own comment described the behaviour we wanted. Both
  tables are now seeded empty at install, as ClassicAPI publishes them at load:
  tInvert(nil) raises where tInvert({}) is an empty map, and pfUI inverts one in
  its own file scope.
- The AceGUI label test's Lua used a one-argument SetPoint, which 1.12 correctly
  raises Usage: on (LUA_TNONE is not LUA_TNIL — script/tests/anchors.rs pins it,
  and the 1.12 AceGUI backport had every one-argument call rewritten for exactly
  that reason).

And a correction in docs/ADDON_COMPAT.md that the last of those turned up:
GetStringHeight was credited with the empty VoiceOver options, on the strength of
AceGUIWidget-Label.lua's call site and a probe showing every Label at 624x1.
Both were wrong. AI_VoiceOver ships four copies of AceGUI and its TOC loads
1.12\embeds.xml — and the 1.12 tree reads label:GetHeight(), never
GetStringHeight. The 624x1 was the harness's own missing text measurer, the trap
its notes already warn about. The method stays as ClassicAPI parity surface with
the claim corrected; the real cause of that symptom is the scroll-child anchor,
fixed in 75432f7d. The lesson, written into the row: an addon shipping several
library copies has to be read through its own TOC chain before any call site in
it is evidence.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
2026-10-17 07:56:52 -06:00
gromandClaude Opus 5 80648267d5 ui_aura: another unit's aura durations, learned from the GO that applies them
The target frame's debuff icons had no countdown. libdebuff was tracking fine —
the probe run said `libdebuff guids: 2` after a fight, and the AURA_CAST_ON_OTHER
events carried the right lengths (spell 772 Rend, dur 9000) — because the timers
pfUI-for-ClassicAPI draws do not come through libdebuff at all. Its unit frames
read duration/expirationTime straight off C_UnitAuras.UnitAuraBySlot
(api/unitframes.lua:1680,1788), and libdebuff's own header says the plumbing
"now runs on ClassicAPI's C_UnitAuras (which provides sourceUnit/sourceGUID and
non-player expirationTime)". So the number came from us, and we answered 0 for
every unit but the player.

There is no packet to answer from: SMSG_UPDATE_AURA_DURATION is self-only, and
SMSG_SET_EXTRA_AURA_INFO — which carries a caster's durations on others — is a
2.0 opcode this protocol does not have. Hence every vanilla duration addon
keeping its own table. This is that table, kept client-side instead, because
this client *is* the client: SMSG_SPELL_GO names the spell and its hit list, so
the apply instant is the packet's own arrival rather than a combat-log line
somebody has to parse, and every reader of the aura lists gets the timer at once
instead of each inventing a tracker.

Keyed by (guid, spell) so a re-cast refreshes in place and a recycled slot
cannot inherit a stale stamp. Length comes from the same
ui_cast_events::aura_duration_ms behind nampower's arg8 and GetSpellDuration, so
the three cannot disagree, and its 0 — permanent, no SpellDuration.dbc row, or a
combo-point/level-scaled one — stamps nothing: a timerless icon beats one
counting to the wrong moment. The join is driven by the unit's own
UNIT_FIELD_AURA, so a stamp for an aura the target resisted is never read, and a
run-out stamp reports nothing rather than a negative remainder.

Not covered, by construction: an aura already on a unit before we streamed it,
and one applied out of our range. No GO, no timer — which is what the mods this
stands in for also do.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
2026-10-17 07:56:35 -06:00
gromandClaude Opus 5 3d4c242c09 sound: decode addon .ogg, which is most of the addon audio on this install
unitscan's rare alert was reported as silent. The binding and the loose-file read were both
already right: kira was built without the `ogg` container and `vorbis` codec features, so
`sfx_from_bytes` returned an error for every Vorbis file and `sound/ui.rs` logged it at `debug!`.
A silent alert with nothing in the log to say why.

It is not one horn. **75 `.ogg` files ship with the installed addons** — DoiteAuras 57, BigWigs
11, unitscan 3, HCWarn 2, TWThreat and Cleanup one each — so every alert any of them raises by
`PlaySoundFile` was dead, HCWarn's hardcore death warnings included. AI_VoiceOver ships `.mp3`
and always played, which is exactly what made this look like an addon-specific fault rather than
a missing decoder.

Stock 1.12 plays wav and mp3 only. The deviation is from 1.12 and toward the client the player
actually runs: these are Turtle/OctoWoW addons shipping ogg because that client plays it.
`docs/ADDON_COMPAT.md` carries the row.

The test decodes a real `.ogg` out of the player's own addon folder and skips when there is none,
the same gate the chain-backed tests use for a WoW install — rather than carrying someone else's
audio into the tree as a fixture.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
2026-10-16 18:45:25 -06:00
gromandClaude Opus 5 0a90795555 compat: publish LOCALIZED_CLASS_NAMES_MALE/_FEMALE, the table tInvert exists to flip
Half this pair was already here. `ClassInfo::class_file`'s own doc says the token is what
FrameXML keys `RAID_CLASS_COLORS` and `LOCALIZED_CLASS_NAMES_MALE` on, and
`compat/classic_api/lua_ext.rs`'s `tInvert` names pfUI's `pfUI.lua:227` as the reason it exists —
the line that inverts exactly this table. The table itself was never published, so that line
inverted `nil`.

Measured against the live chain: `pfUI.env.L["class"]` holds **0 entries**, and
`L["class"]["Warrior"]` is nil. Every lookup through it then finds no class and skips, silently,
because the consumer tests `if class then` — so the guild roster lost its name colours and class
icons (`modules/socialmod.lua:18`), the friends list lost the same, and `modules/panel.lua:359`
fell back to white. Chat stayed coloured throughout, which is what made it look like a pfUI
setting rather than a missing global.

Published from the class table the app already feeds for `C_CreatureInfo.GetClassInfo`, at that
same feed, so there is one source for the names and no second reader of the DBC. Both tables
carry the same string: 1.12's `ChrClasses.dbc` has one name column, so the gendered pair later
clients ship cannot be derived from this data — a copy is what it honestly supports, and omitting
`_FEMALE` would hand a caller nil where its sibling answers. A run that never feeds the table
leaves both absent rather than empty.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
2026-10-16 18:45:25 -06:00
gromandClaude Opus 5 751cac062b compat: C_Spell.GetSpellEffectMechanics, the per-effect sibling
`SuperCleveRoidMacros/ClassicAPI.lua:267` calls it unguarded, so its absence was a raise a
session. It is not a second spelling of `GetSpellMechanicByID`, which this tree already answers:
that one reads `Spell.dbc`'s spell-level `Mechanic`, and vanilla stores some mechanics on an
*effect* instead. The consumer's own comment names the case — Rake's bleed is effect-level, so
the spell-level column says 0 while this answers `{0, 15, 0}` — and a macro conditional asking
whether its target is bleeding gets the wrong answer from the spell-level field alone.

`SpellDisplay::effect_mechanic` was already parsed for the cast-immunity scan, so this is a field
on the probe payload and a reader, not a new lookup. The three-way answer is its sibling's, for
the sibling's reason: nothing at all for an id no spell owns, three zeros for a real spell that
carries no mechanic anywhere, the ids otherwise — collapsing those two would reclassify crowd
control as ordinary damage, which is the hand-kept table this function exists to replace.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
2026-10-16 18:45:25 -06:00
gromandClaude Opus 5 06011e5e5b ui_script: the LoadOnDemand log line stops claiming "not loaded"
A LoadOnDemand addon can already be in memory when the walk reaches its row, because an addon
ahead of it loaded it from its own `ADDON_LOADED` — which is how every AI_VoiceOver data module
actually arrives: 224 ms of Lua inside AI_VoiceOver's handler, before the walk gets to the
`AI_VoiceOverData_*` rows at all. The line was printed unconditionally, so it went on saying "no
LoadAddOn() yet" about addons that had loaded. That reads as a bug and is not one; it cost a
session chasing a missing `GetAddOnEnableState` that was never the problem. Gated on a new
`UiScript::addon_is_loaded`.

`docs/ADDON_COMPAT.md` now records why that global stays unbuilt, since the evidence was gathered
anyway: upstream ClassicAPI's own `docs/API.md` does not define it either, and AI_VoiceOver gates a
modern-shaped `GetAddOnInfo` on the same `if not GetAddOnEnableState`, so defining it would leave
`VoiceOver.lua:84` reading `loadable` where it means `reason` and pop the "delete your old folder"
dialog on every login.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
2026-10-16 18:45:25 -06:00
gromandClaude Opus 5 75f04b4f28 compat: FontString:GetStringHeight, which AceGUI calls unguarded
1.12 registers no `GetStringHeight` (`script/button.rs`: `0x782390` reads the label's height
instead) and ClassicAPI backports it, so `compat/classic_api/frame.rs` is its home. That file had
it on its own not-built list under "needs per-line layout facts the host measurer is not asked for
today" — true of `GetNumLines` and `GetWrappedWidth`, and not of this one: the whole-string height
is the other half of the number `GetStringWidth` already reads. `measure::current_measure` now owns
the staleness rule for both, so the two cannot disagree about when a measure has gone stale, and a
changed string still reads 0 rather than the previous string's height.

It is not cosmetic. AceGUI-3.0 calls it **unguarded** — `AceGUIWidget-Label.lua:54` is
`height = label:GetStringHeight()` and `AceGUIWidget-CheckBox.lua:90` is
`self:SetHeight(28 + self.desc:GetStringHeight())` — so every Ace options panel on this install
drew its labels and checkbox descriptions at the library's own "avoid zero-height labels" fallback
of one pixel: a window with all of its prose invisible. `addon_harness --probe AI_VoiceOver` builds
that panel's full 32-widget tree with zero errors and every `Label` at `624x1`, which is what
"can't see the actual options in VoiceOver" was.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
2026-10-16 18:45:25 -06:00
gromandClaude Opus 5 81d96af35d Everwood: the HD/OctoWoW line of benilla, imported onto upstream
This checkout had no git history at all, so this one commit carries the whole
of the Everwood work as a single import on top of chairmanpow/benilla-octo's
`main` (706cd71). The two trees have genuinely diverged — each has files the
other does not — so this is a fork point, not a catch-up.

Today's work, which is the reason a baseline exists at all:

- Booth previews no longer cast world shadows (`NO_BOOTH_SHADOW`). Portrait
  characters were entering the sun's shadow cascades, so every skinned-rig
  shadow specialization compiled live mid-frame; the hitch detector logged
  "main thread blocked 1316 ms" beside a `compiled LIVE` warning. This was
  the Fashion/Tmog stutter.
- Per-pane dress-up: each `DressUpModel` pane assembles and stages its own
  tried-on look instead of every pane mirroring the live player.
- Item template cache is append-only JSONL written on the IO pool, replacing
  an 11.6 MB whole-map JSON rewrite on the main thread every 10 s.
- `WOW_SYSTEM_TIMING` rebuilt per-thread: it took 2 mutexes per system enter
  and 4 per exit, costing 15 ms a frame (590 fps -> 60) and corrupting the
  measurements it was taken to produce.
- The visibility-churn sweep moved to its own `WOW_FLASH_CHURN` switch, so a
  frame-cost reading no longer pays 2.7 ms a frame for it.

Local state (`benilla-config/`, `logs/`, `target/`) is excluded by the
existing .gitignore; the account credential it holds is not in this commit.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
2026-10-15 15:01:14 -06:00