fix(spells): load complete retail DAT catalog

Replace the incomplete 3,956-row production CSV with one immutable projection of all 6,266 records in portal.dat. Preserve retail formula targeting, shared Spellbook/MagicRuntime metadata ownership, and fail startup when the required SpellTable is absent.

Co-authored-by: OpenAI Codex <codex@openai.com>
This commit is contained in:
Erik 2026-07-15 21:17:13 +02:00
parent 09612f9981
commit 0eab7497c1
19 changed files with 633 additions and 114 deletions

View file

@ -0,0 +1,91 @@
# Retail spell catalog projection
## Scope and oracles
The client behavior oracle remains the September 2013 named retail binary:
- `CSpellTable::UnPack @ 0x00597550`
- `CSpellTable::InqSpellBase @ 0x005670B0`
- `CSpellBase::UnPack @ 0x00597290`
- `CSpellBase::InqSpellFormula @ 0x00596EB0`
- `CSpellBase::InqTargetType @ 0x00597230`
- `CSpellBase::IsUntargeted @ 0x005974A0`
- `CSpellBase::InqSpellLevelByRoughHeuristic @ 0x00597260`
- `SpellFormula::GetTargetingType @ 0x005BC910`
- `SpellComponentTable::GetTargetTypeFromComponentID @ 0x005BBF50`
The installed end-of-retail `client_portal.dat` is the content oracle. Its
SpellTable DID `0x0E00000E` contains 6,266 spell records. The historical
`docs/research/data/spells.csv` contains only 3,956 records and omits 2,310
spell IDs which the server can legitimately teach a character. It is retained
only as a research fixture and is no longer copied into production output.
ACE's `SpellBase` and `SpellComponentsTable.GetSpellWords` ports were used as
independent schema/decryption and spell-word cross-checks (ACE commit
`650c5b75ae909957feaf58db320e46be16502653`). `DatCollection` remains the sole
DAT reader in acdream.
## Catalog projection
```text
LoadMagicCatalog(dats):
componentTable = dats.Get(0x0E00000F)
spellTable = dats.Get(0x0E00000E) or fail startup
for each (spellId, CSpellBase) in spellTable, preserving every record:
formula = the decrypted component list, at most eight SCIDs
formulaTarget = GetTargetingType(formula)
metadata[spellId] =
name, description, school, icon, category, flags
base mana/range, power, economy and component loss
formula version and decrypted components
meta-spell type and duration/degrade/portal fields
caster/target/fizzle effects and recovery fields
display order, non-component target type and mana modifier
spell words derived from Herb + Powder + lower-cased Potion
spellbook level from the rough power-component heuristic
untargeted = (formulaTarget == 0)
return one immutable SpellTable shared by Spellbook and MagicRuntime
```
Learned spell IDs remain server-authoritative. `PlayerDescription` and live
spell events populate `Spellbook`; the local DAT table supplies presentation
and formula metadata for those IDs. Installing metadata refreshes any learned
or enchantment state that arrived before Silk's `OnLoad`, and catalog
replacement is rejected after the one startup install.
## Formula target type versus server target field
These are deliberately separate concepts:
- Retail client target compatibility calls `CSpellBase::InqTargetType`, which
decrypts the formula and calls `SpellFormula::GetTargetingType`.
- `CSpellBase::_non_component_target_type` is also preserved from DAT. ACE uses
it for authoritative server-side target validation and redirect behavior.
Static disassembly of the matching retail executable confirms the native
vtable offset hidden by the pseudo-C. `GetTargetingType` starts with component
slot 4, scans slots 5 through 7 until the formula ends, and maps the final
populated component. It does not search for the first component with a known
target mapping.
```text
GetTargetingType(components[8]):
if component slot 4 is absent: return 0
last = 4
while last < 7 and components[last + 1] != 0:
last++
return GetTargetTypeFromComponentID(components[last])
GetTargetTypeFromComponentID(component):
0x31..0x38, 0x3C..0x3E, 0xBE => 0x00000010
0x39 => 0x00088B8F
0x3B => 0x10010000
otherwise => 0
```
The old CSV's exported target mask disagrees with this retail formula result
for 378 of its 3,956 shared spell IDs. Production therefore never overlays CSV
target values onto the DAT catalog.

View file

@ -1,29 +1,29 @@
# Game-data tables
Reference data tables loaded by acdream at runtime or used as research
input. Tracked in git so subagents + post-compaction sessions inherit
them without round-tripping through `refs/`.
Reference data tables used as research input and focused test fixtures.
Tracked in git so later sessions inherit them without round-tripping through
external repositories. Production spell metadata comes directly from the
installed retail DATs through `DatCollection`.
## Contents
| File | Loader | Notes |
|------|--------|-------|
| `spells.csv` | `src/AcDream.Core/Spells/SpellTable.cs` (load via `SpellTable.LoadFromCsv` at `GameWindow.OnLoad`). Copied to `bin/<config>/net10.0/data/spells.csv` via `<None Update>` in `AcDream.App.csproj`. | 3,956 retail spells × 35 columns. Key fields: `Spell ID`, `Name`, `School`, `Family` (buff stacking bucket — issue #6 needs this), `IconId [Hex]`, `Mana`, `Duration`, `IsDebuff`, `IsFellowship`, `Description`. |
| `spells.csv` | Legacy `SpellTable.LoadFromCsv` test/tool input only; not copied to production output. | Historical 3,956-row server-side export. The installed end-of-retail SpellTable contains 6,266 records, so this file must never be treated as a complete client catalog. |
## Format note — `spells.csv`
RFC 4180 with header row. The `Description` column is quoted with embedded
commas; everything else is plain. See `SpellTable` parser for the canonical
column ordering.
commas; everything else is plain. See `SpellTable` for the legacy parser.
## Provenance
`spells.csv` came from a server-side database export of the retail
spell table (likely a community-shared dump from circa 2017-2019 with
~99% field coverage). Spell IDs match the wire opcodes used by ACE.
The `Family` column is the AC retail spell-family enum used for buff
stacking.
`spells.csv` came from a server-side database export of the retail spell table,
likely a community-shared dump from circa 20172019. Spell IDs match the wire
values used by ACE, but the file omits 2,310 records present in the installed
end-of-retail DAT and some exported target masks differ from the retail
client's formula-derived result.
If the file ever needs updating from a fresher source, replace
`spells.csv` in this directory; the `<None Update PreserveNewest>`
copy will sync it to bin output on next build.
Do not update production behavior by replacing this file. The canonical
runtime projection and its retail references are documented in
`docs/research/2026-07-15-retail-spell-catalog-pseudocode.md`.