At a glance

Where
Battle overlay (PROT 0898); pool at _DAT_8007BD30, 5008 bytes
Input
efect.dat (extraction 0873) - a 2-pack: 14 sprite-animation records + 33 effect scripts; see effect format
Capacity
32 effects (master slots, 28 B each) × 128 sprites (child slots, 32 B each)
Textures
Effect atlas etim.dat (0870, battle-only) + the field-resident character texture band (0874 §2)
Engine
engine-vm::effect_vm (Pool::tick_retail, Pool::child_billboards); live via World::tick_effects
Confidence
Confirmed - walker and spawn traced instruction-for-instruction
Used by
Battle action SM, Move VM effect lists, item and spirit appliers

How it works

Cast a spell and three things happen on screen: the caster poses, a stream of sprites flies to the target, and a hit flash plays. Internally that is one effect script: a list of spawn records, each saying “after N frames, start sprite animation K at this offset”. A master slot walks the record list; each record it reaches claims a child slot, which then walks its own animation frames and drifts along a velocity until its last frame retires it.

effect script (pack1) delay, anim id, offset... sprite anim (pack0) frames: delay, speed, atlas master slot (x32) wait, cursor, origin child slot (x128) wait, frame, pos, vel one textured quad semi-transparent, faded read read spawn (pass 1) draw (pass 2)
Pass 1 runs the timers and spawns; pass 2 draws every live child as one billboard. No opcode is ever decoded.
1Spawnthe action SM asks for an effect id; the spawn API claims a free master slot, seats it at the acting actor’s position and facingspawn API 2Batchwhen the master’s wait hits zero it consumes spawn records - claiming one child per record - until it meets a non-zero delaywalker pass 1 3Tickeach child counts its wait down, steps its position by velocity × frame speed, and advances an animation frame when the wait expireswalker pass 1 4Draweach live child becomes one semi-transparent textured quad over the effect atlas, brightness ramped in and outwalker pass 2 5Retirea child frees itself on its last frame; a master frees itself after its last recordslot flag

Pool layout

OffsetSizeContents
+0x00016 Btable head set by init: motion scale (0x1000) and sprite scale (0xA00, ×10 texel size)
+0x0104096 B128 × 32-byte child slots - one per live sprite
+0x1010896 B32 × 28-byte master slots - one per live effect
Master fieldMeaning
+0 child_counttotal spawn records; 0 = free slot
+1 flagsbit 0 = randomised offsets at spawn
+2 spawn_cursorrecords consumed so far
+3 wait5.3 fixed-point countdown (frames << 3, minus 8 per frame)
+4 angle12-bit spawn facing
+8..+0x10 originworld x/y/z, 16.8 fixed
+0x18 script_cursornext 14-byte spawn record

A child slot mirrors the shape: frame count (doubles as the active flag), random UV-mirror bits, frame cursor, wait, i16 velocity, 16.8 position and an animation cursor. Every wait in the system is the same 5.3 countdown, so effect time tracks wall-clock under frame skip.

Why there is no opcode table

  • The “state” bytes are wait counters. The only data consumed are spawn records (14 bytes each) and animation frames (6 bytes each).
  • Zero-delay records spawn as one burst; zero-delay frames advance as one burst. Both loops repeat while the freshly loaded wait is zero.
  • On pool exhaustion a record is still consumed with no child - effects degrade rather than stall.
  • Delays are stored in a byte after the << 3, so a delay of 32 frames or more wraps modulo 32.
Details: pass 2 render maths and the blend rule
  • Brightness envelope. With n = frame_count >> 3, modulation ramps in over the first eighth of the animation and back out over the rest, clamped at 0x80 (neutral), written as r = g = b.
  • Size. atlas w/h × sprite_scale >> 8, centre projected through the sprite projector, corners offset in view space, inserted into the ordering table by depth.
  • UVs. base and extent from the 8-byte atlas entry; corner order swapped per the child’s random mirror bits; CLUT from atlas +4, texture page from atlas +6.
  • Blend. The packet uses prim code 0x2E - textured quad, semi-transparent - so every effect child blends, and the page’s own ABR bits pick the formula. Pages (320,0)/(448,0) are additive (B + F); page (384,0) is B + 0.25F, which is why walk-dust reads as almost nothing over a bright floor.
  • Retirement quirk. Retiring zeroes the active flag and the wait, but the frame loop tests only the wait, so retail keeps stepping the dead slot until it meets a non-zero delay byte. Harmless - the next seed rewrites every field.

Textures, models and the engine port

Effect sprites sample two texture pools. The flame atlas etim.dat is uploaded at battle entry only (its VRAM columns hold town textures on the field), while the character texture band stays resident from the field through battle - the Gimard summon flame samples the latter. A separate 30-entry model library, etmd.dat, gives 3D effects their meshes.

