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 so-called on-disc
bundle (magic 0x02018B0C) that turns out to be a pack of 29 meshes behind an ordinary chunk header.
At a glance
- Where
efect.dat= extraction PROT 0873, one of the fourbefect_datafiles 0870..0873 (etim/etmd/vdf/efect); the0x02018B0CTMD pack in raw TOC entry 1 and0000_init_data- Magic
- 2-pack: none - two absolute pack offsets behind an 8-byte header. The
0x02018B0Cword is a chunk header, not a magic - 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_container_formats::pack(the TMD pack, detectoreffect_bundle);legaia_actor_formats::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 container-formats::pack. The retail file holds 144 atlas entries, 14 animation batches and 33 scripts.
Both offsets are fixed up to absolute pointers on first init. In the retail file pack0 sits at 0x488 and pack1 at 0x900.
The three record kinds
Atlas entry (8 bytes, between +8 and pack0) - one VRAM rectangle. The texel rectangle is (u, v)..(u+w-1, v+h-1) inside the page.
Anim batch (pack0) - a u8 frame_count, a u8 flags the walker never reads, then frame_count six-byte frames:
Effect script (pack1, one per effect id) - a u8 child_count, u8 flags (bit 0 = randomise spawn offsets), i16 spread, then child_count fourteen-byte spawn records. Each record is consumed once, when its child spawns; the two planar legs are rotated into world space by the effect's spawn angle.
| Field | Meaning |
|---|---|
anim_batch | pack0 index - the child's sprite animation |
delay | frames the effect waits after this spawn before the next record (<<3) |
offset_a / offset_b | planar spawn offset, rotated, >>4. With flags bit 0 set, the spawn API rewrites both in place with rand() % (2*spread) - spread - the resident buffer mutates on every randomised spawn |
height | vertical spawn offset (<<8, subtracted from Y) |
vel_a / vel_b / vel_y | planar velocities rotated >>12; vertical velocity copied straight to the child |
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 container-formats::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 converts the befect_data define into the extraction frame, so its window is exactly 0870..0873.
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 (engine-scene-host::scene_host::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 once per live actor per frame through the rigid-TRS keyframe decoder, in exact lockstep with it, while the move VM stays at noise level - 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_overlay_images::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. The enemy Gimard's move is a different one - Tail Fire, spell id 0x27 - and it renders as a single move-VM part-actor over PROT 0900, not through the TRS path.
The battle numerals live here too
etim.dat's third TIM - page (448, 0), CLUT row 476 - is not all effect texels. Its lower half is the battle screen's value readout sheet: the numeral a landed hit throws, and the labels the combo cluster stacks. Decoded at 4bpp through the sub-palette at VRAM (48, 476) (CBA 0x7703):
| Texels | Content |
|---|---|
v 64..87 | ten 24×24 digit cells, strip order 1234567890 - digit d is at ((d + 9) % 10) * 24 |
u 0..111, v 152..175 | SUPER |
u 0..111, v 176..199 | HYPER |
u 112..215, v 176..199 | the shared tail ARTS!! |
u 0..127, v 200..223 | MIRACLE |
u 128..199, v 200..223 | NEW |
u 0..55, v 224..239 | DAMAGE label |
u 0..31, v 240..255 | HIT label |
u 32..79, v 240..255 | TOTAL label |
The four Arts banners are composed, not stored. Only one ARTS!! exists on the sheet. The banner emitter FUN_801E2650 draws a pair of quads per call - the position-selected word, then always the ARTS!! tail - so the four positions render NEW / HYPER / MIRACLE / SUPER ARTS!!, the halves sliding in from opposite sides to a per-position seam. This sheet is distinct from the HUD's cur / max numerals, which are 8×12 cells off the menu-glyph atlas through CLUT row 510 (battle). In the port: engine_battle_vm::battle_value_readout.
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) |
The three regions account for the pool exactly: 16 + 4096 + 896 = 5008, the size init zeroes.
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)
0x02018B0C is not a magic, and this is not an effect format. The word is a DATA_FIELD chunk header, (0x02 << 24) | payload_len: asset type 0x02 (a pack of meshes) with a 0x018B0C-byte payload, and that payload is an ordinary pack of 29 Legaia TMDs. The fields the bundle reading named are the pack's own words. The detector legaia_container_formats::effect_bundle::detect still finds it.
| Offset | Size | Field | Meaning |
|---|---|---|---|
+0 | u32 | “magic” | chunk header: type 0x02, length 0x018B0C |
+4 | u32 | HEADER_A | pack count = 29 |
+8 | u32 | HEADER_B | first word offset 0x1E: member 0 begins right after the table |
+12 | 28 × u32 | “offset table” | the other 28 word offsets (0x17F4 .. 0x404D); times four, each lands on a TMD header |
| after table | members | 29 Legaia TMDs; member 0 has 382 verts and 760 primitives |
The offsets never lined up with the meshes because they were read as byte offsets. Two identical copies exist: one in raw TOC entry 1 (the GAME OVER screen's data, whose overlay registers each mesh through FUN_80026B4C) and one in extraction entry 0. No battle-effect code reads either. The word 0x01059B84 is the same misreading: it is a DATA_FIELD chunk header, (TIM_LIST << 24) | payload_len, on a scene's texture pack (field-pack).
How we know
Provenance: the battle-overlay consumers and captures
| 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 - as close as the disc gets. There is no string table, so an effect is named only by its space, its id, and the moves that fire it. That join is built and run: 38 of the move-power table's 44 populated records cite 28 distinct effect keys, and each space uses one small contiguous band rather than its whole range. Two of the 28 are the generic hit sprites - one is cited by thirteen moves, another by ten - while thirteen keys belong to a single move each.
- The low-band
readef.DATaux-slot consumer — settled. The slot is an'M' 'E'keyframe archive, streamed byFUN_801F17F8and read once outside the applier, at0x8004BCA8inFUN_8004AD80: the selected entry expands into the scratch atgp[0xA24]and that pointer becomes the art entry’s+0x88stream pointer. See summon-readef. - The
0x02018B0C“bundle” — settled. A TMD-pack chunk header on 29 meshes; the GAME OVER overlay is the consumer of the raw-entry-1 copy.
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.