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
This commit is contained in:
2026-10-24 09:28:05 -06:00
co-authored by Claude Opus 5
parent cf0e157f34
commit f06caa9968
11 changed files with 692 additions and 50 deletions
+39 -2
View File
@@ -726,6 +726,43 @@ in the corpus calls the global at all — the only mention is a comment in
`SuperCleveRoidMacros/ClassicAPI.lua:272` saying it reads the same client field. A bare name match
is not a call site.
### The `Spell.dbc` row — `GetSpellRec` and seven columns `GetSpellRecField` answered nil for
**Built**, and this one is a measured gap rather than a completeness exercise. Counting what the
corpus actually asks `GetSpellRecField` 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** — the binding knew
three names. 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`), so each nil was a feature quietly off.
The names are the DLL's own, read off its pool in `Spell.dbc` column order (`0x91d68`–`0x92460`):
eighty of them. The mapping to `benilla_formats::SpellDisplay` lives in
`benilla-app`'s `ui_script/spell_rec.rs` — beside the struct it reads, because 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 and not guessed.**
`SuperCleveRoidMacros` walks `GetSpellRecField(spellID, "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`, column **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 `u32`s 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 and not an
omission: `attributesEx4`, `targetCreatureType`, `procFlags`, `procCharges`,
`manaPerSecondPerLevel`, `maxTargetLevel`, `maxAffectedTargets`, `dmgClass`, `spellPriority`,
`spellVisual2`, `minFactionId`, `minReputation`, `requiredAuraVision`,
`effectPointsPerComboPoint`, `dmgMultiplier`.
`GetSpellRecField`'s three string columns keep the spellbook fallback the verb had before the
probe: a VM with a book and no catalog — a capture, or the boot VM before `ui_action`'s loader
runs — still answers a learnt spell's name and rank, which is what `GetSpellNameAndRankForId`
beside it does from the same `spell_display`. Every other column is a DBC row and has nowhere else
to come from.
### The whole registered list, accounted for
With the registration table read (the section above), the surface is no longer a count of what a
@@ -742,10 +779,10 @@ comm -23 /tmp/np /tmp/ours
| Bucket | Count | Names |
|---|---|---|
| **Built** | 37 | `CancelPlayerAuraSlot`, `CancelPlayerAuraSpellId`, `CastSpellByName`, `CastSpellByNameNoQueue`, `CastSpellNoQueue`, `FindPlayerItemSlot`, `GetBagItem`, `GetBagItems`, `GetEquippedItem`, `GetEquippedItems`, `GetItemIconTexture`, `GetItemIdCooldown`, `GetItemLevel`, `GetItemStats`, `GetItemStatsField`, `GetNampowerVersion`, `GetPlayerAuraDuration`, `GetSpellDuration`, `GetSpellIconTexture`, `GetSpellIdCooldown`, `GetSpellIdForName`, `GetSpellModifiers`, `GetSpellNameAndRankForId`, `GetSpellRecField`, `GetSpellSlotTypeIdForName`, `GetTrinkets`, `GetUnitField`, `GetUnitGUID`, `IsAuraHidden`, `IsSpellInRange`, `IsSpellUsable`, `PlayerIsMoving`, `PlayerIsRooted`, `PlayerIsSwimming`, `SetMouseoverUnit`, `UseItemIdOrName` |
| **Built** | 38 | `CancelPlayerAuraSlot`, `CancelPlayerAuraSpellId`, `CastSpellByName`, `CastSpellByNameNoQueue`, `CastSpellNoQueue`, `FindPlayerItemSlot`, `GetBagItem`, `GetBagItems`, `GetEquippedItem`, `GetEquippedItems`, `GetItemIconTexture`, `GetItemIdCooldown`, `GetItemLevel`, `GetItemStats`, `GetItemStatsField`, `GetNampowerVersion`, `GetPlayerAuraDuration`, `GetSpellDuration`, `GetSpellIconTexture`, `GetSpellIdCooldown`, `GetSpellIdForName`, `GetSpellModifiers`, `GetSpellNameAndRankForId`, `GetSpellRec`, `GetSpellRecField`, `GetSpellSlotTypeIdForName`, `GetTrinkets`, `GetUnitField`, `GetUnitGUID`, `IsAuraHidden`, `IsSpellInRange`, `IsSpellUsable`, `PlayerIsMoving`, `PlayerIsRooted`, `PlayerIsSwimming`, `SetMouseoverUnit`, `UseItemIdOrName` |
| **Withheld pending grom's say-so** | 8 | `WriteCustomFile`, `ReadCustomFile`, `CustomFileExists`, `ImportFile`, `ExportFile`, `ExecuteCustomLuaFile`, `StartItemExport` (writes a file of its own), `EncryptPassword` + `EncryptedServerLogin` |
| **Not ours to add** | 4 | `QueueSpellByName`, `QueueScript`, `ChannelStopCastingNextTick`, `LearnTalentRank` |
| **To build, in corpus-call order** | 13 | `DisenchantAll` (5 call sites outside the API layer), then `GetSpellPower`, `GetSpellRec`, `GetTrinketCooldown`, `UseTrinket`, `GetUnitData`, `GetSpellRangeData`, `CombatLogFlush`, `GetRaidTargets`, `SetLocalRaidTargetIndex`, `GetQuestLogQuestIds`, `GetQuestDialogQuestId`, `StartItemExport` (0 each — present for completeness, not need) |
| **To build, in corpus-call order** | 12 | `DisenchantAll` (5 call sites outside the API layer), then `GetSpellPower`, `GetTrinketCooldown`, `UseTrinket`, `GetUnitData`, `GetSpellRangeData`, `CombatLogFlush`, `GetRaidTargets`, `SetLocalRaidTargetIndex`, `GetQuestLogQuestIds`, `GetQuestDialogQuestId`, `StartItemExport` (0 each — present for completeness, not need) |
The two that were at the head of that last bucket are now built — see "The cast feed" above for
why they came first and what each decision cost.