Spell table Confirmed
Cast Gimard and the banner names it, 10 MP drain away, and one enemy takes the hit. Name, cost and target all come from one static table inside the game executable: 190 records of 12 bytes, one shared numbering that covers enemy attacks, the party's Seru magic and the Ra-Seru summons. The one thing the table does not carry is a damage number - a spell's magnitude is computed from battle state at cast time.
At a glance
- Where
SCUS_942.54, RAMDAT_800754C8; the name pointers are read through the shifted baseDAT_800754D0(= stats base + 8)- Size
- 190 records (ids
0x00..=0xBD), 12 bytes each. Ends exactly where the description-pointer table0x80075DB0begins - Parser
legaia_asset::spell_names(SPELL_COUNT = 190); CLIasset spell-names <SCUS> [--json]; engine mirrorengine-core::retail_magic- Confidence
- Confirmed - readers traced in the battle and menu overlays; the curated magic chart joins byte-exact on MP
- Used by
- Battle-action SM (cast + banner), field menu (descriptions), spell-cost slider, Magic viewer
Record layout
Record id sits at base + id × 12. Every record has the same shape:
| Offset | Size | Field | Meaning |
|---|---|---|---|
+0 | u8 | cast class | 0x32 player summon, 0x14 plain cast, 0x63 ('c') capture-class module (below) |
+1 | u8 | sub-index | Within the class; for capture-class records the module index (PROT 935 + sub) |
+2 | u8 | target shape | Side / scope bits (below) |
+3 | u8 | MP cost | Deducted from the caster when the cast starts |
+4 | u8 | description index | Into the pause-menu description pointer array; 0 = none |
+5 | 3 | padding | Zero on every record - there is no power byte hiding here |
+8 | u32 | name_ptr | Display-name C string, prefixed by a colour-control code (0xCE, element colour, space) |
The 12-byte walk does not stop at record 0xBD; only the reader bounds it. A tool that sweeps ids up to 255 walks straight into the neighbouring tables, and a write there corrupts the description pointers and the arts-name table on a patched disc. SPELL_COUNT is 190 (reading past the extent).
Id ranges
| Ids | Contents |
|---|---|
0x00..=0x24 | Internal enemy-attack tiers - MP / target populated, empty name, no description |
0x25..=0x7F | Named monster attacks (Fire Breath 0x25, Tail Fire 0x27, …) and capture-class spells |
0x80 | Boundary entry (MP and description both 0) |
0x81..=0x95 | Player Seru magic, Gimard … Gilium. The first 11 (0x81..=0x8B) are the engine-pinned block retail_magic::SERU_MAGIC |
0x9A..=0xA0 | Ra-Seru summons - Palma, Mule, Horn, Jedo, Meta, Terra, Ozma (the hidden eighth, Juggernaut, sits outside this run) |
0xA1..=0xBD | Boss cinematic casts and late capture-class spells (Dead End Crisis 0xA1 … Jugger Power 0xBA) |
Player Seru-magic block (0x81..=0x8B)
MP and target are byte-exact from the executable; element comes from the curated game-data chart, which matches on every MP value.
| Id | Name | Element | MP | Target |
|---|---|---|---|---|
0x81 | Gimard | fire | 10 | one enemy |
0x82 | Theeder | thunder | 24 | one enemy |
0x83 | Vera | light | 6 | one ally |
0x84 | Gizam | water | 28 | all enemies |
0x85 | Nighto | dark | 13 | one enemy |
0x86 | Zenoir | fire | 36 | one enemy |
0x87 | Viguro | thunder | 64 | all enemies |
0x88 | Swordie | wind | 32 | one enemy |
0x89 | Orb | light | 18 | all allies |
0x8A | Freed | water | 40 | all enemies |
0x8B | Nova | wind | 48 | one enemy |
The whole player block shares class = 0x32, sub = 0: the table cannot tell Gimard from Nova, which is the first sign that damage lives elsewhere. Which Seru teaches which id is wired in engine-core::seru_learning::SeruRegistry::retail.
Target shape (+2)
Two independent bits: 0x02 = ally side (clear = enemy side), 0x20 = every target on that side (clear = single).
| Value | Shape |
|---|---|
0x44 | one enemy |
0x64 | all enemies |
0x06 | one ally |
0x26 | all allies |
0x04 | one enemy (the internal enemy-attack tiers) |
One exception: the revive summon Horn ("Resurrector", 0x9C) carries an enemy-side 0x24 even though it revives all allies - the summon projects toward the enemy field and the revive is special-cased by spell id. Decoder SpellEntry::target_shape.
History: the "bit 0x40 = enemies" reading
Equivalent for the four player-block values but wrong for the internal enemy-attack tiers, whose byte is 0x04: bit 0x40 clear yet enemy-targeting. The 0x02-bit reading classifies them correctly. The curated chart's one target error (Mushura / "Crazy Driver" = single enemy) was corrected from this byte.
Cast classes (+0)
The class byte chooses which band of the battle-action state machine runs the cast:
+0 | Class | What happens |
|---|---|---|
0x32 | Player summon | Summon band; pages a per-summon overlay (PROT 903..) that spawns the Ra-Seru body and applies damage |
0x14 | Plain cast | Ordinary magic band with the caster's animation (Tail Fire, Astral Wave, …) |
0x63 | Capture-class | Pages the per-spell cast module PROT 935 + sub and starts its voice cue. Covers Seru capture, the Amulet, and the boss cinematic casts; the module carries its own baked damage constants |
Because the sub-index is static table data, the identity of every capture-class module is readable from the executable alone (spell_names::capture_class_records). The 32 modules (PROT 935..966) are shared: a multi-spell module dispatches per spell id at its head.
Capture-class module index (PROT 935..966)
| PROT | Spells | PROT | Spells |
|---|---|---|---|
| 935 | 0x4A Earthquake | 951 | 0x36 Chaos Flare; 0x5B Scythe Wind |
| 936 | 0x4B Hyper Crush | 952 | 0x5C Bloody Horns; 0xB8 Astral Slash |
| 937 | 0x4C Hyper Lightning | 953 | 0x5D Terio Punch; 0x5E Bull Charge |
| 938 | 0x4E Chaos Breath; 0xB7 Mystic Circle | 954 | 0x5F Fatal Decision |
| 939 | 0x4F Spore Gas | 955 | 0x60 White Shield; 0x6E Kiss of Death; 0x6F Melt Spray; 0x70 Terror Scream; 0x72 Power Charge; 0x73 Void Accessories |
| 940 | 0x3C Glare; 0x50 Divide; 0xAC Mystic Shield; 0xAE Clone | 956 | 0x71 Water Hazard; 0x75 Paralyzing Wave |
| 941 | 0x51 Steal; 0xB9 Stone Circle | 957 | 0x76 Death Game; 0x77 Thunder Storm |
| 942 | 0x52 Power Up; 0xAA Dark Typhoon | 958 | 0x79 Blazing Slash |
| 943 | 0x40 Curse; 0xB5 Lapis Wave | 959 | 0x7A Megaton Press |
| 944 | 0x37 Guilty Cross; 0x53 Curse All | 960 | 0x7B Plasma Strike; 0xA6 Neo Star Slash |
| 945 | 0x54 Water Column; 0xBA Jugger Power | 961 | 0xA1 Dead End Crisis; 0xB4 Final Crisis |
| 946 | 0x55 Call Wave; 0x56 Big Wave | 962 | 0xA2 Blade Breath; 0xA3 Thunder Needle; 0xA4 Gigaton Press; 0xA5 Ultra Charge |
| 947 | 0x57 V-Windhash; 0xA7 Neo Windhash | 963 | 0xB3 Genocidal Cannon |
| 948 | 0x58 Cross Beam | 964 | 0xAF Element Change; 0xB0 Rogue Wind; 0xB1 Rogue Thunder; 0xB2 Rogue Flame |
| 949 | 0x59 Water Crystals | 965 | 0xB6 Doomsday |
| 950 | 0x5A Rolling Flare; 0xAB Shadow Break | 966 | 0xAD Evil Seru Magic |
The sub-id space covers 935..=966 with no orphan slots. The status-only modules (940, 954, 955) are exactly the cells with no damage spells. An older attribution of 965 as a sibling of the battle-tutorial overlay was an entry-size over-read (do-not-re-walk).
How an enemy's cast lands in the same id space
An enemy is named exactly like a party caster. Two selection mechanisms both end in the same byte on the live actor, and the banner reads the row that byte names:
monster_archiveengine-vmbattle-actionspell_namesThe monster archive's own local action ids (0x0C..=0x1F) only gate action cost - they are not this table's id space. Two spells, Curse 0x40 and Curse All 0x53, are sourced by neither mechanism and are casterless in retail.
Notable special-cast switch cases
| Formation id | Casts queued | Trigger |
|---|---|---|
0x4B Zeto | Call Wave 0x55 → Big Wave 0x56 via a chain cell | 40% + MP ≥ 100 |
0x8B Xain | Bull Charge 0x5E → Terio Punch 0x5D; Bloody Horns 0x5C from the record array | rand % 3 |
0xA2..0xA4 Gi / Che / Lu Delilas | spell = formation id − 0x29 (Blazing Slash / Megaton Press / Plasma Strike) | every third round |
0xA6 Sim-Seru Gaza | Neo Star Slash 0xA6 | odd rounds, MP ≥ 200 |
0xA8 Rogue | Element Change 0xAF, then Rogue Wind / Thunder / Flame | element-cycling counter |
0xB3 Songi (Seru-Kai) | Genocidal Cannon 0xB3 | two-turn charge |
0xB4..0xB6 Cort forms | Evil Seru Magic / Mystic Circle / Mystic Shield; Ultra Charge → Final Crisis, Doomsday; the Dead End Crisis ladder | round-scripted |
| species bands | Steal, Power Up, White Shield, Rolling Flare, Power Charge, Void Accessories, Paralyzing Wave, Death Game, Thunder Storm, Stone Circle, Chaos Breath, Jugger Power, Lapis Wave, … | HP-fraction / cadence gates |
The three Delilas jump-table slots share one arm that computes the spell id arithmetically from the formation id; the whole mapping is the single instruction word 0x2442FFD7 (addiu v0,v0,-0x29) at PROT 0898 file offset 0x1CFFC. Two id spaces collide on 0xB5 (spell Lapis Wave, formation Cort); the branch at 0x801E6D04 is a formation test, not a spell test - do-not-re-walk.
Descriptions and names
The pause menu's spell info window reads the +4 byte and indexes a flat pointer array at 0x80075DB0; index 0 means no description. Player-block strings are a title line, a line break, then an effect line. Parser SpellEntry::desc.
Each name string is followed by NUL bytes up to the next word boundary - that padding is the whole edit budget for a rename, and it is word alignment, not a fixed slot (a 13-byte name has three spare bytes, a 15-byte name one). Names nest - a text search for one finds the longer name that contains it - so the only safe way to rewrite one is through its record's pointer (spell_names::name_field).
Reading past the extent
Both neighbouring tables start on exact multiples of the 12-byte stride, so an over-read lands on whole words:
| Ids | What the walk lands in |
|---|---|
0xBE..=0xD4 | the description-pointer table |
0xD5.. | the arts-name table (20-byte stride, name pointer at +0xC) |
Whenever the arts index is 1 mod 3, the arts name pointer and a phantom spell record's name pointer are the same four bytes (spell 0xD7 = arts record 1, Burning Flare). A rename through either coordinate is one write. asset spell-names sweeps 256 ids for inspection, so the aliased rows show above 0xD4; the patcher stops at 190.
History: "+4 is an animation id"
The +4 byte was once recorded as an animation id. The summon animation is dispatched by spell_id - 0x81, and the menu overlay's info window pins +4 as the description index.
Where the damage comes from
The table carries no power field. The cast state reads only MP, the class byte and the name pointer; the static attack-vs-defence kernel is melee / arts only. Magnitude is computed at cast time by whichever overlay runs the spell:
- Damage summons (PROT 0904 / 0912 / 0914 and one arm of 0915) roll
rand % (summon AGL + 1) + summon HP + caster AGL × 2against a defender roll, then apply element affinity, status weakening and the 9999 cap - the kernels on the battle-formulas page, ported inengine-vm::battle_formulas. - Heal summons (PROT 0903 / 0905 / 0910 / 0911 / 0913) compute
(power_byte << 5) + 0xE0, with the power byte read from the caster's character record. - Capture-class casts carry baked constants in their cast module.
- The per-move power scalar that does exist - the move-power table - feeds the arts / physical branch and shares this id space (
0x25..=0x74are the named monster attacks).
The base_power figures in engine-core::retail_magic are MP-scaled placeholders until the summon roll runs in a live battle context.
History: the summon jump-table hypothesis
"The magnitude is in the summon jump table" is falsified: the table at 0x801F69D8 resolves into the resident render overlay - staggered entry points into one per-frame routine that blends animation banks and emits GPU packets, with no multiply and no HP write. The 0xC5-entry substitution table at DAT_80075EC4, once mistaken for a spell-name source, is the Tactical Arts name table. See do-not-re-walk.
How we know
| Function | Address | What it proves | Dump |
|---|---|---|---|
Battle-action SM, cast states 0x28 / 0x3C | FUN_801E295C (PROT 0898) | MP at +3, class at +0, name via DAT_800754D0 + id*0xC; no power read | overlay_battle_action_801e295c.txt |
| Enemy AI picker | FUN_801E9FD4 (PROT 0898) | Record array + formation switch both write the actor action byte; Delilas arm at 0x801EB7C0 | overlay_battle_action_801e9fd4.txt |
| Menu spell info window | FUN_801D2E74 (PROT 0899) | +4 indexes the description pointers at 0x80075DB0 | overlay_menu_801d2e74.txt |
| Melee kernel | FUN_801EC3E4 | Returns early unless the queued action is a physical id - magic never reaches it | overlay_battle_action_801ec3e4.txt |
| Summon damage kernels | FUN_801DD0AC / FUN_801DD864 / FUN_801DDB30 | Summon magnitude is battle-state-derived | overlay_battle_action_801dd0ac.txt + siblings |
| Overlay pager | FUN_8003EC70 | Capture-class sub + 0x28 and summon id - 0x79 resolve to PROT 935.. / 903.. | 8003ec70.txt |
| Disc-gated tests | spell_catalog_disc, magic_vs_disc | All 11 pinned targets + MP byte-exact; curated chart joins on MP for 21 Seru + 7 Ra-Seru | crates/engine-core, crates/gamedata |
Source of record: docs/formats/spell-table.md.