Effect bundles Confirmed
The data behind Legaia's battle visual effects - hit sparks, spell flames, summon flourishes. An
“effect” is a small script that spawns child sprites, each a rectangle cut from a shared texture
atlas, and animates them frame by frame. Two formats share the name: the runtime 2-pack in
efect.dat, which battle code consumes every frame and is most of this page, and a separate on-disc
bundle (magic 0x02018B0C) found in exactly one entry.
At a glance
- Where
efect.dat= extraction PROT 0873, one of the fourbefect_datafiles 0870..0873 (etim/etmd/vdf/efect); the0x02018B0Cbundle only in0000_init_data- Magic
- 2-pack: none - two absolute pack offsets behind an 8-byte header. Bundle:
0x02018B0C - Runtime copy
- post-init buffer at
_DAT_8007BD5C, byte-identical to the disc entry; live pool at_DAT_8007BD30(5008 bytes) - Parser
engine_vm::effect_vm::EffectCatalog::from_efect_dat_bytes(2-pack);legaia_asset::effect_bundle(bundle);legaia_asset::befect_cluster(the four files)- Confidence
- Confirmed - every record field read off its battle-overlay consumer; texel source pixel-verified against battle captures
- Used by
- Effect VM, Battle action SM, move-power effect lists
etim pages.The runtime 2-pack (efect.dat)
When a hit lands, the battle overlay looks up an effect id, spawns a handful of child sprites around the impact point, and animates each from a short frame list. Both packs are offset tables of absolute file offsets - not the word*4 offsets of asset::pack. The retail file holds 144 atlas entries, 14 animation batches and 33 scripts.
| Offset | Size | Field | Meaning |
|---|---|---|---|
+0 | u32 | pack0_offset | absolute offset of pack0; fixed up to a pointer on first init |
+4 | u32 | pack1_offset | absolute offset of pack1; same fixup |
+8 | N × 8 | sprite atlas | VRAM rectangles, up to pack0 |
pack0 | u32 count + u32[count] | anim batches | frame-batched animation records |
pack1 | u32 count + u32[count] | effect scripts | one per effect id |
The three record kinds
| Record | Offset | Field | Meaning |
|---|---|---|---|
| atlas entry (8 B) | +0..+3 | u, v, w, h (u8) | texel rectangle (u, v)..(u+w-1, v+h-1) inside the texture page |
+4 | clut (u16) | CLUT id, copied into the primitive's CLUT field | |
+6 | tpage (u8) | texture-page byte, copied into the primitive's tpage field; +7 reserved | |
| anim batch (pack0) | +0, +1 | frame_count, flags | |
+2 | frames, 6 B each | +0 atlas index; +1..+5 timing / direction bits | |
| effect script (pack1) | +0, +1 | child_count, flags | flags bit 0 = random child distribution |
+2 | spread (u16) | half-range for random child position (8.8 fixed) | |
+4 | children, 14 B each | +0 u16 sprite_id (pack0 index); +2 i16 X half-width; +4 u16 anim flags; +6 i16 Z half-width; +8 6 B curves / sound / timing |
Field order matters. The u16 at atlas +4 is the CLUT, the byte at +6 the tpage. The often-cited 0x7680 is therefore a CLUT - it decodes to framebuffer (0, 474), an effect-CLUT row - not a texture page. The real page byte is e.g. 0x25 = page (320, 0), 4bpp.
The battle effect cluster (befect_data)
efect.dat is one of four dev-named files the battle scene loader pulls in sequence. The loader opens each by dev-path string, which is a trap stub on this build, so the load resolves through the raw TOC index (raw = extraction + 2; see CDNAME numbering).
| Loader case | Dev name | Raw / extraction | Content (byte-checked) |
|---|---|---|---|
0x8 | etim.dat | 0x368 / 0870 | 3-TIM pack: three 64×256 4bpp pages at VRAM (320,0) / (384,0) / (448,0), CLUTs at (0, 474..476). The first TIM's flags word is 0x00010008 - strict TIM parsers reject it. |
0xb | etmd.dat | 0x369 / 0871 | 30-TMD asset::pack - the effect 3D-model library, registered into the mesh pool at battle init. |
0xb | vdf.dat | 0x36A / 0872 | 32-entry offset pack (~96 B records), asset type 0x07. |
0xc | efect.dat | 0x36B / 0873 | The 2-pack above: pack0@0x488, pack1@0x900. |
0x4 | (battle-type conditional) | 0x367 / 0869 or 0x36D / 0875 | Streaming [VAB chunk][type-3 chunk] files outside the block. |
Extraction 0874 is not effect data: it is player.lzs, the field character mesh pack. The CLI asset befect-cluster PROT.DAT --cdname CDNAME.TXT classifies the window; it resolves the symbol in define-number space, so its window is 872..875.
History: raw indices read as extraction indices
Readings that applied raw TOC indices as extraction indices ("etim = entry 872", "PROT 870 = index 0x366") are superseded; extraction 0870 is the case-0x8 load. An earlier cluster analysis read 0874 as "etmd/vdf/etim LZS sections" (it is player.lzs), and a note guessed the billboard texel source was entry 875. See do-not-re-walk.
Where the pixels come from
The atlas carries only VRAM coordinates; the pixels are blitted at load time. Two texture pools serve battle effects:
etim.dat(0870) - battle-only. Uploaded on battle entry into columns the field also uses, so field captures hold unrelated texels there. Pixel-exact against every stable Rim Elm battle capture. Engine:scene::upload_flame_atlas_into_vram, into a throwaway VRAM copy discarded on battle exit.player_data§2 (0874 §2) - field-resident, kept through battle: eight TIMs atfb_y = 256+(three character atlases, two shared 256-colour pages, two extension tiles; full table on character-mesh). Engine:scene::upload_effect_textures_into_vramat scene entry.
A melee hit-spark capture confirms the 2D path: the impact draws as textured quads on the etim pages (320,0) / (448,0) with CLUT rows 473..480, present only in the impact frame - no primitive samples page (0,0) or "tpage 0x7680".
The battle summon model: geometric, library at 0871
A player Seru-magic cast pages in a per-summon code overlay (extraction PROT 903..913; Gimard Burning Attack 0x81 → 903) that supplies the spawn logic. The effect-model library is etmd.dat (0871): the 30-entry pack loads verbatim at 0x800CA25C and registers into DAT_8007C018[3..32]. Gimard's flame is slot 26 (pack entry 23); the engine seeds the same slots at scene entry (scene::seed_effect_model_library_from_etmd, GIMARD_TAIL_FIRE_MODEL_INDEX = 26). None of 0874 §0's field-character TMDs are resident mid-cast; its smallest TMD bakes a flame-like CLUT and survives only as a preview fallback.
Animation is geometric, not CLUT cycling. Two animation-distinct capture frames share a byte-identical CLUT band (rows 470..499) while their framebuffers differ ~21%. A live trace of the player cast shows the per-actor draw firing 35-64× per frame through the rigid-TRS keyframe decoder, while the move VM fires only 2-3× - the player summon is drawn as an ordinary battle actor (engine path engine-vm/anim_vm.rs). The summon overlays do carry real move-VM part records (legaia_asset::summon_overlay, link base 0x801F69D8), driven by the engine as a stand-in summon::SummonScene; the overlay's three conditional CLUT uploads target rows 481+, not the flame's row 478. Scope: the player Burning Attack; the enemy Fire Tail is untraced.
Runtime: pool, spawn, and the bit-7 multiplex
Three battle-overlay routines own the format: an init that zeroes the pool and converts both packs' offsets to pointers; a spawn API (effect_id, world_pos, angle) that takes the first free master slot and points its cursor at script + 4; and a two-pass per-frame walker - pass 1 advances the 32 master slots and spawns children from the next 14-byte instruction, pass 2 turns the 128 child slots into GPU sprite quads from the atlas.
| Offset | Size | Region of _DAT_8007BD30 |
|---|---|---|
+0x000 | 16 | table head: (u16 id, u16 param, buf+8, pack0+4, pack1+4) written by init |
+0x010 | 4096 | 128 × 32-byte child slots (per-sprite render state) |
+0x1010 | 896 | 32 × 28-byte master slots (per-effect instance) |
+0x1390 | 1968 | unused |
A battle move reaches this pool through its move-power effect lists (+0x12 / +0x16). Each list byte multiplexes two id spaces by bit 7: 0x01..=0x63 is the 3D move-FX path (prototype table 0x801F6324, not this pool; 0x64 = screen flash), while 0x80..=0xFE spawns pack1[id & 0x7F] here. Two ids are special-cased: 0x04 and 0x13 resolve to 18-byte move-VM trigger programs in the overlay tail (the two arms of the radial particle burst), not pack1 scripts. The engine models the split as EffectListEntry::{Spawn, AltEffect}.
Side-band streaming files
A separate loader case streams one of two runtime-only files by raw TOC index: summon.dat (0x37F = extraction 893) when bit 7 of _DAT_8007BD24[0x26B] is set, else readef.DAT (0x380 = 894). Both carry per-special-attack CLUT rows, 4bpp pages and summon-creature actor records - decoded on summon-readef.
On-disc effect bundle (magic 0x02018B0C)
A separate container carrying effect geometry, found only in 0000_init_data. Its consumer is unreached, so this section is structural. Parser legaia_asset::effect_bundle::detect.
| Offset | Size | Field | Meaning |
|---|---|---|---|
+0 | u32 | magic | 0x02018B0C |
+4 | u32 | HEADER_A | 0x1D = 29 TMD slots (1 master + 28 sub) |
+8 | u32 | HEADER_B | 0x1E = HEADER_A + 1 (sentinel) |
+12 | 28 × u32 | offset table | strictly ascending (0x17F4 .. 0x404D); slot size = next − this |
| after table | asset region | a master Legaia TMD first (1 object, 382 verts, 760 primitives) |
The 28 offsets do not byte-align with sub-TMD positions; they likely index an abstract runtime buffer. A related shape, the field-pack (magic 0x01059B84), has its own page.
How we know
| Function | Address | What it proves | Dump |
|---|---|---|---|
| Init / pack fixup | 0x801DE914 (via loader case 0xE) | 2-pack wrapper, offset-to-pointer fixup, table-head record, pool size 5008 | funcs/overlay_battle_801de914.txt |
| Spawn API | 0x801DFDF8 | Master-slot allocation, script cursor at +4, ids 0x04 / 0x13 → 0x801F5D90 / 0x801F5CF8 | funcs/overlay_battle_801dfdf8.txt |
| Per-frame walker | 0x801E0088; emit ~0x801E0840..0980 | Child descriptor fields, atlas byte order (CLUT at +4, tpage at +6), sin/cos LUTs _DAT_8007B7F8 / _DAT_8007B81C | funcs/overlay_battle_801e0088.txt |
| Effect-list dispatch | FUN_801E09F8 → FUN_801DFDF0 | The bit-7 multiplex | funcs/overlay_battle_801e09f8.txt |
| Battle scene loader | FUN_800520F0, sub-state at gp+0xA59; raw-index open FUN_8003E8A8 | Case → file → raw index map | funcs/800520f0.txt |
| Image uploader | FUN_800198E0 → FUN_800583C8 (LoadImage); CLUT cache 0x8007BEC0 | How etim pages reach VRAM | funcs/800198e0.txt |
| Mesh registrar | FUN_80026B4C; pool DAT_8007C018 | etmd registers 30 TMDs at slots 3..32 | funcs/80026b4c.txt |
| Side-band handler | 0x801F17F8 → FUN_800558FC | Raw indices 0x37F / 0x380 = extraction 893 / 894 | funcs/overlay_battle_801f17f8.txt |
| Captures | Rim Elm battle states; battle_gimard_tail_fire_a/_b; melee hit-spark | Buffer at _DAT_8007BD5C = disc sector 0x9086; etim pixel-exact; geometric summon animation | scenario manifest |
Open questions and history
- Effect id → name. There is no string table; the only "name" available is an id → triggering-move join off the move-power table.
- The low-band
readef.DATaux-slot consumer - see summon-readef. - The
0x02018B0Cbundle's consumer is unreached.
History: the side-band files were once placed at extraction 895 / 896 by comparing raw indices against extraction numbering; and under the over-reading entry window, 0871's pack body appeared to span an "extended footprint". See prot.
Source of record: docs/formats/effect.md.