Retail pieceEngine mirror
init / spawn / walker pass 1Pool::init, Pool::spawn, Pool::tick_retail (one sweep per retail frame from World::tick_effects)
walker pass 2Pool::child_billboards → World::active_effect_sprites; each host draws a camera-facing textured quad
spawn at the acting actorBattleActionHost::ui_element spawns at the actor’s battle seat with its facing angle
effect catalogEffectCatalog::from_efect_dat_bytes, resident on World::effect_catalog across field / battle
flame atlas / texture bandscene::upload_flame_atlas_into_vram (battle entry) / scene::upload_effect_textures_into_vram (scene entry)
model library etmd.datscene::seed_effect_model_library_from_etmd → World::global_tmd_pool[3..=32]; Gimard Tail Fire = index 26

The player summon (Gimard’s Burning Attack, and friends) is not an effect-pool child at all: it is posed like an enemy body through the battle per-actor draw and the rigid-keyframe decoder, and its flame animation is geometric, not palette cycling. The per-summon stager overlays are dispatched by the battle action SM.

Details: host traps a port hits
  • Half-extents are view-space. Retail offsets the quad corners after transforming the centre, so the battle camera’s 4× base scale never touches them. A port offsetting in world space draws every sprite four times too large. Correction: engine-vm::effect_billboard::world_half_extents.
  • The blend enable comes from the prim code, not the atlas. The atlas stores its page in one byte, so a billboard builder that copies the page into a TSB word never sets the semi-transparent bit and the whole layer rasterises opaque - pale tan blobs instead of glow. Port: effect_sprite_tsb.
  • “Spawns fire, nothing appears” is usually the data. The Rim Elm spar’s effects all use the B + 0.25F page; the bright additive pages belong to effect ids the spar never requests. LEGAIA_DIAG_FX=1 logs per-sprite position, page, brightness and texel residency.
  • The tinted outline is a diagnostic, off by default on both hosts (native env LEGAIA_DIAG_FX=1; browser set_battle_fx_outline(true)). Retail draws no such rectangle.
  • The floating damage numeral is not a pool child; it samples the third etim TIM at page (448,0) through the sub-palette at (48, 476), and rises to screen row 32 while its cells grow to 24 px. Port: engine-vm::battle_value_readout.
History: superseded readings
  • The atlas value 0x7680 is the entry’s CLUT (row 474), not a page-(0,0) 8bpp texture page - the +4/+6 fields are CLUT then page.
  • “Flame flicker is CLUT cycling” is falsified: two animation-distinct frames carry a byte-identical CLUT band while the framebuffer differs.
  • “The summon stagers hold no move-VM records” was a wrong-link-base artifact; they recover under base 0x801F69D8.

More on do-not-re-walk.

Side-band streaming: summon.dat and readef.DAT

A separate loader case streams one of two runtime-only files into 0x10800-byte slots: per-special-attack CLUT rows and 4bpp texture pages, summon-creature actor records, and the player art-animation archives. The loader ignores the path string and consumes a raw TOC index, which is why the names map to extraction entries 893 / 894. Full format: summon-readef.

FileRaw TOC indexExtraction entrySelected when
data\battle\summon.dat0x37F893 (103 slots)_DAT_8007BD24[0x26B] & 0x80 set
data\battle\readef.DAT0x380894 (78 slots)otherwise

Naming an effect id

Effect ids are anonymous - no string table says “fireball”. Two producers feed the spawn wrapper: the move-power record’s effect-id lists, and the per-move cue-group list walked by the battle effect driver, both through the same bit-7 multiplex (effect format). To name an id, correlate the literal byte a caller passes with the action that triggered it.

How we know

FunctionAddressWhat it provesDump
init / pack fix-up0x801DE914pool head scalars; called from FUN_800520F0 case 0xE with (id=0x1000, param=0xA00)overlay_battle_801de914.txt
spawn API0x801DFDF8(effect_id, world_pos*, angle); master seedingoverlay_battle_801dfdf8.txt
per-frame walker0x801E0088pass 1 timers/spawn, pass 2 quads; ready flag DAT_8007BD71 == 0xFF; catch-up factor DAT_1F800393overlay_battle_801e0088.txt
spawn callerFUN_8004998C effect armspawns at the actor’s position (actor+0x34) and facing (actor+0x46)funcs/8004998c.txt
list dispatch / cue groupsFUN_801E09F8, FUN_801E22C8, FUN_800402F4the two producers of the spawn wrapper FUN_801DFDF0battle overlay dumps
streaming handler0x801F17F8 via FUN_800558FCraw-TOC-index load of summon / readef; byte-verified RAM↔disc in a mid-cast state-
summon render pathFUN_80048A08 → FUN_8004998Clive trace: the two run in lockstep, once per live actor per frame; move VM near-zeroPCSX-Redux probe
sprite projectorFUN_800195A8corners offset after the centre transformfuncs/800195a8.txt

See also