Settled reverse-engineering threads
Questions about how Legaia works that have an answer, and how firmly each answer is pinned. This is the archive half of the RE register: the live hunts are on open RE threads, and the readings that turned out wrong are on do not re-walk. Every answer and write-up here is rendered from the source documents when the site is built.
How to read this page
A thread is one question the project stopped to ask - why a boss battle freezes with the camera circling, where the party's battle models come from, which routine swaps the music. A thread is settled when it has an answer that can be pointed at: an instruction sequence, a runtime capture, or a measurement taken off the disc. resolved is a claim, not a warranty - which is what the evidence column is for.
Each area below is one table. A row gives the question, its verdict, the evidence grade, and under Why the full answer - addresses, captures and corrections. A thread whose answer outgrew a table cell links to its write-up under Long write-ups in this area. Every area, row and write-up is rendered from docs/reference/re-settled-threads/ when the site is built, so this page cannot state a verdict the docs have since overturned. The areas run from the overworld inwards (world map, battle, field, text, animation, audio, boot, rendering) and end with the project's own instruments.
- Source of record
docs/reference/re-settled-threads.md- Row lifecycle
- open → settled (here) → falsified if a later reading overturns it
- Function names
- A
FUN_<addr>in a row is described in key functions
What an evidence grade means
Every thread is graded by what its stated evidence rests on. A thread citing more than one kind is graded by the weakest load-bearing claim - the one that breaks the conclusion if it is wrong.
| Grade | The thread cites |
|---|---|
| disassembly | Instructions, addresses, opcode encodings, branch or store sequences read from the MIPS code. The strongest grade. |
| capture | A runtime capture: a save state, an emulator probe, a write watchpoint, a trace, or a measurement taken off the disc. |
| decompiled-C | Ghidra's C output, a FUN_x(...) call signature, a Ghidra label, or a claim about store order / count / a boolean operator with no instruction behind it. |
| inference | Reasoning from surrounding facts, corpus absence, or analogy, with no direct evidence cited. |
decompiled-C is the re-audit bucket, not a wrong-answer bucket. It marks a claim nobody has confirmed against instructions. Three shapes carry most of the risk: evidence citing a call signature or a funcs/<addr>.txt dump rather than instructions, any claim about store order or count, and any claim about which boolean operator a predicate uses. The catalogue of C-rendering artifacts is on Ghidra workflow.
A disassembly grade is only as good as the addresses in its cell. A row that names a function and a behaviour but no instruction address reads the same as one that was checked, and is the same bucket in practice. Re-audits of such rows have found a "nothing increments it" beside an add-one, a "neither subtracts HP" beside two HP stores, and a pad test sitting in a hole in a function dump. A checked row carries the addresses that settle it.
World map / kingdom bundles
# Settled threads: World map / kingdom bundles
| Thread | Answer | Grade |
|---|---|---|
| Is the overworld ground depth-cued, and from which far colour? | yes, toward a literal 0x100 per channelWhy
| disassembly + capture |
| Are the overworld decoration cells depth-cued? | yes, one IR0 per object toward 0xD0WhyBefore each | disassembly |
| Why do the overworld fog sheets read denser and brighter than retail's? | the texture blend's 5-bit truncationWhyThe sheets are additive ( | disassembly + capture |
Is the sparkle burst FUN_801E5338 reachable? | noWhyIts only reference is the tick word of template | disassembly + capture |
| Which ordering-table bucket does the overworld ground link at? | (max corner SZ >> 5) + 14WhyThe bulk terrain emitter | disassembly + capture |
| Which camera does the overworld walk use? | the field zone cameraWhyThe overworld is a mode- | capture + disassembly |
What does 0x1F800394 bit 0 select? | the kingdom overworldWhySet on every non-battle kingdom-overworld state in the mednafen library and clear on every other state, overworld battles included. | disassembly + capture |
Does the disc carry one 4C EA occurrence or more than one? | three - one per kingdom map bundleWhyThree: the scripted game-over has one clean site in each of | capture (disc census, asset field-op-census --only '4C EA') |
| The ninth routine of PROT 0901's draw band | it is not a per-prim leafWhy
| disassembly |
| The world-map camera's sub-9 arm table, and where its horizon gates store | subs 0xA..0xC store in the leaving j's delay slotWhyField-VM op | disassembly |
| World-map walk-view continent ground render | resolvedWhy | capture |
Walk-view decoration draw gate - which grid bit stamps the .MAP record meshes on the overworld? | 0x2000 alone; no flag or mesh-0 testWhyThe resident slot-B kernel | disassembly + capture |
Walk-view untextured landmark prims - does retail draw the F*/G* colour prims of the slot-1 pack meshes (Rim Elm's hut roofs)? | yes, same per-prim dispatch as the textured primsWhy
| disassembly |
DAT_8007C018[45..53] mid-load vertex-pool pointers | structuralWhy | disassembly |
| Field decoration path - does it dispatch the NCC light handlers? | yes - the light-source rows, flags 0x10..=0x17; exec breakpoints in cave01Why | capture |
| Kingdom slot 4 - per-record semantic + consumer | the world-map scene's type-0x05 ANM animation bank; the "vertex pool + cluster-A stream" reading is falsifiedWhy | disassembly + capture |
| The placed-actor mesh resolver on the world map | FUN_80020F88, at spawn time - not in the draw loopWhyCalled from the allocator | disassembly |
MAN sections 2 and 5 - what do _DAT_801C6EA0 / DAT_80073EE0 carry? | both are place-name carriersWhySection 2's body is the scene display name the on-entry banner draws (and the save screen's location row); section 5, the universal zero-length terminator, leaves its pointer on the world-map location table the kingdom MANs trail - 29 records of | disassembly + capture |
Where does the model-pool id space split at 0xF0? | in the callers, not in the resolverWhy
| disassembly |
Is FUN_80026B4C the only writer of the TMD pointer table? | no - PROT 0976 zeroes a slot directlyWhyThe registrar is the only routine that installs an entry, but | disassembly |
| What is PROT 0981? | the overworld map display, game mode 0x0DWhyA | disassembly |
Which image holds the dev-menu row strings at 0x801CF344? | PROT 0897, the field overlayWhyFile offset | disassembly + capture |
What writes the overworld camera's vertical-offset control word scene_ctrl[+0x4A]? | map01's entry script, through op 4C 49's delta armWhy
| capture + disassembly |
What does FUN_800271A8 build? | the overworld's screen-Y curvature table, not a depth rampWhy
| disassembly + capture |
| Why does one overworld state hold a curvature table six rows up? | the map display rebuilt it under the running frame's OFYWhyThe builder stores | disassembly + capture |
| Is the overworld's curvature per sheet or per vertex? | per vertex, overlay rows 12..19 onlyWhyThe world-map leaf at | disassembly |
Does any overworld draw reach the lit rows 8..11 the curvature skips? | no - the port's bend-everything is inertWhyThe overworld dispatch takes a group's row as its header word | disassembly |
| What ink does the map display's pointer draw? | its baked reds, averaged: the colour word's ABR never reaches an untextured packetWhyRetail's table holds the pointer as flat semi packets ( | disassembly + capture |
Long write-ups in this area
World-map walk-view continent ground render
Status: resolved - Evidence: capture. Heightfield geometry plus per-cell, terrain-keyed multi-page texturing; shipped in the engine.
The continent ground is a procedural heightfield drawn as one textured quad per cell, not instanced meshes; the slot-1 pack meshes are only the sparse placed landmarks.
- Height:
FUN_80019278(SCUS, always resident) reads an entity's XZ, gates on the object-grid0x1000cell bit and bilinearly interpolates the floor height from the 2x2 block of+0x4000nibbles (grid[0],[1],[0x80],[0x81], each& 0xF->DAT_1F80035C[nibble], weighted by the sub-tile position,>> 0xE). - Landmarks:
pool = record[+0x10] + prefix, resolved 14 of 14 against the live render list viaFUN_8001ADA4case 5 /FUN_80024D78/FUN_80020F88; spawned byFUN_8003A55C, gated onflags & 0x4(about six objects, pools 36 / 34 / 11 / 7 / 19 / 21). The bulk0x1000cells carry+0x10 == 0. .MAPsource: a raw (uncompressed)0x10000region atPROT.DAT0x655800. The loader's retail branch resolves PROT index*(0x80084540) = 0x55 = 85->toc[87] = 3243. The per-entry extraction's0085_map01.BIN(a count-46 pack at0x668000) is the field object / script pack; the.MAPsits under the overlapping manifest entry 83.- Texturing: per-cell
POLY_FT4(cmd0x2C), one 32x32 quad per visible cell in a row-major world-cell sweep, selected from the cell's object record:+0x14= 8x8 atlas tile (u = (id % 8) * 32,v = (id / 8) * 32),+0x15= PSXtpage(0x1Agrass,0x0Cmountain,0x1B/0x1Cwater,0x0Bforest),+0x16..+0x18= PSXclutword. Tile, page and clut match the record 100% across mountain and coast captures (scripts/ghidra-analysis/analyze-walk-ground-tiles.py --verify-rule). - Port:
legaia_scene_formats::field_objects::build_walk_heightfield/Scene::walk_heightfield- a quad per0x1000cell, corner Y from the+0x4000LUT, per-cell UV and[clut, tpage]baked intoWalkHeightfield::uvs/::cba_tsb; verified against the disc. - Not a positional
(col % 3, row % 3)pick on a single0x1Agrass page: grass cells'+0x14values land in the atlas's top-left 3x3 block, which imitates one. The per-cell terrain renderer is overlay-resident (0x801F76xx), so a static SCUS consumer sweep does not see it. - Not a combined walk + overview mesh pool: 0085's and 0093's slot-0 atlases target the same VRAM pages, so they are mutually exclusive sets that clobber each other if co-loaded.
Owning page: world-map.md (texturing under Ground texturing).
Field decoration path - does it dispatch the NCC light handlers?
Status: resolved (yes - the light-source rows) - Evidence: capture (cave01_attached_light exec breakpoints and frame) with the dispatcher disassembly.
The field decoration pass FUN_801F7088 emits through the per-prim dispatcher FUN_80043390, and TMD groups with flags 0x10..=0x17 reach its four NCCS / NCCT light handlers.
- Dispatch: the body is picked by
flags >> 1(0x800435A4); kinds 8..11 areFUN_8004409C/FUN_8004423C/FUN_80044434/FUN_800445B0, the ROM's only hardware-light code. - Frame:
cave01's rock columns are all flag-0x11groups.cave01_attached_lightholds the scene-load light (angle trio0x994 / 0x9CC / -0x62C, back colour0x202020) and the light matrix0x1F8003A8that trio builds, element for element. Retail's frame shows the wall turned from the light at an eighth of its texel and the facing wall above neutral; shading the lit rows withNCCSunder those inputs reproduces it to within the image channel's noise, where a neutral0x80leaves both walls at their raw texel. - Exec count:
autorun_w4d_light_kind_hits.luaon that state (no warp) hits kind 8 (FUN_8004409C) 882 times and kind 9 (FUN_8004423C) 200 times in 60 vsyncs, every hit returning to0x801F78D4, the decoration pass's call into the dispatcher. town01is consistent: a cold-bootdirty_exec_hotsweep (~46M interpreted hits) finds zero hits in[0x800445B0, 0x80044798), which is the kind-11 body alone, andtown01sets a white back colour (op4C 8A) under which a lit row draws at or above neutral and looks baked. Its houses do carry lit rows, and a ~31K-hittown01window shows the kind-11 body hot.- Coverage limit: that sweep is the script-locked prologue arrival, effectively one viewpoint;
map02/map03and free-roam multi-screen towns are unsampled.
Three instruments cannot test this, and each reads as a negative:
gte_ringrecords onlyRTPS/RTPT(gte_rtp_record, func0x01/0x30) andINTPL(func0x11), neverNCCS/NCCT/DPCS/DPCT(gte.cpprecord hooks). A GTE-ring "zero NCC" is vacuous;dirty_exec_hotis the valid liveness probe.fntracecatches only dispatcher round-trips. The SCUS render handlers are natively compiled and directly called, soFUN_80043390records 0 hits whilefntrace_arm allcatches ~300k dispatches/s.map01-class world maps dispatch through the replaced table0x801F8968to the PROT 0901 leaves (dirty_exec_hothot at0x801F6E6C), not the SCUS0x8004xxxxhandlers - a different renderer, not a light-path test.
Owning page: renderer.md.
Kingdom slot 4 - per-record semantic
Status: resolved - Evidence: disassembly + capture.
Slot 4 is the world-map scene's asset-type-0x05 ANM animation bank, structurally identical to every field scene's type-0x05 section.
- Container:
[u32 count][u32 byte_offsets]; each body is one clip - an 8-byte header (marker0x080C,part_count, au16frame count, an interpolation flag, the sub-frame divisor1 / 2 / 4) thenparts * frames8-byte entries, frame-major:entry(f, p) = body + 8 + (f * part_count + p) * 8(0x8001BAC0..0x8001BAEC). - Entry: three 12-bit signed translations in bytes
0..4(sign-extended at0x8001BF44..0x8001BF6C, pushed through GTEMVMVAat0x8001C0E0) and three 8-bit rotation angles in bytes5..7(angle = byte << 4, read at0x8001C0CC/0x8001C0E4/0x8001C0E8). The only unread field is byte 4's high nibble, zero in all 22,228 entries. - Load:
FUN_8001F05Ccase 5 stores the buffer at_DAT_8007B888(0x8001F3A8). The pointer has six references across SCUS and all 32 extracted overlay images - the store, a reset inFUN_8002541C, one read inFUN_800204F8, three in the Baka Fighter overlay - none in a render path. - Clip select:
FUN_800204F8picks one of three banks (0x80020534..0x80020598): party flagactor[+0x10] & 0x01000000->_DAT_8007B75C; elseactor[+0x5C] < 0x400->_DAT_8007B888(slot 4); else_DAT_8007B840, the type-0x0B"MOVE2" bank. The lookup is 1-based,rec = bank + *(u32*)(bank + (id & 0x3FF) * 4), stored atactor[+0x4C], with the 1/16-frame cursor atactor[+0x68]. - Draw: the animated renderer
FUN_8001B964(FUN_8001ADA4render mode 1, table0x8001042C) walks the frame's parts, refusing unlesschain[0] == part_count(0x8001BAF0), and per part calls the pose decoderFUN_8001BE80(jalat0x8001BB20) thenFUN_80043390(0x8001BC84). - Live: in a
map01field-run state_DAT_8007B888 = 0x8011A624, the 32,304 bytes there are byte-identical to the disc payload, and four animated actors carry+0x5C= 5 / 11 / 14 / 15 with+0x4C = base + offsets[id - 1]and clippart_count= 2 / 12 / 14 / 2, equal to their pool TMD'snobj. The capture's return addresses0x8001BB28and0x8001BC8Care the twojals above. - Detector:
asset player-anm extracted/PROT/0086_map01.BIN --desc-count 7reports one player-ANM bundle withrecord0 marker_1 = 0x080C. - Not a GTE vertex pool
(i16 x, y, z, attr)walked by a "cluster-A command stream": the field boundaries fall on nibbles, not halfwords, andattris the Y / Z rotation pair, read every frame.FUN_80044C14, a per-kind prim handler that does read vertices that way, does not own this buffer. ra = 0x801F78D4in the warp capture is not a slot-4 reader: it is the return ofjal 0x80043390at0x801F78CCin PROT 0900 (PROT 0901 holds no call there), a draw wrapper handed a TMD group pointer.
Which actor plays which clip is scene data, not format: the ids are scene-script literals in actor[+0x5C].
Owning page: world-map-overlay.md.
DAT_8007C018 liveness rule
Status: resolved (structural) - Evidence: disassembly.
An entry of the TMD pointer table is live only at indices 0 ..= _DAT_8007BB38; there is no per-index semantic, and 45..53 in a small field scene is stale carryover.
- Registrar:
FUN_80026B4CwritesDAT_8007C018[cursor](sw a0,0x0(v1)at0x80026BA8,v1 = 0x8007C018 + cursor*4) and mirrors the pre-increment cursor intogp+0x820=_DAT_8007BB38(0x80026BBC). - Walker:
FUN_801D8280loads that counter (0x801D8284) and runsi = 0 ..= counter, stepping the base by 4; entries above the counter are never visited. - Counter writers: an all-forms sweep finds exactly one store corpus-wide, and it is
gp-relative, so an absolute-only scan finds none. - Per-stage reset:
FUN_8001E1B4at0x8001E3AC.
Battle / arts / level-up
# Settled threads: Battle / arts / level-up
| Thread | Answer | Grade |
|---|---|---|
| Does the action SM's idle request restart a body's idle? | no: setup seats the phase, and the request keeps the loopWhy
| disassembly + capture |
| Why did the engine still name Thunder Blast on the frame before a wipe's results? | the Done band's list wipe takes the labelWhyThe | disassembly + capture |
| Why does an Orb's Heal land on other numbers? | the flight strikes, then 0x3F's applier healsWhyThe heal's flight rolls | disassembly + capture |
| Where does Scythe Wind's caster stand when the walk-in arrives? | where retail's does, on retail's frame steps; the gap is the frame stepWhyOn the | capture |
| Which earlier actor did a monster's cast walk from? | DAT_8007BD00, the gauge panel's last memberWhy
| disassembly + capture |
Which pass raised the HUD plaque and readout bar battle_map01_gomboo_steal_mid_cast shows three pixels short of rest? | the capture's own action seedWhyA | capture + disassembly |
| What dims Element Change's ray fan differently from the port's? | the capture emulator's dither, not the raysWhyRasterised under both rules, the still's ordering table places the capture on PCSX-Redux's side: the fans read | capture |
| What backs Aluru's creature off through arms 15..18? | arm 14 stores the running clip's root speedWhyPROT 0916's arm-14 pass writes | disassembly + capture |
| What takes Death Game's move label off the screen? | the module destroys the label's windowWhyArm 2's face pass calls | disassembly + capture |
What holds Xain's rate byte at 4 through Terio Punch? | the commit of the punch's own clipWhyPROT 0953's arm 0 stores | disassembly + capture |
Why does battle_vell_nighto_49_strike's headless camera stand its yaw 51 short of retail's? | the gap no longer reproduces; two units are leftWhyUnder | capture |
Why does battle_geremi_ogre_strike's replay reach its gate on the idle? | it reaches it on the walk; the film's residue was two harness seatsWhyThe replay now gates on the walk (clip | capture |
Why does a full party's summon run step 2 into the flash where retail runs 3? | it no longer does; the 0x32 drive hold counted passesWhyRecordings of | capture |
| Why do Caruban's two swings land their hits where retail's do not? | a blocked swing's seed word, which the burst reading cannot seeWhyThe deal read hit words off spark-burst frames, so Flap's blocked event drew on the replay's own stream. A | capture |
| What draws the speed lines behind an art's banner? | a static ray fan; the capture's miss was its seat, not a rand() dealWhyThe effect-script table arm's code | capture |
Why do the engine's hit-spark parts retire sooner than retail's on battle_vell_nighto_49_strike? | they do not; the engine drew each burst a frame ahead from its seatWhyThe list walk reads | disassembly + capture |
Why does battle_vell_nighto_49_strike's film run its yaw counter slow from 0x20 on? | the frame's draw load raises retail's stepWhyThe probe's | capture |
| How long do the Done band's closing glides hold the next round prompt? | a pass at most in the ordinary flowWhyThe menu SM's gate runs every pass ahead of the action SM ( | disassembly + capture |
| Why does Rogue Thunder's hit take about twice retail's HP in the cast-sequence film? | the film stood the party out of the state's Spirit stance; the roll is retail'sWhyThe | disassembly + capture |
| Does the port grant the summons worn gear teaches? | it does now; it had no code for itWhy
| disassembly + capture |
| Where does the battle HUD read a zeroed maximum MP? | the seat's own +0x152, which the port now carriesWhyBattle init copies the record's maximum into | disassembly + capture |
Do level-ups draw rand()? | eight a level, on the one stream; the port drew none until the stream was lent to the trackerWhyThe eight | disassembly + capture |
| Why does Aluru's arm 19 read as a white ramp running twice retail's? | the wash ended early, no ramp is wrongWhyThe kill of the fade parked at | disassembly + capture |
| What drew the lightning arcs past Aluru's blast in the engine? | the port's emitter ran on a hidden bodyWhyEffect- | disassembly + capture |
Why do Aluru's and Nighto's cast-sequence cues read .33 and .40? | the verdict's draw, not the cuesWhyBoth draw a kill verdict in arm 0. Retail's seed word gives Nighto | capture |
Why do Vera's and Spoon's shots miss retail's yaw by 34? | the oracle's seed, not the castWhyThose arms read | capture |
Why did Zenoir's first hit sound 0x200, 0x16, 0x16? | the wrong stage on the pre-hitWhyArm 8's hit stages | disassembly + capture |
| What were the pale quads Spikefish's arms 5..8 drew in the engine? | the port's seat, not a partWhyThe port seated PROT 0925's whole record set at the band's entry, so arm 7's five additive shells ( | disassembly + capture |
| What does a four-slot event list read? | a byte the fight's heap placement picks, which the disc decidesWhyThe tick reads | disassembly + capture |
Why does a measured juggle window run past 32 * beat / (speed * rate)? | the window counts passesWhyThe commit zeroes the cursor ( | disassembly |
What does the anim commit's read of the juggle byte +0x1F7 decide? | where the re-staged clip's sound-cue track startsWhy
| disassembly |
What does FUN_801EFE44 do at an action seed? | it moves the combatants: a formation recentreWhyCalled from state | disassembly + capture |
| Do a melee's bystanders move out of the attacker's lane? | yes - the make-way stepWhyState | disassembly + capture |
| How often does the battle separation pass push a close pair? | once a battle frame, by a fixed amountWhy
| disassembly |
What do the monster spell entries' +0x04 / +0x08 descriptors hold? | two clip-morph lanes over the monster's own meshWhyThe battle draw's | disassembly + capture |
Is the per-character ability bitfield at record +0xF4 / +0xF8 catalogued? | yes - the 64 passive indicesWhyThe two words are one 64-bit field: the stat aggregator | disassembly |
Does the fight against monster 0xAF (Tetsu) seat and flag as retail does? | yesWhyTetsu's only row, | disassembly |
| Do party statuses survive a battle? | no, except in an arena legWhyThe exit's party loop clears | disassembly |
What is the loss window 0x42? | the result window's twinWhy
| disassembly |
| Does a monster's one-shot clip tween into its queued clip? | yes, and the step it tweens across moves the actorWhyOn the stream's last frame the next entry is frame 0 of the queued clip when HP | disassembly |
Which keyframe does FUN_8004998C's re-blend retry rewrite? | the next frame's, in a scratch journalWhyThe counter | disassembly + capture |
| Which arts directions does Rot disable? | Left 0x08, Right 0x10, Up and Down 0x20Why
| disassembly |
| What does a special battle do at the wipe check? | the whole party counts down when the leader is fully rottedWhyWith | disassembly |
| What does Run do in an arena leg? | the leader acts first and the leg reports a runWhyFlow | disassembly |
| Does a special battle open the result window? | noWhy
| disassembly |
Who writes _DAT_8007BAC0? | eight stores; none between the formation roll and the resultsWhyA | disassembly |
What does the special-battle word _DAT_8007BAC0 suppress, and who makes a fight unescapable? | spoils, absorb, spell XP and monster flee; the formation row's header byteWhyWhole-word readers: | disassembly |
| Why does the Tetsu spar never open on a back attack? | the battle-stage id skips the formation rollWhyIts | disassembly |
Why do three-on-one formations sit +13 in Z? | the per-round recentre FUN_801DB318WhyThe battle flow runs | capture + disassembly |
| What does a downed party member play? | knockdown, then entry 7, then entry 8WhyThe tag- | capture + disassembly |
What does FUN_8004DC68 do? | the near-camera ghost passWhyEvery battle frame ( | disassembly + capture |
| Who forbids the Ra-Seru chip, and where is it read? | bit 0x200 of _DAT_8007BAC0: two raisers, two readersWhyBattle init clears a word of exactly | disassembly |
| What do the Baka Fighter impact pair's templates spawn? | a sprite-arm flip-book and two stage meshesWhyTemplates A ( | disassembly |
What does FUN_801F0450's tail do? | the Auto command's art insertionWhy
| disassembly |
| Where does the target cursor's name plaque sit? | placement record 0x29, centred on x = 0xE8Why
| disassembly + capture |
| Which battle limbs draw dark, and how? | a party seat's Rot limbs, per mesh objectWhy
| disassembly |
Does the tag-0x67 streak ribbon have a caller outside the move-FX dispatcher? | yes, the per-clip pass of FUN_8004CE2CWhyGala's tag- | disassembly |
What is ctx[+0x25]? | the round's skip countWhyActors whose turn the round passes over without acting: | disassembly |
What does 0x801F696C gate? | the strike loop's per-frame swing driftWhy
| disassembly |
| How is the command-phase commit log laid out? | records 0x2B + 3n, seated off the name's widthWhy
| disassembly + capture |
What does the 0x6E commit-confirm screen gate, and where does Reselect land? | party-wide, after the last able member; one step back onto the last able memberWhyEvery commit site stores | disassembly + capture |
What does FUN_801DABA4's first loop do to a dead actor? | zero the key, clamp Spirit, count the skip, refund the ItemWhyFor a slot with | disassembly |
| What ends Koru's four turns, and how long is the strip up? | Koru's own AI arm; the command phaseWhyNo code compares | disassembly |
What does battle state 0x5A do? | the arts target cursorWhyLeft / Right walk | disassembly |
| Does the arts-shout selector remember a last pick per character? | no - one byte party-wideWhy
| disassembly |
| What does the dome hub's first visit draw, and in what order? | course card over title art; last emitted paints firstWhyArms 4 / 5 call the course card | disassembly |
| What triggers retail's in-battle steal? | the killing blow, once per strike chainWhyNot a command and not a hit: | disassembly + capture |
| Does the same arm do anything else? | it returns a thief's lootWhyWhen a monster that stole dies, the item goes back to the bag with a "took" ( | disassembly |
| How does the command ring step back to an earlier member? | cancel with a non-zero step counterWhyThe ring's cancel tests | disassembly |
| What do the first-visit dome hub arms draw? | course card, title art, backdrop rampWhyArms 4 / 5 draw the course card | disassembly + capture |
How many writers does ctx[+0x28B] have? | fiveWhyThe four SCUS raises plus the tick's own clear, | disassembly |
| Which stat does each damage wrapper read? | 801DD4B0 INT, 801DD6B4 ATKWhy
| disassembly |
| What actually breaks a rebuilt PROT 0874 container at battle load? | a truncated packWhy
| disassembly + capture |
What is FUN_801F44A0? | the floating damage-number ring pushWhy
| disassembly |
| Which entry does the battle loader's VDF registration walk? | raw TOC 874 = extraction 872, vdfWhyIt reads raw entries | disassembly |
| What makes a Ra-Seru chip render? | resolvedWhyThree gates in order, and only the third selects a mark: the member's own chip byte | disassembly |
| Does the port draw retail's Rot / Curse marks over refused arms? | yes, on both hostsWhyThe ring stamps Rot on the Attack chip under | disassembly |
What restricts a special battle's commands (0x8007BAC0)? | resolvedWhyBit | disassembly |
| What does a cast cost? | MP, not APWhyThe cast path | disassembly |
0x801E6218 - the multi-cast sweep the port was said to be missing | the latch's fall-through tail, ported; nothing else enters itWhyIt is the tail of the once-per-action teardown latch, entered only by falling through from | disassembly |
Which routine sets battle status bit 0x400? | resolvedWhyPROT 0955's Kiss of Death miss arm, | disassembly |
| PROT 0955's six no-damage tick bodies | resolvedWhyWhite Shield multiplies the defence pair by 3/2 idempotently from | disassembly |
| Which of the capture bodies sharing a module image seat records nobody has placed? | each body names its own; PROT 0955's stager seats the two that drawWhyKiss of Death seats its lips and burst per arm; White Shield's arm 0 seats | disassembly + capture |
| What texture do Kiss of Death's lips draw from in the Lip King fight? | the caster's own readef pages, streamed on its turnWhyThe lips record samples tpage | capture + disassembly |
| What draws Terio Punch's arm-8 starfield? | record 0x801F799C on the side-band page, over a stage the byte 3 keeps darkWhyA camera-space sprite at | capture + disassembly |
| Which slot-B images write the actor stat block? | eight, not 0955's fourWhyEight of the 64 images write | disassembly |
| How many capture trampolines are there, and what keys a tick body? | 21 trampolines / 48 arms; the key is a pairWhyThe band holds 21 trampolines carrying 48 | disassembly |
| What does the AI companion pick read? | it watches the wrong actor by designWhy
| disassembly |
| What are the battle applier's 132 selectors? | 116 of them are the epilogueWhyJump table | disassembly |
| How does a learned skill enter the displayed list? | ordered insert, not head insertWhyBoth list writers insert in ascending order: the party-slot learned-Arts list at | disassembly |
FUN_80043264 - how many equipment slots does it scan? | three, not eightWhyThe counter starts at | disassembly |
What does FUN_801E09F8's hit arm write? | resolvedWhyThe | disassembly |
| Which routine applies a player-Seru module's magnitude? | the tick body, not the stagerWhyThe | disassembly |
| Which player-Seru modules heal, and by what formula? | three: Vera and Orb in the base band, Spoon in the evolved oneWhyPROT 0905 (Vera) restores | disassembly |
| How does the battle AP gauge fill, and what does a level-9 heal do to it? | +8 per action, +32 for Spirit instead, damage taken, and a level-9 light heal doubles itWhyActor | disassembly |
| How many phase stores does a player-Seru body make? | 3..10, counting both store formsWhyPer module: 0903 4, 0904 6, 0905 4, 0906 8, 0907 10, 0908 6, 0909 6, 0910 5, 0911 3, 0912 6, 0913 5; eight of the eleven write the terminal | disassembly |
What is actor +0x1DC at the cast band's reaction sites? | flag bits on the victim, a restage bump on the casterWhyOn the victim it is a bitfield: PROT 0903 and 0904 | disassembly |
| What does PROT 0907 (Nighto) roll? | a kill roll and a resist roll, and the boss case is forcedWhy
| disassembly + capture |
| Which band routine allocates a battle seat? | PROT 0940's 0x50 / 0xAE, and only that oneWhyIt claims | disassembly |
What does PROT 0941's 0x51 (enemy Steal) take? | a bag slot or an item off the steal table; no damageWhyAgainst a party victim it rejection-samples the 256-slot bag at | disassembly |
What does ctx[+0] count in a cast module? | the party, not the whole actor tableWhy
| disassembly |
| What escapes PROT 0906's phase-5 dispatch hole? | the stager's arm 2WhyThe tick's arm 4 advances the phase byte into a slot its own table does not serve, and retail parks there busy until the stager runs: | disassembly |
| What is PROT 0904's arm 12? | the swept quantity is an angle, not a radiusWhy
| disassembly |
| What is PROT 0962's arrival predicate? | it arrives when the test reads zeroWhy
| disassembly |
Is ctx[+0x287] the counter-attack byte? | no - it is the scripted-fight flag, derived at battle initWhy
| disassembly + capture |
| What gates each player-Seru arm's dwell? | measured per arm - and it is not one constantWhyDriving casts from pre-cast states gives per-arm dwell in module ticks for PROT 0903 / 0904 / 0905 / 0907 / 0908 / 0910 / 0911 and 0959. PROT 0907 holds 8 of its 16 arms identical across two fights and moves the other 7, so a single per-arm frame constant is the wrong model. | capture |
| Do the Seru side-effect debuffs and the encounter boost profiles reproduce live? | yes, both, at measured magnitudesWhyA level-9 Seru debuff takes a monster's ATK from 17 to 14 on both halves - the documented 20%. Both boost profiles reproduce off a live fight: a random encounter's | capture |
| What does the dome tally screen draw? | six rows, and the HP accumulator sits between lanes 2 and 3WhyThe rows are | disassembly |
What does the dome arena seed 0x8007BAC0 with before any story flag matches? | 1Why
| disassembly |
Is 0x801F69D8 a routine in PROT 0943? | no - it is a head tableWhyThe five-word table at the image base is dispatched by the body at | disassembly |
Is the monster record's +0x20 a per-monster death-immunity byte? | no - a double-width texture-page flagWhy
| disassembly |
| Where does a slot-B module image's highest spawn record end? | its own move-VM program bounds itWhyWalk the opcode widths to a terminator and round the end up to 4, because the records are word-aligned. | disassembly + capture (disc walk) |
| What are Enemy Steal's acceptance tests, and what does the consume touch? | a third test on shop price, and a window-bounded consumeWhyBeyond the roll and the occupied-slot test, the sampled item is accepted only when its | disassembly + capture |
| How large is the item bag, and what bounds a reader? | 256 slots behind an active windowWhyOne 256-slot array at | disassembly |
Do the two move-0x36 / 0x37 damage wrappers differ from the shared kernel? | the respect wrapper does not, the bypass one doesWhyDriving both against the same seat, | capture |
Does the spell record's +0x00 class byte reach the action seed? | yes - it moves the fold by 21 framesWhyThe action seeder's Magic arm compares the byte against | capture |
| What gates each capture-class arm's dwell? | for all fourteen trampoline arms; the drain is per armWhyThe countdown is drawn down by a per-arm multiplier of the scratchpad frame byte at | capture + disassembly |
Is the battle loader's model-pack pointer *(gp+0x6BC) sane at battle time? | no - the battle load reuses the blockWhyAcross the catalogued state population the pointer names the same heap block in 97 of 98 states; it is sane in 37 of 37 field states and garbage in 61 of 61 battle states. The unclamped registrar inside | capture |
| How does the character-pack loader size its section buffers? | from the container header, so a rebuild cannot change the decoded sizeWhy
| disassembly |
Is FUN_801F6B24 one walk or two? | two dispatchers picked by one wordWhy
| disassembly |
| What are PROT 1221 and 1222? | the party's ringside reaction pairWhyTwo headerless BGR555 stills, | capture + disassembly |
| Where does a retail value-readout quad put its far corner? | inclusivelyWhyRetail's own readout quads name the last texel, not one past it: 31 against 31, 47 against 47, 55 against 55 over the sampled frames. An exclusive corner draws every quad one texel wide; with the inclusive corner the digit gap is 0. | capture |
Is the model-pack registrar ever entered while *(gp+0x6BC) holds a reused block? | no - six routes, five entries, count 5 every timeWhyBreakpointing | capture |
What is FUN_8001E890's gate? | the load-state word gp+0x6AC, not the game modeWhy
| disassembly |
Why do PROT 0943's 0x40 and PROT 0944's 0x53 fault before their first tick? | they do not - the fault is SCUS walking the casterWhyBoth stage clip | capture + disassembly |
What is battle context +0x276? | the side-band applier's stage byteWhyIts writers are | disassembly |
What does FUN_8003DE7C(1) wait on? | a data stream read in flight, not the last voice clipWhyOn the retail path ( | disassembly + capture |
| What does the dance count-in banner draw? | a sprite record, 160 x 32WhyRecord 0 of the 20-byte HUD table at | disassembly |
Does any item reach the Point Card strike (effect class 14)? | yes - item 0xFE, by id rather than by descriptorWhyArm | disassembly |
| What does an arts book write, and to whom? | the class picks the character, the tier is the art idWhy
| disassembly |
| Does retail bound the sprite-stack append its fog sheets use? | no bounds checkWhy
| disassembly |
Which battle-action state owns the only arm that reaches FUN_801F3990? | state 0x3D, and an item is the doorWhyOne reference disc-wide, the | disassembly + capture |
What writes the slot-B stage selector _DAT_8007B64A? | the field entity tick, off system flag 0x19WhyA | disassembly |
| Which extraction entry does the slot-B pager's argument name? | selector + 966Why
| disassembly + capture |
| What do PROT 0968 and 0969 do? | the two boss-stage modules, one function eachWhyNeither is a cast module: none of the three PROT 0898 entry tables names them, and only the pager loads them. Each is one function over its whole image, a phase machine on | disassembly |
What is the != 4 term in the battle selectable scans? | a seat, not an action stateWhy
| disassembly |
What is the auto-command string pair at record +0x1A7 / +0x1B7? | a staged queue and its backupWhyGated on | disassembly |
Is FUN_801D0748 the Muscle Dome's match SM? | no - it is the round SM every battle runsWhyIt has exactly one | disassembly + capture |
What draws the dome panel still at VRAM (384, 0)? | FUN_801D00F8 in the contest hub PROT 0977WhyTwo | disassembly + capture |
What raises the still's fork _DAT_801D1AE0? | the arena init, on re-entryWhyNot the match teardown: | disassembly + capture |
Who raises the Arts banner selector ctx[+0x28B]? | SCUS FUN_8004AD80, in sequence, not as alternativesWhyEvery | disassembly |
Can a RAM clobber of the resident PROT 0874 container trip FUN_8001E890's checksum? | no - the sum is over VRAMWhyForcing | capture |
What does FUN_801D84C0 build? | the four battle-result messages, not party panel labelsWhyIts two arms copy ( | disassembly |
| Where are the steal-result captions composed? | in SCUS FUN_8004AD80WhyThe three | disassembly |
What is FUN_801D32BC? | the command window's member cursor, not a turn orderWhySix | disassembly |
| At what level does a re-entered hub draw the ringside still? | *(0x801D1A7C) across six hub armsWhyThe re-entry init seeds hub arm | disassembly |
| What does the victory drop roll do? | at most one item per battleWhy
| disassembly |
| How does the victory pose pick a party slot? | a rejection loopWhy
| disassembly |
What is FUN_801E91E8? | the already-learned-Seru check of the killing-blow absorbWhy0898 | disassembly |
What does ctx[+0x269] hold? | the Seru the killing blow absorbedWhyThe Done band reads it at | disassembly |
What is state 0x3E's class-5 arm? | the gauge-extension item's presentation, not a Spirit armWhy
| disassembly |
Which monsters take the 0x19 short step? | every record without a tag-0x20 actionWhyThe approach runs a tag search over the monster's installed action table ( | disassembly + disc census |
What is action tag 0x22? | a knockout tauntWhy
| disassembly |
| What does selector 8 (Antidote) clear? | both poison bits, and it skips a dead targetWhy
| disassembly |
How often does battle action state 0x00 run? | every roundWhyBegin sets the flow word to | disassembly |
| When does the commit log glide off the ring? | on a member leaving or returning, never at round startWhyThe flow SM's tail table at | disassembly |
| How does the battle-intro banner name a repeated monster? | drop the last character, then append * NWhyFor a repeated species the label loses its final character (the instance letter) and | disassembly |
What is Swordie's slash routine FUN_801F81DC? | a four-slot progress timer, not a one-shot hitWhyEach slash keeps a progress word at | disassembly + capture |
Does anything read the second 8x8 percentage block after the element-affinity matrix (PROT 0898 0x801F5428)? | no; dead data in USA, PAL and Japanese buildsWhyBoth address scans over every image ( | disassembly |
| Encounter MAN sub-section layout | header shape correctedWhy | disassembly |
| Battle-intro tile shatter - the side-face shade page | a resident field asset, not a transition uploadWhy | capture |
How long does a field-to-battle transition run (DAT_801D2458)? | 132 frames; 252 for the swirlWhy | disassembly |
| Which stage-dome objects does the battle backdrop draw? | drop index 1, not "keep index 0"WhyThe registration edits the object list rather than truncating it: each backdrop actor owns a private | disassembly + capture |
| Battle ground grid depth cue - the far colour | captured at the draw; port fogs the gridWhy | capture |
Which element the screen-element +0x0E kind pair selects, per value | it is a table index, not a style enumWhy | disassembly + capture |
| The element-badge palette selector | the badge record's own palette byteWhy | disassembly + capture |
The status-element badge sheet 0x18..=0x20 | nine 48x16 word tags; ladder assignment independently confirmedWhy | disassembly + capture |
| Does the battle ground grid roll per-cell randomness? | no - a four-entry table walkWhyNo. | disassembly |
Endless camera orbit (Gaza 2 softlock) - the 0x19 attack-approach park | caught live; root-caused; disc fix shippedWhy | capture + disassembly |
Which FUN_801D5854 case-6 arm a running fight takes, and where ctx[+0x6DA] is seeded | resolvedWhy | capture + disassembly |
Which framing the Done band (0x50 / 0x51) takes, and the idle-orbit rate at the Begin/Run prompt | resolvedWhy | capture + disassembly |
0x19 fallback approach drive - which anim-driver field does summon staging leave stale? | pinned + causally reproduced on the parked saveWhy | capture + disassembly |
| Super / Miracle Arts trigger chain | all 15 Supers live-executedWhy | disassembly + capture |
| Xain "Bloody Horns"/"Terio Punch" ignore elemental guards (community mystery) | resolvedWhyNot an element drop - a resist-ladder bypass. Capture-class modules (PROT 944..966) pick one of two damage wrappers: | disassembly + capture |
| Which call applies Astral Slash's damage? | the melee kernel, on the staged clip's four beatsWhyPROT 952's | disassembly + disc data |
| Is a plain monster swing elemental? | yes - the melee kernel's guard ladder keys on the attacker's record elementWhy
| disassembly |
| First boss trigger → Battle | resolvedWhyThe scripted-battle arm is the field-VM op | disassembly |
| How an enemy's signature cast picks the spell id it announces | resolvedWhy
| disassembly |
| How a Delilas sibling's signature move stages its clips | walk order capture-pinnedWhyThe modules drive | capture |
| Is the player battle idle channel-delta encoded, like the readef "ME" bodies? | no - raw packed inline in record[0]WhyEach action entry in a player battle file's | capture |
| Enemy-ally charm battle softlock | both tracks fixedWhyThe state- | disassembly |
Battle-actor +0x16E bit 0x400 applier (guard-disabling status) | one setter, in a cast imageWhyThe only setter is PROT 0955's Kiss of Death miss arm ( | disassembly |
Who calls the battle on-screen test FUN_8005126C? | exhaustive negativeWhy | disassembly |
What spawns the battle XA seek selector FUN_8004DA00? | resolvedWhyNothing calls it - it is the | disassembly |
Effect-VM pass-1 "state token algebra" (FUN_801E0088) | + portedWhy | capture |
| Seru-magic summon visual (e.g. Tail Fire) | player visual; wiredWhy | capture |
| Why a disc-booted port fight never showed an enemy special | the boot catalog lacked the disc's monster-special idsWhyThe picker rolls Gimard's | capture |
The party cast trigger FUN_801DBF9C - outcome producer or params stager? | a params stager; every Seru id takes the summon armWhy | disassembly |
| Player-summon presentation - label, readout, hide, flashes, creature seat | ported behind the band's own seamsWhy | capture + disassembly |
Fade-actor hold word -1 - "no hold" or "hold until killed"? | hold until the actor is killedWhy | disassembly |
summon.dat / readef.DAT side-band streaming | entries + formatWhy | disassembly |
| Monster steal item (Evil God Icon) | resolvedWhy | capture |
| Battle face-stamp issuing site | resolvedWhy | capture |
| Per-spell magic power / multiplier | mechanism + roll portedWhy | disassembly |
| Arts command sequence - independent source | resolvedWhyThe SCUS arts-name table ( | capture |
| Weapon-specialty arm width (off-class widens the Arms command) | resolvedWhyNot a runtime favored-class comparison. The arm command's AP cost is a per-(character, weapon) byte in the player battle file, at the weapon section's swing record ( | capture |
| Stat growth-rate source | validated + wired; core + opt-in jitterWhy | capture |
Character-record HP/MP/AP pair order (+0x104..0x110) is (max, cur) | relabeled throughoutWhy | disassembly |
| Monster stat-record archive source | resolvedWhy | capture |
| Monster mesh + texture pool | resolvedWhy | capture |
| Terra slot-3 / story-flag overlap | resolvedWhy | capture |
Battle party mesh pack other5 = PROT 1204 (battle form; Baka Fighter reuses it) | empiricalWhy | capture |
| MP-cost ability-bit priority (half vs quarter) | re-read against the instructionsWhy | disassembly |
| Scripted Tetsu encounter → Battle (v0.1 oracle Battle leg) | resolvedWhyAll three inputs derive from disc bytes: the formation-row selection is the standard scripted-battle op | capture |
Battle stage backdrop: which scene_tmd_stream a scene fights in | resolvedWhyA scene bundle carries one stage stream per sub-area, and the battle's is not uniformly the block's first - | capture |
| Battle stage backdrop: is the authored half completed, and how | two actors; per-stage transform; object 1 droppedWhy
| capture + disassembly |
Battle-stage overlay band (+0x47) | resolvedWhy
| disassembly |
| Battle-intro tutorial boxes (Tetsu sparring fight) | machine pinned, ported and wiredWhy | disassembly (machine exclusivity byte-anchored; prompt pool shared with 0968) |
What arms the battle-stage id 1 (sparring tutorial) | resolvedWhyNot the formation, scene or monster: a one-shot system-flag arm. | disassembly + disc bytes |
Mid-battle second _DAT_8007B64A stage-id writer (the 0x801fd514 phantom) | coordinate re-keyedWhy
| disassembly |
| "No party seated" vs "party dead" in the wipe scan | port-only state; guardedWhyRetail's | disassembly + capture |
Battle command-flow byte ctx[+0x06] | resolvedWhyThe other battle SM - | disassembly |
Action-SM state 0xFF treated as battle end by the port | path was live-reachable; port fixedWhy | disassembly |
Spine flag 0x142 (Caruban beat / dolk-dolk2 switch) writer | disc writers + engine port + oracleWhy | capture |
Spine flag 0x482 (Drake mist-wall) writer | writer-less; "direct code path" presumption falsifiedWhy | capture |
CDNAME scene-window frame (raw = extraction + 2) in Scene::load | engine converts; misattributions correctedWhyA raw-TOC define used as an extraction index lands two entries late, dropping each block's first two retail entries and bleeding in the next block's; | capture |
Motion-VM (FUN_80038158) bytecode carrier + flag census | carrier pinned; spine flags negativeWhyThe second motion VM's bytecode source is MAN tail-section 1 (installer | capture |
| Debug-menu "STR trigger teleports + sets flags" mechanism | no per-FMV event table; dev-menu tools explain itWhy | disassembly |
Spawned-record player-channel (0xF8) ExecMove/HaltAcquire handshake | retail machine traced; the port's model divergesWhy | disassembly |
| Equipment stat-bonus table - slot model | slot model + passivesWhyThe stat-bonus table ( | disassembly |
Flag 0x63A - the vell/vozz P2[7] gate with NO script writer | script writers exist; the "no writer" premise was the CDNAME +2 skewWhy | capture |
cave01 P2[16] (the 0x15D entry-key setter) - what spawns it | slot-counted spawn chainWhy | capture |
Drake Castle deep interiors (jouinc/jouind) depth decode | door-choreography families, not story gatesWhy | capture |
scene_destinations P1-table scan misses P2-only door names | P2 pass folded inWhyThe P2-only class is the town/dungeon exit door (a P2 door-choreography record): | capture |
0x4C 0x51 byte +3 = [bit7 special-model | facing nibble] vs the glide-speed interim depth & 7 reading | facing wins; the two readings were two different opsWhy | disassembly |
| How an NPC's facing changes after spawn - snap vs ramp, and which writer wins | two laws; order-of-execution priorityWhy | disassembly |
| dolk2/rikuroa MAN source (the "v12-embedded MAN" was an over-read) | streaming carrierWhyTheir own | capture |
kor-family op-0x49 flag window [0x138..0x13F] - what the 8 flags gate | Uru Mais warp-pad destination memoryWhy | disassembly |
Attack-band damage pacing - who calls FUN_801EC3E4, what paces the hits, when the total lands | the anim tick calls it every frame; the clip's +0x10..+0x13 beats fire the hits; one HP write per comboWhy | disassembly |
| The battle-intro enemy-name banner - which placement record raises it | none does - the composer places it itselfWhy | disassembly + capture |
What does the battle overlay's private PRNG FUN_801D0290 feed? | nothing in the battle model - it shapes one effect ribbonWhyAll five draws sit inside | disassembly |
Is 0x801F6950 read across overlays (the overlay_0897_* dumps)? | no - three machine references on the disc, all in PROT 0898WhyThe field overlay's content ends at | disassembly |
What are the halfwords of a 0x80076C10 screen-element record? | a two-seat pair over shared width / height / stringWhySeat A | disassembly |
| How is a slot-B cast / summon module entered? | two link-time tables in PROT 0898; row = extraction - 903WhyThe 32-slot cast-tick table | disassembly |
| How is a capture-class cast module's tick entered? | a third link-time table in PROT 0898, keyed on the spell record's sub-idWhy
| disassembly |
| Is the whole slot-B band mapped? | all 64 entries carry a static-overlays.toml row; 61 are dumped, and 0915 / 0926 / 0935 wait only on their extraction imagesWhyFunction extents recovered by frame matching (prologue → the first | disassembly |
| The slot machine's cabinet emitter | a mesh, not a packet: PROT 1200 descriptor 1WhyPROT 1200 carries three descriptors - | disassembly |
| The slot machine's in-game BGM | host-scene authored, not the overlay'sWhy
| capture (disc read of the scene MANs; script data, not code) |
| Which payline does a forced slot stop land on? | one of the five, chosen per spin by rand % 5Why
| disassembly |
| The Muscle Dome's Auto arm - which routine picks the commands? | none on the screen; the Auto flag makes FUN_801F0450 rebuild the queue every roundWhy
| disassembly |
The dome's three UI cue ids (0x21 / 0x22 / 0x23) | accept / highlight moved / refused-or-backWhy37 | disassembly |
| The arena backdrop's object-1 dust decal | trimmed by the SCUS battle loader unless _DAT_8007B64B is setWhy
| disassembly + capture |
FUN_801F2410 / FUN_801F2E10 - dome routines or shared? | neither is the dome'sWhy
| disassembly |
What is per-character record +0x131, the second byte the new-game seed inits to 1? | nothing - it is write-onlyWhy
| disassembly |
What does item-effect flag bit 0x40 mean? | the descriptor's target side - set = the enemy partyWhyRead once, at | disassembly |
What is the arts-name table's +4 halfword (DAT_80075EC4)? | an authored tier constant with no readerWhyEight sites materialise the table base; a delta-tracked walk of every load off them reaches only | disassembly |
What is the byte at actor[+0x22C] + 0x80 the SFX-cue router folds into the category? | the actor's display object's sound-bank category, values {0, 7, 8} - not an element byteWhy | disassembly |
What moves the evolved-Cort battle off flow ctx[+0x06] = 0x0C? | the boss stage module hands the flow back after its own intro countdown; no input involvedWhy | capture + disassembly |
Is _DAT_8007B64B clear during a Muscle Dome contest? | yes - the dust decal is trimmedWhy
| capture |
| Do the renderer's light-capable prim kinds 8..11 ever execute? | negative (no live light path)WhyZero hits on | capture |
What are PROT 1221 / 1222 (other5 / other6)? | int.tim / int2.tim - the Muscle Dome's two ringside panel stillsWhy | disassembly |
| What do the higher readef groups' aux slots carry? | no new kind - textures, the four ME archives, or a never-staged actor recordWhy
| disassembly |
Is cutscene_str (PROT 0970)'s 123 KB code gap un-dumped code? | a zero-filled reservationWhy
| disassembly |
| Can a scene TMD pack member be grown in place? | nothing outside the pack holds a byte offset into it - rebuild is the only costWhy | disassembly + capture |
| Is any of an enemy signature cast's choreography data-driven? | partially - the spawn layer is data in the art path's own record format; the lift and camera are module codeWhy | disassembly |
| How many damage-clamp shapes does the slot-B cast band use? | three, and one module picks per hitWhyOver the 83 wrapper | disassembly |
| Where does a capture-class cast get its damage constant? | baked into the module image, not read from a tableWhyEach capture-class module carries its own power constant as an immediate ahead of its wrapper call (read here for PROT 0966: | disassembly |
| Which slot-B images touch more than one seat? | two stager routines, and both subtract HPWhyPROT 0927's | disassembly |
How do the 0x801CF56C capture-class arms reach a spell body? | through a per-image trampoline in six of themWhyArm | disassembly |
What calls FUN_801E91E8, and what reads its result? | one jal, and half the result is unconsumedWhyIts single call site is | disassembly |
| Does the battle tutorial's wait state draw anything? | negative (kind 0xD draws nothing)WhyThe tutorial's wait record is widget kind | disassembly |
What are placement-record +0x0E/+0x0F and +0x10? | +0x10 is the widget kind, +0x0E/+0x0F the frame styleWhy
| disassembly |
What is the battle attack angle ctx[+0x6D2]? | a bounded framing term, zeroed after the first hitWhyIt holds | disassembly |
Which spells are 0x27 and 0x81? | 0x27 Tail Fire, 0x81 Gimard / Burning AttackWhyThe two ids live in different spaces. | disassembly |
| Where is the Spirit AP-halving flag? | character record +0xF8, bit 0x800WhyIt is accessory passive | disassembly |
What is battle ctx[+0xD]? | a four-value ladder whose arms compose like two bitsWhy
| disassembly |
| Does an Attack x2 consume the Super Art starter? | no - the starter survivesWhyThe compare is | disassembly |
What are battle ctx[+0x17], ctx[+0x18] and ctx[+0x19]? | a once-per-action teardown latch, a HUD element id, and a per-battle Spirit latchWhy
| disassembly |
What is battle ctx[+0x270]? | FUN_801D5854's second camera rampWhyWritten over | disassembly |
What does the dome's contest restore (FUN_801D0ED8) do? | one-shot per contest, and it leaves the accessories aloneWhyThe caller | disassembly |
| Do the dome's hub screens run on frame counts? | no - they are fade / hold / fade envelopesWhy
| disassembly |
| Which dome chip seats does the AP-cost re-centring apply to? | both x seats, +0x2 and +0xAWhyThe term is | disassembly |
What advances the victory counter gp[+0x9EA] the win-pose mouth flap clocks on? | the battle tick, by the frame stepWhyThe battle tick | disassembly |
| Why are Noa's battle-file sections 2 and 3 swapped against Vahn's and Gala's? | the file follows her record's equip bytesWhyThe battle-file loader matches section | disassembly |
What do the record's +0x00 and +0x04 hold across a captured level-up? | cumulative XP and the next threshold; one level, not fourWhy
| disassembly |
| Which capture bodies still replay their clips ahead of the arms? | none: every stage site sits in its armWhyEach | disassembly |
| What is left of Koru's Ultra Charge film? | the module band's rand() dealWhy
| capture |
| Why does Plasma Strike's victim burn through arm 5? | the module band's rand() dealWhyLu's charge clips resolve seven hit events through arm 5 on both sides; retail's block roll grants all seven, the engine's own stream four, so three connect and burn the victim. Seated on retail's words each pass ( | capture |
| Why do Thunder Storm's ribbons fall short of retail's centre? | the arm's armed wash: its frames build on each otherWhyArm 4 arms the wash with | disassembly + capture |
| What sets the white pulses' phase in Barra's arm 7? | the pass count: the cloud cycle steps once a passWhyThe emitter | disassembly + capture |
| What brightens Barra's arm 7 every ten to twelve vsyncs? | a cloud of the four-record cycle reaching the lensWhyThe four records | disassembly + capture |
Why does Theeder's arm 11 flash read a step ahead once a fade takes its spawning frame's step, and why does Gizam's arm 9 fall with the 0x32 hold? | neither does now; what is left is not the flashWhyOn the Gobu Gobu films the frame luma tracks retail's pair for pair through both arms: Theeder's arm 11 within | capture |
| Where do Gimard's arm-11 flame puffs spawn from? | from the walk's own pose, two a pass, as retail'sWhyThe burning-body emitter sheds two puffs a pass at the creature's pose of that pass. On | capture |
| Why do five enemy casts lose their cues under the console CD timing? | the module bands' rand() deal and one pass on PCSX-Redux's lattice; no cue test reads the stepWhyEngine films under | capture |
| What sheds the rest of Element Strike's burn? | the golem's two after-image ghosts, while its strike clip runs to its natural endWhyThe party seats' selector retires when the tint decay arm eases their word back to neutral, | disassembly + capture |
Long write-ups in this area
The action SM's idle request keeps a playing idle
Grade: disassembly + capture. The port answers the action SM's FUN_801D5854(seat, 6..9) calls with an idle request on the body (World::apply_battle_pose). Retail's routine is the battle camera: across overlay_battle_action_801d5854.txt its stores go to its own frame and the battle context's framing words, none to the actor's +0x1D9..+0x1DC or the node's cursor +0x68. A request therefore never moves a body's idle.
The idle's phase is the loader's. FUN_800513F0 draws one rand() a seated body and stores (rand() % frames) << 4 into the node's +0x68 (jal 0x80056798 at 0x80051544, sh v1,0x68(s1) at 0x80051574; the monster twin from 0x800518D0). The engine held the draw until the body's first idle request, and the install it found carried no idle marker. That was the render build's player at keyframe 0, or the scene host's. So every body swayed in step from the battle's first frame until its own first turn, then jumped to the drawn phase. World::seat_battle_setup_idle now seats clip 0 at the draw when the loader installs the clips, and the SM's requests keep that loop.
The idle's phase also decides when a cast starts. A clip staged under neither +0x1DC stage bit commits only from the anim tick's natural-end path (FUN_80047430 to FUN_8004AD80 at 0x80047B54), so a Spirit or Magic band waits on its caster's idle loop. A restart at the pre-arm had re-based that loop at keyframe 0, which held the commit for most of a cycle. With the loop kept, the wait is what is left of the cycle. Per-tick traces of the pad drives, run on both trees, show this as their first divergence. Noa's opening Magic commits clip 0x11 eight ticks sooner (state 0x48 ends at tick 133 against 141). The heals' Spirit band 0x3D commits clip 9 two ticks sooner, and everything after moves with it.
On the image channel the battle corpus moves both ways. battle_uru2_nurga_heal_mid_cast rises from .973 to .999 and battle_rugi_puera_v2_strike from .989 to 1.000. Eleven pad-driven rows fall on that shifted history, battle_tunnelc_xain_bloody_horns_mid_cast the most (.855 to .797).
A pinned replay opens its fight with no setup draws, so its opening turn's idle phase is its own, and retail's is in no RAM the capture holds: the commit it waits on lies far more than sixteen passes back.
On battle_bylon_theeder_v2_thunderbolt_mid_cast the shift did not move the frame through the cast itself: it tipped the harness's undrift search onto a round eight units of residual nearer, which moved Gala's seeded ground and with it the box the displayed walk starts from. The search now keeps a round only when it is UNDRIFT_MIN nearer, and the still reads .999 (retail-compare).
The super_queue_replace_* drives had gated the Attack arm's seed pass on ctx[+0x87C]. That word counts from the acting seat's last idle re-commit (0x8004BF50..0x8004BF78), so the drives now meet the pass on its yaw rung. A caster idling in the capture band's entry 0x6E is seated on the capture's cursor, as in 0x6F..=0x71 (retail-compare).
An Orb's Heal strikes the ally it heals
Grade: disassembly + capture. A monster's heal or buff (a spell record class under 0x14) runs the action SM's Spirit / Item band, and two sites land it.
The clip's effect script seeds a homing flight, and the flight's hit arm in FUN_801E09F8 does not ask what the cast is: it rolls FUN_801DD0AC on the row the caster's +0x1DF maps to (lbu a0,-0x1(v1) / jal 0x801DD0AC at 0x801E1888..0x801E188C) and takes the roll off the victim through the safe applier (sh v0,0x14c(v1) at 0x801E1960), pushing it onto the readout ring. Heal's map entry is 0, the table's all-zero row, so the roll is the stat terms alone.
Then 0x3F calls FUN_800402F4 with the spell record's pair, (0, 0, ..) for Heal: the static heal table's 200, stored at 0x800408AC with no popup.
A --watch 0x800ED684 recording of the Orb's Heal on its Killer Bee (ropeway_orb_pre_heal, target seat 4) reads both stores: 96 -> 46 from 0x801E1960 (ra 0x801E1894) in 0x3E, then 246 from 0x800408AC (ra 0x801E413C); the frames show the 50 popup and DAMAGE 50, and no heal number. The port folded the catalog's heal at 0x50 with a popup and never struck. Port: World::land_spirit_band_flight_hit and the monster arm of World::fold_battle_item_action; the film reads .843 to .951.
The earlier actor of a monster's walk is DAT_8007BD00
Grade: disassembly + capture. FUN_801D8DE8's case 0x52 (the spirit-gauge panel, entry 0x48 of the jump table at 0x801CEB68) stores DAT_8007BD10[ctx[+0x13]] - 1 to DAT_8007BD00 (sb v0,-0x4300(v1) at 0x801D9094), the acting member's character index. The action seed's Spirit arm raises the panel (li a0,0x52; jal 0x801D8DE8 at 0x801E2FDC), so the byte names the last party member to act on a Spirit, the framing a monster's case-6 walk sets out from.
Over the ten captures whose walk start was read off the frame (DISPLAYED_WALK_PINS), the byte names the fitted seat on all nine that walk from an earlier actor; the tenth, battle_rugi_barra_windcutter_mid_cast, walks off the prompt and reads no seat. No other RAM byte, as a seat, seat plus one, bit or actor pointer, agrees with the fits on more than eight. The harness reads it (RetailBattle::gauge_actor); the pins keep the style and a measured yaw, and the ten stills score as before.
The battle hit tint - what a landing hit writes and how it reaches the pixel
Status: resolved. Evidence: capture (the triple, its ease rate and the pixel law read off two consecutive save states; every write and gate also in disassembly).
A landing hit stamps a three-word tint triple on the struck actor, selected by the acting record's +0x7A (party melee / arts) or the move-power +0x0A (monster specials). The presentation SM eases it back, and the draw stages the words as GTE far colour + IR0, so the texel is modulated, not replaced.
- Stamp.
FUN_801EC3E40x801EE3D4..0x801EE43C:+0x04 = 0x801F53D4[sel - 1],+0x21F = sel,+0x0C = 0x1000, withsel = record[+0x7A], abeq zeropast the arm andsltiu v0,v0,0x6bounding it. No exit precedes the arm, so every connecting swing (a Stone-absorbed one included) reaches it. - The bound is a route, not a table guard. Six archive entries carry
6, and6is the tint-less Curse arm (0x801EE690, a 1-in-4+0x16E |= 0x1000roll). Pin:battle_afterimage_gate_real.rs. - Other stampers.
FUN_801E09F80x801E15AC..0x801E15ECis the monster-special twin offmove_power[+0x0A], at each arm's impact phase (ctx[+0x24E + i] == 3). The clip-0x18arms ofFUN_8004CE2Cstamp the same three words (0x8004D1D4..0x8004D1E4,0x8004D28C..0x8004D29C). - Ease.
FUN_80050120arm 0 (0x800501A4..0x80050210): the word eases to0x20080200at1 * dt * 8lane units per frame, then+0x0Cdrains bydt << 5, then+0x21Fclears. The jump table at0x8001532Croutes states1/3/4/6..=10to fixed colours with+0x0C = 0x1000,2to the capture fade,5to a hold. - Capture.
battle_gimard_tail_fire_b->_ahold Vahn at+0x21F = 1,+0x0C = 0x1000with the red lane at0x35Fthen0x31F- eight frames of the ease.battle_melee_hit_spark(Vahn's Somersault landing on Gimard) holds Gimard at the neutral word,+0x0C = 0,+0x21F = 0: a selector-0 record tints nothing. - Disc census. Every player-file basic entry and every Somersault-class art is selector
0; only five Vahn art records (selector1) and two Gala records (selector2) tint their target. - Pixel.
FUN_8004A908packs the lanes into the render node's+0x74and copies+0x0Cinto+0x78;FUN_80048A08stages them as far colour +IR0(gp[0x9D8]/gp[0x9DC]). The struck Vahn reads red160..248over green / blue8..80across his texture - modulation, not a flat fill.
Port: engine-battle-vm::battle_formulas::tint_sm_step (the SM), World::arm_impact_tint at the three hit seats, MonsterAnimation::impact_class carrying +0x7A; both hosts stage IR0 = blend / 0x1000 toward the unpacked lanes (battle_impact_fx::tint_ir0).
Owning page: battle.md.
Battle end - the results sequencer's timeline, and who strikes the pose
Status: resolved. Evidence: disassembly + capture.
FUN_8004E568 runs every frame the battle-end signal is 0xFE, the battle exits on ctx[+0x6CE] >= 0x43, and the acting slot ctx[+0x13] strikes the pose.
FUN_80046A20stops stepping the action SM onceDAT_8007BD71 == 0xFE(0x80047040) and callsFUN_8004E568every frame (0x800470D0..0x800470E8); the exit test isslti v0,v0,0x43onctx[+0x6CE](0x80046DAC)._DAT_8007BD2Cdoubles as the sequencer's phase word (jump table0x800152FC):0 -> 2 -> 4 -> 5through two CD loads for a victory; a wipe's5lands on the annihilated arm directly.- The results frame stages the pose into
+0x1DAof the seatctx[+0x13]names. The action SM's victory arm writes that seat last:FUN_801E295C'ss5isctx + 0x11(0x801E2994), so itssb v1,0x2(s5)stores (0x801E66EC,0x801E6740,0x801E6760) arectx[+0x13]stores. An earlier reading searched for0x13(offsets only, missed them, and concluded the leader poses. - Timeline on
rim_elm_gimard_victory(PCSX-Redux poll, N = 1): signal v322, results v402, exit fade v657, exit v723. - The three-member
noa_levelup_bannerstate readsctx[+0x13] == 0with seat 0 carrying pose0x14while Noa levelled: Vahn landed that fight's last action.
Port: world::battle::victory. Owning page: battle.md.
What a normal party attack sounds like
Status: resolved. Evidence: disassembly throughout; the live gate by capture (N = 4, probe autorun_w4d_melee_grunt_gate.lua).
An ordinary swing emits neither cue. The XA30 grunt belongs to a strike that commits the defender's +0x1F3 reaction; the 0x10C sting plays only when the effect handle _DAT_8007BD84 is non-null.
- The fork (
FUN_801EC3E4, on_DAT_8007BD84). Zero takesFUN_8003D53C(0x1D, chan, dur)at0x801EEB44(per-character(0,0x26)/(4,0x2E)/(6,0x1A)offDAT_8007BD10[seat]) and the re-read at0x801EEB60skips the cue. Non-zero branches over the grunt (0x801EEAC8) intoFUN_8004FE5C(0x10C, seat)at0x801EEBE8, whose voice leg is further gated onFUN_8003DE7C(1) == 0(0x8004FE9C). The grunt's seat iss6(0x801EEA70), the sting'ss4. - The gate ahead of both.
0x801EEA88/0x801EEAA0requires7 != 0ands7 == actor[s4][+0x1F3],s7being the staged pose byte committed to+0x1DAat0x801EEC6C; only one of the fourteen definitions reaching the compare loads+0x1F3. The+0x1F3reaction sits behindsltu s0,s1at0x801EC878, whose threshold is not pinned. - Third gate.
0x801EEAB8 slti v0,v0,0x2reads_DAT_8007BC20, the executable's ownxa_flagdebug counter (XA-drive state:FUN_80016B6Cprints it at0x80016EB8..0x80016EC0,FUN_8004DA00zeroes it on five arms) - not a character level. _DAT_8007BD84is an effect-instance handle, not a mode word, and the same cell the damage finisher reads as the enemy-defender halve. An all-forms sweep (thegpform0xA6C(gp)yields zero) over SCUS, all 1233 PROT entries and the overlay images finds exactly three stores:0x8004D658and0x80056080, bothsw zero(battle start, round reset), and one non-zero at PROT 0940 file+0xCA0=0x801F7678, the Cort "Mystic Shield" stager saving theFUN_80021B04handle it spawned.FUN_8004CE2Cdereferences it (0x8004D548;+0x56/+0x72written,+0x10read) and consume-releases it at0x8004D658alongside cue0x10D. An ordinary swing pages in no capture module, so the cell reads null.- Banks (disc demux).
XA27is an eight-channel stereo sting bank,XA30a ten-channel mono grunt bank. - Live. All four captured party swings skip the gate:
s7comes from the+0x1EF/+0x1F0/+0x1F1reaction-pose loads, never+0x1F3, and neitherFUN_8003D53CnorFUN_8004FE5Cis called.
Port: World::fire_melee_impact_cue + read_battle_xa_clip_bank.
FUN_8003EAE4 is a seek plus bookkeeping - and the driver is not untraced
Status: resolved. Evidence: disassembly.
FUN_8003EAE4 seeks and raises gp+0x908; a lone call streams nothing. The CD-callback sequencer it does not arm is FUN_8003D764.
- What it does. Cancels any in-flight read, positions the drive on the clip file's record (
FUN_8005C160(2, 0x801C6ED8 + slot*8, 0x8007BC10)- the argument is the record pointer, not the slot index), issues CD command0x15(li a0,0x15; jal 0x8005C034at0x8003EB68), and stores1atgp+0x908/gp+0x910and the slot atgp+0x890. - The driver.
FUN_8003D764(dumped;functions/script-vms.md) dispatches solely on the state ringgp+0x928, whose only writer isFUN_8003D53C(0x8003D6F4,0x8003D724), which also registers the callback.FUN_8003EAE4never writesgp+0x928; it reads it as an entry gate that makes the whole routine a no-op while a clip is armed. - Its three cells. Only
gp+0x908has a reader (a "streamed clip busy" level);gp+0x910andgp+0x890are write-only acrossSCUS_942.54and all 1233 PROT entries.
Formation species limit - what the battle setup does with 3 distinct monster ids
Status: resolved. Evidence: capture (write watchpoints + cell readback + heap walk; the rebuild loop also read in disassembly).
A formation [a,b,c] of three distinct species loads as [c,c,a] on half of all rolls and verbatim (three streamed blocks) on the other half.
- The loop.
FUN_80055B6C(0x80055C80..0x80055D2C) counts copies ofcells[0]and holds "the other species" in a single register, then - behind a 50% coin flip (FUN_80056798() & 1) - rebuilds the cells as[other x n, first x m]. For two distinct species that is an exact multiset swap (the species-order variety shuffle); retail authoring never exceeds two. - Three species. The third overwrites the register: the middle id vanishes and the last is duplicated.
- Capture.
autorun_formation_cell_writers.lua(write PCs0x80055D14/18): installs of[133,151,94]/[94,133,151]/[151,133,94]/[32,34,14]all read back[c2,c2,c0]at battle main, seat 1 sharing seat 0's record pointer, in both the map03 and rikuroa contexts. - Verbatim side. All three distinct blocks stream, which turns an over-budget trio into a probabilistic battle-load hang (freeze captures: the heap-budget section of
battle.md). - Randomizer. The unconditional battle-load safety pass
enforce_species_limits(crates/patcher/src/encounter.rs) caps every random formation at 2 distinct species and at the disc's authored heap-cost maximum.
Attack-band damage pacing - the hit-event model
Status: resolved. Evidence: disassembly (FUN_801EC3E4 head 0x801EC41C..0x801EC494, apply arm 0x801EE984..0x801EEA78, epilogue 0x801EECDC..0x801EECE8; FUN_80047430 call sites 0x800478A0 / 0x80047BF0 and the bit-1 cut 0x80047900..0x80047948; FUN_8004AD80 0x8004B064) + capture (two PCSX-Redux hit-event timelines, a plain Somersault and a Tri-Somersault Super).
The anim tick calls the damage kernel every frame a battle clip plays, the clip's +0x10..+0x13 beats fire the hits, and live HP moves once per combo.
- Who calls it. The strike loop of the action SM (
FUN_801E295Cstate0x1E) stages one byte into+0x1DAand sets+0x1DCbit 1; it never calls a damage kernel. The kernel runs from the anim tick with the cursor frame ina2. - Head guard (is this frame a hit).
ctx[7] != 0x5A; the committed entry's byte 0 in0x0C..=0x1F; the per-clip hit index+0x1F4 < 4;entry[+0x10 + idx] != 0;frame + 1 >= entry[+0x10 + idx]. - An admitted hit resolves with
entry[idx]as its power byte, adds the damage to the target's combo word+0x0and its HP-bar word+0x10(0x801EDB40/0x801EDB58) and bumps+0x1F4; every commit zeroes the index. - The HP write (
s2 = 0mode). The hit that lands after the strike loop has parked the cursor at0xFF(ctx[+0x15],0x801EE9A4) and is its clip's last listed beat (entry[+0x11 + idx] == 0 || idx == 3) subtracts the whole accumulator from+0x14Cand zeroes it (0x801EEA10..0x801EEA74). - The other two
s2arms (0x801EDEE4..0x801EE130, monster-attacker copy0x801EE790..0x801EE980).s2 != 0: a look-ahead over every remaining hit of the action that finds none able to connect with the target's+0x1Esize class applies the total at once.s2 = 0xFF: the War God Icon's Attack x2 (ability bit0x0D) withctx[+0x16] < 2withholds it so the pair lands as one. - Bit-1 cut. The tick commits the next staged byte once
entry[+0x10] + 2 < framewithentry[+0x76] == 0, which chains one swing into the next mid-clip; art records carry+0x76 = 1and play to their natural end. - Loop window. The loop-window arm re-zeroes
+0x1F4on every rewind for a party slot on0x11with a latched id>= 0x2B(0x80047840..0x80047878), so a windowed Hyper / Super clip re-fires its hits per cycle.
Captures (N = 2, scripts/pcsx-redux/autorun_hit_event_timeline.lua over party_basic_attack_vs_gobu_gobu and battle_vahn_tri_somersault_super):
- Plain three-arrow Somersault. Swing
0F(p0 = 0x18, e0 = 7, lock0) commits and hits at frame 6; swing0E(p0 = 0x13) commits 8 vsyncs later at frame 10 (the bit-1 cut) and hits at frame 6; the0x19starter (slot0x10,e0 = 0) plays with no events;0x27installs at slot0x11(e0 = 5, lock1) and hits at frame 4 while the SM sits in0x20, and the target's HP falls 76 -> 23 in that one frame. - Tri-Somersault Super. Seven hits across
0F, the two-hit0x1F(p = [0x17, 0x17],e = [8, 19]),0E(p0 = 0x1D), the0x1ASpecialStarter (slot0x11, loop window[13, 14] x 5, solo byte1) and three0x2Bclips; HP lands 3542 -> 2318 on the last one. - Probe caveat.
*(actor + 0x22C)alternates between two draw nodes on consecutive vsyncs, so a per-vsync cursor sample can read the node the tick did not advance (three frames behind); the in-sync stream satisfies the head guard on every hit. - Port timeline (
legaia-engine play-window --battle 4,RUST_LOG=legaia_engine_core=debug, N = 2 runs) reads the same shape. A two-swing Auto queue commits0C, hits at frame 6 of itse0 = 7entry and accumulates, commits0Dat the boundary and lands the total on its hit. A typed↑↓↑becomes0F 0E 1A 27: both swings hit at their beats, the starter parks[13, 14] x 5with no hit, and the Somersault entry (p0 = 0x18, e0 = 5, lock1) hits at frame 4 and lands the total once.
Port: legaia_engine_battle_vm::battle_action::hit_event (the head), World::tick_battle_hit_events / land_melee_hit / apply_combo_total (engine-core). Owning page: battle-action.md.
The battle HUD's per-phase surfaces are the sub-draw script table
Status: resolved. Evidence: disassembly, corroborated by the handle lists of sixteen states.
Which readouts a battle frame shows (roster card, full-width pill, plaque, AP plate, target plaque, move name) is not computed: it is a table of per-step element lists.
- The menu SM
FUN_801D0748runsFUN_801D388C(step)on everyctx[+0x06]edge (0x801D0EE4,0x801D109C,0x801D13F0,0x801D14C8,0x801D1658, ...). stepindexesPTR_DAT_801F4D34: fifty[count][anim][panel]+countx(placement record, mode)records in the battle overlay's rodata, walked at0x801D4BA4..0x801D4CBC.anim = 1hard-resets the handle list first, so the pairs are the whole screen.FUN_801D8DE8(record, mode)takes the record straight into0x80076C10 + id * 0x18. Mode bit 0 picks the spawn seat (+0x02/+0x04or+0x0A/+0x0C) and the glide runs to the other; bit 1 suppresses the glide (0x801D92E0..0x801D93DC).- The action SM's openers are the
0x0Cseed (0x801E2F24: bar for a party target byte+0x1DD, panels for8) and the Item / Spirit pre-arm0x3C(0x801E3DA0); every close is in the0x51band (0x801E6170..0x801E6364). - The item window's
0x64"target strip" is record 7 again (step0x12:07/0,34/1), and its packet-pinned pens are the ring bar's seat for seat - the port draws the bar there and the item window keeps only its breadcrumbs. - Each
mednafenstate'sctx[+0x1074]walk agrees element for element; the disc-gatedcrates/engine-scene/tests/battle_hud_subdraw_disc.rsholds the port's constants to the table's bytes.
The resulting rule (card at the round prompt and while a window is browsed, pill in the ring and the target steps, nothing during a party attack on a monster, the target's pill during a monster cast) is on the owning page: battle.md.
The ring's element chip reads - or the Ra-Seru, and the gate is the equipment byte
Status: resolved. Evidence: disassembly, corroborated across twenty-nine states.
- The chip.
FUN_801D8DE8's case for record 10 (0x801D8EC8..0x801D8F2C) writes the chip's string pointer as0x801F4B9E + char_id * 10(the runMeta/Terra/Ozmaat indices1..=3, a lone-at index 4) whenctx[+0x25F + member]is set, and the-entry when it is clear. - The gate's one writer is the party battle-actor init
FUN_80053CB8(0x800541D0..0x80054270):lbu v0,0x761(v0)off the0x80084140display alias, i.e. the live character record's+0x199, the Ra-Seru slot of the eight equipment bytes at+0x196- or+0x760(+0x198) on the arm character id2(Noa) takes. - States. In every catalogued state the three gate bytes equal
(+0x199 != 0)for the seated members:0in the sparring fight,1for Vahn from the Meta capture on,0for Noa beside Terra, all1at the level-99 Rage state.
Port: engine-battle-hud::battle_magic_chip.
The combo counter's glide is placement record 80's
Status: resolved. Evidence: capture (the live glide record); the stepper by disassembly.
- The
HIT/TOTAL/DAMAGEcluster hangs off screen-element record 80: seat A(328, 170), seat B(168, 170), opened at mode 0 and closed in the0x51band (FUN_801D8DE8(0x50, 1)at0x801E6360, gated on the damage finisher's_DAT_8007BD14). battle_melee_hit_sparkcarries the glide record live atctx[+0x11B4 + slot * 0xC]: total0x10, elapsed0x0C, start(328, 168), target(168, 168), handle atx = 208- the+40every packet of the cluster shows against the settled seats ofplayer_steal_skeleton_banner.FUN_801D9BBCsteps it linearly (start + (target - start) * elapsed / total, snap on arrival), so the cluster slides 160 px in over sixteen frames.
Port: engine-battle-vm::battle_value_readout::combo_slide.
The chrome kind byte is an index into the widget-class table
Status: resolved. Evidence: disassembly + capture.
A screen-element record's +0x0E byte is a record index into the widget-class table at 0x800732A4; the record is the sprite the element is framed in.
- Dispatcher.
FUN_8002C69C(ghidra/scripts/funcs/8002c69c.txt, thePOLY_FT4/SPRTemitter) reads the kind out ofgp+0x14C, multiplies by0x0C(sll/addu/sllat0x8002C7A0..0x8002C7AC) and adds0x800732A4. - Exact join. Every kind byte, high and low, on all 103 initialised placement records names a real widget record.
- It is a pair.
+0x0Eand+0x0Fare both indices; they are equal on all but five records, where the pair reads(gold, blue). - Chain field. Widget
+0x02is a signed hop (lb v1, 0x2(s7)at0x8002FF00, added into the index and re-entered at0x8002C780unless zero), which is why one kind draws a whole readout. Walking the0x2Bchain against the bar's own pen(16, 192)reproduces(80, 194),(136, 188),(192, 194),(240, 188)- the four seats the packet walk measures.
| Kind | Widget record | What it is |
|---|---|---|
0x01 | (192, 0) 16x20, sub-palette 4, class 3 | blue plate body - the command chips |
0x02 | (192, 64) 16x20, sub-palette 12, class 3 | carved-gold plate body - the name plaque |
0x03 / 0x04 / 0x44 | class 0, tile-set 0 | the rectangular gold 9-slice window |
0x07 | chain 0x07 → 0x08 → 0x09 | a roster panel: HP row, MP row, 102x48 plate |
0x2B | chain 0x2B → 0x2C → 0x2D → 0x2E → 0x2F | the active-actor bar's two label / separator pairs, then its plate |
0x33 / 0x34 / 0x35 | the panel chain plus the status marker | the three roster panels, one kind per party slot |
The panel placement records (6, 78, 79) carry kind 0x07; 0x33 / 0x34 / 0x35 are the sibling kinds that add the level / status marker, special-cased at the head of FUN_8002C69C into FUN_8002C2E4(slot).
Parser legaia_ui_formats::ui_widgets, disc oracle crates/ui-formats/tests/ui_widgets_real.rs. Owning page (layout, classes, chains, palette decode): battle.md.
The element-badge palette selector
Status: resolved. Evidence: disassembly + capture.
The selector is the badge record's own palette byte.
- Each badge is a widget record (
0x8B..=0x92,20 x 12at a 32-texel pitch fromu = 6on rowv = 192) whose+0x03palette byte is0x40 + index. - Bit 6 of that byte switches both draw routines onto a second CLUT decode,
fb_y = 498 + ((b & 0x3F) >> 2),fb_x = 896 + (b & 3) * 16- a 4-wide by 2-tall block of sub-palettes (low two bits pick the column, the next two the row). The decode is((((b & 0x3F) >> 2) + 0x1F2) << 6) + 0x38 + (b & 3)at0x8002C5E0..0x8002C604inFUN_8002C488, and the same sequence at each ofFUN_8002C69C's packet sites (0x8002C8B8,0x8002C97C, ...):
badge i -> palette 0x40 + i -> CLUT ( 896 + (i % 4) * 16 , 498 + i / 4 )
- That reproduces all four captured pairs from the disc alone:
u = 6with(896, 498),38with(912, 498),166with(912, 499),230with(944, 499). - The
v = 208"winged" strip is a separate set of eight records (0x94..=0x9B),28 x 12fromu = 2, on the second CLUT block (0x48 + index, rows 500 / 501). Record0x9Breadsv = 192, so the eighth wide badge samples the square-framed art on the plain row while its seven siblings sample the winged row; the winged eighth badge exists in VRAM and no record selects it.
The status-element badge sheet (0x18..=0x20)
Status: resolved. Evidence: disassembly + capture.
The nine ids FUN_8002C2E4's exclusive ladder emits are widget records 0x18..=0x20: 48x16 cells in a two-column block on the resident system-UI sheet (VRAM page (896, 256)), each on its own row-511 sub-palette, drawn by FUN_8002C488 at the caller's seat with no bias. The ladder is the li a2 chain 0x8002C39C..0x8002C464 into one jal 0x8002C488 at 0x8002C468, tested in this order: HP +0x6CE == 0 (0x20), then status +0x6F6 bits 0x4, 0x400, 0x800, 0x380, 0x78, 0x1000, 0x2, 0x1; the first hit wins. The decoded art is a word tag, not an icon, which confirms the ladder's per-bit assignment independently of the accessory-guard derivation (accessory-passive-table.md).
| Sprite | Mask | Cell | Sub-palette | Reads |
|---|---|---|---|---|
0x18 | 0x0001 | (0, 48) | 9 | Venom |
0x19 | 0x0002 | (48, 48) | 10 | Toxic |
0x1A | 0x0004 | (48, 80) | 16 | Stone |
0x1B | 0x0078 | (48, 112) | 14 | Rot |
0x1C | 0x0380 | (0, 96) | 17 | Rage |
0x1D | 0x0400 | (0, 64) | 11 | Numb |
0x1E | 0x0800 | (0, 80) | 15 | Sleep |
0x1F | 0x1000 | (48, 64) | 13 | Curse |
0x20 | HP == 0 | (48, 96) | 18 | Faint |
- The zero-HP arm reading
Faintshows the KO test and the bit ladder are one selector over one sheet. - The block's tenth cell (
(0, 112)) is other art; there is no tenth badge. - Row 511's sub-palette strip is wider than the sixteen the chrome plates use: these badges alone reach index 18, i.e. VRAM x 288.
Battle-intro tile shatter - the side-face shade page
Status: resolved. Evidence: capture.
The shade page is a resident field asset, not a transition upload.
- The page. The 4bpp page at VRAM
(448, 0)under the shatter's four semi-transparent side faces is the top-left64 x 64texel corner oflegaia_actor_formats::field_char_texturesentry 0 (PROT 0874 §2), a256 x 2564bpp TIM declared at(448, 0), uploaded at field init and resident for the whole field session. - CLUT and blend.
clut 0x7641=(16, 473), CLUT index 1 of the same entry's 16-CLUT block (a256 x 1black-to-bright, STP-set ramp on row 473).tpage 0x0027carries ABR mode 1, so the side faces add the ramped texels over their opaque siblings. - Capture.
scripts/pcsx-redux/autorun_tile_shatter_page.luawalkskaristo_sol_pre_encounterinto a random encounter, exec-breaks on the style-2 tickFUN_801D0D24, saves states on shatter frames 1 / 8 / 24 and logs everyLoadImage/MoveImagerect. The(448, 0)rect and the row-473 CLUT are byte-identical to the pack entry before the encounter, mid-shatter and across two field scenes; no upload touches them in the transition window. - Emitter runtime inputs (same capture). The per-tile view matrix at scratch
0x1F8003C8is identity rotation with zero translation from the second shatter frame on; frame one still holds the field camera's value, so retail's first frame draws no tiles (_DAT_8007B6CC, the "not the first frame" flag, is that signal). The FT4 near cutoff0x1F80037Ereads0x10;ZSF4is0x400, so OT depth is the plain four-corner SZ average.
Owning page (full spec + engine wiring): cutscene.md.
Battle-intro transition length - DAT_801D2458
Status: resolved. Evidence: disassembly.
132 display frames for every style, 252 for the swirl.
- The intro overlay's init stores
0x84unconditionally (addiu v0,zero,0x84at0x801CED14,sw v0,0x2458(v1)at0x801CED2C), two instructions before thesltiu v0,a0,0x5style switch onDAT_801D2460. - One arm overrides it: jump-table slot
4(the swirl; table0x801CE840, body0x801CEFEC) re-stores0xFCat0x801CEFF4/0x801CEFFC. - Read off PROT 0979's instruction words at load base
0x801CE818; the decompiledFUN_801ce8ccreaches past that dump's window, so the C alone does not settle it. - It is the denominator of the whole transition (fade leads, ready bits at
- 0x1E/- 6, shatter records held untildelay < elapsed * 0x3Cwithdelay = rand() % 5000): the grid needs ~84 frames merely to finish starting, and a shorter window leaves part of it at its seeded pose throughout.
Port: render-kernels::battle_intro_styles::intro_duration_frames. Owning page: cutscene.md.
Battle ground grid depth cue - the far colour
Status: resolved. Evidence: capture (control registers read at the grid draw).
The grid's far colour is the backdrop's staged far colour; there is no separate grid base.
- The emitter.
func_0x801d02c0runsDPCSper projected lattice vertex withIR0 = SZ >> 2loaded bare (srl+mtc2, so no saturation - pastSZ = 0x4000the blend extrapolates until the DPCS output clamp bounds it) and contains zeroctc2: the far colour it consumes is whatever the control file holds on entry. A save-state read therefore cannot attribute the value to the grid pass (snapshots read(0, 0, 0)or(4096, 4096, 4096)depending on what ran last). - The probe.
scripts/pcsx-redux/autorun_grid_far_colour.luasets exec breakpoints on the emitter entry and its firstDPCSsite (0x801d061c) and dumps control regs 21-23 at the draw. - The law. Every battle hit shows
FC= the backdrop far-colour staging word at0x8007BB48, times 16 into the 28.4 control registers. - Settled values.
(0x40, 0x40, 0x40)on ordinary stages (two town01 battles, stage ids0x15/0x0C) and(0xFE, 0xFE, 0xFE)on an overworld battle (stage id0x55, on the 13-idDAT_80078C1Coutdoor table at SCUS file0x6941C). Both are the neutral base0x808080throughFUN_80050120's two derivation arms:>> 1indoor,(c - 0x010101) * 2outdoor. - Intro ramp. The battle-intro fade ramps the staged word up from near-black at
+0x020202per frame for ~28 frames before it settles; a Queen Bee sample reading(6, 6, 6)is frame 1 of that ramp, not a per-stage ambience. - Other constants.
DQA = -64/DQB = 320 << 16at every hit. A field state never enters the emitter: the aliased field-overlay code at the same VA idles at the white(4096)^3field FC.
Port: legaia_engine_battle_vm::battle_ground_grid carries the laws (grid_ir0, grid_far_colour, OutdoorCueTable, the ramp constants), with the SCUS table pinned by crates/engine-battle-vm/tests/battle_grid_cue_scus_real.rs. engine-render stages a per-draw DrawCue (retail sets the DPCS inputs per drawn object) and the play-window grid draws under the SZ >> 2 ramp toward the per-stage far colour. The browser play page draws the same grid from the same table: play_battle_ground_cue_json hands the page the engine-resolved far colour and the page renderer attaches it as a per-draw cue.
Who calls the battle on-screen test FUN_8005126C?
Status: resolved - exhaustive negative. Evidence: disassembly.
Nobody calls it, and the same holds for two of its neighbours; there is no draw-pass consumer of its verdict.
- The sweep. A five-form sweep of
SCUS_942.54, all statically based overlay images and the raw bytes of every extractedPROT.DATentry finds no reference to0x8005126C: no literal address word (so no dispatch table and no actor template), nojal, noj, no PC-relative branch, nolui+addiumaterialisation (address-reference-scan.md). - Same result for the passive-name draw
FUN_80035274and the angle tweenFUN_80050D40. Each follows a cleanjr raepilogue whose delay slot closes the previous frame, so they are entry points rather than interior labels. Two of the three open frames of their own;FUN_80050D40is a frameless leaf. FUN_80025054is the same finding one level up: it is a template tick, and the template record0x80070614that would install it is what nothing materialises. Its table was swept whole (a record reached asbase + indexis not a materialisation pair): the head0x800705FCis named once, in the field overlay, and handed straight to the allocator as one record rather than indexed.- Positive controls the scan reproduces: the template word for
FUN_8004DA00at0x800767FC, the 21jalsites of the billboard projectorFUN_800195A8, an intra-function branch target insideFUN_8005126Citself, and the menu overlay's sub-screen pointer table at0x801E4F40. - Limits. An LZS-compressed PROT entry would hide a reference (overlay code is stored raw, so this does not affect the images callers live in), and an address assembled in more than two instructions is not a
lui+addiupair. - The one positive.
FUN_8004DA00's spawner is the battle scene-loaderFUN_800513F0.
Port: battle_on_screen stays inert on purpose. The three rows are unreachable retail code, settled under the ignore list's unreferenced section (worklist-classification.md). Write-ups: battle.md § Unreferenced SCUS entry points. The same sweep over every disclosed-inert port anchor separates rows waiting on wiring from rows waiting on nothing; the closed list of the latter, SCUS and overlay alike, is on address-reference-scan.md.
Action-SM state 0xFF treated as battle end by the port
Status: resolved. Evidence: disassembly (retail half); the port path is live-reachable and fixed.
Retail 0xFF is the round boundary, not the battle's end: its only writer is the non-wipe arm of the 0x5A end-of-action gate (li v0,0xff / sb v0,0x7(a2) at 0x801E67DC / 0x801E67E4, the only 0xFF store to ctx[+0x07] in FUN_801E295C), and wipes signal through DAT_8007BD71 = 0xFE (sb v0,-0x428f(v1) at 0x801E6674) without writing a state byte. A port that maps 0xFF to battle_end(BattleEndCause::MonsterWipe) grants a spurious victory (loot and XP) after one round with both sides standing.
- Why a live battle reaches it. The counter is the action context's turn cursor
ctx[+0x1A](BattleActionCtx::turn_cursor), not a field of the actor record;Beginseeds it from the formation-advantage byte and never fromctx.queued_action. The0x5Agate's non-wipe arm bumps it and compares againstparty_alive + monsters_alive(0x801E679C..0x801E67CC:ctx[+0x1A] + 1againstctx[+0x00] + ctx[+0x01] - ctx[+0x25]), so the threshold is reached once each living combatant has acted - the ordinary end of a round. - Dispatch. The gate runs whenever a driver leaves the SM parked at
EndOfActionacross a tick (after a folded monster spell cast and after a Sleep / Stone skipped turn), so the boundary is reached in the first round of a battle that leaves both sides standing. - Port. The state is
ActionState::RoundEnd: its handler clears every actor's acted counter and hands control back throughEndOfAction(the state the arming driver keys the next turn on). The retail0xFFbody (ctx[+0x28A]round bump,FUN_801F45A4settle) runs host-side inengine-core's live loop.battle_end(..)fires only from the paths that raise retail's0xFEsignal: the0x5Awipe arms and the escape teardown0x66. - Regressions. engine-vm
full_round_with_both_sides_alive_does_not_end_the_battle; engine-coreround_boundary_state_is_not_a_spurious_victory.
Owning page: battle-action.md.
Endless camera orbit - the 0x19 attack-approach park
Status: resolved; a one-word disc fix ships as legaia-patcher --approach-softlock-fix. Evidence: capture (the fingerprinted scenario battle_gaza2_park_0x19, caught from ordinary play under the poll-only hunter autorun_gaza2_park_hunter.lua; interpreter replay autorun_gaza2_range_wedge.lua; RAM-table read of the parked save) + disassembly (overlay_battle_action_801e295c.txt, 0x801E31F4..0x801E32DC).
The community-reported "endless camera orbit" (Gaza rematch; JP exhibit too) is the battle-action SM parked in state 0x19 while the idle camera azimuth sweep (FUN_801D0748) keeps orbiting. The orbit is a symptom.
- The park. State
0x14(attack approach setup), finding the target out of range, looks up the walk animation (action tag0x20) in the acting monster's table viaFUN_80050E2C. A table without one - bosses generally; Gaza's 12 tags read[00 01 02 03 04 05 0B 0E 13 0C 23 23]- takes the fallback: stage the tag-1"Move" float loop and drop into0x19, the range re-poll, whose arm has no movement code and no timeout (its not-in-range edge only bumpsctx[+0x6D4], read solely by the arms-resolver roll). - Why it usually works. The fallback normally still approaches during
0x19(~19 units/vsync, driven by the staged Move clip's playback, not the SM). In the caught parks the drive dies ~12 vsyncs in (anim pair back to0/0), frozen beyond reach. - Trigger. A summon immediately followed by the boss's melee (scenario
battle_gaza2_park_0x19_summon_melee); the stale field is actor+0x1DCbit 2 (details ↓). The disc fix is indifferent to it. - Stale sentinels. The
+0x1DD == 8targets on the idle party actors are stale all-target sentinels; the round is stuck on the boss's action alone. - The sibling
0x51HP-settle park is a fully decoded mechanism that remains injection-only: a three-capture retail campaign (twelve Lost-Grail revives, no harness HP writes) measured out both of its candidate generators (re-do-not-re-walk.md), and the0x19class explains the community exhibits without any HP desync. Whether any retail sequence can produce a0x51park is unproven either way; nothing observed requires it.
Owning page (anatomy, fix, engine-port note): battle-action.md.
The in-fight action framing and the yaw-counter ladder
Status: resolved. Evidence: capture + disassembly.
Of FUN_801D5854 case 6's two arms, the 0x801D64C4 arm films every ordinary action - party and monster alike - for the whole of a running fight.
- The fork byte
DAT_8007BD71(0x801D5CEC..0x801D5CF4) is the battle-end signal and reads0xFFuntil a wipe or an escape. Not a party / monster split: re-do-not-re-walk.md. - The pose.
pitch 0,yaw = (ctx[+0x6DA] − actor[+0x46]) & 0xFFF,TR = (0, 0x500, ctx[+0x6D0])(the depth prescaled byFUN_801D829C), focus the negatedactor[+0x34/+0x38]pair with the height left at zero; then thectx[+0xD]style tweaks (1/3add a half turn;2/3setTR.y = 0x400and add0x80of pitch) and the character-4override. - Yaw-base seeds (
ctx[+0x6DA], each read off the instruction):sh zero,0x4(s7)in the round-begin arm (0x801E2B40);li 0x800in the0x0Cseed arm (0x801E2CF8);li 0x200beside thectx[7] = 0x14store (0x801E2F20); andFUN_8004E13C's(rand() % 2) << 11 + 0x280(0x8004E288..0x8004E2B0) under a three-way gate - argument2,ctx[+0x243] != 2,ctx[+0x13] < 3- whose argument is the committed clip's header byte+0x87fromFUN_8004AD80(0x8004BE18..0x8004BE2C). - Per-frame advance. The prologue adds
max(1, 4 × frame_step / 3)(0x801E29E4..0x801E2A24).battle_melee_hit_sparkreadsctx[+0x6DA] = 0x298mid-art:0x280plus 24 frames of it. - Captures. Three PCSX-Redux
.sstatecaptures parked inctx[7] == 0x19with Gaza (seat 3) acting:TR (0, 1280, 5324)=prescale(0xD00)withctx[+0x6D0] = 0xD00in all three; focus(−433, 0, −291)/(785, 0, −39)/(0, 0, −1490)against Gaza's+0x34/+0x38of(433, 291)/(−785, 39)/(0, 1490); thectx[+0xD] == 2capture at pitch0x80overTR.y = 0x400; and in each the live yaw eight units behind(ctx[+0x6DA] − actor[+0x46]) & 0xFFF(1657vs1665,1412vs1420,574vs582) - the per-pass re-arm chasing a counter that moves.
Port: legaia_engine_battle_vm::battle_cam_script - ActionFraming::battle_over names the fork byte, action_framing drops the focus height, and BattleCamera::observe_action_state applies the seed ladder on the action-state edges. The swing-clip commit is stood in by the edge into 0x1E, since the engine's animation player does not expose the clip header byte. Both hosts feed the same action_state.
The Done band framing and the two orbit writers
Status: resolved. Evidence: capture + disassembly.
After an action resolves, FUN_801E295C sits in 0x50 / 0x51 for the ctx[+0x6D8] tail (0x3C display frames by default). The camera there is on a per-action framing chosen by category, not the far framing; the Begin/Run prompt orbits at −2 yaw units per display frame (−4 per two-frame camera step).
- The selection. Both Done arms read
actor[+0x1DE]before the framing call (0x801E5E90..0x801E5EF4in0x50,0x801E5FC0..0x801E6018in0x51), re-armed every pass:
| Condition | Framing |
|---|---|
category 5 (Run) | none; runs the yaw orbit |
category 3 (Attack) | li a1,0x8 |
a party slot whose target's live HP +0x14C is zero | li a1,0x8 |
| anything else | li a1,0x6 |
- The far framing returns with the next round's prompt. Between two actors the last shot stands:
0x5Are-arms the category's framing (0x801E6428..0x801E6470) and the setup states arm nothing. - Done-band capture.
zora_glare_petrify_post(mednafen,ctx[7] == 0x51, slot 3 acting after a spell,ctx[+0x6D0] = 0xC00,ctx[+0x6DA] = 1966, Zora at(649, −47)facing3297) reads pitch0, yaw2735,TR (0, 1275, 4820), focus the negated(624, 0, −40): case 6's in-fight pose(0, 0x500, 4915)on the caster's seat with the tween one step short, and yaw(1966 − 3297) & 0xFFF = 2765being chased. - A round's first actor.
evil_medallion_rage_battle(ctx[7] == 0x0A, flow byte0xFF) reads pitch32,TR (0, 1280, 7920), focus at the origin - case 9 over±825seats, the prompt's framing, with no earlier actor in the round. - A later actor.
kor_ghost_knight_pre_v_windhash(ctx[7] == 0x0A,ctx[+0x13] = 1with a spent key) reads pitch128,TR (0, 1024, 3276), focus Noa's seat: her Spirit's case 6, landed. - Orbit writers.
scripts/pcsx-redux/autorun_battle_cam_orbit.luaonbattle_gaza2_prompt(240 vsyncs,ctx[+6] == 0x1E,ctx[7] == 0x00) with Exec breakpoints on bothsh v0,0x2(a0)stores: the SM's at0x801E2A6Cnever fires; the battle tick's at0x801D07CCfires once per tick and the yaw drops2 × DAT_1F800393each time (−6per 3 vsyncs under the interpreter'sstep = 3, i.e.−2per vsync).
Port: legaia_engine_battle_vm::battle_cam_script::done_band_phase over DoneBandInputs (category / party seat / dead target), filled by both hosts; DONE_STATES is split from ACTION_END_STATES; the orbit rate is ORBIT_STEP = 4 per camera step.
The summon-then-melee park trigger - the stale field is +0x1DC bit 2
Status: resolved. Evidence: disassembly (the driver pair FUN_80047430 / FUN_8004AD80 in ghidra/scripts/funcs/80047430.txt / 8004ad80.txt; the flinch staging at 0x80042124..0x80042170 in 800402f4.txt; the SM's 0x14 fallback stores at 0x801E32B0 / 0x801E32D4 in overlay_battle_action_801e295c.txt) + capture (causal control / experiment replay on the parked save battle_gaza2_park_0x19_summon_melee, probe scripts/pcsx-redux/autorun_gaza2_stale_flag_repro.lua with write-watchpoints on +0x1DA / +0x1D9 / +0x1DC logging writer PCs).
The field the summon staging leaves stale is the battle actor's anim event-flag byte +0x1DC, bit 2 (mask 0x4), the stage-idle-at-clip-end flag - not a frame cursor or a clip-length latch.
- The summon's hit stages Gaza's light flinch with
+0x1DC |= 4|1(exit-to-idle + commit-now,FUN_800402F4); the flag is normally consumed at the flinch's natural end. - The boss's melee follows immediately: state
0x14's walk-less fallback stages the Move clip with|= 1first. - The tick's event-path commit clears only bits 0-1 (
andi 0xFC; the natural-end path clears 0-2,andi 0xF8), so the Move clip installs with bit 2 set. - At the Move cycle's first natural end (5 frames, rate 2, speed scale 8 → ~12 vsyncs) the tick sees bit 2 and stages idle over the queued clip (
sb zero,0x1daat0x80047B44) instead of re-looping: pair0/0, idle per-tick speed 0, and0x19re-polls forever. - Control / experiment. With the flag clear the bounce (state →
0x14) loops the clip across its natural end (pair stays1/1) and arrives ~21 vsyncs later; re-arming bit 2 first reproduces the kill write from0x80047B44at exactly the first natural end, 12 vsyncs after engage, position frozen thereafter. - Visibility. The park save reads
+0x1DC == 0because the killing commit consumed the flag; it is only visible in flight.
Owning pages: battle-action.md (mechanism); monster-animation.md (driver + flag byte).
Super / Miracle Arts trigger chain
Status: resolved - matcher, tables, builder chain and runtime effect all pinned. Evidence: disassembly + capture.
- Preseed.
FUN_801DA34Ccopies the saved chain from the char record+0x76F/+0x77Fverbatim (lbu +0x76F → sb +0x1DF): the char-record chain uses the queue-space encoding directly (0x0C/0x0D/0x0E/0x0F= L/R/D/U,0x1Astarter,0x1B..0x32art constants). - Builder.
FUN_801EED1C(battle overlay 0898, ActionSeed state0x0C) rewrites arrow runs to art constants and applies the Miracle replacement inline, then delegates the Super find → tail-replace toFUN_801EF9E4. Miracle-before-Super is structural. - Super tables. Keyed on
(actor slot, char index): find cells[len][bytes]at0x801F6524 + char*65 + row*13, replace at0x801F65E8 + char*80 + row*16, first match wins. - Queue. Exactly 16 bytes,
actor[+0x1DF..+0x1EE];+0x1EF..is neighbouring data. - Verification. The resident find / replace tables match the modeled
crates/art/src/{miracle,super_art}.rsbyte-exact, and all 15 Supers are live-executed: the applier-entry injection probescripts/pcsx-redux/autorun_super_art_queue_inject.luabreakpointsFUN_801EF9E4, writes the target Super'sfindbytes into the queue, retargets the char-index register, and reads the tail-replaced queue back at the return site - 15/15 byte-exact, with the two hand-driven combos (Noa's Miracle, Vahn's Tri-Somersault) as positive controls. One post-applier library state per character is re-checked bycrates/parity/tests/super_art_queue_replace.rs.
Owning page: battle-action.md.
Super Art connectors and physical inputs - the tokenizer keeps the leading arrows
Status: resolved. Evidence: disassembly (FUN_801EED1C 0x801EF2EC..0x801EF858) + capture (reproduces the in-the-wild Tri-Somersault queue byte-exact) + an independent table (fourteen of fifteen walkthrough inputs agree).
The 0F / 0E "connector" directions in a Super's find pattern are the leading arrows of the arts themselves: the queue builder does not compact a matched arrow run down to 19 <art>.
- Normalisation loop. On a full match it writes the starter over the run's last arrow (
sb v1,0x1df(v0)at0x801EF6F8,v1=FUN_801EFBFC's verdict+ 0x18), shifts the tail up one slot (0x801EF708..0x801EF750) and inserts the constant after it (0x801EF7A0). The leading arrows stay. - Walk order. The outer walk runs tail-first (
s8from 15 down,0x801EF848) and restarts ats8 + 1after every match, so runs overlap. - Examples.
↑↓↑alone tokenizes to0F 0E 19 27. Tri-Somersault's19 27 0F 19 1F 0E 19 27is what the seven-arrow input↑↓↑↑↑↓↑tokenizes to (Somersault 0..2, Cyclone 1..4, Somersault 4..6); the capture's leading0F 0Eare Somersault's own first two arrows. Laying the three arts end to end tokenizes to four arts and never triggers. - Derived inputs. Every retail Super's pattern derives to a unique shortest input, all 7..=9 arrows (
legaia_art::tokenize::derive_super_input, searched over the chain arts' directions). The curated walkthrough table agrees on fourteen; the fifteenth (Dragon Fangs, printed as six arrows in the walkthrough) drops one and never performs Swan Driver, so the curated entry carries the seven-arrow form. - Downstream.
--show-super-artsdraws each Super Art's real arrow string: sixty bytes for all fifteen, against the ~330 a concatenation of chain strings costs.
Port: legaia_art::tokenize. Owning page (table + citations): art-data.md.
The runtime art-record chase - the +4 and the - 0x10 grid origin
Status: resolved - record(c) = *(*(DAT_801C9360[char]) + 0x58) + 4 + (c - 0x10) * 0xD0, with +0x10 the name field and +0x24 the power bytes.
- The chase (
0x8004B6FC..0x8004B718):lui/addiubuildsDAT_801C9360,lwthe per-character block,lw +0x58the array pointer,addiu s0,v0,0x4. - What
s0is the base of.FUN_8004AD80's own three uses ofs0each multiply the action constant by0xD0with the identicalsll 1 / addu / sll 2 / addu / sll 4chain and then subtract a constant:
| Site | Expression | Resolves to | Consumed as |
|---|---|---|---|
0x8004BBE8..0x8004BC10 | s0 + 0xD0*c - 0xCF0 | record + 0x10 | the art's display-name pointer (sw into 0x8007634C/0x80076344) |
0x8004BC60..0x8004BC80 | s0 + 0xD0*c - 0xCDC | record + 0x24 | the per-strike power bytes |
0x8004BCA0..0x8004BCC4 | s0 + 0xD0*c - 0xCF6 | record + 0x0A | a byte handed to FUN_8002B28C |
0xD0 * 0x10 = 0xD00, so each constant is0xD00 - field_offset: the array is indexed from constant0x10ands0is the base of record0x10itself.- The
+0x10name and+0x24power offsets are the same onessuper_art_poweredits off the decodedrecord0, so the runtime base and the file-sideart_block_baseare one address in two spaces. --show-super-artschases the name through this chain; its planner re-proves it per disc by requiring all fifteen Super Art records to carry their own names at+0x10before writing anything.
Character-record HP/MP/AP pair order
Status: resolved. Evidence: disassembly.
+0x104 / +0x108 / +0x10C are the effective maxima; +0x106 / +0x10A / +0x10E are the currents.
- Clamp triple. The stat aggregator's closing sequence at
0x80042CE4islhu v1,0x104(s0)/lhu v0,0x106(s0)/sltu/sh v1,0x106(s0), repeated identically for0x108/0x10aand0x10c/0x10e: it clamps the second halfword of each pair to the first. - Cap ladder (
0x80042C0C..0x80042C50) is per-field, not a flat 999:+0x104→ 9999,+0x108→ 999,+0x10C→ 100,+0x110→ 280, then 999 for five more. A 100-cap on+0x10Cis the AP maximum, which corroborates the pair order. - Walk-regen
FUN_801D0B90bumps+0x106, clamping at+0x104. - The aggregator rewrites
+0x104per frame from base stats plus %-passives; a per-frame recompute cannot be current HP. - GameShark "Infinite HP" codes write
+0x106at every character stride - they pin the current. - A fresh save cannot distinguish the orders, because
cur == maxat seed.
Port: legaia_save::HpMpSp and every consumer carry the (max, cur) order; the status AP gauge reads the AP current at +0x10E.
Effect-VM pass-1 "state token algebra" (FUN_801E0088)
Status: resolved + ported. Evidence: capture.
The "state" bytes are 5.3 fixed-point wait counters, not opcodes: two countdown-driven cursor walks (master spawn cadence over 14-byte pack1 records; child anim / motion over 6-byte pack0 frames).
Port: Pool::tick_retail executes the algebra operator-for-operator (pass 2 = Pool::child_billboards), disc-verified over all 33 efect.dat scripts. The engine's live path runs it: engine-core::World::tick_effects sweeps tick_retail per retail frame and active_effect_sprites maps child_billboards one-for-one; dev debug spawns live outside the pool.
Owning page: effect-vm.md.
Battle face-stamp issuing site
Status: resolved. Evidence: capture.
The facial-texel overwrite is the per-frame facial animator FUN_8004C7B4, called from the render-node update with the clip's frame cursor (Terra skipped).
- Action-entry facial tracks at
+0x8C(eyes) /+0x98(mouth) select frames from static per-character SCUS tables, stamped byMoveImageevery frame. - Pinned live across a battle entry (
karisto_sol_pre_encounter+ the MoveImage trace probe). FUN_8004CCD4is not a stamp: it is the equipment mesh-variant swap (same caller + guards, re-run per ghost by the arts trail renderer), driven by the entry's third track at+0xA4; retail windows Noa-only.
Owning page: battle-data-pack.md § Facial animation tracks + § Equipment-variant track.
Spine flag 0x482 (Drake mist-wall) writer
Status: resolved - writer-less. Evidence: capture.
No writer ever fires: the map01 P2[34..36] C1 spawn-block never latches, and wall despawn is not flag-driven.
- Byte write-watch on
0x800857E8(autorun_flag_writer_watch.lua) across the whole post-Zeto beat (battle exit, mist-clear FMV,map01arrival): the only write to the byte is the SET helper re-latching0x484(storeFUN_8003CE08+0x28,ra 0x801E3598);0x482never flips. - Every catalogued state through the Karisto era holds it clear (neighbours
0x484..0x487at0x0F), and all 37 census sites stayDESYNCED. - Only an engine-side C1-latch-on-fire (pad-walk into a wall) could give the flag a role.
Flag 0x63A - the vell/vozz P2[7] gate with NO script writer
Status: resolved - script writers exist. Evidence: capture.
- Under the retail-frame scene windows the census shows eight clean sites: Set / Clear pairs in the rikuroa post-Caruban variant MAN (PROT 0157 P2[29]/[30], op
0x56/0x66), rikuroa2 (PROT 0122 variant), retockin (PROT 0281 P2[7]/[8]) and edretoin (PROT 0800 P2[7]/[8]). - These are late-game beats, so the vell/vozz
C1=[0x63A, 0x7]spawn-block passes for the whole first visit. - Retail states corroborate:
0x63Areads clear through the Karisto era while its bank byte already holds0x0C(0x63C/0x63Dset). - The byte for
0x63Ais0x80085758 + (0x63A >> 3) = 0x8008581F(not0x800858C7).
Spine flag 0x142 (Caruban beat / dolk-dolk2 switch) writer
Status: resolved (disc writers + engine port + oracle). Evidence: capture.
The story writers are two records, not the six an arm count reports.
- Writers. rikuroa's post-victory
P2[50]in the streaming variant MAN (PROT 0157), whose own C1 gate is0x142itself (the self-latching one-shot shape), and dolk2's carrierP1[0], which re-asserts it. - Not writers. The other four clean
51 42/61 42arms are developer flag-menu rows: rikuroaP1[10..12]is a nine-flag Set ladder with its mirrored Clear ladder, and dolk2P1[1]/ dolkP1[26]sit in the same shape. An opcode census cannot tell a beat from a menu row; both decode clean. - Live. Firehose-caught (
ra 0x801E3598), and the resident script heap byte-matches the carrier. The census must walk the streaming carriers to see it; pinsman_variant_carrier_census_disc.rs.
Port: the engine sets it organically - rikuroa P2[50] executes from its own script bytes on the Battle-to-Field edge (organic_beat_records_disc.rs).
Drake Castle deep interiors (jouinc/jouind) depth decode
Status: resolved - door-choreography families, not story gates. Evidence: capture.
jouinc's 58-recordC1=[0x00F]P2 family is a busy-mutex door family: each record SETs0x00Ffirst and CLEARs it last, so the C1 gate is a mutual-exclusion lock, and the bodies are per-door walk-through choreography.jouindP2[10..13]'s0x4BE..0x4C2band is per-visit door / lift state, cleared byjouina P1[0]on entry - not a later-chapter revisit gate pair.jouinb P2[6..8]is the interior beat band (0x44E..0x450latches plus the jouinb-local0x461state flag).- The decode depends on the disassembler's whole-nibble operand widths for
0x4Cnibbles 9/A/C/D/F.
Owning page: script-vm.md § door-choreography record families.
cave01 P2[16] spawner - the slot-counted interact chain
Status: resolved. Evidence: capture.
The ungated 0x15D setter P2[16] is spawned by P2[12] once a slot counter reaches 8, and each creature interaction bumps the counter. PROT cites are the extraction frame (cave01 = PROT extraction 38).
| Record | Role | Bytes / offsets |
|---|---|---|
P2[16] (global 0x1E) | sets 0x15D, ungated | 51 5D at body +0x22, MAN 0x3C10 |
P2[12] (global 0x1A) | bumps slot 0, gates and spawns P2[16] | opens 4C CB 00 01 00 (slot 0 += 1); gate 4E 00 50 08 00 06 00 at +0x15; 44 1E at body +0x1C, MAN 0x35B9 |
P1[3..7] | the five creature-interact scripts; each spawns P2[12] once per interaction | 44 1A at the first-interact branch tail: P1[3] +0x2CC = MAN 0x1CE4, siblings at 0x1FCC / 0x22B6 / 0x259E / 0x2888 |
P1[2] | lead-NPC ladder record testing 0x15E/0x15D/…/0x157; zeroes slot 0 | 4C CA 00 00 00 at +0x0C |
- The gate is an op-
0x4Esub-5 slot-table compare: while slot0x801C6460[0]< 8 it skips forward past the spawn (to the0x166→0x167→0x168progressive counter at+0x20); at 8 it falls into the44 1E. - Why the count reaches 8. The per-NPC talked latches
0x161..0x165are re-cleared inside the interact scripts (P1[3]+0x82..+0x8A), so interactions repeat. - Every op-
0x4Esub-op0..9is a compare: the 0x4E details.
NPC dynamic facing - two laws and an execution order
Status: resolved. Evidence: disassembly.
How a facing changes after spawn (the spawn heading itself: below).
- Walking snaps. Every walk kernel - the
0x47tail inFUN_8003774Cand the directional / wander steps inFUN_80038158- quantises the frame's step to the eight-entry compass LUT at0x80073F04(entry[i] = i * 0x200,0= -Z) and writes+0x26outright. Retail has no walk-turn interpolation. - Rotate ops ramp. The four dedicated rotate ops (
0x38/0x4C,0x04/0x0D) steparc * speed / frames_remainingoff the live heading over a budget the op carries, with an exact snap on the terminal frame. - Priority is execution order, not a field.
FUN_8003BC08runs the dialog SM, thenFUN_8003774C, thenFUN_80038158, then the anim consumer; an actor running both a scripted leg and an ambient stream ends the frame facing wherever the ambient stream put it. - Case bodies. Op
0x38's case body is0x800379FC; only0x4Clives at0x80037DE0. The jump table at0x80010EE0settles all 22 slots. - The LUT is eight entries of
0x200, not sixteen of0x100. 0x4Csub-modes0x85/0x8E/0x8Fall take one arm;0x8Falone forces the direction.+0x16is the terrain-conform angle sampled from the scene grid byFUN_80019278, not a facing. The yaw is always+0x26.- Live. A cold-boot
town01sample off the static recompilation reads every field actor's+0x26: all on-field headings are multiples of0x200with all eight points present, the only exceptions being actors parked on the(0x7F, 0x7F)sentinel tile.
Owning pages: field-locomotion.md + motion-vm.md.
0x4C 0x51 byte +3 reconcile - facing wins; no motion-bytecode synthesis
Status: resolved. Evidence: disassembly.
4C 51case-1 (dispatcheroverlay_0897_801de840.txt, case 5 sub 1) consumes byte+3only as[bit7 -> actor render flag 0x1000000 (special model) | low nibble -> +0x26 = heading LUT 0x80073F04[b & 0xF]]. The op carries no speed operand: byte+4is the move-anim id written to+0x5C(consumed by the anim-stream stepperFUN_800204F8); non-player targets also get the+0x8C/+0x8Dcurrent-tile bookkeeping, and the trailingFUN_801D81E0is an active-list relink (the unlink/relink pairFUN_800204A4/FUN_80020454), not a bytecode builder.- The
depth & 7base-step selector belongs to the walk-kernel op0x47's own third operand:FUN_8003774Ccase0x47computes4 << (b & 7)(per-frame step0x80 * dt / that) with the high nibble an approach-mode selector; ops0x37/0x41encode their base step as(op0 >> 5 & 4) | (op1 >> 6)of their own two operand bytes (ghidra/scripts/funcs/8003774c.txt). - There is no motion-bytecode synthesis step: the field-VM yield-class ops
0x37/0x41/0x47(and0x38with a nonzero duration) park the current instruction pointer at actor+0x94, zero the progress cursor+0x54and set actor flag0x400(dispatcher cases0x37/0x41,0x38,0x47), andFUN_8003774Cinterprets the record bytes in place - it even resolves the field VM's0x80extended-target convention (0xF8player /0xFBworld-map entity / placement id vs actor+0x50).
Port: placement_glide_speed derives the base step from the real 0x37/0x41/0x47 yield operands (placement_yield_step) and the tail-section-1 wander ops (placement_wander_step), with the facing-nibble reading kept only as a documented last-resort heuristic; 4C 51 byte +3 sets facing + the special-model flag only (placement_initial_facing). Owning page: field-locomotion.md § NPC initial facing / § NPC glide speed.
kor-family op-0x49 flag window [0x138..0x13F] - Uru Mais warp-pad picker
Status: resolved. Evidence: disassembly.
Each flag is one destination row of the Uru Mais dream-shrine teleport-pad picker.
- Pad records (kor
P2[17..20], kor3P2[9..12], kor4P2[4..7]; extraction PROT 483/492/501) clear the whole window, pre-set their own row (kor pads ->0x138..0x13B, kor3 ->0x13C/0x13D, kor4 ->0x13E/0x13F), run theFUN_801EF014picker, then dispatch an 8-way0x71test ladder in which each arm clears0x612, fades, stops the BGM and executes a named0x3FSceneChange (korP2[17]body+0x8D..+0x1C6):
| rows | destination |
|---|---|
| 0..3 | KOR entries (0x0E,0x35) / (0x1E,0x35) / (0x2E,0x35) / (0x3E,0x35) |
| 4..5 | KOR3 entries (0x70,0x25) / (0x0D,0x36) |
| 6..7 | KOR4 entries (0x27,0x27) / (0x1E,0x3E) |
- Picker behaviour. State 0 cursors to the pre-set bit (= "you are here") and clears the window; confirming a different row sets
base + selection; picking the current row or cancelling sets nothing, so the test ladder falls through to the stay-put arm (+0x1C9: clear0x136/0x137, fade back, park). - Widget semantics (
ghidra/scripts/funcs/801ef014.txt): descriptor+2default= first visible row,+3rows= visible row count. The paired descriptors are the full 8-row menu (selected by state flag0x136) vs the rows-4..7 chambers-only menu (0x137; kor3/kor4 carry per-pad record pairs, one per variant). 0x137and the kor pads. The korP2[17..20]records set0x136exclusively and never0x137; neither flag should be read as reachable from any pad.- Menu pixel height
rows * 16reaches the renderer as a height. Thesll v0,v0,0x4at0x801EF160stores to0x801F2B98 + 0x196= record 14, field+0xE, of the0x1C-stride window-descriptor array (stride fromFUN_801E9B3C'ssll 3 / subu / sll 2); the picker's command list at0x801F3304is0001 000E, window id 14; andFUN_80032434moves+0xEinto the live window's+0x10(0x80032484→0x800325D4), the field the menu-side creatorFUN_800326ACfills from its descriptor'sh. - Cells. System-UI plates from the widget table at SCUS
0x800732A4: record0x4Fis the "B1F" plate,0x50..0x56the digits 1..7,0x57the "F" suffix,0x58..0x60the same set marked in yellow. The legend is the Cross / Circle icons (0x37/0x38) and the field-overlay strings at0x801CF0B8/0x801CF0C4(records checked against a mednafen state's VRAM).
Port: engine-field::field_submode_flag_window hosts slot 0x23 on the field path, drawn on both hosts.
Encounter MAN sub-section layout
Status: resolved
FUN_8003AEB0 is fully decoded: the MAN header chains six sections, and the per-scene encounter section is read straight from disc bytes.
- Header:
+0x22,+0x24,+0x26are signed-16 record counts of 3-byte records (assembled fromlbupairs thensll 16/sra 16at0x8003B04C..0x8003B098);+0x28is a u24 (0x8003B108..0x8003B120). They are not four signed-16 section offsets. - Region table (section 3): the count-prefixed array of 18-byte records at
_DAT_801c6ea4 + 0x4.byte[0]= kind selector;bytes[1..4]= tile-space box[minX, minZ, maxX, maxZ]queried byFUN_801dba20(tileX, tileZ)withtile = (player_pos - 0x40) >> 7;bytes[5..17]= a per-region camera preset (three mode-keyed splits onbyte[5] >> 4into the camera globals0x8007B607..0x8007B627, consumed by the camera-param builderFUN_801dab90, the arrival handlerFUN_801dbec4and camera-configFUN_801dbc20). - Mask-kind records side-copy
bytes[1..4]to scratchpad0x1F8003E8..EB(mirrors0x801F2778..84): the visible tile window. - Port:
legaia_engine_battle::encounter_man::scene_encounter_from_manbuilds per-sceneEncounterTables for the standalone towns and the kingdom-bundle scenes (thecount = 6MAN form resolves throughfind_bundle);legaia_engine_field::field_regions::zone_query(FUN_801dba20, with theFUN_80017fbc.MAPregion scan +FUN_800180ecattribute refresh) drivesWorld::refresh_field_regionsper tile crossing. - Outside this thread: the world-overview actor-placement section consumed by
FUN_8003A1E4(world-overview threads).
Owning page: encounter.md.
Seru-magic summon visual (e.g. Tail Fire)
Status: resolved - player visual wired; enemy "Fire Tail" characterized
A player Seru-magic summon renders as its namesake battle_data creature through the ordinary rigid TRS-keyframe battle draw. The per-spell slot-B overlay is a spawn stager of move-VM scene-graph records, not the creature's draw path; PROT 0900 is a screen-effect + top-view-grid overlay that does not run during a summon. The enemy "Fire Tail" boss move is a single live move-VM part-actor.
Dispatch and stager assignment
- Battle SM
FUN_801E295Cstate0x29resolves spell id0x81..0x8bviaPTR_801f6734[id-0x81]+FUN_8003EC70(id-0x79);FUN_8003EC70(param)loads extraction entryparam + 0x37F(prot.md§ In-RAM TOC), so0x81..=0x8b→ PROT903..=913(Gimard0x81→ 0903, spell0x83→ 0905). Overlays timeshare the slot-B buffer at link base0x801F69D8(*DAT_80010390). - Capture: one mid-cast state per spell (the
gimard_summon_*+<seru>_summon_mid_castscenarios inscripts/scenarios.toml) holds the loader-B current-id0x8007BC4Cat exactlyspell_id - 0x79for all eleven ids,0x81Gimard → 903 through0x8BNova → 913. - Stager blocks (
legaia_overlay_images::summon_overlay):PLAYER_SUMMON_STAGER_PROT0903..=0913;EVOLVED_SUMMON_STAGER_PROT0914..=0923 (0x8c..=0x95, same(id - 0x81) + 903run; 8 of 10 legs capture-pinned,0x90/0x91by arithmetic);HIGH_SUMMON_STAGER_PROT0927..=0934;ENEMY_BOSS_STAGER_PROT= the six Cort stagers 0938 / 0940 / 0944 / 0961 / 0962 / 0966. Ordinary-enemy casts bind the same way: the Delilas brothers → 0958 / 0959 / 0960, Zeto → 0946 (enemy_stager_binding). - Entry 0907 (Nighto) heads with the ASCII title
Hell's Music+ a normal MIPS prologue. The title is the attack's display name (SCUS spell-table stringHell's Music|Kill or confuse enemy.;summon.datlists it among the attack-name records, parallel to Gimard'sBurning Attack). Not a dance-minigame dual use: the dance overlay (0980) has zero slot-B loader callsites in any form (jal / tail-call / pointer-word / lui+addiu); its only loader-reaching call is the SCUSFUN_80025BA0wrapper (ids 5 / 6 → the 0900 / 0901 move-FX pair) and its music is sequenced BGM via the sound streaming loader.
Stager records
- A stager spawns each part through the SCUS part-stager
FUN_80021B04(a1= world pos,a2= record pointer,a3 = 0x1000):actor[+0x48]= record move-buffer base,actor[+0x70] = 2(PC), thenjal FUN_80023070ticks the move VM onrecord+4. High and enemy stagers spawn mostly through the pool wrapperFUN_80050ED4(→FUN_80021B04, poolDAT_801C90F0). - Record:
[i16 model_sel][u16 reserved][move-VM bytecode @+4], variable length, passed by absolute pointer (lui 0x8020 / addiu).model_seldispatch inFUN_80021B04: negative → transform / pivot node (dominant; the mesh is bound by the move-VM anim-bank ops);0x4000/0x4001→ render-mode node+0x5A = 3/5; otherwiseDAT_8007C018[model_sel + gp[0x754]]. The basegp[0x754]is one per-battle value,party_count + 2(see Per-spell magic power). - PROT 0905: 22
FUN_80021B04sites → 17 part records, all transform nodes, at file0x180C..0x1E00(runtime0x801F81E4..). The addresses resolve only under link base0x801F69D8; under0x801F0000they fall past the0x5800file. Parserlegaia_overlay_images::summon_overlay::parsescans the spawn calls (direct + pool wrapper); disc-gatedsummon_overlay_real,summon_overlay_block,enemy_stager_real; CLIasset summon-overlay. - Trim before parsing. A stager's extraction
.BINover-reads past the next entry's start LBA; the real footprint is(next_start_lba - start_lba) * 0x800(unique_content_len), a boundary the Cort mid-cast saves pin byte-exactly against the slot-B resident image. Trimmed, record first words across the whole corpus are only-1/ small library indices /0x4000. Untrimmed reads show0x1000/0x8000-class "sentinels" and extra mesh records that are neighbouring stagers' offsets dereferenced in the wrong file window. - Mesh-bearing records are rare. Gimard's stager (0903) is a pure transform rig (every record
-1), so its draw list is legitimately empty; Nighto (0x85→ 0907) carries one mesh record.summon_scene_realdrives Gimard for the tick path and Nighto for the draw path. 0x4000render-mode records live in five stagers, all player casts: Palma 0928 (4), Mule 0929, Jedo 0931, and the evolved casts 0916 (0x8e, 4) and 0921 (0x93, 6).- No catalogued state holds a live
0x4000node: the Cort enemy states' pooled part-actors all carry-1records (+0x56 = 4/+0x5A = 2after move-VM rebinding), and the three Sim-Seru player-cast states hold no live stager part at all (zero RAM references to any stager record while the stager is byte-resident at slot B). An enemy casting a Sim-Seru creature would seat one (crates/parity/tests/summon_render_mode_node.rs). The node's behaviour is decoded statically (next block).
Part render modes (FUN_80021DF4)
- The per-part tick
FUN_80021DF4dispatches+0x5Ainto six modes:2/6parameter / colour tween;3(the0x4000node) moving particle (FUN_80019D50);4VRAM-blit beam (LoadImage/MoveImage/StoreImage=0x8005842C/0x80058490/0x800583C8);5(the0x4001node) a 3D positional sound emitter (range / volume + SE trigger, not a visual node);7matrix transform + billboard; otherwise transform pivot. Reference:move-vm.md§ Part render-tail. - Port: the classification
engine-effects::summon::RenderMode(from_model_sel,// PORT: FUN_80021B04), consumed bySummonScene::special_render_nodes/part_drawsto split the audio-only node off the mesh draw path (testsrender_mode_classifies_only_the_sentinel_nodes,special_render_nodes_are_split_from_the_mesh_draw_list); the move-VM call gate ismove_vm::actor_tick.FUN_80021DF4's per-mode GP0 / SPU / VRAM emit paths are documented, not ported. - 239 field-resident prescript render-mode nodes (a resident-overworld mednafen read) are the non-summon validation source for the integration.
PROT 0900 - the slot-B screen-effect + top-view-grid overlay
- Residency: file
0x0640..0x2660is byte-resident at0x801F7018..0x801F9038inbattle_gimard_tail_fire_a(pin:0x801F8000↔ file0x1628); function bodies are instruction-identical to the dance / Baka Fighter dumps. FUN_801F811Cis the screen-mask (iris) widget handler, not a part transform: its four tweened channels are the edges of a screen rect and its four quads are the black border bands (GP00x28flat quads). Mid-tween the latched current values do not move - each frame re-interpolates from the fixed start, latching only at+0x9C == +0x9E.- It is kind 1 of a four-kind 2D widget family (scripted sprite
FUN_801F7A9C, maskFUN_801F811C, image panelFUN_801F849C, letterboxFUN_801F8A34) controlled from field-VM sub-ops insideFUN_801DE840; the ten ending scenes drive it through field-VM op0x43. Handler descriptors, control APIs, call sites and channel offsets:move-vm.md§ screen-effect widget family. - Port:
engine-effects::screen_fx(mask / sprite / panel / letterbox + the 4-modeFUN_801DE4C8interpolator; disc-gatedscreen_fx_disc). - Apparent references to these handlers from stagers 0910..0915 are VA aliasing: in-file
FUN_80021B04part records at coincident addresses under the shared slot-B base.summon::apply_translation_updatekeeps the tween shape as an interpreted glide; the faithful port of the routine isscreen_fx::MaskWidget. - The matrix code in PROT 0900 is the top-view grid-instance renderer
FUN_801F7088plus a parallel second-cluster sibling (RotMatrixX/Y/Zx6, GTEMVMVA). Per grid cell:TR = R_cam * cell_pos + TR_camandR = R_base * Rx(rec+8) * Ry(rec+0xa) * Rz(rec+0xc), withR_basefrom the camera Euler_DAT_8007B790/2/4, per-axis skipped by record flags0x80/0x100/0x200; modelDAT_8007C018[rec+0x10 + base@0x8007B6F8]into cluster-AFUN_80043390. It is not the summon or move-FX path (0 calls during a player summon, below).
The player summon is a battle creature
- PCSX-Redux capture of a player Gimard "Burning Attack" cast (Vahn solo; scenarios
gimard_summon_start/gimard_summon_visible/gimard_burning_attack), exec-breakpoint counts across all three phases:FUN_801F7088= 0; move VMFUN_80023070= 2-3; part-stagerFUN_80021B04= 1; battle per-actor drawFUN_80048A08= 35-64 per frame. Draw path:FUN_80048A08→ per-object rigid-TRS keyframe decoderFUN_8004998C→ cluster-AFUN_80043390, each object's Euler composed byRotMatrixX/Y/Z(ported incrates/engine-vm/src/anim_vm.rs). Probesautorun_summon_rotation.lua+autorun_summon_path_reconcile.lua; RAM dumps undercaptures/summon_rotation/. - Creature identity, from the fingerprint-verified frame-0 RAM of
gimard_summon_visible(8aa0...): battle actor tableDAT_801C9370has slot 0 = Vahn (HP 196) casting0x81, slot 3 = a Gobu Gobu enemy (HP 76, 13 parts / ~10 actions; actor0x8008350C,+0x5a = 3, 13-group mesh table at+0x44, archive0x800B2694at+0x4C,+0x88self-ptr →+0x8C, 13x18 =battle_dataid 4 action 0), and a distinct 11-part / 2-action entity whose idle (0x800BBB20, 11x40) byte-matchesbattle_dataid 10 "Gimard" action 0. - Spell → creature map
0x81..=0x8b(by name; the"$2"/"$3"enemy variants excluded): Gimard0x81→ 10, Theeder0x82→ 25, Vera0x83→ 28, Gizam0x84→ 55, Nighto0x85→ 49, Zenoir0x86→ 64, Viguro0x87→ 74, Swordie0x88→ 86, Orb0x89→ 83, Freed0x8a→ 92, Nova0x8b→ 95 (legaia_engine_effects::summon::summon_creature_id, disc-gatedsummon_creature_map_real). - Evolved block
0x8c..=0x95, pinned by mesh identity (eachsummon.datgroup's actor-record TMD byte-matches an archive record by longest common prefix, 8-17 KB each, for all of0x81..=0x95): Gola Gola0x8c→ 98, Mushura0x8d→ 101, Aluru0x8e→ 80, Barra0x8f→ 141, Kemaro0x90→ 144, Spoon0x91→ 147, Slippery0x92→ 150, Iota0x93→ 153, Puera0x94→ 156, Gilium0x95→ 159. Maplegaia_actor_formats::summon_creatures::SUMMON_CREATURES, disc-gatedsummon_creature_tmd_map_real. - High block
0x99..=0xA0(Juggernaut / Palma / Mule / Horn / Jedo / Meta / Terra / Ozma) matches no archive record: those summons carry a bespoke mesh in thesummon.datgroup's raw part-pool slot (the same oracle asserts the non-match).
Enemy "Fire Tail"
- In the two mid-cast frames
battle_gimard_tail_fire_a/_b(disc + library gatedfiretail_movefx_liveness) the slot-B occupant is PROT 0900 itself (loader-B id5), not a per-spell stager, and its widget family is dormant: an effect-actor-list walk finds zero live widgets. - The live effect is one move-VM part-actor in pool
DAT_801C90F0, ticked by the SCUS actor tickFUN_80021DF4(→FUN_80023070; bound atactor[+0xC]). Its record (actor[+0x48]) lives in battle-overlay (0898) resident data at0x801F5xxx, below the slot-B base;model_selreads-1(transform node) /5(library mesh).
Assets and engine
- The CLUT band is byte-identical across the two animation-distinct frames (motion is geometric, not palette cycling). Flame texture = PROT 870 (three 64x256 4bpp TIMs → battle VRAM
(320/384/448, 0), CLUT rows 474..476); flame mesh = PROT 871 (etmd.dat, 30-TMD pack) atDAT_8007C018[26]. - Flame-atlas loader
FUN_80020050(SCUS): uploads PROT entry0x366into VRAM twice viaFUN_8001fc00(→FUN_8003e8a8), VRAM region set up byFUN_80017888/FUN_8001e54c(param0xf000); gated on_DAT_8007b868 == 0(the gateFUN_801dbe9creads); independent of theFUN_800520F0battle-bundle path (which pulls0x367..0x36d). - Engine assets: PROT 871 →
World::global_tmd_pool[3..=32]; the flame atlas uploads on battle entry and the static flame renders with the row-478 CLUT (GIMARD_TAIL_FIRE_MODEL_INDEX = 26). - Summon render: a cast (
spell_idin0x81..=0x8b) callsWorld::request_summon_spawnfrom both cast paths (World::fold_battle_eventonBattleEvent::SpellAnimTrigger, andcast_spell_on_slots); the host drainsWorld::take_pending_summon_spawnandspawn_summon_creatureseats the creature, drawn throughmonster_archive::battle_render_mesh+MonsterAnimPlayer+tmd_to_vram_mesh_posed_rot(mesh, texture and animation all from PROT 867). - Battle-actor animation pipeline:
legaia_actor_formats::monster_archive::idle_animation(action 0, the+0x8c9-byte TRS stream) →legaia_engine_battle::battle_anim::MonsterAnimPlayer(8.8 fixed-point loop cursor producing alegaia_anm::PoseFrame) →legaia_tmd::mesh::tmd_to_vram_mesh_posed_rot(R*v + T,Rz*Ry*Rx).enter_battle_renderattaches the clip per actor andWorld::tick_battle_animationsadvances it; disc-gatedbattle_anim_real(monster 1 = 28 frames x 15 parts). - Stager scene-graph:
engine_effects::summon::SummonSceneseeds one move-VMActorStateper parsed part (PC = 2 →record+4) and ticks it through the move VM (World::spawn_summon/tick_summon/active_summon_part_draws;summon::summon_stager_prot_entrymaps id → entry;play-windowGdebug-spawns the Gimard stager). Every Gimard part runs without an unimplemented opcode (disc-gatedsummon_scene_real). It is the stager-record driver and the model for move-VM part effects, not the creature draw.
The party cast trigger is a params stager
Status: resolved
FUN_801DBF9C(party, spell_id) runs at the end of state 0x29's wait for a party caster and only writes the caster's action-parameter stream; no store touches HP, MP or a target. The outcome is the streamed module's (overlay_battle_action_801dbf9c.txt).
- Two arms on
sltiu v0,a1,0x25. At or above0x25:actor[+0x1E0] = 9,+0x1E1 = 0x12,+0x1E2 = 0xFF, return (0x801DC064..0x801DC09C). Below: index0x801F4E64 + id - 1selects an 8-byte anim-pair list at0x801F4EDC, copied into+0x1E0..up to the0xFFterminator. - Every player Seru id is
>= 0x25, so every player cast - healing included - takes the summon arm. State0x29reads the sub-route back (lbu v1,0x1e0(s3); li v0,0x9; bneat0x801E45EC). - The staged
0x12is a cast-effect id, not a clip: the argument the summon band handsFUN_801DC0A0each frame while the caster's+0x1D9stays9(capturegimard_summon_start). - Port:
BattleHostImpl::spell_anim_trigger(engine-core,// PORT: FUN_801DBF9C) arms the stager on the summon arm; the< 0x25pair lists are read off the disc intoBattleState::spell_anim_pairs.
Player-summon presentation
Status: resolved
Read off the capture corpus and the summon band's disassembly:
- No spell-name label for a party caster. State
0x28's label block is skipped for an acting id< 3(sltiu v0,v0,0x3; bne v0,zero,0x801e4460at0x801E43D8); the mednafen*_summon_mid_castdisplay crops show the acting-actor plaque, the caster close-up or the flash's white and the additive burst, and no label. The party readout follows the hide: absent in every0x33/0x34crop and ingola_gola(0x35, all seats hidden), back under the caster invera(0x35, stager phase 3, the caster's+0x21Ccleared while the other seats stay0xFF). - The hide.
0x34zeroes the prim word and sets+0x21C = 0xFFon every party seat and every living monster (0x801E4B30..0x801E4B6C); the PCSXgimard_summon_visible/_burning_attackstates read exactly that on slots 0..3 while the creature at slot 7 stays drawn;0x36restores. - The creature seat. Slot 7 at
x=185, z=-2272(caster82, -542) with the caster's facing on the idle clip at stager phase 6, thenz=-1606on clip1at phase 11: it walks in from behind the party toward the target. - The two flashes. Flash-in at
0x33(additive, delay0x14, ramp0x14black → white, hold-1, id1) plus cue0x63; flash-out at0x34(additive, ramp0x78white → black, hold1). Thevisiblestate carries the flash-out actor finished (+0x10 & 8, hold counted below 0). - The duck.
_DAT_8007B910reads161against level215in both mid-cast states - the75/100floor of0x35, exactly.
Port: the summon band (engine-battle-vm::battle_action::summon), the stager and the hide / fade seams (engine-core::world::battle::cast_band), both hosts' draw gates and fade_prim composition.
The fade actor's hold word
Status: resolved. Evidence: disassembly, re-read against the listing.
A hold of -1 means hold the landed colour until the actor is killed (FUN_80020C14, 80020c14.txt).
a1 = actor + 0x7C, so the start delay is+0x98, the hold+0x9Aand the duration+0x9C. While the delay is positive the tick returns-1(0x80020C28..0x80020C54).- After the duration goes negative the tick sets
actor[+0x62] |= 0x100(0x80020C88), thenlh v0,0x1e(a1); bltz v0,0x80020cd4(0x80020C90..0x80020C98): a negative hold skips the countdown and the tick goes on ramping and drawing. - A non-negative hold counts down by the frame step and, once it is no longer positive (
bgtzat0x80020CB4), setsactor[+0x10] |= 8(finished,0x80020CC4) and returns-1(draw nothing). - Consequences: the escape white-out persists until the battle unloads; the summon flash-in persists until
0x34kills it. - Port:
engine-system::fade::FadeState::step.
summon.dat / readef.DAT side-band streaming
Status: resolved (entries + format)
The two 0x10800-slot battle streaming files are extraction PROT 893 (summon.dat) and 894 (readef.DAT), both decoded. Parser legaia_actor_formats::summon_readef, disc-gated summon_readef_real.
- Entries.
FUN_800558FCignores its path string in retail (_DAT_8007B8C2 != 0, verified live) and consumes its 4th argument as a raw-TOC index:summon.dat=0x37F,readef.DAT=0x380(raw - 2 = extraction, the same offset as the overlay loaders'param + 0x381). The footprints divide into exactly 103 / 78 slots of0x10800. Inbattle_gimard_tail_fire_athe stream buffer at*0x8007BD74equals entry 894 slot 1, and slot 0's CLUT row / texture page match VRAM(0,488)/(512,0)byte-for-byte. - Slot selection.
FUN_801E295Ccase0x32maps action id → base slot:3*(id-1)forid < 0x9A, else4*id + 0x63; bit 7 selects the file. The applierFUN_801F12D0streams slotsbase..base+3(readef groups stop afterbase+1unlessbase == 0x36) and uploads CLUT rows + texture pages;FUN_801F19ECinstalls the final slot as the summon creature (viaFUN_80055468). Summon group 0 (spell0x81) carries the "Burning Attack" record. - Per-turn seed.
FUN_801DABA4seeds the group base per turn (party3*(char-1); enemy3 * monster_record[+0x1C]); the battle-end arms request3*char+2directly. - Aux slots. The eight aux slots of readef groups 0..3 (slots
3c+1/3c+2, c = Vahn / Noa / Gala / Terra) are the player art-animation"ME"stream archives, consumed byFUN_8002B28Cout of the*0x8007BD74buffer (parserlegaia_actor_formats::me_archive); the main-vs-base pick is per battle phase (turn staging vs battle-end win-pose staging). LoaderFUN_801F17F8(raw TOC0x380, slot* 0x10800) has a single stagingjalat0x801F17A0; the cast band originates no request of its own. Higher groups' aux slots are textures, the four ME archives, or a never-staged actor record (the aux-slot row in this area's table). The ME read is gated to party seats0..2, and a sweep ofDAT_8007BD10over the state corpus finds char 4 at seat 1 in one battle, so group 3's archives are reachable. - Unenumerated: the full
actor+0x1DFid ↔ enemy-special mapping (themap[actor+0x1df]128-byte band). The Tail Fire capture is consistent with action id 1 → readef group 0. - CDNAME number space.
#definenumbers are raw-TOC indices, a uniform -2 to extraction: every byte-pinned loader constant for a dev-named file equals the same-named define (summon.dat/readef.DAT0x37F/0x380=bat_back_dat 895/896), and semantic scoring over decidable blocks is 217/225 at -2 vs 209/225 at 0 (scripts/asset-investigation/cdname_shift_analysis.py).legaia_prot::cdname::block_for_extraction_indexgives the retail-space name; the identities and exceptions are tabulated on the owning page.
Owning pages: summon-readef.md, battle-data-pack.md § "ME" stream archives, cdname.md § numbering space.
Monster steal item (Evil God Icon)
Status: resolved - static SCUS table DAT_80077828
What the player steals with the Evil God Icon equipped comes from a static SCUS_942.54 table, not from the PROT 867 monster record.
- Table
DAT_80077828(file offset0x68028), indexed by 1-based monster id: entry atDAT_80077828 + id*2, a 2-byte[steal_chance_pct, steal_item_id]pair. Chance first, item second - the reverse of the monster record's[item, chance]drop fields. - Capture: a live player-steal RAM read of Skeleton (id 13) gives
1e 8a= 30% Incense, matching the on-screen banner; the table is byte-exact against the complete published steal table (item and chance) across every resolvable monster id. - Not in the record, disc-measured: for the 185 monster ids both populated in PROT 867 and stealable in the SCUS table, no offset carries the pair in either field order - not in the 13,030,964 bytes of LZS-decoded monster blocks (every offset, full block length), nor in the 15,155,200 raw bytes of the
0x14000slots. Best agreement in any layer is 2/185 for[chance,item]and 2/185 for[item,chance]. - Single-byte offset
0x48scores 31/185, but0x48isdrop_item, and steal and drop draw from the same 39-item consumable pool; none of the 31 also agree on chance at0x49. The best non-drop offset is 7/185. - Monster ids
187..190are stealable in the SCUS table but have no archive slot (PROT 867 is 194 slots of0x14000, 186 populated). - Parser
legaia_game_tables::steal_table; randomizerlegaia_patcher::steal.enemies.tomlstealis ground-truth labelling; the SCUS table is authoritative.
Owning page: steal-table.md.
Per-spell magic power / multiplier
Status: resolved - mechanism decoded, kernels ported and wired
A summon's "power" is caster / summon battle-state-derived, not a static per-spell scalar (SCUS spell-table +5..+8 are zero). Damage runs the three-stage chain FUN_801dd0ac roll → FUN_801dd864 scale → FUN_801ddb30 finish; the per-move power table at 0x801F4F5C belongs to monster special attacks, not to magic or party arts.
Where the magnitude is applied
- Each module's tick body applies it, not its stager: the
actor+0x14cwrite lives in thectx+0x279phase machine the0x801CF4ECtable arms, a different routine in the same image. PROT 0903, 0910 and 0913 are damage modules; per-module classification (PROT 0909 included) is oncast-module.md. - Not the 7-entry jump table
FUN_801f2d68reads (jr *(0x801F69D8 + state*4)): it resolves to PROT 0900 file offset 0, five staggered entry points into one per-frame routine that lerps move-VM anim banks (FUN_8003ce9c/ce64/ceb8) and emits GPU packets into scratchpad0x1F800314- zeromult/div, noactor+0x14cwrite, no power read. - Damage bodies call
FUN_801dd0ac(a0= per-summon move-type const0x10..0x12,a1= a baked7on the player half,a2= target slot), clamp in one of three shapes, accumulate the popup atactor+0x10, then subtract. - Two bodies heal, each a closed form over the caster's per-magic level byte (the 32-slot search matching
actor[+0x1DF]against the id list at+0x705, reading the parallel byte at+0x729): PROT 0905 (Vera) restoresrecord[+0x729 + slot] * 0x20 + 0xe0, clamped to the+0x14C/+0x14Epair, skipped atHP == 0or+0x16E & 4, pushed to+0x10as a negated popup at0x801F7D0C; PROT 0911 (Orb) restores(level << 6) + 0x1c0over the party row. Both unlock a status cleanse at level>= 3. Neither is(power_byte << 5) + 0xe0.
The roll (FUN_801dd0ac, overlay_battle_action_801dd0ac.txt)
- Summon path (attacker slot
7, the summon creature's seat inDAT_801C9370):roll = rand % (INT@+0x168 + 1) + HP@+0x14c + DAT_801C9370[ctx+0x13]_INT * 2, theHPandINTbeing the creature's and the secondINTthe caster's; returnsroll - defender_mitigation. Re-read against the listing:bne a1,7at0x801DD104,lhu 0x168(s1)+div+mfhiat0x801DD114..0x801DD148,lhu 0x14C(s1)unshifted at0x801DD16C, the caster'slhu 0x168doubled at0x801DD170..0x801DD17C, and a store of2to0x80084448on the way (0x801DD178). The non-summon path is different in kind: two draws,rand % ((power >> 2) + 1) + rand % ((INT >> 1) + 1) + (HP >> 8) + power + INT * 2(0x801DD18C..0x801DD25C). - Non-summon path reads the 26-byte-stride move-power table at
0x801F4F5C(static battle-overlay data, PROT 0898; parserlegaia_game_tables::move_power) through the 128-byte id → index map at0x801F4E63(param_1 = map[actor[+0x1df]]). - The whole 26-byte record is decoded on
move-power.md:+0power,+0x0aimpact-effect selector,+0x0btrail texture page,+0x0dsound cue,+0x12/+0x16effect-id lists, and+0x0can unusedC/E/Gdesigner tag with no runtime reader. - The move-id space is the spell-table id space, and the map covers 44 ids: idx
0x10..=0x2b= the named monster special attacks (0x25..=0x74), idx0x01..=0x0f= unnamed internal enemy-attack tiers (0x04..=0x07/0x12..=0x1F). The basic-attack / art bands0x08..=0x11and0x16..=0x18are unmapped (live capture: Vahn's Somersault0x0Fis unmapped and would roll against zero-power record 0). Party arts take damage from the per-strike art-record power byte (art_strike.rs);apply_basic_attackuses the flatart_strike_damage_defaultfor a no-art generic hit.
Scale and finish
FUN_801dd864: 8x8 element-affinity matrix0x801F53E8(PROT 0898 file0x26BD0, parserlegaia_game_tables::element_affinity) + status bits + the summon magic-power tailroll += roll*(power-1)>>3. Orientation ismatrix[attacker][defender]; values are a +/-4% nudge (diagonal 96, opposite pairs 104, default 100), not a x0 / x2 weakness table.- Per-character element table
0x801F5480: Vahn = fire, Noa = wind, Gala = thunder, Terra = wind. The enemy element is read record-direct:lbu ...,0x1d(record)withrecord = 0x801C9348[slot-3](the per-enemy record-pointer table, not a copied live-actor field), i.e.MonsterRecord::element(+0x1D) - the same record the victory-spoils path reads+0x44/+0x46/+0x48from. - Per-caster summon power-percent table
0x801F5468(index(char_id-1)*8 + summon_element; PROT 0898 file0x26C50,ElementAffinity::summon_power): own element 100, opposed 40, Gala dark 60. FUN_801ddb30: equipment elemental-resistance halving, guard halve,rand%9+8no-damage floor, summon power-% scale, 9999 cap, spirit gauge, MP drain, per-element stat debuffs.
Port
- Kernels in
legaia_engine_battle_vm::battle_formulas:summon_attacker_roll/summon_defender_roll/summon_predamage/ theapply_*helpers /heal_summon_amount/damage_finish/spirit_gauge_fill. The finisher's state-mutating tail (damage-popup accumulator, per-seat readout table, MP drain, per-element stat-debuff switch) lives in the live battle context. - Monster special attacks: the table loads onto
World::tables.move_power(engine-battle::move_power::MovePowerCatalog) andcast_spell_on_slotsoverrides a damaging monster cast's magnitude witharts_physical_predamage_lazyseeded by the move's+0power (World::enemy_move_predamage: INT frombattle.accuracy, defense terms frombattle.defense_split). The attacker x2 + defender x1rand()draws are taken up front and the bonus pair lazily, only when the bonus arm fires, so the RNG cursor advances by exactly three or five draws inFUN_801dd0ac's call order. - Player summon roll:
World::player_summon_predamage- summon-body HP / INT from the namesakebattle_datacreature record, caster INT frombattle.accuracy, the caster magic-power byte from the character record's spell list (+0x13Dids /+0x161levels, theFUN_801dd864search), then theFUN_801ddb30finisher with the0x801F5468percent. Modelling limits: the slot-7 actor's HP at roll time is the creature record's spawn HP (a damaged mid-battle summon is not modelled), and status / guard default to none. - Affinity: the monster path scales by
matrix[enemy_element][party_member_element](World::enemy_affinity_pct, applied inside the roll before the conditional bonus-arm threshold, as retail orders scale → bonus); the player Seru-magic path scales bymatrix[summon-creature element][target element](World::cast_affinity_pct; attacker element fromWorld::summon_attacker_element, defender fromWorld::battle_slot_element), post-roll with the RNG untouched. - All of these are gated on the tables being installed, so a disc-free battle reproduces the no-table baseline bit-identically (magnitude + RNG stream).
Move-FX spawn records (the +0x12 / +0x16 lists)
- Auxiliary tables:
EffectAuxTablesfor the0x801F6324prototype-pointer and0x801F6418SFX tables;parse_impact_effect_tablefor the+0x0aselector's0x801F53D4entries, which are packedu32config words, not pointers. - Each
0x801F6324entry is an overlay VA to a variable-length move-VM scene-graph record in the summon-part format (+0x00 i16 model_sel,+0x02 u16 reserved,+0x04bytecode), packed, not a fixed0x20stride; spawned byFUN_80050ed4→FUN_80021B04. List bytes with the high bit (0x80) route to the 2Defect.datpool instead (FUN_801dfdf0→EffectCatalog, ported asspawn_by_ui_id). - Model-library base
gp[0x754](global0x8007BA6C):0with no battle effect-model library resident, elseparty_count + 2-3for the 1-member training party,5for the 3-member party (two fixed pool slots + the live party meshes precede the library). One per-battle value drives both move-FX and summon-part spawns;model_selis library-relative. Evidence: PCSX-Redux exec-bp onFUN_80021B04(probeautorun_summon_model_base; chainFUN_801e09f8 → FUN_80050ed4 → FUN_80021B04,ra = 0x80050F08,a3 = 0x1000, prototype table0x801F6324+ effect-list id0x22in registers) plus0x8007BA6Cagainst the party count0x80084594across the mednafen corpus; pinned bycrates/mednafen/tests/summon_model_base.rs. - Engine:
World::spawn_move_fxparses the records (MoveFxviaMovePowerCatalog::fx_for_move_id), stages them as aSummonSceneat the library base (PROT 0871 registered at a fixedDAT_8007C018[3..]) and ticks them through the move VM (tick_move_fx/active_move_fx_part_draws;play-windowHdebug-spawn). The part draw transform is the engine's anim-bank-derived interpretation; the retail one is theFUN_80021DF4render tail. - Presentation fields: trail texpage (
+0x0b→0x7700 + id) onWorld::active_move_fx_trail_texpage(); sound cue (+0x0d) viaWorld::take_pending_move_fx_cue(), resolved bylegaia_engine_audio::classify_cue→CueDispatch(theFUN_8004fcc8dispatch decode) inengine-session::battle_fx::spawn_pending_move_fx, which both play hosts enqueue. The voice arm's third argument is a read span, not a pitch:FUN_8003D53Crange-checks it against0x2A31at0x8003D5C8and touches no pitch register. - SFX bank: the cue's
program/tone(staticDAT_8006F198table,sfx-table.md) index the per-scene music VAB the BGM sequencer already has open (FUN_80065034reads the libsnd current-bank globals; byte-identical to the discmusic_01VAB for that scene) -SfxBank::play_one_shot(spu, scene_vab), no separate bank. - Afterimage: the streak pass
FUN_801e1ab0islegaia_render_kernels::afterimage::build_afterimage_quad(jittered semi-transparentPOLY_FT4: per-cornerrandwobble, brightness band, UV / CLUT / texpage layout). Its corner projectorFUN_800195a8(camera-coupled GTE billboard: view-space MVMVA centre, +/-half-size corner fan-out, rotation + translation reset, RTPT x3 + RTPS; detail) islegaia_render_kernels::billboard::project_billboard, with theFUN_801e1ab0call shape (+0x120Y push, dynamic half-widthstate+0x6c6 - 0x200, half-height0x100) asafterimage::project_streak_corners. TheRotMatrix*sin / cos LUT istrunc(4096*sin)(disc-gatedgte_sin_lut_real).
Owning pages: battle-formulas.md, move-power.md, cast-module.md.
Stat growth-rate source
Status: resolved + validated + wired (core + opt-in jitter)
The per-character stat-grant source is static SCUS_942.54 tables read by the level-up applier FUN_801E9504.
- Parameter block
DAT_80076918: per character (stride0x3C), 8 contiguous 6-byte sub-records{u16 start, u16 max, u8 jitter, u8 row}.start= base stat (Gala matches the new-game template on all 8);rowselects one of 3 curves atDAT_800769CC. - Per-level gain =
max(1, (max-start) * curve[row][level-1] / D + rand() % (2*jitter+1) - jitter), then caps.D = 0x24C0, the curve normalizer (each curve sums to0x24C0, so growth accumulates to exactlymax-startby L99), for every stat but AGL, whose block divides by0x18C0(its own0xA57EB503magic at0x801E993C); AGL meets its0x118cap well before L99. - Capture: a single-level step (Noa L2 → L3, the
noa_levelup_*saves) has all 8 deltas within the core +/- jitter band. Multi-level corpus observations (noa/gala_4_level_jump) are unreliable for this check. Not the "Seru struct+0x74". - Parser
legaia_game_tables::level_up_tables::GrowthTables::{char_params,level_gain_core}(disc-gated). - Engine:
StatGaincarries HP / MP + the six battle stats;LevelUpTracker::with_growth_tables+BootSessioninstall per-character curves from the user's SCUS, andapply_to_recordgrows the record-side window then mirrors to live (disc-gated boot test pins Noa's L2 → L3 core).LevelUpTracker::with_level_up_jitter(seed)drives a PSX BIOS-rand LCG (BiosRand), onerand()per stat per level on the unfloored core before themax(1, ...)floor; it is off by default so determinism oracles stay bit-identical (bit-exactness needs the runtime BIOS-rand seed). - The slots-1/2 XP-threshold correction is documented on the owning page.
Owning page: level-up.md.
Monster stat-record archive source
Status: resolved
The monster archive is PROT entry 0867_battle_data (extended footprint; the 15.9 MB archive lives in the entry's trailing-gap sectors). Retail-semantically it is the monster_data block: the define monster_data 869 names extraction entry 867 under the raw-TOC -2 correction (cdname.md).
FUN_800542C8streams per-monster0x14000LZS slots at(id-1)*0x14000, each[u32 dec_size][LZS], decoding to a block whose head is theFUN_80054CB0stat record.- Record: name
@0x00; battle-model TMD offset@0x04; HP@0x0C; MP@0x10; stat u16s@0x0E/0x12/0x14/0x16/0x18/0x1A(ATK / UDF / LDF / INT / SPD / AGL); rewards (XP / drop) inline at@0x44..0x49; magic count@0x4A; spell-ptr array@0x4C. - Capture: live-battle PCSX-Redux watchpoint (
autorun_monster_record_source.lua) - relative seek(id-1)*40sectors +disc_readCdlLOC → PROT.DAT0x38AF000= entry 867; three records match live actor stats byte-for-byte. - Parser
legaia_actor_formats::monster_archive; bridgelegaia_engine_battle::monster_catalog::catalog_from_monster_archive, wired intoenter_field_scene.
Monster mesh + texture pool
Status: resolved
The monster's 3D battle model is a Legaia TMD embedded in each PROT 867 archive block at the offset in stat record +0x04; its texture / CLUT pool is at record +0x08.
- The TMD is installed at battle-actor
+0x230; the0x1C-stride recordsFUN_80049858/FUN_800495C8walk are its object table. 186 of 194 slots parse cleanly. - Pool layout (from the battle loader
FUN_80055468): a0x1E0-byte region of fifteen 16-colour CLUTs, then a 4bpp page (always 256 rows tall, 128 or 256 texels wide; palette =cba & 0x3F). Byte-exact against pool sizes. - The on-disc CBA / TSB are nominal defaults the loader relocates per slot, so the raw pool does not appear verbatim in a battle VRAM dump; the loader layout is the ground truth.
- Parser
legaia_actor_formats::monster_archive::{mesh, MonsterMesh::texture}; CLI--obj+--texture-png; WASMmonster_mesh_*+monster_texture_*accessors drive the enemy-table site page's per-row WebGL viewer.
Terra slot-3 / story-flag overlap
Status: resolved
The character-record base is game+0x3C8 (live RAM 0x80084708), with the display name at record offset +0x2A7; 0x66F is the name field, not the header size. Terra's record needs no special case.
- Six in-game RAM captures: mid-game stats at
record+0x104/+0x11Cread back the expected per-character HP / MP for all four slots. - The four-slot array runs into the global region: slot 3's tail (record offset
>= +0x2BC=game+0x12C0) aliases the story-flag bitmap and inventory. Terra's meaningful fields (name, live stats, RecordStats) sit before that boundary. Terra is the New Game template's fourth roster entry (HP 400) but never a savable battle-party member, so the aliasing is benign. - Code:
RETAIL_CHAR_RECORD_HEADER_SIZE = 0x3C8;legaia_save::CharacterRecord::name()/set_name()atNAME_OFFSET(+0x2A7);Party::from_retail_sc_blockreads a populated save's stats from the right fields (checked by synthesising an SC block from a live RAM dump and comparing the parsed HP).
Battle party meshes = assembled from the player battle files (PROT 1204 = Baka Fighter / default-equipment sibling)
Status: resolved (static chain + byte-verified)
A main-game battle renders each party member from a merged TMD assembled at battle setup out of that character's player battle file (data\battle\PLAYER<n>, extraction 0863..0866), one section per equipment slot selected by the equipped item ids (char record +0x196..+0x19A). Textures and the palette come from the same file. PROT 1204 is the Baka Fighter / default-equipment sibling, not the runtime source, and battle does not reuse the field pack 0874 section 0.
Assembly chain
FUN_80052770case 4 (section select: an equipment-id-matched entry or theid == 0separator = unequipped default) →FUN_80052FA0(assembler, blob atctx+0x50) →FUN_800536BCx5 (object splice;nobj += section_nobj, bone-id byte per object, surplus objects tagged = equipment visual meshes) →FUN_80053898(retag 200 / 201 / 100+, attach bones atblob+nobj, sort) →FUN_800513F0registersblob+0x18intoDAT_8007C018[slot].nobj15 → 17 is the weapon + Ra-Seru sections' extra objects (notFUN_8001EBEC, which only toggles a pose transform).- Byte-verified in the full-party Gobu Gobu save:
DAT_8007C018[0] = ctx+0x50+0x18exactly,nobj = 17, bone bytes[0..14,200,201], attach[5,8], and all 17 vertex pools found in PLAYER1's sections with equipment-selective matches. Only 12 of Vahn's 17 pools match 1204 (the default-equipment geometry, byte-shared); the 5 equipped-variant objects (Hunter Clothes body x2, Survival Knife piece + extra, the Ra-Seru piece) appear only in the player-file sections. - Baka Fighter loads PROT 1204 (
overlay_baka_fighterloadsdata\field\other5.lzs+ PROT 1205 / 1206, debug"OTHER5 %d %d"): the same characters with default equipment. Field-pack distinctness:battle_char_pack_real::battle_pack_is_distinct_from_field_pack.
Install into DAT_8007C018[0..=2]
- Static SCUS, through the generic registrar
FUN_80026B4C(store0x80026BA8), from two battle state handlers reached by indirect dispatch (so a static cross-reference on0x8007C018finds no writer):FUN_800513F0(lead / active actors -tmd_register(*(actor+0x50)+0x18, 0)in awhile<3loop over the active-actor table0x801C9360, right after theFUN_80052FA0palette decode) andFUN_800542C8(additional members - loop bounded by*(rec+0x4a),tmd_register(*(*rec+4), 0)). - The battle loader
FUN_800520F0registers PROT0x36a(etmd.dat) into the effect windowDAT_8007C018[3..], not the party slots. - Capture: write-watchpoint on
DAT_8007C018[0..2]across the auto-starting Queen Bee field → battle transition (autorun_battle_party_mesh_install.lua): all three installs fire atgame_mode 0x15, and the pointers byte-match the battle form (Vahn →0x80165F48). Dumpsfuncs/800513f0.txt/800542c8.txt; trace infuncs/8002541c.txt/800198e0.txt/800520f0.txt.
Player-file load
"data\battle\PLAYER1"is a dev-tree label, not an ISO9660 file: the retail openFUN_800608f0is atrapstub, soFUN_800558fcalways takes its debug branch →FUN_8003e8a8(char+0x360)readstoc[idx+2](in-RAM PROT TOC0x801C70F0) as a sector offset into PROT.DAT.- Raw
0x361..0x364(Vahn / Noa / Gala / Terra) = PROT.DAT0x36E8000/0x3791000/0x3828800/0x3897800→ extraction 0863..0866 (raw index - 2). Extraction 0861 / 0862 are 1-sector stubs whose over-read tail begins Vahn's file0x1000in, so a match against the "0861 window" is the same bytes.
Textures and relocation
- Texels upload from the player files' per-section texture pools at the static rect table
0x800775B8(FUN_80052FA0→FUN_80053B9CLoadImage front-end): >= 99.6% band reproduction against clean full-party battles. The 1204 atlases match only 73-98%; the shortfall is the equipped-variant texels. Seebattle-data-pack.md§ Texture-pool VRAM placement. - At battle entry every prim's TSB + CBA is rewritten into a packed per-slot runtime band; the CBA column is preserved and a character's two disc rows collapse to one runtime row (one 256-colour palette per character):
| Char | Disc pages, rows | Runtime pages, row |
|---|---|---|
| Vahn | (640,0) / (704,0), rows 490 / 491 | (512,256) / (576,256), row 481 |
| Noa | (640,256) / (704,256), rows 492 / 493 | (640,256) / (704,256), row 482 |
| Gala | (512,0) / (576,0), rows 494 / 495 | (768,256) / (832,256), row 483 |
- Disc sub-CLUT use: Vahn 0,1,4,5 / 0,1,7,8; Noa 0,1,2,5,6,7 / 0,3,4,8; the two auxiliary 1204 meshes use row 496 page (448,256) and row 497 page (512,256). The disc TSB / CBA are the authoring layout Baka Fighter uses directly; walked as-is in a normal battle the mesh renders incoherently. Evidence: the runtime TMD dumped from a clean battle save (
flags=1, absolute pointers; convertp → p - base - 12) renders the correct characters from the save's VRAM. - The texpage → CLUT-row table
0x8007BEC0(32 x u16, written byFUN_800198E0astable[texpage & 0x1f] = clut_row) is the scene renderer's; rows 490..497 hold scene environment palette shared by a scene's field and battle modes, not party palette.
Palette
- A battle-allocated resident block per character, DMA'd to rows 481 / 482 / 483: contiguous at
0x800ebee8/0x800ec0c8/0x800ec2a8(Vahn / Noa / Gala) in a clean full-party save, stride0x1E0= 15 x 16-colour sub-CLUTs, one per disc mesh object. It sits atarena_base + 0x4048; the work arena is zeroed at load by thesw $zeroloop at SCUS0x80055F14(base = *(0x8007BD3C),0x1e8dwords). - Character-intrinsic and produced at battle load: absent in name-entry / front-of-Tetsu / load-initiating saves, present as a single copy once the battle is up, byte-identical between the Tetsu and Drake fights.
- Assembler
FUN_80053B9C(per-colour storesh a0, 0x894(v0)at0x80053C6C; write-watchpointautorun_battle_palette_writer.lua, clean Tetsu fight): copies a source CLUT struct[u16 base][u16 count][BGR555]todst = arena + slot*0x1E0 + (base+idx)*2, OR-ing0xFFFF8000(STP / bit 15) onto every non-zero colour. Source pointers0 = *(*(0x801C92F0)+8) + per-char-off, a transient0x800Dxxxxbuffer filled by the LZS decoderFUN_8001A55C(write-watchpoint on the struct header0x800D6C98). - So the disc form is LZS-compressed and bit-15-clear (
0x1D40...) while the runtime form is bit-15-set (0x9D40...): the palette exists nowhere verbatim, compressed or not (lzs-decode findover every PROT entry, SCUS andinit_data; 6372 strict TIMs; LZS output windows to 24 KB). - Source records: the player file is self-describing relative to
record[0](header offsets,[id, running_a, size]descriptor entries,id == 0= section separator), with 5 sub-records scattered on disc atsec_base = rec0 + align_up(recbase - rec0, 0x2000)(a0x1000alignment fits Vahn and Noa but lands Gala's subs on a zero-padded0x7000block). The0x2000stride seen in RAM is only the loader's staging buffer. Layout on the owning page. - Decode:
FUN_8001A55C's first argument is an output-byte budget (decremented per literal and per match-copied byte; loopwhile budget > 0). Onepsx_lzs::decompress(stream, budget)per record, withrecord[0]+ the 5 staged sub-records decoded into one work buffer asFUN_80052FA0does, reproduces Vahn's palette byte-exact in all 3 bands:base=0x00=record[0]'s CLUT B,base=0x40= sub#0's trailing CLUT,base=0x70= sub#4's trailing CLUT. A budget-less decode runs into the next record (29/32 with 3 diffs). - Noa = PROT 0864, Gala = PROT 0865, by matching each
record0CLUT against full-party battle VRAM: Noa → row 482 98%, Gala → row 483 100% (the 1-2% misses are equipment patches in late-game captures). Gala's bands at0x00/0x30/0x50/0x80cover all mesh columns. Party order from the names at0x80084708 + n*0x414 + 0x2A7. Probeautorun_clut_decode_capture.luacaptured the 5 sub-record streams. - Distinctness: only 10 of Vahn's 130 battle-novel colours (0 of Noa's / Gala's) are in any field-pack CLUT; 146 of Vahn's 256 runtime colours are in no CLUT the 1204 pack ships (bundled row-492 CLUT vs retail row 492 = 0/256).
- Parser
legaia_actor_formats::battle_char_palette(find_record0+parse_record+collect_palette: record0 CLUT A / B + each separator'sid=0trailing CLUT + the final record, filtered to the columns the mesh samples; STP bit set on upload); disc-gatedbattle_char_palette_real(byte-exact against extraction 0863 withrecord0at file offset 0) andnoa_gala_collected_palettes_cover_mesh_columns.
Port
- Assembly
legaia_actor_formats::battle_char_assembly; palettelegaia_actor_formats::battle_char_palette; PROT 1204legaia_actor_formats::battle_char_pack. The web viewer'sbattle_char_mesh_cba_tsbreturns the nominal disc CBA, which is the right pairing for the 1204 authoring-layout (Baka form) render.
GPU upload path and battle-init reads
- Scene TIM upload:
FUN_800520F0(battle loader) →FUN_800198E0(per-TIM uploader) →FUN_800583C8(PsyQLoadImage) →FUN_8005A1C0(GPU-queue enqueue, op-type 8 =FUN_80059BD4via handler table0x80078D0C) → ring0x801C9590→FUN_8005A4A0(once-per-frame flush) →FUN_80059BD4(GP00xA0/ DMA2;a0 = RECT{x,y,w,h},a1 = src_ptr). Seefunctions.md. - Battle-init disc reads are party-independent (PCSX-Redux, Vahn-only vs full-party: byte-identical raw-TOC index set; raw → extraction is -2): monster
0x365→ 867; conditional stream +etim+etmd0x367/8/9→ 869 / 870 / 871;efect0x36B→ 873;readef0x380→ 894; overlay0x384→ 898;0x37A→ 888; music raw 1016; field-scene re-read0x5A→ 88. The player files stream earlier and no character-CLUT read fires at battle entry. - Scene-side facts from the same captures: a map01 battle uploads CLUT rows 488 / 490 / 495..499 and character image pages 512 / 576 / 640 / 704 / 768 / 832 / 864 / 960 at y=0 through
FUN_80059BD4; the row-490 source is the resident field-scene buffer (0x800E9690).map01/map02sec0 carry row 490 as a flag-0x80000008256x1 TIM, whose reserved high bit makesparse_strictreject it. Per-scene row-49x 16x1 CLUTs (35 scenes incl. town01) are field-actor palettes. - Not the party palette source: scene-bundle sec0 CLUTs (0400_doman rows 488-492, 0061_dolk, PROT 1200
other4rows 490-494 are field-form / other-pack palettes), the 1204 bundled CLUTs (1204 / 1205 / 1206 are uncompressed copies of the same authoring defaults), or atown0cscene-bundle LZS stream at0x23430(that write is the scene bundle decompressing into the shared work arena:0x800ebee8held0x7965481F, not the Vahn palette0x409d...). - Capture tooling:
autorun_clut_upload_hook.luaandautorun_clut_upload_watch_live.lua(per-upload(rect, src)capture),autorun_clut_uploader_pc.lua(read-watchpoint that pinnedFUN_80059BD4),autorun_find_clut_decode.lua(LZS-output scanner),autorun_battle_char_clut_source.lua(disc-read logger) +map_clut_disc_reads.py.
Owning pages: battle-data-pack.md, character-mesh.md § Battle form.
MP-cost ability-bit priority (half vs quarter)
Status: resolved. Evidence: disassembly, re-read against the listing.
Half wins over quarter, and both subtract a right-shifted copy rather than floor-divide (overlay_battle_action_801e295c.txt). The bits are the caster record's +0xF4 word (lw v1,0x6bc(v0) off 0x80084140, 0x801E4560 / 0x801E3D04); the shift sits in each branch's delay slot (srl v0,s0,0x1 at 0x801E4570, srl v0,s0,0x2 at 0x801E457C), so the no-bit path's beq to 0x801E4584 jumps the one subu s0,s0,v0 at 0x801E4580.
- The block is inlined twice, byte-identical:
0x801E4568in state0x28(right after that state's capture-archivejal 0x8003EC70at0x801E44EC) and0x801E3D0Cin state0x3C(right after that state's Pomander+0x1DF == 0xFEcase at0x801E3C4C). - Priority:
andi 0x20; bne <half>thenandi 0x10; beq <none>=if (bits & 0x20) {half} else if (bits & 0x10) {quarter}. - Formula: half =
cost - (cost>>1)(rounds up on odd costs); "MP-quarter" =cost - (cost>>2)= pay 3/4, notcost/4. - Port:
battle_formulas::mp_cost_after_ability_bits, shared by all three cast paths (the two SM blocks +cast_spell_on_slots);MpCostModifier::from_ability_flags. MP cost consumes no RNG.
Scripted Tetsu encounter → Battle (v0.1 oracle Battle leg)
Status: mostly
The opening Tetsu fight is launched by a dedicated MAN-placed field entity (the sparring partner), dialogue-driven, and the engine reaches Battle from a new-game cold boot by walking to the partner, talking and accepting. One step is staged by the test rather than played: the cold boot skips the opening sequence's carrier reposition.
- Launch (
FUN_801DA51C+ corpus RAM): on reaching SM state 1 the carrier copies itsentity[+0x94]formation into cell0x8007BD0Cand, via thecase 2/3fall-through, writes_DAT_8007B83C = 8(the battle handoff). It is not scene-entry-driven and not an inline arm op: an opcode-aware walk of town01's partition-1 scripts finds zero[1][0x4F]arm sites. - Formation install: the standard field-VM scripted-battle op
3E FF 04inP1[10]at record offset+0x7F7(MAN body0x01B67) pointsactor[+0x94]at town01 MAN formation index 4 = the lone monster archive id0x4F(Tetsu),EncounterRecord::rim_elm_training(). Same case-0x3Edirect-install arm as garmel's Zeto (3E FF 09) and rikuroa's Caruban (3E FF 11). It sits in the post-"Come at me!" branch (WaitFrames 16+ flag sets ahead of it; the adjacentTest 0x227/JmpReltargets land on op boundaries). Testrim_elm_sparring_carrier.rs::town01_p1_10_carries_the_tetsu_3e_ff_04_install. - Carrier placement: town01 P1 placement at tile (76, 65), model
0x6A- its post-tutorial village spot, in a sub-area not walk-reachable from the spawn (BFS: 2855 reachable sub-cells, carrier not among them). - Opening reposition:
P1[10](start 0x01370) carries, twice, at record offsets+0x1D/+0x28(MAN body0x0138D/0x01398), the op4C 51 15 0E 07 22=MenuCtrlnibble-5NpcRun { x_enc: 21, z_enc: 14, depth: 7, move_id: 0x22 }(field_disasmMenuCtrlKind::Nibble5NpcRun). Tile(21,14)→ world(21*128+64, 14*128+64)=(2752, 1856)=RIM_ELM_SPARRING_CARRIER_TUTORIAL_POS;P1[10]is the unique record NpcRun-ing there. The two identical ops are the story-flag two-branch scene-entry prologue. The dialogue-accept capture'sactor[+0x90]resolves to the(76,65)/0x6Arecord. Not op0x23 MOVE_TO: its only hits are false decodes in the desyncing dialog region. - Frames: the runtime actor frame equals the MAN placement frame -
FUN_8003A1E4spawns attile*128 + 0x40viaFUN_80024C88with no anchor, and the player cold-spawn0xA40istile 20*128 + 0x40. - Talk and Yes / No: talking is a button press, not a field-VM opcode (op
0x3Ewithop0 < 100is the scripted-battle install). The Yes / No is an MES-embedded option picker inside the NPC's inline0x1Fdialog segment: a0x29menu-open followed by anN*2-byte signed relative-jump table (handlerFUN_80038050, theFUN_80039B7Cdialog-SM family), withnew_pc = (open + 1 + index*2) + i16_LE(entry[index]). There is no read-and-compare opcode, which is why these records desync under linear disassembly. Portlegaia_mes::Picker::jump_target+InlineDialogue::last_choice(crates/engine-field/src/inline_dialogue.rs). - Engine:
- Carriers derive from the scene MAN (
man_field_scripts::derive_field_carriers+World::install_field_carriers_from_man) and tick throughtick_field_carriers. World::tick_field_interaction_probe(FUN_801cf9f4) runs theDAT_801f2254facing probe: a radius-64 compass point ahead of the player's facing, box-tested at +/-72 against the talkable NPCs' placement positions (World::npcs.positions). On the action button it talks to the match and turns the player toward it.- Talking arms the engage (
World::carriers.slots→World::carriers.pending_engage); accepting the prompt (the0x4Cn5 sub-4 dialog dismiss) engages it. World::nav_step_towardwalks a BFS route over the real collision grid.
- Carriers derive from the scene MAN (
- Tests:
training_battle.rs(formation index, carrier SM, dialogue-accept,training_reaches_battle_via_interaction_probe);v0_1_playthrough.rs::v0_1_battle_leg_reaches_battle_from_new_game(BootSession::begin_new_gameseeds Vahn at 180 HP - the new-game state is retail's pre-fight state) andv0_1_battle_leg_walk_talk_accept(walk → talk → accept, no teleport; it places the carrier atRIM_ELM_SPARRING_CARRIER_TUTORIAL_POSfirst, a ~6-tile reachable hop from the spawn).
The battle-intro enemy-name banner
Status: resolved - the question had a false premise.
No placement record raises it: the flow-0x0A composer FUN_801D9D3C (sole reference in the corpus: the jal at 0x801D0DFC) lays the labels out itself and hands each to the text-actor spawner FUN_8003541C with immediate geometry.
- One label per distinct monster group: id = group index
0..=3, class0, kind3, pen(laid-out x, 48), boxmeasured width x 12. - The only placement-table field the intro touches is record 67's
+0x14string-pointer cell (the back-attack / pre-emptive line), drawn under id4at pen(16, 12), 288 wide, as literals. Record 67 proper is opened only afterwards, by the post-intro sub-draw, under its+0x01id0x2B. The discw = 0width overwrite belongs to record 68 (spawned at the measured name width), not to the intro. - Capture (
scripts/pcsx-redux/autorun_battle_intro_banner.lua, breakpointing the spawner and the teardown sweepFUN_800355F0, walking the text-actor list atgp[+0x148]): an ambush raisesQueen Beeat(176, 48)55 wide,Killer Bee * 3at(78, 48)79 wide andAmbushed!at(16, 12)288 wide, all from$rainside the composer, held for 120 frames; an ordinary encounter raises two labels for 90 frames and no line. Nothing glides - the composer calls neitherFUN_801D8DE8nor the glideFUN_801DB7B0. - One monster suppresses the banner: monster-slot-0 id
0xB5(evolved Cort) skips the composer and parksctx[+0x06] = 0x0C, which the flow ladder at0x801D0C84has no arm for; the boss stage module moves it on (see The evolved-Cort flow park).
Owning page: battle.md (full law, naming rule and seat arithmetic).
What a port owes the slot-B module band
Status: resolved as a per-address verdict. Evidence: disassembly
Of the 65 catalogued addresses in PROT 0903..0966:
| Count | What |
|---|---|
| 58 | the module's 0x801F6734 move-VM spawn-stager entry |
| 6 | cast tick bodies reached from the module's own 0x801CF56C trampoline |
| 1 | a framed routine nothing references |
By content:
- 45 are pure spawn choreography - an arm switch whose arms only call
FUN_80021B04/FUN_80050ED4/FUN_801DFDF0/FUN_80024E80with a module-resident record and a scale literal- expressible as the spawn-record data layer.
- 13 carry game logic: two stagers apply damage (PROT 0927 through
FUN_801DD0AC(0x12, 7, seat), PROT 0966 throughFUN_801DD4B0(0x100, ..), each with the HP+0x14Cclamp); four more write actor orctxstate. - 7 are empty (
jr ra; nop).
Owning page: cast-module.md (per-address table).
readef groups 19..21 and monster record +0x1C
Status: resolved-negative - nothing names them. Evidence: disassembly + disc census
Monster record +0x1C is the readef animation-group index, and no shipped record selects groups 19..21.
- Reader: the initiative scheduler
FUN_801DABA4reads it record-direct (lbu v1,0x1c(v0)at0x801DB098/0x801DB0C8) and seeds the streaming applier's base slot with3 * group. - Census over the 186 populated records: the byte stays in
0..=25and never takes 1, 2, 5, 12, 19, 20, 21 or 23; group 0 is the default. - Second reader: the AI spell picker
FUN_801E9FD4(0x801EBB90) compares the first enemy seat's byte against0x17; no shipped record satisfies it. - Groups 19..21's duplicated actor records are therefore unreachable through the enemy path (slots 58 / 59 / 71 and 64 / 65 / 68 are byte-identical).
- Parser field
readef_groupinlegaia_actor_formats::monster_archive.
Owning page: summon-readef.md.
The dome panel-still arming
Status: resolved - a generic battle-end teardown, not a dome condition. Evidence: disassembly
ctx[+0xC] = 1is written at0x800474CCin the per-frame anim-node tickFUN_80047430, for an enemy node (node[+0x5A] >= 3) undergp[+0xA48] & 0x80andgp[+0x9F4] != 0xB5, together withnode[+0x10] |= 8.ctx[+0xC]is a three-value teardown machine:1frees the actor table and writes2;2ticksFUN_80025358, which stages PROT 0978 into the freed space - from three sites (0x8004E65Cescape,0x8004F82Cvictory tail,0x80056428inFUN_80056208).ctx[+0x7] == 0x67is only the successful-escape hold written by case0x66ofFUN_801E295C.- Open: what draws the still.
Owning page: minigame-muscle-dome.md.
The Miracle marker is an equipment byte
Status: resolved - there is no input recognizer. Evidence: disassembly (+ capture for the card corroboration)
ctx[+0x25F + slot]has exactly one store in the dump corpus:sb v1,0x25f(v0)at0x80054270inside the SCUS party battle-actor seeding routineFUN_80053CB8.- The value is the acting character's Ra-Seru equipment byte: record-relative
+0x199, or+0x198for roster character id2(beq v0,a3at0x800541E4; the weapon-index table_DAT_8007B42C=2, 3, 2names the other member of the pair). - The marker gates all four special art records at
0x801EF4C8, not only the Miracle. - Memory cards: the byte is small and per-character banded, zero for a member not yet bonded.
Two battle-context bytes read wrong
Status: resolved. Evidence: disassembly
ctx[+0x26]is the level-up banner's UI element id (0x65). Its reader0x801E61B4passes the byte asFUN_801D8DE8's element-id argument in a run of sibling unloads with literal ids; writers0x801E723C(the only assignment) and0x801E6D3C(an increment); cleared at0x801E2CFC. It drives the Done band's0x50seed override (0x96for0x3C) and the0x51banner skip (59 unskippable frames, then a press).ctx[+0xD]is the per-action battle-camera angle variant0..=3: a four-way switch at0x801D6510/0x801D6698/0x801D689CinsideFUN_801D5854, seededrand() % 4at ActionSeed and narrowed per category from jump table0x801CF144.
Stone and Curse in the SCUS effect applier
Status: resolved. Evidence: disassembly
FUN_800402F4's first-level jump table at0x80014FA0(132 entries, guardsltiu 0x84) sends class9to the Stone arm and class10to Curse.- Stone makes three stores after the accuracy roll:
+0x16E |= 4; a refund of the target's reserved item throughFUN_800421D4, gated on+0x1DE == 1 && +0x16C != 0(+0x16Cis the initiative key, not a cooldown); then+0x1DE = 0. Curse sets its bit only. - No item-effect record (
0x800752C0, 130 records) carries class9or10, so both arms are reachable only from the streamed capture-class modules.
Attack x2, the swing class and the apply-mode arms
Status: resolved. Evidence: disassembly + measurement
ctx[+0x16]has two writers, both in the strike loop and both keyed on the War God Icon (record+0xF4 & 0x2000):- the end-of-stream refill at
0x801E3A20..0x801E3A64runs only while the counter is zero (bnezat0x801E3A18), lifts it to1, rewinds the strike cursor and rewrites every marked queue slot to0x19; - the stage site's tail bumps it at
0x801E37AC..0x801E37BC, only when it is already non-zero (beqzat0x801E37B4), so the second pass's first stage lifts it to2- the value that ends the damage kernel's carry arm (battle-action.md).
- the end-of-stream refill at
- Monster record
+0x1Eis the limb-vs-height swing class, read through0x801C9348byFUN_801EED1C(class2gets one low swing) and byFUN_801EC3E4twice: its head miss gate (0x801EC488..0x801EC554: a party hit of the wrong class does nothing but raisectx[+0x263]) and its apply-mode look-ahead (class2connects only with power bytes0x01..=0x10, class3only with0x11..=0x15). - Across the archive's 186 records the class reads
0x127,1x1,2x52,3x6, so both arms are ordinary play. Both kernel copies gate everything on a monster target.
How a cast reaches its slot-B module
Status: resolved. Evidence: disassembly
Both PROT 0898 tick dispatchers key the same 64-entry band.
| Dispatcher | Key | Table → PROT |
|---|---|---|
FUN_801F1ED4 | queued action id | 0x801CF4EC row id - 0x81 = PROT 903 + row |
FUN_801F2160 | spell record's +0x01 byte | 0x801CF56C row sub = PROT 935 + sub |
FUN_801F2160has a single call site,0x801E50C8, in battle phase0x70's hold.- Data half: the spawn records the modules pass
FUN_80050ED4/FUN_80021B04are recovered with the sharedsummon_overlayreader into an engine pool (legaia_overlay_images::cast_effect_pool), staged at both retail seams. - Code half: module tick bodies are ported in
engine-vm(cast_module_ticks,cast_arm_ticks,seru_magic_ticks,cast_module_camera); which modules' bodies and camera arms remain is listed per module oncast-module.md.
What an art body costs in AP
Status: resolved - computed, not disc data. Evidence: disassembly
FUN_801EED1C derives it:
- multiplier
11/10/6by the builder's visit ordinal (0,1..3,>= 4) - not the arts-grid display index; - halved before the multiply under the actor's
0x800flag (srlat0x801EF378); - times the art's command count;
- accrued into
actor[+0x224]and spent from Spiritactor[+0x170]at0x801E5D74.
Port: engine-battle::ap_gauge::art_spirit_cost.
Tutorial prompt machine exclusivity
Status: resolved - the machine is byte-exclusive to 0967; the prompt pool is shared with 0968. Evidence: disassembly
The sparring-tutorial prompts are resident in stage overlay 967, so the battle SM alone never emits them.
- Machine:
FUN_801F6B70is entry 967 file+0x198(0x801F69D8 + 0x198reproduces the VA), a 91-entry jump-table hook onctx[+0x06]with nine live slots, each switching onctx[+0x28A]. - Exclusivity: five needles from 48 to 2316 bytes - one a 116-byte window free of
lui/j/jal, which a relinked copy could not evade - return one physical copy across every PROT entry, SCUS and DMY.DAT (967 is stored raw). - Pool: entry 0968 carries a byte-identical 852-byte prefix of the prompt pool at the same offset
0xCAC, inside a0x5D8-byte run the two entries share; 0968 has its own 7-entry dispatcher and no copy of the machine. - Port:
engine-battle::battle_tutorial.
The _DAT_8007BA78 census is closed
Status: resolved. Evidence: disassembly
A sweep in every reference form over SCUS, all 1233 PROT entries and the overlay images finds every reference to _DAT_8007BA78:
- two stores:
0x801E30F4= PROT 0897+0x148DC(the4C E2op) and0x801DDCE8= PROT 0899+0xF4D0(the title tick); - four loads, all in PROT 0970;
- one literal-word reference at PROT 0971
+0x1238=0x801CFA50, the debug menu's editable-globals pointer table - the static witness for dev-menu editing in the corpus states; - zero
gp-relative0x760(gp)references (the form an absolute-only scan cannot see).
Limits: a raw scan cannot see code inside an LZS section. A "PROT 0896 carries the same span shifted by 0x9000" reading is an entry-size over-read artifact.
Owning page: cutscene.md.
The 0xF8 halt-acquire handshake
Status: resolved - retail machine traced; the port's model diverges. Evidence: disassembly
0xF8 resolves to the player object _DAT_8007C364 (FUN_8003C83C: li v0,0xf8 / lw v0,-0x3c9c(v0); inlined again at 0x800377A0 and 0x80037E04).
- ExecMove arms nothing:
0x801DE998writes only+0x5C/+0x5E/+0x56and callsFUN_800204F8. - The halt-acquire creates the wait object:
0x801DF384sets+0x94and ORs0x400into+0x10(plus the caller's when the target is the player, at0x801DF404);0x801DF5AC jal 0x801d25ecspawns the glide actor and the release helper0x801D5D60, which pollsandi v0,v0,8at0x801D5DB4and clears the halt at0x801D5DD4/0x801D5DFC. - The record parks at the next cross-context op, in the prologue busy gate
0x801DE90C..0x801DE944, whose unadvanced PC the run loop reads as "stop" (0x8003CFF0). The acquire itself advances 9 bytes (11 for sub-A/B), 0 on failure. - No backward resume PC: the operand
+3/+5halfwords areFUN_801D25ECtween arguments.
Owning page: cutscene.md.
Growing a scene TMD pack member
Status: resolved - rebuild is the only cost. Evidence: disassembly (the walk) + capture (the disc-wide offset census)
Every external reference to a pack member is an index, so a member can grow if the pack is rebuilt.
- The mesh pool is the descriptor walk's registration order (
FUN_8001F05C→FUN_80026B4C(buf + offsets[i] * 4):lw a0,0x4(s1)/sll a0,a0,0x2/jal 0x80026B4Cwithaddu a0,s2,a0in its delay slot,0x8001F2AC..0x8001F2BC, looping to the pack's count word at0x8001F2D4); placements name a pool slot; scene ANM records name a record number; the bundle's descriptors hold offsets into the bundle entry, not the separately streamed pack. - Disc-wide, each of PROT 0639's members
106 / 107 / 108word offsets occurs exactly once (in the pack's own table), the byte-offset form zero times, and the declared length347476zero times outside its chunk header.
Owning page: man-relocation.md.
What of a signature cast is data
Status: resolved (partially). Evidence: disassembly
A signature cast's spawn records are data; its lift is code.
- At all 15 / 41 / 24
jal FUN_80050ED4sites in PROT 0958 / 0959 / 0960 the record pointera2is a module-resident constant anda3a scale literal; the records are the summon part-record shape the art path's0x801F6324prototypes use. - Moving them into the art path is blocked by residency and capacity: the prototype table's 61 entries are all populated, the module blocks sit above the slot-B base while the prototypes sit below it, and the battle overlay has 247 bytes of zero slack.
- The lift is
sbinto+0x1DA(16 / 6 / 8 sites) from the modules' phase arms, which no record can express.
Owning page: cast-module.md.
Op 0x23 MOVE_TO keys on ctx identity
Status: resolved. Evidence: disassembly
The player arm is chosen by ctx pointer identity, not by the player-class bit.
0x801DEC7C bne s5,v0compares the executing ctx against_DAT_8007C364(0x8007C348 + 0x1C); only that identity reaches the camera re-centrefunc_0x80017EC8(0x801DEC84..0x801DECA8).- Every other ctx takes
0x801DECAC's facing + movement-init arm on its own actor. - A spawned partition-2 record inherits the
0x1000000player-class bit, so keying on the bit teleports the player to wherever the record seats its own actor (keikoku's arrival record, once op0x45does not restart it).
Dome ringside panel stills PROT 1221 and 1222
Status: resolved. Evidence: disassembly
Two 320x256 BGR555 stills in the dome's other6 bundle; the pair is picked by a computed index, so no literal names them.
- Streamed in four
0xA000strips byFUN_801F6B24(PROT 0978) into VRAM(384, 0)(rect0x801F735C);320*256*2is the entry size exactly, the first five sectors a 16-line top pad. - Selection:
addiu a0,s0,0x4c7at0x801F6C3Cwiths0 = (party slot 0 current HP < max / 2), soint2.timis the below-half-HP variant. A literal sweep cannot see a computed index. - Corroboration: the overlay's dev path strings
h:\prot\field\other6\tim\int.tim/int2.tim, selected by the sames0.
Owning page: minigame-muscle-dome.md.
The display object's sound-bank category
Status: resolved. Evidence: disassembly
actor[+0x22C] points at the actor's spawned display object, whose +0x80 is the sound-bank category.
- The pointer comes from
FUN_80024C88→FUN_80020DE0and is installed at0x800515E8/0x8005196CinFUN_800513F0. +0x80is zeroed at spawn (0x80020F50) and set at battle setup to7(party arm,0x80051548) or7/8(the four enemy seats, loops at0x80052238..and0x800522D0..), each loop paired withjal 0x8003E104carrying the same literal ina1- the bank-load slot.- The router copies it into the descriptor's category column (
0x8004FFD8..0x8004FFE4,0x80050070..0x8005007C). - The monster element byte is record
+0x1D, a different field.
The evolved-Cort flow park
Status: resolved. Evidence: capture + disassembly
The boss stage module, not the flow ladder, moves flow 0x0C on.
- The
0x0Aarm writes0x0Bunconditionally and overwrites it with0x0Cfor formation0xB5(0x801D0DE0..0x801D0E14); the ladder idles on0x0C. - PROT 0968 (loader-B tracker
0x49) runs a 7-phase intro cinematic offctx[+0x289](jump table at0x801F69D8) and writes0x0Bback at0x801F713C(ra = 0x800564A0) when a dt countdown on its local word0x801F73F8expires - 3207 vsyncs later in the capture, with0x0B → 0x14 → 0x1Efollowing at once. - A ten-button sweep produces zero writes: no input shortens it.
- Probe
autorun_w4d_cort_flow_writer.lua.
Owning page: battle.md.
A spell its caster has not learned rolls 9999
disassembly + capture. An injected Zenoir or Gola Gola reads 9998 / 9999 in retail where the engine once read a few hundred. The summon roll looks the spell up on the caster's list (FUN_801DD864), a spell off the list reads list slot 0x20, and that slot's level is 0: the power's srl by level - 1 wraps, and the roll caps. It is an artefact of the injection, and no cast in play gets there.
Owning page: battle-formulas.md.
A scatter's density and Mushura's late rampage hit
Grade: capture and disassembly. An arm's per-pass spawn call seated once per one-vsync module tick, and a pass runs a frame step of those ticks (World::module_pass), so a cast held a step times the records: PROT 0915's 0x801F8108 trail stood at sixteen live parts against retail's five in one RAM read, and PROT 0957's sparks at twice retail's at step 2. A call now runs on the pass's first tick (CastFxState::module_pass_repeat). Mushura's first rampage hit lands on the frame retail's does since arm 9's pass stores the creature's heading on its target (jal 0x80019B28, sh v0,0x46(s4) at 0x801F7578..0x801F7594): the port had left arm 8's spin in +0x46, and arm 0xC's chase set off along it, sound cue score .500 to 1.000.
The case 0xA yaw clock
Grade: disassembly and capture. The word 0x801DC8B8 reads is the anim node's +0x68 cursor, signed, in sixteenths of a keyframe, and each state in the magic band that runs the case holds an endpoint of ring[1] * 2 + 0x700 - facing, slot 1 of the cursor ring being the pass's value ahead of its anim tick. The engine handed the case, and FUN_801D71B8's arms, the cursor rounded down to a whole keyframe, which aimed Rugi's Poisonous Breath 17 units short and Devil Pump's Multi Cutter 24. A moving endpoint also takes the displayed frame's walk back: the capture channel's clip_clock_end_shift gives the walk its 24 a pass, and Poisonous Breath's 3350 under 3368 walks to the 3320 an image scan of the frame finds independently.
Owning page: battle-stage-camera.md.
Noa's stand in Scythe Wind
Grade: capture. Noa stood 60 px off retail's in battle_jou_nova_v3_scythe_wind_mid_cast because the frame was on the replay's camera. PROT 0951's arms 3..=5 re-aim every pass, so the child's alignment agreed on no component and dropped the pose it was given, which is also why a yaw scan of that frame moved nothing. Standing on the walked-back pose reads .691 to .942, and the two other Scythe Wind states rise with it.
Owning page: retail-compare.md.
Why a monster's strike gate read a short age
Evidence: disassembly for the laws, capture for the states. Three separate faults read as one camera of about .87.
- The replay rolled a shorter stream. The strike loop stages a byte and tests the terminator at the new cursor on the same pass (
0x801E3998..0x801E39AC), so a one-byte queue leaves for0x1Fon its first pass.battle_cave01_mushroom_strikeholds06 06 00, cursor1, the swing staged over the arrival's idle (+0x1DCbit 1). The replay drew05 00, left0x1Eat age0, andage_shortre-ran it on the state's first tick, ahead of the first case-7 arm. Such a capture now takes the stream fallback. - A walk-clip capture's accumulator does not date the arrival. It counts from the walk's last loop re-commit.
battle_nilboa_warfish_strikereads288at gap1490, increment150, one pass under case 7. The increments fall on a ladder from0x700(ceil(gap / 0xC), thengap -= inc * step), so the increment names the pass (strike_passes_off_table). - The monster's pull-in never opened.
FUN_801E93C8keepsctx[+0x243]while the seat's clip has+0x87set (0x801E9458..0x801E94A4); the live host had no such read, the gate stood open and the strike loop's tail cleared the latch the commit had stored. Case 7 then tested a stand-in for it, and its glide chased the unpulled depth.
Owning pages: retail-compare.md, battle-stage-camera.md.
How long 0x6E and 0x6F hold, and what the hold moves
Grade capture for the lengths, disassembly for what runs through them. The two states poll CD reads (jal 0x8003DE7C at 0x801E4F08, FUN_8003F2B8(1) in 0x6F), and over the harvest-state casts they hold 14..=17 and 4..=6 vsyncs whichever module or fight. The polls are not idle, so a load the engine answers at once still costs retail's look: 0x6E walks the framing each pass (0x801E4F00) and 0x6F pulls ctx[+0x6D0] in. Held in the engine, the walk matches retail to the unit through 0x6E, and the shots built on ctx[+0x6D0] land where retail's do.
What stands the camera on the Windcutter frames
Grade capture, with the walk disassembly. Both Windcutter frames show the case-6 walk ahead of a one-frame shot, and neither is a mid-retake of something the seed cannot reproduce. The body filling battle_doman_boogie_pump_windcutter_mid_cast was the walk's start, not a depth or a ghost: seats 1 and 2 had both spent a Spirit, and the selector FUN_801DABA4 leaves no order behind, so the law's default took seat 2.
The walk's focus closes on the caster from that seat's framing, and the residue left after ten step-3 passes stands the eye 94 units nearer Gala on the shot's axis. She then clears the near gate 0xA1 (0x8001AEE8) and fills the frame. From seat 1 she falls under the gate and the frame matches, with the walk's TR z untouched; the earlier continuous search that wanted TR z 4569 was compensating for this. FUN_8004DC68 keeping her solid is correct and plays no part.
battle_rugi_barra_windcutter_mid_cast is the same walk from the round's first action, so it starts on the prompt's far framing. That framing's orbit is a clock the RAM does not date, and the yaw is read off the frame. Which of two Spirit seats went last is also not read from the RAM: the actor records of the seats differ in their idle clip's phase, and no rule over that has been tested. Each capture carries a walk pin, which moves no game state.
Owning pages: retail-compare.md, battle-action.md.
Why Death Game's replayed deal differed from retail's
Grade capture, with the deal disassembly. The draws were never the difference; the stream's position was. A breakpoint on rand() (0x80056798) over the first vsync of battle_jou_puera_v3_death_game_mid_cast logs eight calls: the battle frame driver's discarded draw ($ra 0x80046D34), the deal's six (0x801F7BEC), and one after (0x8004E2C4). Six draws from the state's own seed word 0x80009010, one past the driver's, deal retail's [1,1,1,2]; from the word itself they deal [2,1,1,1].
The engine runs the driver's draw ahead of its module tick too, so a replay with its stream set to the state's word at action state 0x70, module phase 0 (LEGAIA_CAST_RAND_SEED=112:0:<word>) deals [1,1,1,2] and puts the four cards' labels on retail's pixels. A replay seeded only at the encounter entry deals off another position.
Owning pages: cast-module.md, cast-sequence-oracle.md.
A battle target surface's replay
Grade disassembly for the states, capture for the frames. The command-flow dispatch tree tests eight sub-cursor values the selection band does not hold (0x801D0D30..0x801D0DA4): 0x5B..=0x5E and 0x64..=0x67, one per window and target shape, each with its own handler, confirm and cancel. BattleFlowState now decodes all eight, the World stands the flow byte on the one the open window's session is in, and the magic window's all-target spells wait for their confirm as 0x5E / 0x67 do instead of casting on the list's press. The menu drive walks the window's cursor onto the row the seat's +0x1DF names before it confirms. battle_magic_target_gaza2_retail (0x5C) and battle_item_allparty_gaza2_retail (0x66) are catalogued and reached on phase 1.000. On the second, the All Allies plate stands on record 0x3E's seat, which FUN_801D57E8 copies onto the target plaque (image .961 to .996); on the first, the breadcrumb's third tab reads Magic, spell 0's name, for a Seru spell (.990 to .991).
Owning page: battle-command-flow.md.
A walked-back pose about twenty units of yaw off
Grade capture. battle_map01_gomboo_steal_mid_cast (action state 0x6F) holds yaw 3757 on increment 21 toward 3940, and its frame peaks two passes back at 3610, where a walk back on a fixed endpoint lands at 3631. A PCSX-Redux film from the state (autorun_cast_sequence.lua, LEGAIA_PHYSICAL=1, recompiler) shows the next pass re-arm case 6 before it steps, on an endpoint four units on (3944, increment ceil(187 / 12) = 16, the yaw 3805), and the endpoint stays the yaw counter ctx[+0x6DA] plus 1864 on every pass, the counter climbing 4 a pass at step 3. So the pass before the RAM's chased 3936 and stood at 3694, and the pass before that at 3610 (increment ceil(326 / 12) = 28). The capture channel's walk back already lands there (image 1.000 in the baseline), so the gap was the older fixed-endpoint walk.
The focus follows the same law with an endpoint the bodies move. A film of battle_ropeway_party_gala_flying_knee_mid from the strike loop to the fade-down holds focus = previous + step * ceil((endpoint - previous) / 12) on 42 of 48 pass edges through 0x1E, 0x1F and the return 0x20 (the rest land within a unit of the endpoint, or split a pass across two rows). Walked back two passes on each pass's own endpoint, the focus lands within 8 units of the frame's; on the RAM's endpoint alone, up to 36 units off during the return.
Owning page: retail-compare.md.
Which frame the effect-script walk reads as a loop ends
Status: resolved - the advanced cursor, on the walk ahead of the commit. Evidence: disassembly + capture
FUN_80047430 hands (+0x68 >> 4) & 0xFF to FUN_801DEA50 at 0x800478B8, ahead of both the +0x1DC cut (0x800478F4) and the natural-end test (0x80047A48). A PCSX-Redux run of cort_evolved_card_first_menu, breaking on the walker's floor-part seat (0x801DEF08), shows the evolved Cort's first approach stomp its walk entry's gates 1, 4, 7, 10 and then gate 13 on frame 12 of the 12-keyframe clip (cursor 200), on the pass the arrival's staged idle cuts the walk. battle_jou_cort_181_strike holds the same walk: three rings 12 vsyncs apart, the newest on the ground Cort arrived on, spaced as the body pair stands at keyframes 6, 9 and 12.
On that pass the cut commits the staged idle, so the walk is not re-committed and the new cycle's gate 1 does not fire beside gate 13. A walk that runs past its clip's end without arriving re-commits the loop and walks frame 0 on the same pass (0x80047B54, then the post-commit walk at 0x80047C08); no recording holds that pass.
Owning page: battle-action.md.
Field / locomotion
# Settled threads: Field / locomotion
| Thread | Answer | Grade |
|---|---|---|
| What starts the hunters' return in Rim Elm, and why does stepping out after the measuring not? | Tetsu's talk after the sparWhy
| disassembly + capture |
| Does a Tetsu spar fought before the measuring stop the hunters' return in retail? | yes, for goodWhyThe spar's return raises | disassembly + capture |
| Does Mei's door close behind her in the Genesis Tree beat? | yes, on her clip cursorWhy
| disassembly |
What draws on deene's left rock face? | a stretched ground cellWhyThe ground cell's own tile: record | disassembly + capture |
Which rule drops rikuroa_field_card_walk's near wall quad, whose near cut read as w units? | the capturing emulator's polygon limitWhyPCSX-Redux rejects a whole quad on any corner pair past the limit ( | capture + emulator source |
What draws the light wedge in rikuroa_field_card_walk's bottom-left, which the port left black? | a floor quad only the capturing emulator drawsWhyThe state's ordering table holds a | capture + emulator source |
What sets a field primitive's place in the frame, and so the sky over concend's ridge and Octam's trees? | its ordering-table linkWhyEvery primitive links at its corners' mean | disassembly + capture |
Does a later keikoku variant MAN replace P2[7]'s pushback? | no - the inner doorway is a story wall on every visitWhyThe Ravine ships one MAN carrier, its bundle MAN (no streaming variant). | capture (disc census) |
| Does retail run a field script context once per vsync or once per game frame? | once per game frameWhyEvery | capture + disassembly |
| Does retail step helper contexts while a timeline sits on an open text box? | yes; only a context on its own text waitsWhy
| disassembly |
| Do retail's ending vignettes accept the pad while their entry record runs? | noWhyAll 5 end-credits field-run states hold the player's engaged bit ( | capture + disassembly |
Which scene installs the code lock FUN_801EED58? | doman onlyWhyField-VM | disassembly + capture |
What is FUN_801E6F70? | the coin counter's entry panel, field overlayWhyThe painter word of panel-window record 10 (PROT 0897, geometry | disassembly |
| How is the fishing venue's menu reached? | from the idle shore, on Triangle / SelectWhyState | disassembly |
| Do concurrent script contexts lock the pad, or only the cutscene timeline? | every stepped record locks itWhy
| disassembly |
Why does the menu button open nothing after a Door of Light arrival on map01? | a walk-on script holds the player; boundedWhyThe Door seats the player on map01's cave-mouth walk-on trigger | capture + disassembly |
Who sets a field NPC's moving-class bit +0x10 & 0x20000? | the placement seater, and op 31 11; nothing clears itWhy
| disassembly + capture |
What does FUN_801D79E8 do? | the per-actor visibility cullWhyUnless | disassembly + capture |
| What arms the place-name banner? | system flag 2, a one-shotWhyThe kingdom MANs' entrance records (and two in | disassembly + capture |
| When does the system channel's clip-base store reach the settle? | on every tick the player is not movement-lockedWhy
| capture + disassembly |
| Where does a Door of Light / Door of Wind use go, tick by tick? | the pause-menu session hands off to a travel artWhyThe session | capture + disassembly |
What does _DAT_80084540 hold? | the loaded scene's raw CDNAME defineWhyTwo above the extraction-frame | capture + disassembly |
| Why can't the pause menu open over a shop? | the shop's overlay displaces the controllerWhyThe menu accept lives in the field overlay's pad controller | disassembly + capture |
| Which clip does the player hold through a kind-0 warp? | idleWhyThe system channel | capture + disassembly |
How long does the warp timer's -1000 sentinel live? | the rest of the landing tickWhy
| capture + disassembly |
| Where does an inn conversation end? | at the last page's closeWhyThe sub- | capture + disassembly |
What does the region battle-setup half of FUN_801D9E1C store? | a backdrop variant, two Door gates, an object-keep bit, a world-map return pointWhyOn every region hit ( | disassembly |
Why does kor5's 0x619 read set on entry and clear after the chain? | a spawn-section writeWhy
| disassembly + capture |
| Where does a talk begin in an NPC record? | the interaction cursor +0x9E, never script_pc0WhyThe interaction dispatch runs | disassembly |
Who writes the field clip base _DAT_8007BDD8, and which clip is run? | the pad step; run is bank slot 2Why
| disassembly |
| What does a kind-0 walk-on tile do? | a timed warp, not an instant oneWhy
| disassembly + capture |
What does op 0x43's acquire do when the target is mid-arc? | waits and retriesWhyThe acquire ( | disassembly |
Which FUN_801CF8AC arm do the ambient walkers take? | the class arm, all of themWhy
| disassembly |
What does op 0x3E do with op0 < 100? | the scripted-battle install, one body with op0 = 0xFFWhy
| disassembly |
| How many sites re-roll the encounter step counter? | five, into one wordWhy
| disassembly |
Does P2[5] write kor5's 0x436 organically? | yesWhyFrom | capture (synthetic: two trigger-tile pokes, Gaza's HP held at 1) |
Where does the op-0x43 arc's chained record point, and what does op 0x34 sub-1 spawn? | a release watcher; an attached billboardWhy
| disassembly |
What is FUN_801E58A8? | an actor anim-clip pickWhyIt writes | disassembly |
When does 4C 86 install its reflection controller? | at scene entry, from the record's spawn prologueWhyAll ten shipped sites sit after the record's leading | disassembly + capture |
Is juui1 dark in retail outside its tint beats? | noWhyWith | capture (synthetic gate) |
| Which flags do the retock / doman / nilboa entries write? | per-scene entry familiesWhyEach from a pre-entry card-boot state, every write through the field VM's SET ( | capture |
| Why did a walk-on tile poke under the movement lock never fire? | a crossing made while locked is consumedWhy
| disassembly + capture |
Which records write the kor5 tail, doman and son flags? | with kor5's 0x436 input pokedWhy
| capture |
What are motion-VM ops 0x37 / 0x41? | an eight-direction compass walkWhyBoth step along the compass table at | disassembly |
What do field-VM 4C E4 and 4C DB do? | a box test with a relative skip; the CLUT blend fadeWhy
| disassembly |
| What colour does retail clear the field to? | blackWhyUncovered pixels read | capture |
| Does a scripted camera tile window survive into the next scene? | yes - by 78 vsyncsWhyA per-vsync poll across a real | capture |
What is field-VM 4C 14? | the actor clone, and it is eight bytesWhyThe only eight-byte instruction in outer nibble 1: the nibble's prologue advances seven and the | disassembly |
What do 4C 86 and 4C 87 do? | the reflection controller's install and teardown; neither parksWhy
| disassembly |
| Which model owns the field screen-effect fade? | the pushWhy
| capture + disassembly |
Why does a juui1 name hijack draw black? | the tint belongs to the departing scriptWhyOf 498 clean op- | capture (disc census) |
| Do shipped scene scripts carry developer flag-setting menus? | yes - nine bundle MANs name theirs; the total is largerWhy
| capture (disc census) |
| What gates the fishing catch HUD's depth and tension block? | one word, DAT_801d91b4WhyThe catch HUD | disassembly |
| Where does a field actor's heading live? | it is the middle of a rotation triple at +0x24Why
| disassembly |
What is _DAT_8007B854? | the ambient-particle master gate, not an input lockWhyField-VM op | disassembly |
| teien's hedge-base ground fill - does retail draw a kind-2 cell? | no - retail has no 0x0800-cell draw channelWhyA live | capture |
Actor +0x16 - heading, facing, or footing? | Y of the position tripleWhy
| disassembly |
| Which index does the fishing bring-up rewrite? | the rod index, not the lureWhy
| disassembly |
| What writes a scene's kind-2 (height-override) trigger cells at runtime? | exactly one writerWhyField-VM op | disassembly |
| The ending-scene widget family - how many sites, in how many scenes? | 311 sites, ten scenesWhyThe op- | disassembly + capture |
FUN_801D6058 - a cutscene element? | a field-overlay template, spawned onceWhyIt is the | disassembly |
| Rim Elm's south gate - why a seated player exits and a walking one does not | resolvedWhyThe exit record is ungated, the second walk-on band is five inert bytes, and the wall is a collision row the gate object's own script paints; the open paint is nibble-7 sub 0 ( | disassembly |
| Town/field free-movement locomotion | resolvedWhy | capture |
| Field ambient animation - what makes jou's ground pulse and the water shimmer | resolvedWhyThree mechanisms: the bundle type-6 CLUT-walk table (12 carriers, 9 of them field scenes), the ambient move-VM tree the MAN P1 placements install at entry, and jou's flesh pulse = the mode-3 CLUT-cell HSV cycler ( | disassembly |
Ambient render-mode 4 - what the op-0x1E seat animates | resolvedWhyA cyclic VRAM-rect scroller, and it is what makes waterfalls fall. Per fired period ( | disassembly |
| Master ambient record 0 - what reads the 8-byte rows | it is not a stager record at allWhyThe per-scene sound-effect descriptor bank for cue ids | disassembly |
| town0e's morph-record installer | partition-1 placement 29WhyThe install is the ordinary | disassembly |
Which op-0x34 sub-3 installs fire at scene entry | resolvedWhyThe ones the placement spawn-prologue slice ( | disassembly |
| Scene bundle type-7 slot content (VDF) | resolvedWhyThe scene's vertex-morph delta pack (61 bundles populated; jou = 17 sub-entries), installed at | disassembly |
| VDF morph render substitution - what draws the staged vertices, and what arms the lanes | resolvedWhyPer drawn group of a part whose flags carry op- | disassembly |
| What opens an inn stay in retail? | nothing - there is no inn sessionWhyRetail composes a stay inline in the scene MAN out of generic ops (dialogue, an MES picker, an op- | capture |
| Field collision-map source | the .MAP supplies the base gridWhy | disassembly |
| Tile-board grid mode | resolvedWhyThe | disassembly |
| game_mode 0x03 = field/town gameplay | resolvedWhy | capture |
| Scene prescript: field-VM event scripts vs move-VM stagers (dual consumer) | resolvedWhySingle consumer. The op- | capture |
| Engine VRAM byte-exactness for town01 | (major source); minor residueWhy | capture |
CLUT row 510 population (env meshes' (64,510) CBA) | (boot-resident system-UI strip band); residue = the exact boot walker call siteWhy | capture |
Scene-transition (0x3F door) destination indexing | resolvedWhy | capture |
| Intra-town (house / interior) door mechanism | resolvedWhy | disassembly |
| Field/town environment-geometry placement | rendersWhy | capture |
| Overworld / town entrance story-flag gating | resolvedWhyAn entrance's unlock is its own partition-2 record's C1/C2 gate ( | capture |
Overworld story-conditional destination (dolk→dolk2) | mechanism + engine portWhyBeyond the record-level C1/C2 gate, an entrance record can switch its | capture |
| Retail-vs-engine NPC + story-flag state parity across the capture library | breadth oracleWhyThe sweep oracle | capture |
Entry pre-run channel slice ends on a no-mask 4C 70 wall paint | the slice continuesWhyAll four nibble-7 paints continue. | disassembly |
Writer of the Rim Elm opening flag (549) | a self-latching script SETWhytown01 | capture |
Field .MAP PROT resolution - which entry holds a scene's map | census-pinnedWhy | capture |
| World-map CLUT cycling beyond the ocean head | closed (operand table + emitter + cadence pinned)Why | capture |
init_data UI-tile page residency; the map03 terrain column | both premises falsifiedWhy | capture |
| What transitions retail into game over? | resolvedWhyRetail has no mode- | capture + disassembly |
| Mid-visit NPC re-arrangement beats (dolk2 market crowd; garmel pre-Zeto staging) | resolvedWhydolk2: the swap is | disassembly + capture |
| Region story-flag gate families (record-header C1/C2 gates) | structure, play order by milestone brackets, the two undecided orders unobservableWhy | capture + disassembly |
Extraction-0874 §2 (player.lzs) F-variant pixels | installing event namedWhy | capture + disassembly |
| Which chapter-1 scenes the engine can load, script, walk and leave | as a per-scene verdict; four of the five late "one-way" rooms leave in-engineWhy | capture |
How a player leaves the Uru Mais chain (uru, urudre1..3) and jouine | all five have walk-on exits carried by the scene's .PCH trigger sidecarWhy | disassembly + capture |
Why did the port drop roughly half the disc's 0x3F destinations? | the clean-label gate was lower-case-only; retail never compares the operand against a name tableWhy | disassembly |
Why do bubu1 and edbubu resolve no MAN? | both ship a count = 5 asset table; the detector bounded count to 6 or 7Why | disassembly + capture |
| Why did 28 scenes of the frontier closure become unwalkable at once? | a scene change mid-ledge-hop leaked the hop's one-way steering lock - an engine bug, fixedWhy | disassembly + capture |
What consumes the scratchpad window 0x1F8003E8..EB? | the renderer's visible tile window; the 0x801F2778..84 mirrors are write-onlyWhyFour signed bytes | disassembly |
What is the field run-button mask 0x800846DC? | 0x48 = Cross | R1, seeded once by the new-game data init; not configurableWhyThe last of four button-mask words at | disassembly |
| Who latches the clip-end bit for a conversation's cross-context clip pokes | port residual namedWhyThe poked actor's own anim tick, on the poked actor's own | disassembly + capture |
What is the scene_asset_table header's +0x04 word? | sum of the descriptors' decompressed sizes; never readWhyEquals | disassembly |
How does a scene bundle reach _DAT_8007B85C? | whole-sector block copyWhy
| disassembly |
| What consumes the "field-pack" entries? | the scene texture pack at block +4Why
| disassembly |
What do the asset-table Flag(0x0A / 0x0F / 0x14) descriptors do? | they are the streamed-file loader's mode argumentWhy
| disassembly |
Which screen opens menu window 46 (FUN_801D603C)? | the casino prize counter's Yes / No confirmWhyScript | disassembly |
Which prescript copy does FUN_800252EC install from - the sister entry or the .PCH +0x800 copy? | the next PROT entry, through the efect.dat window; there is no splitWhy
| disassembly |
What does the 0x801F2858 template's tick do? | the scene shutter blackout, FUN_801DD784 0x801DD784..0x801DD9B4WhyTwo full-width bars ease in from the top and bottom and meet, holding the screen black - a scene-change shutter, not a cinematic letterbox (falsified). Its one spawner is | disassembly |
What is field-VM op 0x4C nibble 9? | the scene floor-height ladderWhySub- | disassembly |
What is 0x801D44CC in the dance overlay? | the step-marker mesh flipbookWhyIt selects the marker actor's mesh row from its | disassembly |
Who calls the move-VM extension dispatcher FUN_801D362C, and is the port live? | one caller disc-wide, and the port is reachedWhyThe only reference of any form is SCUS | disassembly |
| How does the shop's buy list order and ink its rows? | a three-row Platinum Card hoist, with a last-rule-wins dimWhyCase | disassembly + capture |
| Where do the tile-board's cells live? | heap, one byte per cell, allocated at installWhy
| disassembly |
| What are the menu's HP / MP ink thresholds? | FUN_800349EC / FUN_80035EA8, with a fixed ailment arm orderWhyThe two routines carry the tier tests, and the ailment arms are evaluated in a fixed order, so a readout's colour is decided by the first arm that matches, not by a priority table. Rows in | disassembly |
Who clears the -1 menu entry-context park? | the submode dispatcher's retire arm writes Done; no menu teardown clears itWhyWhen the pause-menu session clears | capture + disassembly |
| Does a save point open the pause menu by itself? | yes - op 0x49's -1 rows are a scripted menu-button pressWhyThe Idle arm spawns the menu button's subsystem actor ( | capture + disassembly |
| What keys the menu entry-context byte? | the record kind, not the screen's positionWhy
| disassembly |
What arms _DAT_8007B8B8? | a one-shot entry-mode argument, not a latchWhyTen writers and 25 readers over 84 images. One writer is | disassembly |
Which way round is actor +0x8A bit 0? | it suppresses the scripted motion VMWhy
| disassembly |
What are motion ops 0x06, 0x0C and 0x0E? | a home-relative wander, a tint/draw-mode fade, and a two-base model bindWhy
| disassembly |
How do motion ops 0x10 / 0x11 / 0x12 address their target? | one selector over five halfwordsWhyThe candidates are | disassembly |
| How does the field follow camera get its pose? | record -> parameter block -> compose -> ease or snapWhy
| disassembly + capture |
| Who runs the camera zone query in retail? | the field VM, not the arrival actorWhy
| disassembly |
| What draws field fog, and what raises it? | a pool of textured sheets, gated by one script wordWhy
| disassembly + capture |
What composes the field camera's TR? | the live eye trio is the eye-space translation; there is no eye-back depth constantWhy
| capture + disassembly |
| Retail's field camera focus Y | 0, on every sampled frameWhyOnly X and Z of the focus trio are ever written in the field - | capture |
| Why the port's field framing sat low against retail | entirely the focus-Y termWhyOver the states whose pitch, yaw and | capture |
| The two scene-entry eye-trio writers - which runs last? | they do not race, and the first field frame reads neitherWhyField MAIN INIT | capture |
| Which sites re-query the camera zone? | seven jal sites plus one gated per-frame site; a bare tile crossing is not oneWhy
| disassembly |
| Is the port's camera-relative pad remap retail's? | exact over the whole input spaceWhyEight camera octants times eight held directions, latched from the same call of | capture |
| Is retail's actor facing the engine's heading plus a half turn? | player+0x26 == render_26 + 0x800Why61 of the same 64 cells agree exactly; the three misses are the sweep's three 1536-unit turns, each 512 short at the cell's last held vsync because the actor eases toward the new facing. | capture |
| Does a zero field focus Y frame retail's field? | yes - it is the whole vertical errorWhyOver the eight library states whose camera words match retail's, a focus anchored on the footing frames 15.8 px of 240 low on average and is exact only where the footing is zero; with focus Y zeroed the mean falls to 2.2 px, the four states with exact camera words are pixel-exact (player-box IoU 1.000), and a pure-shift pixel fit on | capture |
| What is the bag-normalize gate's base register? | 0x80084140, the live game-state blockWhy
| disassembly |
| Is the resident scene-name string an input to the loader? | no - it is the loader's outputWhyOverwriting | capture |
What does edbylon select when the tile query misses? | nothing selects - the block is held from an earlier tileWhyThe ending-vignette state stands on tile | disassembly + capture |
| Is the Throw Out cursor a bag slot or a list row? | a bag slot, and the list hides empty slotsWhy
| capture + disassembly |
Is gp+0x2D8 (the pad-ring octant) camera-derived? | authored - it is scene contentWhySix writers disc-wide, all field-overlay: op | disassembly |
Who reads 0455_urudre1's descriptor 0? | nobody - it is a switched-off duplicateWhyThe descriptor's type byte is | disassembly |
The 0x4C outer dispatch table, and its two non-handler arms | sixteen entries at 0x801CEE60; nibbles B and F are the error printerWhyThe outer table sits at | disassembly |
| What the field state machine's slot 7 holds | the submode return state, not a mode of its ownWhyThe enter half installs it at | disassembly |
| What does a field submode return to? | nothing reads the parked wordWhyThe op- | disassembly + capture |
| Does the field passive-ability badge column have a suppress gate of its own? | no - it sits inside the party readout'sWhy
| disassembly |
| What do the fishing bite tick's two per-frame map reads do? | one gates water, the other drifts the lureWhyBoth take the same point - the tick's own actor | disassembly |
| Which screen opens window 25, and which opens window 41? | different screens; no program opens bothWhyThe menu image holds exactly one open-op ( | disassembly |
| What is the equip compare panel's category byte? | the accessory passive indexWhyThe lookup reads the item record's class byte first, then one of two tables indexed by that record's | disassembly + capture |
| How many equip slots does the menu stat block sum? | fiveWhy
| disassembly |
| What is the equip browse row's index space? | a two-table slot map, not the equip-byte indexWhyRow | disassembly |
| Which way does an obstructed sub-cell drift the fishing lure? | by the cast counter's low bitWhyForcing the branch at | capture |
What writes the fishing cast counter _DAT_80084460? | the bite tick itself, once per hookWhyReached through the save-block base rather than its own address - | disassembly |
| What consumes the fishing bite tick's per-cell fish weight? | it is the modulus of the caught fish's size rollWhyOne use: | disassembly |
What is the Baka Fighter VRAM-rect table DAT_801DBE84? | exactly two recordsWhy
| disassembly + disc bytes |
| Does retail's equip screen offer a Goods-slot candidate, and out of which id space? | yes - item class 2, and the filter carries no character maskWhyThe screen has two candidate families. The slot-browse step writes window 23's | disassembly + measurement |
| What does the shop quantity window print? | quantity / bound, not quantity x priceWhyWindow 35's value row is a pair with a separator glyph between them ( | disassembly |
How does sub-screen 0x15 exchange two list rows? | a two-press latch in one wordWhyBit | disassembly |
| Which equip byte is the Ra-Seru slot? | per character, from 0x8007B424WhyThe table reads | disassembly + disc bytes |
Is the 0x801E43E8 byte run one table or two? | three tables and a pad byteWhyThe browse row -> equip-byte map is seven bytes and stops. | disassembly |
| What does retail's equip candidate step draw? | three stacked panels; the compare category follows the hovered itemWhyOne script, | capture |
Where is retail's own entry to sub-screen 0x15, and which lists can it reach? | the root picker's row 3 = Status; steps 3 and 4 onlyWhy
| disassembly + capture |
| Does retail's wall slide fire in ordinary play? | yesWhyOn | capture |
| Is the camera's visible-tile window a property of the scene? | no - of the regionWhyA 3000-vsync pad-driven | capture |
What does field-VM 4C D8 spawn? | a morph-weight actor; the two u16s are envelope ratesWhy
| disassembly |
| Which target flow does a confirmed menu spell open? | the spell's own stats +2 bit 0x20 picksWhySet routes to the no-pick group sub-screen | disassembly |
Does retail's cold entry into conc clear flag 0x6DE? | yes, twice - and the flag is a live position predicateWhyA single-flag write watch from the memory-card load screen sees the scene load clear it from | capture |
What position does a system script's CD F8 box test read? | the live player's, on every evaluationWhyRetail resolves cross-context target | disassembly + capture |
What do the two screen-effect pushers of a conc -> conc2 door write? | two channels, told apart by the OT bucketWhyDeparture: 33 passes at a two-vsync cadence from the field overlay's | capture |
What does 4C D8's model operand index? | the scene bank, not the global poolWhyThe arm adds the scene-bank base before the call - | disassembly |
| Does retail walk the morph block with one record pitch? | three, and they agree only on a one-record blockWhyThe spawner's size sum steps | disassembly |
Does any beat take op 0x34 sub-0's forked arm? | no writer of the gate bit executesWhyThe arm forks on | disassembly + capture |
Does any MAN author motion-VM ops 0x10 / 0x11? | noWhyZero of the 573 section-1 stream variants carry either, so the | disassembly |
What does FUN_8001FA00's one caller seed? | the fog-particle pool's free stackWhyIts only | disassembly |
Is the 192-byte block at 0x801F21B4 one twelve-row probe table? | three tablesWhyThree consumers form three bases: the actor-collision probes at | disassembly |
| What clears the two halt bits an inn acquire sets? | the acquire's own FaceTarget leg in the walk kernelWhy
| capture + disassembly |
| Whom does a talk acquire's FaceTarget turn toward? | the actor its bind namesWhyThe kernel resolves the bind at op | disassembly + disc census |
What does op 0x42 mode 1 test? | the held pad against a compassWhy
| disassembly |
Who places the pair concnow and concend hold on arrival? | the arrival record, not a spawn prologueWhy
| disassembly |
What wrote flat 72's seat in town0b_field_door_hop? | a port defect, fixedWhy
| disassembly + capture |
What is 0x1F800394 & 0x80000? | the walk-on same-tile re-pollWhyWhile it is set and the movement lock clear, the dispatcher re-runs the tile's kind-1 record on an unchanged tile ( | disassembly + disc census |
What tag does the timed warp put on 0x801DA7F0? | the 4C E1 text balloonWhyThe timer-running arm ( | disassembly |
What does FUN_801DB510 do while 0x8007B606 is clear? | it pins the focus and runs the shake tailWhy
| disassembly |
Does FUN_801CF8AC change anything past its box test? | only the +0x10 & 3 exempt early-outWhyIts no-class arm needs bit 17 clear, which nothing on the disc clears, and all three callers keep only | disassembly + disc census |
Is MAIN_INIT's FUN_80017BEC refresh observable? | yes, in three scenesWhyCalled unconditionally at | capture + disassembly |
Who spawns the scripted-scene actor FUN_801D4A60's openers? | the two world-map travel artsWhyThe only other | disassembly |
| What does Incense do? | it suppresses encounters outright for a walk-tick windowWhyItem class | disassembly |
| Does the field Magic screen charge the MP-saver discount? | yes, on every pathWhySCUS | disassembly |
| When does an Incense leave the bag? | at the confirm, one copyWhy
| disassembly |
| What does the Incense wear-off show? | a one-line noticeWhyEntry kind | disassembly |
| What is menu window 8? | the art-learned notice a Hyper-Art book raisesWhy
| disassembly |
| Which sub-screen does the menu driver open for an entry context? | a kind table, with a literal-1 escapeWhy
| disassembly |
What are FUN_801DD330's arguments to FUN_801DA9F8? | a window id and an exit sub-screenWhy
| disassembly |
| Which Magic-list rows does the field menu grey? | every spell that fails any of three testsWhyThe list build ( | disassembly |
| How does the fishing strike credit count pad nudges? | one per mask hit on the newly-pressed wordWhyPROT 0972 | disassembly |
| When does Baka Fighter book an exchange? | on the winner's strike keyframeWhyFighter block | disassembly |
What is Baka Fighter's FUN_801D49E8? | the special attack's afterimageWhySpawned on the special commit ( | disassembly |
| Who is Baka Fighter's round-start cameo? | scene model 3, the ring girl, who winksWhy
| capture + disassembly |
| Can a retail disc reach the Baka Fighter keyframe editor? | noWhyState | disassembly |
Why does opdeene run long after its apply 4800 camera move? | five record mechanisms; the port is within 2 % of retailWhyNPC turns run on while the record advances, | capture + disassembly |
| Does a field item a player has picked up still block the tile it stands on? | yes, for the rest of the visitWhyA placement leaves | disassembly + capture |
Long write-ups in this area
opdeene runs long after its apply 4800 camera move
Status: resolved, evidence grade capture + disassembly.
The record's length is the sum of five field-VM pacing mechanisms, all in the engine. Ground truth is a per-vsync capture of the zero-input chain from the s1_newgame_field state (autorun_opdeene_pacing.lua): the record's ctx[+0x9E] PC, the camera mover's progress, one vignette actor's flag word, clip ids, +0x62 / +0x68 / +0x6A, position, heading and the crawl roller's retire count.
- NPC turns do not park the record. The op-
0x38budget arm advances by 3 for every target (li s7,3in the delay slot at0x801DEEFC) and parks only for the player; the record reaches the next op after aB8 08 82 21on the same frame. AD <id> 08spins on the poked actor's clip end latch. A spin lasts the clip's remaining length (clamped) or the time to the next wrap (looping): actor0x07's clip0x18holds+0x4E4for 90 frames, actor0x05's clips8/9for 27 / 57. The port binds the actor's clip cursor at the0x22poke from the scene bundle's record header (gate and divisor) and steps it at the live+0x6A, whichCC <id> 41halves - a clip at rate4runs twice as long.B2 <id> 0Aends the actor's leg. The actor tick runs the walk kernel only while+0x10 & 0x400is up (FUN_8003BC08), so the halt clear stops a compass walk or turn where it stands. Two Seru legs cut this way account for about 300 frames.- The
4C 45ramp does not yield. The arm adds its 5 bytes at0x801E12A4and leaves through the common or the scheduler exit; the capture runs eightCC <id> 45 .. 28 00ramps inside one frame. - Only
B3 F8 0Awaits for a crawl. The crawl spawn is a halt-acquire on the player (0x801E1F24) and the roller clears the bit as it retires its last page.opurud's record tests it before its3Fand changes scene four vsyncs after the last page;opdeene's does not, and its3Fruns with the 8-page roller three pages short of retiring (cutscene.md). - An NPC compass walk on an actor nothing has moved starts from the context's seat: actor
0x05'sC1 05 00 C4takes 129 frames.
Measured: from the record's second opening wait (+0x289) to its 3F the engine runs 3773 frames against retail's 3828; from the apply 4800 beat to the 3F, 2268 against 2313. The gap is the frame step: the capture holds DAT_1F800393 at 3, so retail's waits and clip ticks land on three-frame boundaries the engine's one-frame step does not reproduce; at +0xB4D that decides whether a looping clip's latch lands before or after the AC 04 08 that clears it (one loop, 60 frames). The retail-compare state parked on +0x6F6 keeps its exact camera. Pinned by opdeene_record_pacing.rs (3 % band).
The field follow camera's pose chain
Status: resolved - the chain is pinned by disassembly and the pose measured over the walkable state population
Retail derives the follow camera per scene and per tile through a four-stage chain; none of its outputs is a constant.
| Stage | Routine | What it does |
|---|---|---|
| Load | FUN_801DBC20 | Splits one 18-byte MAN section-3 camera-region record into the parameter block at 0x8007B607..0x8007B627, choosing among three layouts on the high nibble of record[5]. |
| Compose | FUN_801DAB90 | Reads that block, the player's position and floor height, and the walk-region attribute box at scratchpad 0x1F800384..87; writes the staging descriptor at 0x801F3580. |
| Ease | FUN_801DB510 | Walks the six-entry descriptor list at 0x801F2798 toward the staging fields by delta >> shift plus the sign of the delta; shift from the sixteen-byte table at 0x801F2804 indexed by DAT_8007B60B >> 4. |
| Snap | FUN_801DB8EC | The same compose plus list walk with a plain copy. |
- Compose samples the floor height through
FUN_80019278with the MAN's static elevation LUT swapped in, so a scripted floor-tier bob moves the player without shaking the camera. The pitch couples to that floor height, not to a heading. - Ease runs only on frames the player's position changed, so the camera stays short when he stops mid-glide.
- The trig LUT pointers are
_DAT_8007B81Csine and_DAT_8007B7F8cosine. FUN_8005B0B8is a PsyQ-shapedSquareRoot0over the 192-entry mantissa table at0x80078E84, not a bit-packing helper.- Checked against the listing: the load's nibble test and layout forks (
0x801DBC34..0x801DBD38), and the ease's staging pointer, shift table and descriptor list (0x801DB5B4..0x801DB5FC).
Owning page: encounter.md.
Chapter-1 scene frontier
Status: resolved as a per-scene verdict (evidence capture: a disc-derived ladder plus the capture library); the five scenes that read as sealed are not - see the Uru Mais and jouine exits
The chapter-1 reachable set is the BFS closure of town01 over each scene's own decoded 0x3F destinations. It is 27 scenes, it ends at exactly one kingdom boundary (jiji -> map02), and it holds the whole Drake kingdom past the Ravine: the boss chain, the four-deep Drake Castle interior, the Uru Mais rooms. All 27 load their assets, parse their MAN, enter in Field / WorldMap, settle their entry script, carry a door, and can be walked out by pad alone.
- The closure is a reachability partition over
0x3Fonly. Scenes reached by the sibling0x3Edoor warp (a scene-type selector, not a name) are outside it. urudre2walks out once op0x45CAMERA APPLY is decoded correctly (falsified reading).
Door shapes, from three decoders over the same MAN (the clean per-partition fall-through walk, the recovering destination-table pass, and the .MAP gate-1 trigger -> partition-2 record -> 0x3F join):
| Shape | Scenes | What has a door |
|---|---|---|
| op, table and walk-on trigger | 22 of 27 | all three decoders |
op behind text pages, trigger in the .PCH sidecar | uru, urudre1, urudre2, urudre3 | all three decoders, plus the sidecar for the trigger; the clean walk crosses the inline 0x1F text kilobytes ahead of the 0x3F because a text segment is one decoded stride |
FMV hand-off, trigger in the .PCH sidecar | jouine | no 0x3F at all - the exit is 4C E2 08 (FMV 8 -> town0e), on the FMV dispatch table |
Probe limits that read as "sealed" on the bottom two rows:
- The walk-on sweep stops at 48 deduped gate-1 tiles;
urucarries 118 with its exit band at positions 63..66,urudre2carries 186. - The 24-tick post-step budget cannot run a record that spends 300+ frames in explicit waits or, for
jouine, a 6.8 KB boss cutscene. - The
.MAPgate-1 join is half the join: these exits are carried by the scene's.PCHtrigger sidecar, which the fallback read ofFUN_801D5630reaches.
On a first visit izumi's C1-gated spring record relocates the player about thirty tiles with the pad released, so a driven probe there cannot beat its own released-pad control; a revisit probe walks normally. That is a script, not a locomotion defect.
Measured by crates/parity/tests/chapter1_frontier_ladder.rs; nine of the closure's scenes are cross-checked against the capture library's main RAM and all nine enter in-engine.
The Uru Mais chain and jouine exits
Status: resolved - grade disassembly + capture (the uru exit fired live).
All five scenes carry a walk-on exit, and every exit band lives in the scene's .PCH sidecar (scene-v12-table.md), not its .MAP; only urudre2's is doubled into the .MAP.
| Scene | Exit record | Gate-1 band | Tail op (MAN offset) | Destination |
|---|---|---|---|---|
uru | P2[42] | (36..39, 5), .PCH rows 23..26 | 0x3F at 0x0D4B7 | MAP03 (0x24, 0x46) |
uru | P2[37] (C2 = [0x36F]) | (37..39, 44) | 4C E2 07 at 0x0CB11 | uru2 via FMV 7 |
urudre1 | P2[2] | (35..37, 22..24) | 0x3F at 0x01804 | uru (0x40, 0x40) |
urudre2 | P2[9] | (26, 14) + (24, 13) | 0x3F at 0x01D78 | map01 (0x26, 0x51) |
urudre3 | P2[0] | (51, 90) | 0x3F at 0x02461 | uru (0x40, 0x40) |
jouine | P2[16] | (17, 17..19), .PCH rows 3..5 | 4C E2 08 at 0x03E90 | town0e via FMV 8 |
urualso carries the three dream entrances (P2[29]->urudre1,P2[33]->urudre2,P2[31]->urudre3), all.PCH-only and ungated.- The four
0x3Frecords share one byte-exact tail:B1 F8 13(set flag 19),34 05 FF FF FF 41 00(white fade), the0x3F, then the26 FF FF/21/26 FE FFpark pair. jouine's exit is the FMV hand-off ofstr-fmv-table.md(0689_jouine->fmv_id 8->MV6.STR->town0e, door0x2E5), mapped by the engine'sfmv_post_play_handoff. It cannot be pad-probed from the catalogued state, which is already insideP2[16]running the evolved-Cort fight.- Live pin. From
uru_field_run(tile(38, 6)), holding the pad toward(38, 5)firesFUN_8003BDE0(36, 5, 42, 1)and thenFUN_8001FD44("MAP03")withra = 0x801DEB1C(the field VM's0x3Farm); probescripts/pcsx-redux/autorun_uru_exit_probe.lua. The row(36, 5, 42, 1)exists only in the.PCH;uru's.MAPgate-1 records are[1, 3, 43]. - Ruled out in all five: a
0x3Edoor warp withop0 >= 100, a0x4Cstaged-menu-warp arm, a kind-0 teleport row in the.PCH. The scripted-motion VM has no scene-change opcode. - Unexplained:
urudre2returns tomap01(Drake) whileuruexits toMAP03(Karisto).
Owning page: world-map.md.
The upper-case destination fold
Status: resolved - grade disassembly
A 0x3F destination name is case-insensitive in effect, because it ends in an ISO 9660 file open and ISO identifiers are upper case.
FUN_8001FD44strcpys the operand into0x80084548; the field asset loaderFUN_8001F7C0(0x8001F7E8..0x8001F88C)strcats it intoDATA\FIELD\<name>.MAPforFUN_8003E6BC->FUN_800608F0, the ISO file open.- 40+ distinct upper-case destination names appear across the 99 scene MANs (
MAP01/02/03,KOR*,DREAM,RETOCK*,ROPEWAY,NILBOA, theED*ending chain), every one a CDNAME label. The disc carries no mixed-case0x3Fname run. legaia_scene_formats::field_disasm::clean_scene_nameaccepts a uniformly-cased 3..=12-byte alphanumeric label and folds it. With the fold thetown01closure is 68 scenes with 20 kingdom-boundary edges.
The count-5 asset tables
Status: resolved - grade disassembly + capture
Retail bounds the descriptor count nowhere: FUN_80020224 reads +0x00 (lw s3,0x0(s4) at 0x80020288) and loops that many descriptors - the only exit is slt v0,s1,s3 / bne at 0x800202C0..0x800202C4, each pass reading the descriptor pair at +0x08 / +0x0C of an 8-byte stride. The reliable signal is descriptor 0's anchor at 8 + count * 8 (0x30 for count 5).
- The count-5 tuple is
(TimList, Man, Move, Anm, Flag(0x14))- the canonical seven minusTmdandVdf. - Census of every PROT entry parsing as this table: 1 / 2 / 10 / 4 / 8 / 80 entries at counts 1 / 3 / 4 / 5 / 6 / 7. Of the sub-6 set only two carry a MAN.
- The detector admits
count >= 4gated on a type-3 descriptor, which reclassifies exactly those two entries.
Owning page: scene-bundles.md.
The ledge-hop lock leak
Status: resolved (engine bug, fixed) - grade disassembly + capture
start_field_ledge_hop ORs 0x0008_0000 into the player's move_state.flags (retail 0x801D25A8..0x801D25B8 on the player context's +0x10) and only the hop phase machine's end arm clears it. A scene transition landing mid-hop tears the machine down without that arm, and the locomotion step returns early on the bit in every later scene. Retail cannot reach the state: its hop always finishes before a transition.
- Reproduction: walking the closure in one host,
towerhops and ends at tile(0, 0), and all 28 scenes after it report the flag with zero driven tiles while each walks normally on a fresh host. - Fix:
SceneHost::enter_field_scenedrops the hop and the lock together.
Clip-end latch for cross-context clip pokes
Status: resolved from the disassembly of both halves and ported; residual is port fidelity, not an open question
ctx[+0x62] is the clip-control word and bit 8 (0x0100) is its end latch (script-vm.md). When a flag triple carries a 0x80-prefix target, the op runs against a different actor record, and the latch is written by that actor's own anim tick FUN_800204F8 - the same routine a prop reaches.
Resolver FUN_8003C83C:
0xF8returns_DAT_8007C364, the live player object, without walking any list (li v0,0xf8/bne a0,v0,0x8003c858/lw v0,-0x3c9c(v0)/jr ra).0xFBwalks a second list for the entry whose+0xChandler is0x801DA51C.- Every other id walks
_DAT_8007C354matching*(u16*)(ctx+0x50).
Anim tick FUN_800204F8 (ghidra/scripts/funcs/800204f8.txt):
| Half | What it does |
|---|---|
Binder (0x80020570..0x800205A8) | Only when +0x5C != +0x5E: remember the id, sh zero,0x68 (cursor to frame 0), point +0x4C at the clip. +0x62 is not touched - hold / clamp / reverse are the script's to set, and clearing the latch is its AC <t> 08. |
Advancer (0x800205AC..) | Consume +0x62 bit 0x200 (restart), clear bit 0x100, step +0x68 by +0x6A unless bit 0x2 (hold) is set, then wrap or clamp at either end and set bit 0x100 there. |
- The advancer scales its step by the scratchpad frame-step byte
_DAT_1F800393(mult a0,v0at0x80020660/0x80020680).PropAnim::tickadvances one step per call, which is identical whenever that byte is1. - Worked example, the retock innkeeper's Yes branch:
AC F8 01(un-hold),A2 F8 04(poke the clip),AC F8 08/AD F8 08(clear, spin),AC F8 03(un-clamp),4A 03 00,AB F8 03(re-clamp),AC F8 08/AD F8 08again, thenA2 F8 02handing the player back to the locomotion move. Every bit is anANIM_*bit; none is a per-record local flag.
Port. engine-scene::field_env::PropAnimBank holds a cross-context cursor per target byte (actor_clips, keyed the way the resolver keys its walk). World::step_inline_dialogue binds the poked actor's +0x62 into the executing context around each 2B / 2C / 2D, mirrors it back, and parks on the spin - the bind / re-sync / mirror-back discipline World::step_prop_interaction runs a prop's whole record under, narrowed to one word. PropAnim::tick is the port's only latch writer, and the cursor advances once per frame whichever driver reaches it first (the field frame's tick_prop_interactions, or the runner itself for a host that drives only a conversation). Pinned by crates/engine-core/tests/inline_clip_latch.rs: the latch appears on the poked actor's cursor, never on the record's own flag word, and a stalled cursor never lets the spin through.
Port fidelity. An actor's drawn clip and its latch cursor are two objects in the port where retail has one struct: the player's gesture is played by the host's FieldPlayerAnim off World::locomotion.player_move_cues, an NPC's by the host's clip player, and the latch is timed by the bank's cursor. Both take their step from actor_tick::clip_step with the record's gate and divisor and are sized from the scene ANM bundle where it resolves the poked id; a clip the bundle cannot name falls back to a stand-in length. No capture rules out another runtime writer of bit 8; a script could set it with a 2B <t> 08 of its own, and none of the records read does.
Ambient render-mode 4 - the VRAM-rect scroller
Status: resolved - decoded from the disassembly and ported
Render mode 4 is a cyclic VRAM-rect rotation, seated by move-VM op 0x1E and fired by the render tail on a countdown.
- Seat (
80023070.txt0x80023694..0x800236F0):+0x5A = 4, then seven operands into+0xC4(period reload),+0xCC/+0xCE(horizontal / vertical step) and the rect+0xD0..+0xD6. The live countdown+0xC6is not seated, so a new part fires on its first tick. - Fire (
80021df4.txt0x80022CB8..0x80022EE0): drains+0xC6byDAT_1F800393alone (the mode-3 sibling also folds in theDAT_1F80037Dspeed scalar) and fires on the tick the stored halfword's sign bit sets (sll v0,0x10; bgez). - Rotate, horizontal then vertical:
FUN_8005842Ccaptures the leading strip into a buffer bump-allocated off0x1F8003A0,FUN_80058490slides the remainder,FUN_800583C8re-inserts the strip at the far edge. - Carriers. Seventeen scenes seat one from their plain scene-entry ambient tree. Sixteen scroll vertically only, upward, over a rect at
x >= 0x200(falling water, energy columns);tunnelc's second seat is a full-width one-row rect at(0, 508)stepping right - a CLUT row. Count carriers by walking the records through the move VM: the records jump, so a linear scan finds0x1E-shaped bytes inside operand streams. - Port.
engine-field::vram_scroll, applied in tick order byWorld::step_ambient_fx(the rotate is destructive; the mode-3 write is recomputed each frame from a cached capture). Disc-gatedcrates/engine-core/tests/ambient_mode4_scroll_disc.rs.
Owning page (per-scene rect table): field-ambient-fx.md.
Which op-0x34 sub-3 installs fire at scene entry
Status: resolved - decoded from the disassembly and ported
FUN_8003A1E4, the pre-run the placement spawn loop calls per just-spawned placement, carries its own copy of the per-actor script runner's frame slice rather than calling FUN_80039B7C. An install fires at entry exactly when it sits inside that slice.
0x8003A480:lbuthe first opcode,addiu v0,v1,-0x24,sltiu v0,v0,0x2,beq v0,zero,<skip>- unless the first byte is0x24or0x25the VM loop is skipped and the script never runs at load.0x8003A498..0x8003A4F4: run while(opcode & 0x7F) >= 0x20. After dispatching,beq s1,s4againstli s4,0x21breaks the slice on the raw byte0x21(a cross-context0xA1does not break), as does an unchanged returned PC.0x21and0x25both disassemble as "nop" and only0x21ends the slice. A record written25 / 34 30 00 / ...fires its install in the load slice whatever follows, so a dialogue-bearing placed actor installs its ambient tree like a dedicated effect-actor script; an install placed after a0x21(edkoroutP1[15]) does not fire at plain entry.
Port: the scene-entry prologue pre-run executes the slice itself, so each install fires once at its context's position. The static census engine-field::man_field_scripts::scene_entry_ambient_installs takes the unconditional prefix of the slice for tests and the .glb export - a deliberate under-approximation, since a flag-gated install deeper in a record (nilboa P1[3], suimon P1[4]) depends on runtime state. Disc-gated coverage crates/engine-core/tests/ambient_entry_install_census_disc.rs.
Owning page: field-ambient-fx.md.
Master ambient record 0 - the per-scene SFX descriptor bank
Status: resolved - the premise ("a stager record with unknown rows") was wrong
0x8007B8D0 is a shared current-bundle pointer, and record 0 of the bundle it points at is the runtime sound-effect descriptor bank for cue ids >= 0x200.
- The field asset loader points the slot at the scene prescript bundle (
0x8001F850..0x8001F864:lw v0,0xd8(s3)withs3 = 0x1F800314, plus0x12800), which is howFUN_800252ECreaches stager records through it. FUN_800250D4(0x800250FC..0x8002514C):desc = base + offsets[0] + (id - 0x200)*8, thenSpuKeyOns+3 & 0x1Fconsecutive voices.FUN_80016B6C(0x80016C24..0x80016CB0), the cue-ring drain: same address arithmetic; it hands bytes+0..+4to the debug print"setbl p:%d t:%d l:%d n:%d id:%d".offsets[0]is the same wordFUN_800252ECreads for stager id 0.- Disc shape: every populated row has category
+4 = 3(a variable VAB slot), voice count 1-2, a level in the low 60s and a zero+5..+7trailer - the layout of the static table insfx-table.md. Size is per scene:joureserves 96 rows and populates 40,rugicarries 21.jou's own tree cues0x20Band0x20E..0x211- rows 11 and 14..17 of its record 0. - The boot sound-bank loader
FUN_8001FA88writes the same slot and immediately saves its bank's record-0 address atgp+0x678, because the next scene load overwrites the slot.
Rim Elm's south gate
Status: resolved
The game's first scene exit is held shut by the collision grid, not by a trigger gate. The exit record is ungated; grid row 47 walls z in [5888, 5951] across the doorway until a story-flag paint opens it.
The gate's two .MAP kind-1 gate-1 bands:
| Record | Tiles | Script |
|---|---|---|
P2[10] | (24..26, 45), (25, 44) | 21 21 26 FE FF - Nop; Nop; JmpRel-to-self. Five bytes, no scene change (inert). |
P2[0] | (24..26, 46) | CFlag.Set, an Effect fade, 0x3F naming map01 at entry (0x60, 0x19). C1=[] C2=[] (ungated). |
The wall is cut by town01 P0[20], the gate object's own record, bound by the gate-0 kind-1 trigger at tile (23, 43) and executed by the scene-init bind prologue (FUN_8003A55C). It clears the approach with three 4C 70 paints, then branches on system flags 327 / 321:
327 | 321 | Paints | Gate |
|---|---|---|---|
| clear | - | none; the base row-47 wall stands | shut |
| set | clear | re-blocks rows 44..46, seats the gate at (24, 44) | shut |
| set | set | 4C 70 18 2D 19 2E - cols 24..25, rows 46..47 | open |
- A cold boot cannot leave Rim Elm in retail or in the port. On the port's loaded grid the three flag states give exactly the three collision states above, with col 26 re-blocked in the open one.
- An oracle that seats the player onto
(25, 46)fires the exit; a player walking toward it is held by the wall, so the walk-on dispatch is not at fault. - Carrier:
town0cholds the paint sequence twice (entry scriptP1[0]andP0[20]);town01holds it only inP0[20]. Applying nibble-7 deltas from entry scripts alone leavestown01's gate sealed in every story state. - Pinned by
crates/engine-scene-host/tests/south_gate_disc.rs; the pad-driven exit is a rung ofcrates/engine-shell/tests/critical_path_replay.rs.
Owning page: script-vm.md.
Town/field free-movement locomotion
Status: resolved
The player free-movement controller is FUN_801d01b0 (field overlay 0897), pinned by a runtime write-watchpoint on *(0x8007c364) + 0x14/0x18 (autorun_player_pos_watch.lua). It camera-remaps the held pad, computes a per-frame speed, and steps the player 2 units at a time with per-axis collision. The 801db81c..801dbf9c cluster is the field camera system, not movement.
- Controller. Pad remap
func_0x800467e8+FUN_80046494-> direction bits& 0xf000; speedbase_step * player[+0x72] >> 12 * DAT_1f800393with terrain-slow and diagonal modifiers; facing inplayer[+0x26]. - Wall collision,
FUN_801cfe4c(overlay0897@0x801CE818, bias tableDAT_801f2214): three leading-edge footprint probes (48 units ahead in the positive directions, 47 in the negative, ±16 lateral), each sub-cell derived aszc = (z>>6)+2,xc = ((x+0x3f)>>6)-1(0x801CFF04..0x801CFF40). The+2Z bias is authored into the wall bits; the floor samplerFUN_80019278reads the same bytes with plain floor indexing. Pinned by therimelm_wall_press_down(step-exact 47-unit standoff) andrimelm_wall_press_leftcaptures. - Actor collision,
FUN_801cfc40(bits1/4): walks the active-actor tableDAT_801c93c8, box-testing the threeDAT_801f21b4probe points (64 / 63 ahead, ±32 lateral). A static entity anchors at its MAN object record (tile*128 + sub*16, half-extent0x40+0x10, footprint offset including the+0x52 & 8correction from record flag bit0x8, verified against four captures' spawned static actors); a moving actor uses its live position with caller extents (±40). Each 2-unit step is gated on the actor bits and the wall bit together. - Village NPCs take the moving-actor arm (bit
1, ±40 box). Inrimelm_npc_press_tetsuthe mutual+0x98collision link is active both ways and the NPC'sflags+0x10 = 0x08020884carries the0x20000bit. - Touch / interact dispatch,
FUN_801d5b5c(decoded from a live overlay image; the static 0897 copy is garbled at that VA): sets the player engaged flag0x80000and the actor touched mark0x100, bumps the counters, saves facing to+0x5A, and kicks theFUN_8003c9acNPC-motion pause. It fires per contact step for static props (bit4), and for NPCs on the just-pressed interact button through the probe tableDAT_801f2254(overlay file0x23A3C: a radius-64 compass point per 45-degree facing sector, extents0x20-> ±72 NPC box) with a face-the-NPC turn (func_0x80019b28). - Interaction end. The dialog SM
FUN_80039b7cexit path restores the actor facing from+0x5A, drains the+0x2A/+0xAtouch-counter pair, and clears the player's0x80000flag andctrl+0x60when no interactions remain.
Port.
- Walls:
World::field_tile_is_wall(retail's derivation;sample_field_floor_heightkeeps floor indexing) andWorld::field_dir_blocked(the three-probe footprint), gated byWorld::locomotion.leading_edge_wall_probes- on by default inplay-window,--no-edge-collisionclears it; the centre test stays theWorlddefault for the oracles and nav drivers. The engine stepper reproduces both retail rest positions byte-exactly, also through a realenter_field_livescene entry. - Actors:
World::field_actor_dir_blockedoverWorld::npcs.positions, gated byWorld::npcs.solid(--no-solid-npcsclears it); props viaScene::field_object_placementscollider centres (field_prop_colliders_live.rs); interact probeWorld::field_interact_probe_slot. - NPC motion:
man_field_scripts::placement_motion_routedecodes each placement's pre-text0x4C 0x51move-to-tile waypoints andWorld::tick_field_npc_motionsdrives them through the ported motion VM (FUN_8003774C), writing live positions back. Autonomous patrol is gated byWorld::npcs.animate(--no-live-npcsclears it); an interaction prologue's0x4C 0x51walks the interacted NPC regardless. - Cutscene cross-context walks: a partition-2 record's targeted
0x47yield (C7 <id> <tx> <tz> <mode>) parks the record onCutsceneTimeline::walk_waitand glides the target (NPC channel or the0xF8player anchor) to the tile at the op's own speed; the pairedA2 <id> <move_id>ExecMove surfaces the walk / idle clip cue (thetown01Mei walk-in; see script-vm.md § yield family). - Walk-touch:
placement_walk_touch_eventclassifies0x3Edoor warps and cross-context player-channel0x23teleports;World::check_field_walk_touchposts once per ±80-box contact throughtrigger_field_interact. A.MAPobject bind's contact resumes the object's record through the prop runner; only a partition-1 placement's contact applies the decoded effect. - Not modelled one-to-one: the full
FUN_801d5b5cpost-kernel state (engaged flag, facing save / restore, touch counters), per-actor field-VM channel execution for yield-paced patrol scripts (the engine loops the decoded waypoints), the exact retail NPC glide speed, and prop scripts beyond the two decoded walk-touch classes.
Tests: parity/tests/field_collision_discriminator.rs (probe-model + engine-rest legs, including npc_press_pins_moving_actor_arm); field_npc_motion_disc.rs / field_walk_touch_disc.rs; unit equivalence world.rs::tests::field_tile_is_wall_matches_retail_subcell_derivation; standoff leading_edge_wall_probes_rest_at_retail_standoff. Both wall-press sessions park in town0c, whose .MAP (PROT 0019) is byte-identical to town01's.
Owning page: field-locomotion.md.
Field collision-map source
Status: resolved
The collision grid at *(_DAT_1f8003ec) + 0x4000 (1 byte per 128-unit tile) arrives whole from the scene's .MAP - the live grid byte-matches PROT 0109 with zero diffs - and the field VM paints story-conditional deltas over it.
- Paint op:
0x4Couter-nibble 7 (op0in0x70..0x7F), inline operands[4C, 0x7s, col0, row0, col1, row1, mask]; sub-op = clear-walkable / block-all / clear-mask / set-mask. 6 bytes for subs 0 / 1, 7 for subs 2 / 3. - The handler
0x801e1c64is entry[7]of the jump table at0x801CEE60- an intra-function label, not a function. Its loops run columnscol0 ..= col1and rowsrow0 + 1 ..= row1 + 1(addiu a3,v0,0x1/addiu t2,v1,0x2at0x801E1C6C/70,addiu a2,v0,0x1at0x801E1CBC): the row operands are one above the painted row. Sub0masks& 0x0F(0x801E1CFC), sub1ORs0xF0(0x801E1D70), and both advance six (0x801E1D24). - High nibble = 4 sub-cell wall bits. Low nibble = floor-elevation tier, a 4-bit index into a 16-entry
shortheight LUT at scratchpad0x1f80035c, filled at scene entry byFUN_8003aeb0from the MAN header (_DAT_8007b898+2, 16 negated values) and consumed by the object spawn iteratorFUN_8003a55cto offset each placed object's Y. +0x8000is a per-tileu16object / attribute map, not a terrain-flag grid: low 9 bits = object-record index into the+0x0000table; bit0x400= footprint flag ORed in byFUN_8003aeb0.- Nothing zero-initialises the grid; there is no
+0x4000init site to find. town01runs at game mode0x03, the same as the runtime-pinned fieldmap03.
Owning page: field-locomotion.md.
Field .MAP PROT resolution - define − 2, universal
Status: resolved (census-pinned; engine resolver corrected)
A scene's field .MAP is its retail block's first entry - extraction index define - 2, because CDNAME defines are raw-TOC indices shifted +2 from the extraction frame (cdname.md) - for every field scene. Scene::load converts windows to the retail frame, so Scene::field_map_index is the block's first entry and walk_field_map_index is an alias.
- Census (
crates/engine-shell/examples/field_grid_census.rs): each save's live field buffer (scratchpad_DAT_1f8003ec->+0x4000grid) classified against candidate on-disc bases.keikokusessions match PROT 0109 (define 111 - 2) with zero diffs while 0118 differs by 3855 bytes;koin3matches 0559 exactly (0568 differs by 531);town01sessions match 0010, which equals 0001. - The in-block decoy. In an unshifted window the first
0x12000entry is the next scene's map; corpus-wide, every block's in-block0x12000hit is exactly the next block'sdefine - 2entry. The per-entry extractor's shifted filename labels attribute a map to the previous block's tail. - Object-index grid (
+0x8000, theScene::field_object_placements/field_terrain_tilessource): residuals of 0..96 bytes against the resolved entry acrosstown01/town0c/keikoku/koin3sessions (story-conditional cell mutations - opened chests, prescript object toggles), thousands against every other candidate. Guarded by the disc + save-library gatedparity/tests/field_map_object_grid_live.rs. town0c's.MAPis PROT 0019, byte-identical totown01's (0001 / 0010). PROT 0028 isizumi's (define 30 - 2), nottown0c's.- The footprint is corroboration, never the resolver. 111 entries are exactly
0x12000bytes and only 101 are maps. Five of the ten others sit inside named scene blocks (dolk+5,dolk2+5,taiku+9,taiku+10,rugi+7) and arescene_tmd_streamentries ([u32 size]then the0x80000002TMD magic), so a footprint scan within a block can land on a mesh stream.
Owning page: field-map.md.
game_mode 0x03 = field/town gameplay
Status: resolved
_DAT_8007B83C = 0x03 is the in-town / on-field gameplay mode, and the in-field pause menu runs under mode 0x17 (23, the CARD pair).
- Captures.
v0_1_pre_battle_tetsu(Vahn walking in Rim Elm /town01) and the runtime-pinned free-movement controller onmap03both hold0x03. All six menu-open library captures (equipment / status / options, fieldmap01+ towntown01) hold0x17. - Handler map. The index -> handler / param / name map is read off the disc by
legaia_game_tables::mode_table(asset mode-table; disc-gatedmode_table_real). Field / town is modes 2 / 3 MAIN;MAPDSIP(12 / 13) is the world-map display mode, not the field. 12 of the 14 per-frame modes share the generic per-frame handler0x80025EEC; only mode 13 (world map) and mode 23 (memory card) carry their own. - Next-mode field. The retail
+0x0Afield isModeEntry::next_mode:-1= self-managed,0= fall back to mode 0. The word0xFFFF0000is-1over a zero low half, not a sentinel. - Port.
legaia_engine_mode::mode::GameMode::scene_mode()mapsMainMode (3) -> SceneMode::Field;legaia_engine_mode::modeholdsSceneMode::Fieldfor both modes 2 / 3 (an init mode holds its successor's scene mode, as the Mapdisp / Battle / Str pairs do) and maps the CARD pair toSceneMode::Menu.BootSessionhosts the field-menu session headlessly (open_field_menu/ the Start-edge path intick), and the windowed host layers its sub-session UI on the same session. - Oracles.
mode_trace_e3+v0_1_playthroughdrive the engine into the field (enter_field_live) and converge on the retail0x03snapshot;mode_trace_e3drives menu scenarios with a scripted Start press and asserts scene mode, active scene and the engine-emittedgame_mode = 0x17. The table's name / param / next fields are cross-checked against the disc map by the disc-gatedmode_table_reconcile.
Owning page: boot.md.
Engine VRAM byte-exactness for town01
Status: resolved (major source); minor residue
Single-snapshot byte-exact VRAM is not achievable: about 40% of the texpage band is dynamic or residual (two town01 captures disagree on about 40%). The oracle vram_oracle_e1 compares the static mask - words stable across same-scene captures, excluding the runtime NPC / character CLUT band - and with the field pre-pass doing DMA-every-TIM (BuildOptions.upload_all_tims) town01 passes byte-exact on every static pixel it uploads.
- Extraction-0874 §2 TIMs (retail
player_data/player.lzs§2, the field-character texture band;etim.datis extraction 0870, a different file): 4bpp pages atfb(320/384,256)etc., field-resident, pixel-matched 256 rows byte-exact. The engine uploads them at field entry (scene::upload_effect_textures_into_vram) - image pages only, since retail uploads their CLUTs at battle entry. - The menu-glyph atlas (
PROT.DAT[0x11218]) is boot-resident: its image page and flat-strip CLUT match the disc bytes in every captured phase, title included. The(960,400)60x24 rect belongs to the next bundle TIM (PROT.DAT[0x19438]), which retail uploads after the atlas and which overlays that part of the atlas image. Uploading the whole system-UI bundle in on-disc order reproduces the retail band; see CLUT row 510 population. - Residue,
x=896..1024, y=256(about 12k words): (a) the boot-resident system-UI band (the(960,256)atlas page + its overlay TIMs; static disc bytes), and (b) the character / party-texture region uploaded by the battle / character targeted-CLUT pass, which the field pre-pass excludes by design, plus about 2.5k words of UI residue.
"Stable across same-scene captures" is not the same as static in two capture-pinned cases:
- The extraction-0874 §2 texture band is global, history-dependent state. Row 271 holds a 3-word F-variant in some lineages (
(853,271):0xFFFFwords) and the disc bytes (0x3333) in others, and the first battle effect use restores the disc bytes - see the F-variant stamp. The oracle demands cross-scene staticity insidescene::effect_texture_image_rects. - The world-map walk view palette-cycles specific columns of the kingdom terrain CLUT rows 506 / 508 / 509 in place.
vram_oracle::WORLD_MAP_CLUT_CYCLE_CELLSexcludes exactly those columns for world-map scenes; row 507 and the static columns of 506 / 508 / 509 are asserted.
World-map CLUT cycling beyond the ocean head - CLOSED (operand table + emitter + cadence all pinned)
Status: closed. The head-walk operands are a literal disc table - kingdom-bundle slot 5 (type byte 0x06), a 516-byte 8-entry CLUT-walk animation table byte-identical across all three kingdoms; the emitter is the SCUS actor walker, not the script-driven CLUT-cell family; the cadence is the table's own per-frame hold bytes.
The chain, each link byte-verified against live RAM and the disc:
| Step | Routine | What it does |
|---|---|---|
| Load | FUN_8001F05C case 6 | Sets DAT_8007B7C8 to the decoded slot-5 table. |
| Spawn | field-init FUN_801D6704 | One render-mode-0xB actor per entry via FUN_80024CFC; entry pointer at actor+0x4C, accumulator actor+0x68 seeded 100 so the first copy fires at scene entry. |
| Emit | FUN_8001ADA4 case 0xB | acc += DAT_1F800393; on acc >= frame.hold issues a 16x1 MoveImage from the frame's source cell to the entry's destination cell, resets acc = 0, advances the frame index. |
- Cadence (PCSX-Redux
MoveImageexec-BP traces on all three kingdoms): intervals are constant atceil(hold/dt)*dtvsyncs - hold 8 -> 9, hold 10 -> 12, hold 20 -> 21 at overworlddt = 3. The non-multiples rule out subtract-remainder semantics. - All eight entries fire their first frame on the same vsync at world-map entry, then free-run independent phases with zero drift.
- The 18-step head cycle is
A,B,f0..f7,(f6,f7)x2,f8..f11: two extra wave frames parked before theOCEAN_ANIM_FRAME0_HEADsignature in kingdom slot 0; ocean frame 12 is never shown. - Checked against the listing: the spawn loop
0x801D6C8C..0x801D6CD4, the seeds inFUN_80024CFC(0x80024D58..0x80024D64) and the emit arm0x8001B664..0x8001B700. - The field overlay's script-driven CLUT-cell family (
FUN_801E4C58/FUN_801E4794) is not the emitter. It carries only the row-498 park one-shots / fades (map01's eight4C 61ops;scene_clut_cell_fx, disc-gatedmap01_clut_fx_disc). At overworld idle row 498 is a source strip for the(32,508)/(48,500)walkers; themap01-only row-508 "mirror" is slot-5 entry 6 copying from the script-parked row-498 cells. - Row-506 cols 32..47 are written wholesale by slot-5 entries 3 / 4 from the row-503 / 502 strips - parked disc bytes walked in place, not runtime colour math.
- Destination cells are literal u16s in the table, not
park row + 8. - Port. The engine consumes the table directly (
WaterAnim::Walkinplay-window;vram_oracle::WORLD_MAP_CLUT_CYCLE_CELLS= the slot-5 destination fold). The park strips are raw CLUT-block records, not TIMs, so the scene pre-pass does not upload them; themap02/map03bundles ship only rows{501, 503, 505}and retail relies on VRAM residency from themap01upload, which the engine mirrors by parking the byte-identical Drake complement.
Owning page: world-map.md "Ocean animation"; parser legaia_scene_formats::clut_walk.
init_data UI-tile pages - journey-dependent residency (resolved); map03 texture column (resolved - "not uploaded" premise falsified)
Status: the keikoku oracle drift is resolved (residency class pinned); the map03 texture divergence is resolved - the "engine fails to upload PROT 0392" premise is falsified, the current pre-pass does write the real terrain
init_data (PROT 0) carries two 64-word x 256 UI-tile TIMs at fb (704, 0) / (704, 256), and those rects are journey-dependent residency, not stable shared texture.
- Overworld transit leaves kingdom-bundle content over parts of the rect: every Drake-stage capture (
keikoku, the field-menu states) holds the same kingdom bytes at(704, 256)where the boot-freshtown01states hold the disc tiles. - Town scenes mask this because their own scene TIM overwrites the slot;
keikokucarries none. - The parity oracle pools captures across all scenes against
scene::block_image_rects(index, "init_data"), the same cross-scene treatment as the befect band.
The map03 terrain rect is map03's own content, and the engine pre-pass writes it.
asset tim-scan: PROT 0392 uploads 8 real 4bpp TIMs into fbx=576..640, y=320..448. Thefbx=576 fby=32096x96 4bpp TIM (PROT 0392,lzs0_off 0x03BDEC) byte-matches the retail resident VRAM at(576,320), 2304 / 2304 halfwords.- Direct pre-pass measurement:
map03uploads 58 TIMs, and the576..640 x 320..448region holds 7945 real terrain texels with 37 stray0x3332cells scattered in-tile - no hole. - PROT 0392 slot 0 is byte-identical to 0391 slot 0, which the engine uploads (the kingdom sibling-skip in
crates/engine-scene/src/scene_resources.rs), so uploading 0392 as well would write identical bytes to identical cells. - Limit of the evidence: the comparison is a pre-pass measurement, not a full VRAM oracle, because the capture corpus has no
map03-WorldMap-resident save.
CLUT row 510 population (boot-resident system-UI strip band)
Status: resolved (source + upload semantics + retail residency pinned; engine pre-pass uploads the bundle - legaia_ui_formats::system_ui_bundle); residue = the exact boot-time walker call site only
CLUT rows 510 / 511 are the flat-strip CLUT band of the boot-resident system-UI TIM bundle, so the prims that sample them are validly textured in retail. The prims in question: town01 env-pack slots 21 / 26 / 74 and rikuroa slots 50 / 51 / 63, CBA (64, 510), texpage (960, 256) 4bpp; no scene TIM uploads row 510.
- Source. The
prot::timpackat raw PROT TOC entry 0 (LBA wordstoc[0]=3/toc[1]=55precedeinit_data's 121 - indexed, just below the extraction space; CDNAME's#define init_data 0names this block). A second single-TIM pack sits at raw entry 1. - Upload semantics. The per-TIM uploader
FUN_800198E0uploads every TIM CLUT block as aw*h x 1strip at the declared origin (ghidra/scripts/funcs/800198e0.txt). The atlas atPROT.DAT[0x11218](declared CLUT(0,510,16,16), image(960,256)64x256) lands as the 256-entry strip on row 510x=0..255; the0x19438UI-strip TIM addsx=256..319; three more bundle TIMs tile row 511x=0..319. - Residency census. Across mednafen library states in every phase -
title_screen_new_game,new_game_cutscene_intro_a,v0_1_pre_battle_tetsu,keikoku_chest_pre,mei_house_inside,sebucus_overworld_resident,v0_1_battle_start_tetsu- the row-510 / 511 strips are byte-identical to the on-disc CLUT data (256/256 + 64/64 + 256/256 + 48/48 + 16/16 halfwords per strip), and the(960,256)image page matches the disc TIM on every row not covered by a later bundle member. - Row patches. Compositing the bundle in on-disc order (images at declared rects, CLUTs as strips) reproduces the whole retail
(960, 256..511)band. The six 64-word rows aty=456..458/460..462are bare row-patch members of the same pack: raw-entry-0 members 10..15 atPROT.DAT 0x1A018..0x1AA7C, a[u32, u32]preamble + TIM-style[u32 bnum][u16 x,y,w,h]block declaring(960, y, 256, 1), byte-exact against live captures (RowPatchinlegaia_ui_formats::system_ui_bundle). - What the prims sample. CBA
(64,510)= atlas strip entries 64..79; UVs (u0..2, v240..242) hit a constant mid-grey texel patch - a flat material through the textured pipeline. - Not scene-loaded or a runtime targeted upload (it is resident before the title screen), and not a CBA misread:
x=(cba&0x3F)*16, y=(cba>>6)&0x1FFis correct. - Unpinned: which boot routine issues the
byindex-style read of raw TOC entries 0 / 1 and walks the pack intoFUN_800198E0. A cold-boot write-watch on the row-510 upload (scripts/pcsx-redux/autorun_town01_vram_upload_census.lua) would name it.
Owning page: npc-palette.md.
Scene-transition (0x3F door) destination indexing
Status: resolved
A field scene reaches another through the field-VM 0x3F named-scene-change op, which carries the destination scene name inline and is selected by partition-2 slot, never by absolute reference.
- Dispatch trace (
autorun_door_dispatch_trace.luaondrake_castle_to_worldmap): the0x3Fops are partition-2 MAN records reached through the partition-2 record-offset table. The controller sets the VM bytecode base toman_base + data_region + partition2[slot]and runs the record by fall-through (a0 - man_base == data_region + partition2[0]exactly). - The op's
indexfield is only the destination-scene id passed to the warp packet (FUN_8001FD44). - Census (clean partition walk): 160 destination ops in 48 scenes, 153 in partition 2, zero absolute-reference ops at or after any destination op.
- Consequence. Variable-length door editing is safe: resizing a destination name is a partition-table + section-offset + intra-record-jump-delta + descriptor-size fixup, implemented in
legaia_scene_formats::man_editand shipped as the door randomizer (man-relocation.md). - The
0x3Edoor warp (7-idmap_id) is SCUS-resident end to end:FUN_80025980(mode-24 OTHER INIT entry),FUN_80026018(exit). It carries no destination name; the sub-id selects a minigame overlay (extraction PROT 972..977, 980 via the loader mathparam + 0x37F). Its name handling is a backup / restore (an eight-byte copy at0x800259B4, the define at0x800259BC/0x800259FC; the loader call is0x80025A1C..0x80025A30) of the current scene name (0x80084548<->0x8007BAE8, plus_DAT_80084540<->0x8007BAC4) so the exit re-enters mode 2 on the original scene.
Owning page: script-vm.md § 0x3E warp.
Intra-town (house / interior) door mechanism
Status: resolved
Entering a house is not a scene change. It is an intra-scene reposition: the scene-name buffers 0x8007050C / 0x80084548 stay put and only the player's position jumps to an interior sub-area of the same loaded scene.
- Writer. Field-VM dispatcher
FUN_801de840case 0x23(0x801debc4 sh v0,0x14(s5)), converting the tile operand to world astile*128 + 0x40. Pinned by theprobe.step.find_writerLua primitive, a width-correct range write-watch over the player position block. A width-2 watch at+0x14catches only a 2-byte no-op re-store in the ledge-hopFUN_801d1878. - Captures.
door_warp_rim_elm_to_mei_house/mei_house_inside(mednafen),mei_house_door_pcsx/mei_house_inside_pcsx(PCSX). - Door marker. House-door warps use the cross-context form
0xA3 0xF8 xb zb: opcode0x23 | 0x80dispatched into the player system channel0xF8. Plain0x23moves the executing actor (NPC / prop positioning). - Carrier records. Partition-0 records with their own header form (
[u8 n][n x 2 SJIS name][u8 attr], distinct from partition 1) and a naming convention pairing entries with exits: fullwidthIN/OUT,入口/出口gates,A/Belevator endpoints, optional digit suffixes. The Mei's-house warp is0xA3 0xF8 0x61 0x36intown01partition-0 record 34, anINrecord. - Three player-move forms.
A3 F8 <xb> <zb>(op0x23, instant),CC F8 51 <xb> <zb> <depth> <mv>(op0x4Cnibble-5 sub-1, teleport + move anim), andC7 F8 <xb> <zb> <mode>(op0x47, animated walk). A door record is a branching script whose arm is selected by story flags, so a door can also be a0x44SPAWN_RECORD of a partition-2 choreography that does the seating itself. - The bind position is the
.MAPobject's contact box, not the trigger tile, which is a lookup key and usually a wall. - Kind-0 tile doors. The
.MAPtrigger block's kind-0 sub-table is a second, larger door class:[tile_x][tile_z][dest_x][dest_z], no object and no script. Crossing the tile seats the player at(dest_x*64 + 64, (dest_z + 1)*64)(FUN_801D1EC4's kind-0 arm at0x801d21c0latches the record; the seat is the drain's0x801D1F88..0x801D1FB0). 2330 records across 73 scenes; most house exits are these. Vahn's house has anINrecord and noOUTbecause its exit is the kind-0 tile(97,9)inside the room, ungated by any story flag. - Randomizer.
legaia_patcher::house_doorshuffles only the classified door warps, class-preserving (INamongIN,OUTamongOUT), so every exit still lands outside (randomizer.md).
Owning page: field-locomotion.md.
Field/town environment-geometry placement
Status: resolved (renders)
A town's environment meshes (terrain, buildings, props) are object-local Legaia TMDs in the LZS streams of the scene's scene_asset_table PROT entry (town01 = entry 4), placed by FUN_8003a55c from the field .MAP.
- Placement. The object-index grid at
+0x8000(cell & 0x1FF= object id) selects a0x20-byte record in the+0x0000table. Placed tiles (record+0x12bit0x4) give the world transform:world_y = -floorHeightLUT[nibble] + y_off, the LUT being 16s16at the MAN header+0x02. - Mesh per object is the record's
+0x10, for every object id (retailFUN_80020f88,actor+0x64 = record[+0x10] + prefix);anim_idonly animates. Records1/2/3are no exception: neitherFUN_80020f88(0x80020FDC..0x80020FF0) nor the spawnerFUN_8003A55Ctests the id, and a liveconc2actor list holds record1(the archway double door,+0x10 = 61) at+0x64 = 0x42. Reading them as "protagonist / NPC meshes from the shared pool" left that door, and the record-2props ofrikuroaandretona, undrawn. - Validation. A live
town01save (Vahn's house id137-> mesh 36), and the retail GPU prim pool:town0ccell(30, 17)(id99, record+0x10 = 2) draws its surface from env mesh 2 - the quad'scba/tsb/ UVs match that mesh's primitive byte-for-byte - not from mesh94. - Not a positional "field-actor band" (
obj_idx - 5, ids93..=118): that rule swaps ten town meshes per Rim Elm map, drops the terrain slab south-east of the spawn and leaves a clear-colour hole in the ground. - Port. Parser
legaia_scene_formats::field_objects;Scene::field_object_placements;play-windowrenders the town viaresolve_field_placement_draws.
All 46 town01 placements draw, on two pipelines (disc-gated field_object_placement_disc::town01_dropped_placements_split_untextured_vs_missing_clut):
- 45 on the textured VRAM path. That includes pack
74/ obj347(6 placements, 4 textured prims), whose CBA row 510 / texpage(960,256)source is the boot-resident system-UI bundle layered under the field build (BuildOptions::system_ui); no placement drops for a missing CLUT. Object114resolves through+0x10to the textured pack84. - 1 on the vertex-colour path: pack
31/ obj315, 30 untextured (per-vertex-RGB) prims. The textured-only buildertmd_to_vram_mesh_filteredskips prims with no UVs, solegaia_tmd::legaia_primsdecodes the colour blocks intoPrim::colors,legaia_tmd::mesh::tmd_to_color_meshbuilds aColorMesh, andengine-renderdraws it through a dedicated pipeline (upload_color_mesh/Scene::color_draws). Pack109(12 untextured prims) builds a colour mesh the same way. - Mixed meshes render both halves: the colour mesh is built unconditionally and is disjoint from the VRAM mesh (
tmd_to_color_meshskips textured groups). - A textured prim whose CLUT is not resident is dropped rather than drawn flat with
CLUT[0]; a per-vertex-RGB fallback would render it wrong. - Untextured colour block (per-mode record layouts F4 / G3 / G4, the
00 01 03 02quad winding remap, no per-prim normal):tmd.md§ Per-prim color / texture block.
Owning page: field-locomotion.md.
Region story-flag gate families
Status: resolved as structure across the chapter-2/3 regions. Play order is capture-confirmed for retona, dohaty, taiku, the Sebucus spine, korb3, the kor5 chain head and the map03 hub latch; in-bracket order is pinned by disassembly for rayman, bubu2, retock and deroa's 0x46D; the two orders no gate decides are unobservable (see the play-order residual).
Every field scene's MAN carries one partition-2 record per cutscene or story beat. Each record's header holds two flag lists the spawn evaluator FUN_8003BDE0 checks before running it:
- C1, one-shot: the record is suppressed once any listed flag is set.
- C2, requires-all: the record spawns only when every listed flag is set.
Regional progression is expressed almost entirely through these header gates. They are not inline 0x50 / 0x60 / 0x70 opcodes, so the inline flag census (man-scripts --system-flag-census) cannot see them, and a flag read only by a header gate looks write-only there. legaia_engine_field::man_field_scripts::partition2_record_gates decodes them; the anchor tests named below pin each region's exact lists.
Self-latches. A record that both sets a flag and lists it as its own C1 gate.
0x1BE(Jeremi's arrival):geremi P2[0](anchorgeremi_p2_0_is_the_0x1be_self_latch).549/0x225(the Rim Elm opening): the same shape across the Rim Elm variants, readable once the4C 0xE_op widths are decoded.
Chapter 2 - Sebucus (map02 and its dungeon spokes). The spine needs no chapter-specific engine code: each beat's script latches its flag through the ordinary field-VM SysFlag.Set path, so the generic seeder drives the arc. Chain: teien (0x1C8 -> 0x1C9 -> 0x332) into tower (0x1C7, gated on the teien arc) into a post-tower geremi beat, with balden self-latching 0x5B3 and map02 P2[9] mirroring the teien arc onto the overworld. Proven by chapter2_sebucus_spine_oracle, chapter2_sebucus_gate_spine and chapter2_sebucus_hub_sweep_disc (which drives the arc through real 0x3F scene transitions).
| Spoke | Family | Anchor |
|---|---|---|
taiku / doman / rayman | Self-latch pairs plus the linear rayman chain 0x201 -> 0x1FB -> 0x200 -> 0x1FC. rayman2 is the same MAN with a shared C1 on the low flag 0x7, a variant discriminator. | chapter2_dungeon_gate_families |
balden / balden2 / station | balden is an arc around its reached-flag 0x1D5; balden2 is a sibling carrier with an identical gate family, selected by streaming slot rather than a flag. balden gates on the ropeway2 switches; station / station3 gate on taiku's 0x38F. | chapter2_balden_station_gate_families |
ropeway / ropeway2 / jiji | ropeway2 hosts a four-bit switch puzzle (0x3FF-0x402); see below. | chapter2_ropeway_jiji_gate_families |
retona | Five-step ladder 0x353 -> 0x354 / 0x355 -> 0x356 -> 0x357; see below. | - |
dohaty / retock / retockin / stone | dohaty opens with a six-record 0xF first-visit group; retock depends cross-scene on balden's 0x1D5 and gates on retona's 0x357 before its own 0x502; retockin is the 0x7-gated interior variant sharing 0x502 / 0x357; stone is a single one-shot. | chapter2_dohaty_retock_stone_gate_families |
tunnelb / tunnelc | Small internal one-shots, read back only by the tunnels themselves. | - |
map02 hub | A router: two gated records, both overworld mirrors of a dungeon-arc completion. | chapter2_map02_hub_gate_family |
Sebucus detail:
raymanstreaming variant adds aP2[18..20]tail latching0x34D/0x34C(P2[18]body+0x2C2, at aJmpRelbranch arm after0x1FE/0x1FFtests).taikuvariantP2[16]SETs the pair0x380+0x382at its head (body+0x11/+0x21, betweenSceneFadeand the particle emitters).0x382is a cross-chapter gate:son P1[14]branches its NPC dialogue on it (body+0x4A), and the clean census reads spandoman(V)/retockin/ropeway/ropeway2/map03/koin2/korout.ropeway2payoff recordsP2[31..=34]are C2-gated on all four switches plus the0x359commit - an internal consumer the inline census cannot see.jiji P2[8]latches0x304from three branch arms of one cutscene (each4C CD->Set->JmpRelto the shared tail; bodies+0x912/+0xCD6/ ..).retonarecords.P2[8..14]gate on0x353/0x354/0x356;P2[15]chains C2=0x354/ C1=0x355;P2[17](C1=0x357, C2=0x356) is the pre-beat rendition;P2[18](C2=0x356) is the beat that SETs0x357(body+0x5EF, after the4C 73tile run + BGM cue).P2[10]separately latches0x354(+0x673), read byrugi.retonabackstop. The entry scriptP1[0]normalises:Test 0x357-> skip;Test 0x3AD->Set 0x357at+0xF4.0x3ADis also the C2 ofmap02 P2[10], the overworld mirror0x357retires.0x357is the Jeremi-arc cross-scene gate - clean reads inretock/retockin/map02/geremi/edretoin- so the0x357half of retock's0x357 -> 0x502chain is retona's output.stonepartition-0 walk-on scripts latch a local band:P0[2]->0x32B,P0[3]->0x32A,P0[4]->0x32D,P0[5]->0x32C(+0xB7F, thenSpawnRecord 0x1E).0x32Cis a write-only latch; no reader exists. Every census read (about 50 scenes) is the ASCIIs,bigram in dialogue, and no C1 / C2 list in the pinned regions carries it. A word-aligned scan ofSCUS_942.54plus all 15 static overlay images (crates/asset/data/static-overlays.toml) finds no immediate0x32Cload into any register, no access to the flag byte0x800857BDunder any viablelui/addiuencoding, and no constant0x32Cargument at any flag-helper call site (FUN_8003CE08set /FUN_8003CE34clear /FUN_8003CE64test). The only remaining reader is the 0897 dev-menu flag browser, which reads any flag on demand.tunnelb P2[34]latches0x322/0x326.tunnelc P1[4]latches0x360+0x362from two branch arms (bodies+0x107..+0x110/+0x2AB..+0x2B4);tunnelc P2[6]latches0x34A.
Rim Elm town variants. town01, town0b and town0c share the opening chain (549 -> 0x226 -> 0x227, plus sub-chains) byte-for-byte in P2[3..=11]: story-state renditions of one town. town0d is the 0x7-gated later variant. A town0c visit in the chapter-2 capture is a revisit (the "scene" listed beside it in the poll is the capture CSV's column header, not a map). Anchor town0c_is_a_rim_elm_state_variant_not_a_ch2_spoke.
Rim Elm revisit chain (town0b band 0x228..0x233). A second flag band alongside the opening chain.
| Record | C1 | C2 | Writes |
|---|---|---|---|
town0b P2[7] | 0x22B, 0x141 | 0x147 | self-latches 0x22B at its head (+0x26, before the flash + waits); SETs 0x228 / 0x229 / 0x22A from branch arms (+0x377 / +0x804 / +0x8F9, each at a JmpRel boundary inside camera / emitter choreography) |
town01 / town0c P2[7] | 0x22B | 0x147 | the same gate shell; only town0b's copy mints the band |
town0b P2[8] | 0x231 | 0x22F | sets 0x231 |
town0b P2[9] | 0x232 | 0x141 | sets 0x232 |
town0b P2[10] | 0x233 | 0x232 | sets 0x233 |
town0b P2[11] | 0x141 | 0x231 | - |
P1[1]is the state seeder: sets0x22F+0x147, clears0x141(same record intown0c).- Readers:
town01 P0[1](the entry walk-on) branches on0x22F/0x229(+0x69/+0x6D); the NPC recordtown0b P1[39]selects dialogue over0x22F/0x148/0x147/0x228/0x229/0x22Ain sequence. - Late one-shots
town0b P2[30]/town0c P2[29]latch0x5C4(+0x3CD, behind aTest 0x35battle-victory guard), read by the ending sceneedlast. - There are three renditions, not four.
gameover_data's CDNAME window is a subset oftown01's and holds no asset-table bundle; a MAN read out of it istown01's own, reached by an entry-size over-read (script-vm.md).
Rim Elm final variant (town0e), per-NPC band 0x5DC..0x5F0 + 0x6DC. Every NPC interaction record P1[1..24] opens with the same head: Test <own flag> -> skip, Set <own flag>, then Test the neighbouring NPCs' flags (P1[2]: Set 0x5DC at +0x20, then tests 0x5D8..0x5DB). It is a talked-to-everyone tracker - scene-local flavour state, not progression; record indices map 1:1 onto the band.
Uru Mais (uru / uru2) beat band. uru's cutscene tail latches 0x3BE (P2[30]), 0x3BF (P2[34]), 0x3C0 (P2[32]) and 0x3FC (P2[38], body +0x8B7 after a BGM cue). P2[30] is the party-recompose beat: PartyAdd char 1 + Set 0x11, PartyAdd char 2 + Set 0x12, then Set 0x3BE (+0x72) under a camera reconfigure. All four flags read back only within uru.
Nivora Ravine (nilboa). An entry group sharing 0x456, a 0x47x puzzle cluster, and a cross-scene successor gated on 0x370; nilboa2 is the 0xF-gated variant carrier. Anchor nilboa_nivora_ravine_gate_family.
0x456:nilboa P2[11]both SETs and CLEARs it (Set 0x455+Set 0x456at+0x37..+0x39, inside aCC .. C3per-actor run).0x370writer:domanvariantP1[15]at MAN offset0x06397, a53 70SET in a clean choreography run whose loop-backJmpRelre-enters the record's gate-test head; the head's ownTest 0x370 -> +0x301Ejump lands on the very next op (the Dr. Usha "Do you understand? The first TimeSpace..." briefing branch) - the self-latch shape. The record's other three53 70occurrences are "TimeSpace Bomb" prose aliases.0x370readers: thedomanP1[3..=18]clean head TESTs (arc-gate dispatch chain, alias-immune operands). Pinned byman_variant_carrier_census_disc.rs::flag_0x370_writer_is_the_doman_p1_15_usha_latch; a live organic SET (the poll auto-snapshots flag 880) confirms play order.
Chapter 3 - Karisto (map03 and its spokes). Anchor map03_karisto_region_gate_families.
map03is a router with no gated records. Ungated hub state exists as inline latches:map03 P2[15]SETs0x378(+0x9E, between a 180-frame camera hold and the particle emitters), read back bydomanandmap03itself.bubu2: a small requires-all chain.bubu1carries no field MAN.sonandderoa: sparse one-shots;deroaleads to the undergroundchitei2.son's NPC records use the per-NPC one-shot head (P1[14]:Test 0x62E-> skip /Set 0x62Eat+0x52) and branch on taiku's0x382.korb3, the Karisto castle approach: a nine-record collection groupP2[5..=13], each record gated on a distinct flag under one shared0x403"all done" latch.
Chapter 3 - Karisto castle depth (kor / koin cluster + chitei2). Anchors chapter3_karisto_castle_gate_families + chapter3_koin_family_and_writer_pins.
kor: one-shot beats (0x408read bykorout, self-latches0x409/0x40A) plus a door group C2-gated on0x612, an arm-then-consume mechanic - the partition-0 entry scripts SET0x612and each door record clears it.kor3/kor4gate their doors on the same flag.kor5: a three-step chain0x43A -> 0x436 -> 0x6C4.koin1biskoin1's story-state sibling (same gate shape + a spliced0x00Btoggle pair); it owns the0x3DASETkoin1gates on.koin1'sP2[9..10]are a0x50Aset / clear toggle pair.chitei2: the0x470/0x4F0and0x4C4/0x4C6/0x4C8/0x4C9families;0x4C8is co-written bymap03 P2[19].korb2/koin2/koin6are gateless.koin3 P2[8]and its stale sibling copyother7 P2[5]co-latch0x430(koin3body+0xA40, aJmpRelbranch-arm set insideCCcamera choreography), read by the ending sceneedlast.0x50Ais the Sol game-hall minigame result toggle, written natively by the mode-24 minigame overlays - a space the MAN script census cannot see. The Muscle Dome module (PROT 0977) CLEARs it in the post-match settle (0x801D0FF8) and win-re-SETs it (0x801D101C, labelled by the overlay's ownWIn on/WIn offdebug strings). The dance trio (0978..0980) SETs it at session start (0x801CF968) and CLEARs it on a missed goal (0x801CFF10).koin1hosts the Muscle Dome + Baka doors (3E 69/3E 68),koin3the dance doors (3E 6A), andkoin1 P2[9](C2=[0x50A]) is the returned-victorious beat.0x5D6is a script self-latch.koin4 P1[15]sets the flag it gates on: behind the "If you have money" line sit48(a one-byte no-op) and55 D6, and itsP2[3]twin carries the same block. The bytes to search for are55 D6, notD6 05, and a walk that stops at a record's first text segment misses the site. The native-space sweep (scripts/asset-investigation/flag_helper_call_sweep.py, the move-VM ext flag sub-ops, the motion-VM census) is a negative for native writers only. See script-vm.md § native flag-bank writers; guardkoin_gates_0x50a_writer_less_0x5d6_self_latched.- Runtime oracle
chapter3_karisto_spine_oracle.rs: the Conkram -> deroa -> chitei2 bridge, thekor5chain, the door arm-then-consume and the koin toggle all sequence throughp2_record_gates_pass+install_gated_p2_recordwith no chapter-specific engine code.
Chapter 3 - Conkram (conc*, the "past" arc). Anchor chapter3_conkram_gate_families.
- Pivot pair
0x3E1/0x3E5:conc2 P2[12]SETs0x3E1, the flagderoaC2-gates thechitei2descent on (the cross-region bridge).conc3self-latches0x3E5(P2[10]) and SETs0x3F9(ungatedP2[9]);conc P2[10]chains on both. conc/concnowcarryr1..rNsoldier rows, all C1-gated on the low flag0x007, which is SET byconcnow P0[34]+conc2 P0[21](a "soldiers disperse" beat).conchas eleven doors on0x6DE, armed by the entry script's player-position BBoxTest run (the same mechanic as kor's0x612). All four carriers' entry scripts (conc/conc2/conc3/concnow P1[0]) SET0x6DE.concendis a single ungated epilogue record.concnowone-shot ladder, each C1 gate a self-latch in its own record:P2[13]->0x3ED,P2[14]->0x3EE,P2[15]->0x3D2(at its tail+0x1483),P2[16]->0x3CE,P2[18]->0x423, plusP2[20]->0x3CF.0x3EFis the chapter-wide "Conkram revelation" gate.concnow P2[15]SETs it from a branch arm (+0xDDD, after the emitter run + BGM cue, jumping straight to the record tail). Its operand byte is outside ASCII, so census reads are alias-immune: cleanTestsites in fifteen scenes spanning Sebucus (balden/balden2/bylon/dolk2/geremi/jiji/rayman/rayman2/retock/ropeway) and Karisto (koin1/koin2/son/doman).0x423is a cross-scene message, not a one-shot.conc2 P1[0]consumes it on entry (Test 0x423->Clear 0x423,Set 0x664,SpawnRecord 0x69at+0xDB..+0xE8): theconcnowbeat posts the flag and the nextconc2visit converts it into0x664(read byconc) plus a spawned follow-up record.
Cross-cutting patterns.
- Two low-numbered flags are variant discriminators, gating nearly every record of an alternate or interior carrier:
0x7(rayman2,retockin,town0d) and0xF(dohaty,nilboa,nilboa2). 0x7latches inside the opening-commit beat (theopdeene->town01handoff of a fresh new game, alongside549/0x226;captures/state_poll/2026-07-29T20-20-05Z), so the rendition selection is armed from the start of play.- Region hubs hold little or no gate state; the progression logic lives in the spoke dungeons.
- The story-numbered band
0x522..0x531is engine scratch: a one-hot exit selector + fade handshake repeated in nearly every scene's entry script (script-vm.md § the0x527..0x531scene-transition scratch band). - Clean-tagged census rows over flags whose operand byte is printable ASCII can be dialogue bigrams (
ta/s,/Sp); the wide reader lists of0x461and0x32Cdissolve entirely under that check (script-vm.md § ASCII dialogue aliases).
Play-order captures (poll tier). Corpus captures/state_poll/2026-07-29T20-20-05Z / 2026-07-29T22-21-04Z / 2026-07-29T22-53-56Z, mined via analyze_state_poll.py --only flags. Screening rule: a save-state load emits a sub-bulk flag delta whose signature is a mode churn plus flagclr rows plus a full inventory re-key at one tick, with the destination scene registering about 43 ticks later; every burst with that signature is excluded.
| Region | Live SET order (tile) |
|---|---|
retona | 0x354 @(67,80) -> 0x355 @(69,78) -> 0x356 @(68,80) -> 0x3AD @(66,79); 0x357 latches during the next scene entry (mode-2 transition frame after an overworld round trip - the P1[0] backstop converting 0x3AD); then 0x367 @(67,78). 0x353 is already latched in the starting state. |
dohaty | One corridor walk: 0x343 + 0x63D @(23,42) -> 0x344 @(23,50) -> 0x345 @(23,53), leads 0x39F / 0x65A interleaved; the P2[10] 0x344 one-shot and the 0x63D pair both fire. The last beat @(23,56) latches 0x1D4, a flag in balden P2[0]'s C1 list (a cross-scene edge). |
taiku | 0x517 @(54,17) -> 0x519 @(54,7) -> 0x38F @(54,40) -> the P2[16] pair 0x380 + 0x382 @(16,28), one beat one tick. The other self-latch pair 0x390 does not fire (branch / optional content). |
kor5 | 0x43A @(32,92) -> 0x436 @(32,40). 0x6C4 appears only inside a load delta. |
korb3 | 0x41D @(36,21) (its C2 includes kor5's 0x436, satisfied organically beforehand) -> 0x41E @(36,23) -> 0x41F @(37,24). |
map03 hub | P2[15] 0x378 @(93,69). |
| Sebucus spine | teien 0x1C8 @(41,45) -> 0x1C9 @(44,43) -> 0x332 @(44,47) -> tower 0x1C7 @(13,69) -> geremi 0x1BF @(34,117). tunnelc P2[6] 0x34A @(23,45); its P1[4] 0x360 / 0x362 do not fire (branch arms). balden's 0x1D5 latches at the tunnelc exit tile (60,16); its 0x5B3 self-latch does not fire. |
| Rim Elm opening | 549 at the opening commit, with 0x226 in the same beat. |
korb3's collection group plays backwards from its gloss.0x403latches on themap03overworld at the castle-approach node (72,41) before any collection flag, and all nine C2 flags (0x43E..=0x444,0x459,0x45C) mint together in a singlekorb3arrival burst (BGM stop -> nine sets -> new BGM, no load signature). The group's records are C1-retired before ever spawning.ropeway/ropeway2/jijiare walked organically in the capture corpus as well.- Walked in this corpus without an organic family SET:
retock/retockin(entered with0x357latched;0x502does not fire),doman(only the unpinned lead0x379fires;0x3FBdoes not),nilboa/nilboa2andson(entered mid-arc from loaded states; the only nilboa flag burst is a load frame).
In-bracket write order (disassembly). The playthrough cards place each remaining family between two milestone saves; where a family writes more than once inside one bracket, the scripts fix the order. Counting only story SET sites (clean, not a prose alias, not a developer flag-menu arm):
rayman:0x201 -> 0x1FB -> 0x200 -> 0x1FC, forced.0x201is set by the talk recordP1[47];0x1FBbyP2[12](C20x201),0x200byP2[18](C20x1FB),0x1FCbyP2[19](C20x200). Each link has one writer and each writer requires the link before.bubu2:0x609then0x3D3, one beat. Once0x608is set (the bracket before), the live writer isP2[0](C10x3D3, C20x608), which SETs0x609and0x3D3on adjacent instructions (body+0x1E/+0x20).P2[2]carries0x608in its C1 andP2[3]is retired byP2[0]'s0x609.- retock:
0x502before0x33B.0x502's writer isretock P2[33](C10x502, C20x357), spawned only by Eliza's talk dispatchP1[31], which tests0x33Bfirst and diverts to her post-0x33Blines when it is set, then spawnsP2[33]once0x63C(set byP2[16], the first talk) is up.0x33B's writer isjagaroom P2[8](C20x351, which thejagaroom P1[8]actor sets when it spawns), and nothing there waits on0x502. A play that latches0x33Bfirst can never write0x502; the Dohati save holds both. Full chain:0x357 -> 0x63C -> 0x502 -> 0x33B -> 0x34F(jagaroom P2[9], C20x33B). - deroa:
0x3E1before0x46D.P2[4](C10x46D, C20x3E1);P2[5]/P2[6]carry only their own latch. retock P1[1]/retockin P1[1]also write0x33B,0x502and0x357, but as developer flag-menu arms (flag-op runs ending in aJmpRelback to the picker), not story writers.
Pinned by man_variant_carrier_census_disc.rs::region_gate_in_bracket_write_order.
The play-order residual
What is not played organically, and why none of it leaves a question open. The generic C1 / C2 seeder drives every family.
Arrival measured, family SETs unplayed: rayman, station, station3, bubu1 / bubu2 and deroa, each entered from a card-boot state (script-vm.md).
- No gate family fires on arrival. Each spoke writes only its own arrival latch in
0x491..0x49Cat entry-script+0x18,map03keeps a one-hot story-stage word in0x56D..0x570, and every entry clears the band0x526..0x52E. - The chains fire only on walk-on and talk beats:
rayman's0x201 -> 0x1FB -> 0x200 -> 0x1FC,bubu2's requires-all list,station/station3behindtaiku's0x38F,deroa's0x3E1-gated descent.rayman2also needs flag0x1D5, which every ladder save from Mt. Letona on carries.
Walked without an organic family SET: retock / retockin - 0x357 is pre-latched and 0x502 does not fire, its writer hanging off Eliza's talk loop. Captured outside the poll corpus: doman's 0x3FB (P2[4]), son's arrival family, and the kor5 tail's 0x6C4 (P2[8], with its 0x436 input poked rather than played). The retock / doman / nilboa entry families are measured (field / locomotion). retock, doman, nilboa, son and the kor5 tail are walkable from a catalogued card block; the remaining spokes are the ones no card reaches.
Card brackets place every remaining family between two milestones. The two playthrough cards hold one retail save per story milestone, so a flag clear in one save and set in the next is written by the play between them (region_gate_card_brackets, save-library gated).
| Flags | Between |
|---|---|
rayman's 0x201, 0x1FB, 0x200, 0x1FC | Sky Gardens and the Fire Path |
0x1D5 | the Fire Path and Mt. Letona |
retock's 0x357 | Mt. Letona and Ratayu |
0x33B, 0x502 | Ratayu and Dohati's Castle |
bubu2's 0x608 | Dohati and Sol Tower |
bubu2's 0x3D3 / 0x609, doman's 0x3FB | the Sol Tower B2 and Usha saves |
the kor5 tail | the two Sol Tower saves |
0x370 | Usha and Nivora |
0x378, 0x3A6, 0x60D | Nivora and Zora |
0x38F, 0x3A7 | Zora and Conkram |
deroa's 0x3E1 / 0x46D..0x46F | Rogue's Tower and Jette's Fortress |
Two in-bracket orders are not script facts, and neither is observable:
- doman's
0x3FBagainst bubu2's pair.doman P2[4]carries only its own C1 latch, so the order is the route the player takes between the two scenes. - deroa's
0x46E/0x46Fagainst0x46D.P2[5]andP2[6]carry only their own C1 latch and no op0x44in deroa spawns them, so a walk-on tile does and the order is where the player walks. - Every reader of the three flags reads that one flag alone.
0x3FBhas no field-VMTESTanywhere; its only C1 holders are its writerdoman P2[4]and the two ending epiloguesedbalden/eddomanP2[3], each listing[0x3FB]alone. 0x46E's one reader isderoa P2[8], whose whole body is that test in front of[4C 38](the camera-region query at the player's tile) and an idle loop.0x46F's two readers are deroa's scene objects:P0[0]parks its object atMoveTo 7F 7Fwhen the flag is set, andP0[5]takes[4C 42]target-600when it is set and-1500when it is clear.- No record lists any of the three in a C2 gate, and the SCUS and overlay images carry no literal call of the flag helpers
FUN_8003CE08/_CE34/_CE64with these ids. Each flag's value is pinned by the milestone saves on both sides of the bracket. Pinned byregion_gate_unordered_flags_have_no_joint_reader.
Measuring a spoke's arrival without a walk. A walk-on door is an exact tile match in the .MAP kind-1 trigger table, so writing the player object's position onto a door tile crosses it in about ninety vsyncs (autorun_w5a_poke_walk.lua; door tiles come from each .MAP's +0x10000 / +0x12000 gate rows). A door that opens a Yes / No picker takes one confirm press on top. Paired with the single-flag write watch (autorun_w5a_flag_watch.lua) that is one run per spoke from a card block that reaches its region; it measures arrival, never the walk. A flag written every other frame from an entry script's per-frame body is a one-hot selector or a position predicate, not a progress latch (chitei2, conc; script-vm.md).
Extraction-0874 §2 (player.lzs) F-variant pixels - a one-shot opening face-frame stamp, not a menu writer
Status: resolved - the installing event is named
The three F-variant halfwords in the field-character texture band are stamped once per game by the Rim Elm opening cutscene, through a field-VM MoveImage op.
- The words.
(853,271)3333 -> ffff,(856,271)3333 -> fff3,(857,271)1e33 -> 1e3f- row 271 cols 1 / 4 / 5 of the Noa strip (TIM 2 at(852,256)20x128; rows 271 / 273 = its rows 15 / 17). - Installer.
town01MANP2[3](★OP, the Rim Elm opening timeline record, C1-gated on the opening latch0x225), body+0x392/+0x3A0. After the opening's white flash + 60-frame wait it stamps the Noa face cell:MoveImage (852,336,6,16) -> (852,268)and(852,368,4,8) -> (853,284). - Op.
4C 60, a literal-operand MoveImage[4C 60 src_x src_y w h dst_x dst_y]with six misaligned u16s read viaFUN_8003CE9C; handler arm0x801E1B28..0x801E1B90,jal FUN_80058490at0x801E1B84. - Disc location. MAN offsets
0x735A/0x7368(PROT 0004 §1, LZS at container0x25BEB). The operands are misaligned u16s, so an aligned scan does not find them. - Live catch. At
ra = 0x801E1B8Cthe stamp reproduces the s3 anchor band byte-exact (autorun_s2s3_atlas_stamp.lua). The parked alternate frame differs from the boot cell at exactly the three halfwords. - Lifetime. The
0x225C1 gate fires once per game, so every post-opening save carries the variant; the first battle effect-texture re-upload restores the disc bytes. A freshly booted game does not hold it: the title screen is all-zero and the mode-2 field-entry load uploads the disc bytes. - Not the pause menu (grade
capture, exhaustive): with every DMA2 kick chain-walked forA0/80/E3/E4/E5packets and GP0 PIO stores hooked, the whole pause walk issues zero image transfers and the band is byte-identical before and after. A 49-state library census shows plain field saves carrying the variant with no menu in their lineage whiles1/s2hold disc bytes; the flip brackets inside thetown01opening (s2 -> s3). - Not a parked wrap-scroll phase. The three words equal the disc words at
(x,273)by frame-content coincidence: the Noa strip is not shift-invariant, so a parked +2-row rotation would move dozens of rows. The wrap-scroll installers (move-VM op0x1E, body0x80023694; the op0x45sibling) and theFUN_80021DF4dispatch-4 arm never fire across a full s2 -> s3 replay while the flip reproduces (autorun_s2s3_scroll_installer.lua).
Owning page: character-mesh.md.
What the op-0x49 entry-context kind byte is, and which screens it selects
Status: resolved (disassembly + disc measurement) - save-screen.md § Root command picker
The pause / save driver FUN_801DC6B4 routes on *_DAT_8007B450, and the byte it reads there is the op-0x49 sub-op.
- Writers. Ten
sw rt,0xb450(rs)sites exist acrossSCUS_942.54and every extracted PROT entry. Two store a dereferenceable pointer: the field VM's op-0x49Idle arm (0x801E09A8, storing the script's operand pointer, whose first byte the arm reads at0x801E0984) andFUN_801D0B90's countdown expiry (0x801D0D04, pointing at the static recordDAT_801F2278, kind0x0B). The other eight store0or the1Done sentinel; the resume clears the slot at0x801E08D8. - Kinds that select a screen, each at exactly one selector write in PROT 0899 (of 66
sw rt,0x46a4(rs)writers):0-> sub-screen0x1A,1->0x19,7->0x20(the casino prize exchange),0x0D->4. Kind0x0Dalso sends the root picker's cancel to sub-screen3(0x801D6D18). - Kind
0x0Dis a scripted pre-battle party menu, briefed on entry and ready-checked on exit. Sub-screen4draws window6, six static VAs in the overlay pool loaded at0x801d636c..0x801d6448(a pre-battle briefing); sub-screen3draws window5, whose two headings (0x801CEC78/0x801CEC94) are a battle-start ready check, not a "really leave?" gate. - Reachability.
crates/engine-scene/tests/op49_sub_op_census.rswalks every scene MAN's field-VM script and tallies the sub-op operands twice - a bounded, offset-deduped opcode walk and a raw byte upper bound - and kinds7and0x0Dappear in both. The walk decodes no tile-board sub-op (5) that the byte bound finds, so an absent walk row means "not decoded here", not "not on the disc". - Port.
World::record_op49_park/World::menu_entry_context_kind,FieldMenuSession::{open_entry_screen, Notice, ReadyConfirm}.
What Soren Camp's sky is
Status: resolved (disassembly + capture)
The sky over son is an object actor like any other, seated by a cutscene: P0[5], the kind-1 bind of the map's one placed record (cell (16, 103), pack mesh 57), opens with 23 7F 7F and parks, and the system script seats it for each shot, CC 05 51 08 10 05 00 here. The 4C 51 arm writes tile and heading into whichever actor the op names (0x801E1880, 0x801E1898, 0x801E1900); son_field_door_hop holds the actor at (1088, 146, 2112), heading 0xA00. Port: the object arm of run_to_tile, and both hosts keep a record parked at scene entry in their bake and hide it per frame (World::hidden_object_records), so a script can bring it back.
What the wedge at a party member's wrist is
Status: resolved (disassembly + capture)
The character's Ra-Seru. See character-mesh.md: FUN_8001EBEC copies the arm group with the Ra-Seru over the bare arm once the record's slot is non-zero. The centre dancer's green 0x7785 texels in minigame_dance_retail_go_in, Vahn's red bracer and Gala's blue glove are that one swap.
Text / fonts / dialog
# Settled threads: Text / fonts / dialog
| Thread | Answer | Grade |
|---|---|---|
Where does the escape-expansion buffer at 0x800740EC end? | the longest shipped string uses 179 bytes of the 0x206-byte zero runWhy
| disassembly + census |
| How wide may an item or spell description line be? | 144 px, unwrappedWhyNo routine on the path tests a width. | disassembly |
| When does a sliding picker first take input, and does the inn picker's cursor wrap? | the call after the count reaches zero; 0x2A clampsWhyThe call that takes the slide count to zero ends | disassembly |
Does the PAL (SCES) text renderer remap 0xD7 and 0xF8? | no - retail draws the placeholder boxesWhyAll four PAL executables share one renderer (FR | disassembly + capture |
| Does a PAL executable rewrite text at draw time? | French only: oe / OE to the ligaturesWhyThe French renderer folds | disassembly |
| How does a dialog picker open? | it slides in from off screenWhyA press in | disassembly + capture |
| Does the USA dialog font carry accented glyphs? | yes - 32 inked high cellsWhyThe font TIM at | capture |
| When does a dialog picker open, and what presses for the player? | only on a 0x19 press; _DAT_80073F00 presses automaticallyWhyThe picker states | disassembly + capture |
| What does a confirm press do during a short-row hold? | clears the hold and latches skip speed 0x25WhyOn the 72-tick hold the press clears the hold and latches | capture |
| Does retail wrap dialog text to the box? | no - no text surface wrapsWhy
| disassembly |
| How wide can a field dialog row be? | 244 px at one extra pixel per glyphWhyRows draw at the box x with | disassembly + capture |
| What ends an NPC talk? | FUN_80038050's verdict on the byte after the boxWhy
| disassembly + capture |
When does op 0x4C's sub-5 acquire refuse? | two testsWhyIn the arm | disassembly |
| Where does an NPC's first dialog line start when it opens on an escape? | at the escapeWhy113 of the 1586 talkable partition-1 records open their first line on an escape, | disassembly |
| What draws the battle command chips' words? | text: placement-record payload stringsWhyEach chip is a screen-element placement record whose | disassembly |
| How does a localized build lay out its UI strings? | by length, so pools move while their references stayWhyThe Spanish build's | disassembly |
| Do PAL builds keep the narration crawls' page counts? | no - they reflow with blank pagesWhyA crawl's page count is part of the script ( | capture (disc read of both builds) |
| Where does the narration crawl's geometry come from? | a scene config block seeded by CC F8 E8Why
| disassembly + capture |
| Dialog font extraction | resolvedWhyThe glyph page lives at VRAM | capture |
Inline dialog-box format (0x1F-lead segments) | resolvedWhy | disassembly |
| Tetsu 4-option spar menu mechanism | resolvedWhyThe menu is a standard | capture |
| Pause Items/Magic screens: remaining sub-flows | resolvedWhy | disassembly + capture |
What does PROT 0892 (card_data) carry, and who loads it? | the memory-card screen's JIS X 0208 level-1 kanji font; a container-formats::pack of two TIMs, not a truncated streamWhy | disassembly + capture |
Which routine samples the card-screen kanji page (VRAM (320..447, 256..511), CLUT rows 475..482)? | negative (nothing does - it is loaded and parked)WhyA sampler would have to carry tpage | capture (image sweep + GPU-FIFO watch) |
| What is the battle plaque's element badge, and how is it selected? | a caret escape inside the monster's own name stringWhyThe badge is not a separate field: the name carries | disassembly + census |
How is the text-cell placement run seeded, and what is the overlay_0897_801dbc30 dump? | three placement records, four stores each in a fixed order; the dump is a chimera of two PROT entriesWhyThe loop is PROT 0898's | disassembly |
What does FUN_80036044 count past a two-byte unit? | one extra byte past the NUL per unitWhyA pre-walk sizes the loop to the string's byte length, and the loop decrements that size once per iteration whatever it consumed, so each | disassembly |
| How fast does the field pager type, and when does a row end? | one unit a frame; a row ends on its glyph countWhyEvery pager open stores speed | disassembly |
| When does the field pager finish a typed row? | one call after the reveal counter passes the glyph countWhyThe row gate compares with | disassembly + capture |
| What does the field pager do when a page holds more rows than the box? | it scrolls; a page ends only at a control byteWhyAfter a row, | disassembly + capture |
Long write-ups in this area
Pause Items/Magic screens - remaining sub-flows
Status: resolved. Evidence: disassembly + capture.
The four sub-flows of the pause Items / Magic screens, and the 0x800 row dim bit, are traced from disassembly and ported in engine-ui / pause_screens.
| Sub-flow | Retail | Note |
|---|---|---|
| Window-14 target panel | FUN_801D0520 | Its preview modes are the permanent-stat Water previews, not an HP-restore preview. |
| PAGE sprite | UI-icon 0x76 | |
| Kind-4 list kernel | FUN_80032A44 + allocator FUN_80030104 | SCUS-resident. |
Class-0x80..0x82 Use routes | submenus 0xA..0xD | Single-target apply FUN_801D8308; Door of Light / Wind FUN_801D8A58 / FUN_801D8B90; Incense FUN_801D8D94. |
The 0x800 dim bit is set at build time, never on focus:
- Writer: the SCUS content builder
FUN_80030628, content-id-3 case. It dispatches on live window+0x1C, copied from descriptor byte+0x0at create (0x80032990). - Rule, in order: equipment is always dim; Door ids
0x88/0x89are scratchpad-gated; the field-usable bit0x2; then the applicability probeFUN_8003043C(battle context gates bit0x4). - No focus-dependent write exists. The white-to-grey flip is the kernel's mode-4 park override; a capture shows the row words bit-identical across focus states.
Owning page: field-menu.md; the row build is at field-menu.md.
Inline dialog-box format (0x1F-lead segments)
Status: resolved (prologue, pager-side dispatch, option-list format, multi-segment box packing). Evidence: disassembly.
Placement-NPC and event dialogue is inline in the field-VM interaction record, not in the scene MES: a run of 0x1F-lead, 0x00-terminated segments of MES glyph bytecode, each one line. The record is ordinary field-VM bytecode around those lines, and the dialog SM FUN_80039B7C runs the field VM over it, handing a text byte to the pager FUN_801D84D0.
Where the text is
- The opcode-decoded
text_idis a box-config id; it never resolves throughSceneMes::message_offset(0 of 13 town01 placement-NPC ids resolve). - The text is recovered structurally, not from the
0x3Fop'slen: a text-heavy record desyncs under linear disassembly (a literal>is0x3E, the warp / interact opcode; ASCII punctuation hits the0x37/0x41yield bytes), solenreads empty for every town01 NPC. - Each record holds the NPC's whole line set - every line of every story-state branch, with
Yes/Nolabels interspersed (Village Elder 80 segments, Val 59). Multi-page speech is several0x1Fsegments, not0x80..=0x9Fcontrol bytes inside one. 36 town01 placements carry renderable dialogue (the sparring partner, Meta the dog, villagers, leftover "dummy" developer placeholders, and a developer story-flag toggle menu at placement P1[1]). - No "box-geometry header" exists. The bytes between
script_pc0and the first0x1Fare the interaction prologue:CFlag/SysFlag.Test/JmpRel/Nop/0x4C 0x51(move to tile) /0x4C 0x52(menu-activation poll). The prologue's story-flag branchesJmpRelto the segment the box opens on.
The dialog SM FUN_80039B7C
- State 0 calls the field-VM dispatcher
FUN_801DE840on the stream and enters the pager only when the PC rests on a byte with& 0x7F < 0x20(a0x1Flead or the0x21terminator). - It advances
actor[+0x9C]0 -> 1 -> 2 through the segments. Its state-2 advance masks(byte & 0xF0) == 0xC0and consumes the escape's data byte, so a0xC?escape whose argument is0x00..=0x1E(0xC1 0x00) does not end the line. - Retail's interaction cursor is one instruction past the
0x21the spawn section executed, notscript_pc0(script-vm.md). It persists across talks: the forwarded PC is stored before the0x21test (0x80039E24), so a chest's second touch resumes behind its grant. - A raw
0x21after the prologue has spawned a record ends the talk (FUN_8003CF7Cbreaks on it):vozzP1[7] spawns P2[13] and stops. - The talk ends on
FUN_80038050's verdict on the byte after the box (see the "What ends an NPC talk?" row); the pager itself only clears rows and returns no status. - Not the renderer:
FUN_8001EBECis a per-character TMD-pose copier (party slots 0..2, indexed by the slot-4 freeze flag_DAT_8007B824; sevenu32s from TMD+0x124..+0x140or+0x140..+0x15C, gated on record flag+0x75E; see op0x4Csub-3 inscript-vm.md).FUN_8003AB2Cis the per-frame field-VM driver andFUN_8003BDE0the partition-record dispatcher.
Box packing
- Consecutive
0x1Flines pack into one window of_DAT_801F2740 = 3rows: a0x00terminator directly followed by0x1Fis "same box, next row". A fourth row scrolls the window (state0x0C); the page runs on to the next control byte. - The contiguous run stops where the pool hands control back to the field VM (a non-pager control byte,
Dispatch::Unknown).
Post-page dispatch (pager state 0x19) reads the control byte past the box, matched as byte & 0x7F (so 0xA7..0xA9 are accepted; the field corpus stores the bare form):
| Byte | Next state | Meaning |
|---|---|---|
0x25 | 0 | end |
0x24 | 3 | next page, same box (own prologue, jumps away at 0x801D916C) |
0x48 | 9 | new box |
0x4C 0xFF | 6 | terminate |
0x2A | 0x11 -> 0x12 | inn Yes / No picker |
0x27 / 0x28 / 0x29 | 0x13 / 0x15 / 0x17 -> 0x14 / 0x16 / 0x18 | 2 / 3 / 4-option picker |
| other | 9 | new box |
- Each odd picker state is the slide-in (see the picker rows above).
- Three arms run the box-reset tail: states 0, 6 and 9. They are byte-identical over their
0x98bytes except theli v0,Nchoosing the successor (0 -> 1, 6 -> 7, 9 ->0xA). JT[1] == JT[4] == JT[7] == 0x801D8708is the teardown (early return in state 4, so0x24keeps its rows);JT[0xA] == 0x801D92A4is the box-open animation. So0x25and0x4C 0xFFbehave alike and both differ from0x48- the port'sEnd/TerminateversusNewBoxgrouping.
Option list (picker control region)
- Layout:
[open][N * 2-byte i16 LE jump table][continuation?][N * 0x1F label segments]. - The continuation byte is optional: a post-page dispatch byte (
0x24/0x25/0x48/0x4C), or absent with the labels starting at once (the immediate-labels form - the Tetsu spar and town01's other pickers). - Each 2-byte entry is a signed relative jump
FUN_80038050applies on confirm:new_pc = (open + 1 + index*2) + i16_LE(entry[index]). The entries are not the labels. The fourizumire-emissions shift every entry by one per-emission delta, and every option lands in-bounds. - Each picker arm sets the box size from a per-N table and clamps the cursor at
*(DAT_801C6EA4 + 0xC); on confirm it reads the continuation byte atopen[N*2 + 1]through the post-page table.
Port
| Piece | Location | Pin |
|---|---|---|
| First-line finder | man_field_scripts::first_inline_dialog_offset (printable-ratio gated); classify_placement -> PlacementKind::Npc::dialog_inline | |
| Segment pool | dialog::decode_inline_segments | field_actor_placements_disc::inline_dialogue_decodes_into_full_segment_pool |
| Prologue decode | field_disasm::LinearWalker | field_actor_placements_disc::dialog_prefix_decodes_as_field_vm_bytecode |
| Box packing | legaia_mes::dialog_box (pack_box / pack_boxes, LINES_PER_BOX = 3, Dispatch) | field_dialog_boxpack_disc (all 561 town01 boxes <= 3 lines; the spar opening = three 0x24-chained pages -> a 4-option Picker; the Mist appeared, .., but line survives its 0xC1 0x00) |
| Pickers | legaia_mes::picker (scan_pickers / parse_picker_at / Picker::jump_target) | field_dialog_pickers_disc (config On / Off / Exit, shop haggling, the Genesis-Tree quiz; in-bounds jumps) |
| Dialog SM | engine_field::inline_dialogue, World::step_inline_dialogue (PORT FUN_80039B7C) | field_interact_dialogue_disc, inline_dialogue_prologue_selects_segment_by_story_flag, ..._falls_back_when_it_cannot_reach_a_segment |
- The dialog SM path runs the record through the field VM, so an option's branch executes its flag ops and scene changes before the reply. It is gated by
World::toggles.use_vm_dialogue, theplay-windowdefault;--simple-dialoguetypesWorld::npcs.dialogthroughOwnedDialogPanel::from_inline_dialoginstead. - The untruncated record is kept as
World::npcs.dialog_prologue(man_field_scripts::placement_interaction_record, entered at the interaction cursor). On a first talk the runner falls back to the first segment when the prologue reaches none. A talk resumed from the cursor an earlier talk wrote back ends on an executed raw0x21before any box, as the SM does, and never falls back (InlineDialogPrologue::resume): a chest's second touch runs its "empty" loop, a pickup spot's is silent (the interaction cursor).
Owning page: mes.md (pager and picker layout).
PROT 0892 is the card-screen kanji font
Status: resolved. Evidence: disassembly + capture.
PROT 0892 (card_data) is a container-formats::pack of two TIMs carrying a JIS X 0208 level-1 kanji font for the memory-card screen; the entry is 33 sectors.
- Loader: mode-22
CARD INITFUN_8002574Conly - alloc0x19000at0x800257D0, retail legli a0,0x37e+jal 0x8003eb98at0x8002580C(raw TOC0x37E= extraction 0892), then a pack walk at0x8002581C..0x80025850handing each member toFUN_800198E0. - Members: two
0x8220-byte 4bpp TIMs, CLUT(0, 475), pages(320, 256)/(384, 256). - Encoding: the CLUT is a bit-plane selector over a 1bpp font packed four planes deep - 12 px pitch, 2,965 inked cells (the level-1 kanji count), plane 0 in ku-ten order.
Owning page: data-field.md; the page is never sampled in the USA build (save-screen.md).
Animation
# Settled threads: Animation
| Thread | Answer | Grade |
|---|---|---|
| At what cadence does retail step a gated field clip? | (rate*2 + div - 1) / divWhy
| capture + disassembly |
Does FUN_8001BE80's blend gate equal the clip tick's scaled-step bit? | yes - one header bitWhyBoth test clip byte | disassembly |
| What does the field frame blender interpolate? | translations linearly, angles through FUN_8001D088, with a retryWhyTranslations blend as | disassembly |
Does FUN_80021DF4's default motion block run for dispatch 3? | no - arms 3 and 5 both branch past itWhy
| disassembly |
| Player ANM per-record layout | byte-4 low nibble carries high4(t_z)Why | disassembly |
| Battle anim-id space + record[0] "strike family" | resolvedWhyAnim ids are entry indices (commit | capture |
FUN_80047430 caller | resolvedWhyA single dispatch site (capture | capture |
Record[0] +0x5C pointer + art-anim bank stream source | +0x5C = vestigial paired-relocationWhyArt streams = | disassembly (SCUS exhaustive; overlays partial) |
| Do the dance hall's beams move? | yes - plain floor actors on a clipWhyThe floor pass | disassembly + capture |
Which carriers arm the effect ribbon FUN_801CFA48? | op-0x42 nodes in four cast modules, battle onlyWhyMove-VM op | disassembly |
Is the ambient ramp pool FUN_80036D80 part of the motion VM? | no - an actor template of its ownWhyIts only reference is the tick word at | disassembly |
| How is the effect ribbon drawn? | as the actor's own TMD modelWhy
| disassembly |
Where does move-VM op 0x34 store? | through actor + 0x90WhyArm | disassembly |
Long write-ups in this area
Player ANM per-record layout
Status: resolved (container + per-(bone, frame) semantic). Evidence: disassembly.
A record is a 16-byte head followed by one 8-byte entry per (bone, frame); each entry is a nibble-packed translation plus three byte angles, decoded as the retail sampler FUN_8001BE80 (ghidra/scripts/funcs/8001be80.txt) does.
- Size:
record_size = 16 + 8 * (a & 0xFF) * b, witha & 0xFFthe bone count andbthe frame count. It holds byte-exact on all 296 records of the 5 pinned scenes, and on every other scene bundle the corpus sweep finds. - Head: 8-byte
(a, b, marker_1 = 0x080C, flag)header + 8-byte per-anim prologue. Offsets in the bundle's offset table are absolute byte offsets, not relative to+4. - Translation (bytes 0..4): three signed 12-bit values
(t_x, t_y, t_z). Byte 2 ishigh4(t_y) << 4 | high4(t_x); byte 4's low nibble ishigh4(t_z)(andi v0,v0,0xfat0x8001BF38) and its high nibble is unused. Sign-extend on bit 11. - Rotation (bytes 5/6/7): three
u8angles(r_x, r_y, r_z), each<< 4to a PSX 12-bit angle (4096= 360 degrees), composed Z, then Y, then X throughFUN_8004638C/FUN_8004629C/FUN_800461A4. - Posing: a piece poses
R*v + Tabout its own object origin (no centroid subtraction); frame 0 of an idle clip is the rest pose. - Blend: when clip byte
+1bit 0 is set,FUN_8001BE80blends two frames on the 4-bit fraction*(u16*)(actor+0x68) & 0xF: translations asa + (((b - a) * frac) >> 4), angles through the wraparound-awareFUN_8001D088, composing into scratchpad0x1F8002C0. - Not this layout:
FUN_80021DF4's+0x5A == 6block uses a separate 24-byte-per-bone keyframe layout.
Port: legaia_actor_formats::player_anm - BoneTransform::decode (one entry, pinned by bone_transform_decode_signed_12bit on town01 record 17) and PlayerAnmBundle::sample_bone (the two-frame blend); the size invariant is the disc-gated crates/actor-formats/tests/player_anm_real.rs. The site characters page runs the same (t, r) pipeline.
Owning page: anm.md.
Audio
# Settled threads: Audio
| Thread | Answer | Grade |
|---|---|---|
Why does track 2016 score worse under Rim Elm's river? | the river's volume writes delay the score's key-onsWhyThe emitter re-volumes its voices each part tick through | disassembly + capture |
| Does a walk into East Voz Forest keep the overworld theme? | no walk: map01 P2[31] is a cutscene the entry script spawns, and the engine keeps its musicWhy
| capture + disassembly |
| Does a field script change the BGM word inside a swap's read? | no script record doesWhyA walk of every scene MAN record finds no second sub-op | disassembly |
What clears the drive word 0x8007BC20 before a clip ends? | the stop FUN_8003ED04, which no battle path issues over a sounding clipWhySeeks and stream loads stop a clip, but in battle the selector, the module seeks and the side-band applier all keep off a sounding one. Write-up. | disassembly + capture |
| What raises a summon voice's CD volume past the cue level? | resolvedWhyAny change of the audio level cell | disassembly + capture |
| What renders a cast's cues for the real-PCM channels? | resolvedWhyThe engine itself: the retail comparison corpus's seed of the battle state, re-run with its director pulled once a tick and recorded from the gate ( | capture |
| What does PROT 0944's voice follow? | resolvedWhyThe roll arm | disassembly |
Why does one 2064 key-on go out with ADSR2 0? | resolvedWhyA layer the scan drops: | disassembly + retail interpreter |
| Why did the keying differential pick other voices? | resolvedWhyThe port's silent history started at index | disassembly + differential |
| Does a note-off look at the channel? | resolvedWhyNo. | disassembly + differential |
| When is a cast's voice heard? | resolvedWhyOn the arm of | disassembly + capture |
| How long does a CD-XA clip take to start after its trigger? | on mednafenWhy
| capture |
Is the zeto_call_wave_mid_cast track level a mix-law gap? | noWhyThe | capture |
| Why does Palma's parked flinch sound in the engine and not in retail? | resolvedWhyNothing holds retail's clip: | disassembly + capture |
| Why does a flee's walk sound no steps? | resolvedWhyThe flee staging zeroes the eight cue slots of every party slot's walk record, bank entry | disassembly + capture |
What is 0x8007B724? | resolvedWhyThe last sustained cue the drainer keyed ( | disassembly + capture |
| When does a fight's music change? | resolvedWhyThe intro init keys the field track off for sound set | disassembly + capture |
| Does the sequencer key notes on a coarser grid than retail? | noWhyUnder PCSX-Redux | capture + inference |
| Does a channel hold its pitch-bend wheel for later notes? | noWhy
| disassembly + capture |
| Does any XA stream on the NA disc use 8-bit ADPCM? | no; every channel is 4-bit at 37.8 kHzWhy
| capture |
| Does a BGM track change close the side-band (3) or reward (11) VAB slot? | noWhy
| disassembly |
| Does retail age SFX cues under the pause menu or a shop? | yesWhyMode | disassembly |
Is a field track the script left running audible under a mid-game movie's XA, town0d and jouine included? | yes, where the script left it soundingWhyThe movie path makes no BGM-slot or | capture + disassembly |
| What does BGM sub-op 7 do, and sub-op 5? | 7 stores the next scene's entry level ramp; 5 is a timed releaseWhySub-op 7 only stores | disassembly |
| What does a movie do to the BGM? | nothing; only the title attract actsWhyNone of the movie entry | disassembly |
| What do BGM sub-ops 2, 3 and 4 do to the sequence? | stop+rewind, key-off pause, replay from the topWhySub-op 2 is | disassembly |
| What does a scene-local BGM id (below 2000) load? | global slot 2, never a scene bankWhy
| disassembly + capture |
| Where does the credits bank land in SPU RAM? | VAB 10 at slot 0's base, over the SFX banksWhy
| disassembly |
| How does a Muscle Dome round enter battle mode? | the arena stores mode word 0x14WhyPROT 0977 stores | disassembly |
| When does battle mode init close the field bank? | only when the mode word reads 0x14Why
| disassembly + capture |
| Which field scripts push SFX cues, and how are the ids keyed? | op 0x36 sub 0 / 4 and motion op 0x09, ids straight from the bytecodeWhy
| disassembly |
Which bank does an op-0x36 sub-1 request stream? | a vab_01 bank, by four overwriting armsWhy
| disassembly + capture |
Who writes the frame-step floor DAT_8007B9D8? | each mode's init, and the scene through move-VM ext 0x2FWhyField init writes | disassembly + capture |
| How do the field bank and the class-2 bank share SPU memory? | one region, refilled per mode, behind a latchWhySlots 2 and 6 share a base. The field init loads PROT 0876 into slot 6 only while | disassembly |
| What does a cue whose bank is closed play? | nothingWhyThe drainer | disassembly |
What is FUN_80035B50? | a round-robin ring of four, not a delay queueWhyIt writes the cue and a zero timer into slot | disassembly |
| Why does the port sustain more sounding voices than retail on the same track? | two instrument defects, no engine change owedWhyThe pairing was never aligned: frame 0 of an engine trace is tick 0 of a track while a capture sits wherever play parked it, and the | capture |
| Which voice clip does a Seru cast play? | the module names it, not the casterWhyEach slot-B cast image hardcodes a literal cue id near its head - 62 of the 64, with PROT 0936 and 0937 forming theirs at run time - so the mapping is per module. Resolved through the cue dispatcher the ids land on fifteen streamed-audio files: | disassembly |
| What bounds the cue dispatcher's streamed-voice leg? | two decline gates, and nothing bounds the table indexWhy
| disassembly |
| Which tracks do the in-world minigames start? | through the piecewise extraction mapWhyThe loaders name PROT entries | disassembly |
| What does the sequencer's pause do to sounding notes? | it is a key-off, not a freezeWhy
| disassembly |
| What drives the CD / XA transport? | resolvedWhy
| disassembly |
| SPU reverb live routing (C7-REVERB) | wired; Studio C, globalWhy | capture |
| XA channel map / STR demux SM | static decompile of PROT 0970 + SCUSWhy | disassembly |
FUN_80018DB0 is a rumble cadence, not an audio one | libpad, not SsAPI; no cue to pinWhy | disassembly |
| Key-on pitch: what does retail put in the voice pitch register? | unity on centre; the port was an octave lowWhy | disassembly |
| SFX cue bank routing - the category byte selects the VAB slot | mechanism + the two pinned banks; portedWhy | capture |
| Which PROT entries fill SFX VAB slots 1 / 3 / 6 / 11 | slot 6 = 0876, slot 11 = 0889; 1 / 3 are variable banksWhy | disassembly |
The FUN_8006EF18 trio is a BIOS kernel-patch sequence, not an SPU init | negativeWhy | disassembly |
_DAT_8007B910 is the live audio level, not screen brightness | both hosts' labels correctedWhy | disassembly |
XA clip-table writer + (clip_id, chan) cue census | writer pinned statically; census in audio.mdWhy | disassembly |
| Hyper Arts fanfare selector - what audio fires when a Hyper executes | per-(char, art) coin flip over a fixed channel pair of the even-slot fanfare bankWhy | disassembly |
Op-0x35 sub-op 0xA - what the "unhalt-pause toggle" waits on | it is the track-swap commit; both globals pinned; portedWhy | disassembly |
What do bse.dat's record columns mean? | the static SFX-table columns; +4 is a u8 categoryWhy | disassembly |
Who consumes the bse.dat record-table pointer gp+0x678 (0x8007B990)? | cue router FUN_8004FE5C + the overlay-0971 sound test, via lui+lw the word scan cannot seeWhy | disassembly |
When does bse.dat load? | battle-scene setup, not bootWhy
| disassembly |
bse.dat 888 vs its 1195 sibling | one format, two occupants of the >= 0x200 bank roleWhy888 is the battle occupant; a scene prescript's record 0 fills the same slot in the field ( | disassembly |
Is there a second bse.dat record family past the table (0888 0x94C, 1062 0x51AD)? | no - a neighbouring file's PsyQ VagAtr tone rows left in the sectorWhy | capture |
What is _DAT_8007BA9C? | the BGM-swap force-reload latchWhyRead at | disassembly |
What is FUN_80065034? | SsUtKeyOnVWhyIts arguments are | disassembly |
| How is a retail VAB carried in its DATA_FIELD stream? | two chunks, and the bodies are the secondWhyThe 4-byte word in front of every | capture (disc census, crates/vab/tests/corpus_chunk_carriage.rs) |
What is the resident monster.snd index? | resolvedWhy
| disassembly |
A VAB's VAG-body origin - is the +4 a format property? | two wrongs that cancel on all but six carriersWhyThe parse reports a body origin four bytes early and the upload re-slices by the same four, so the pair cancels on 212 of 218 carriers and the skew read as a property of the format. Six entries break the cancellation - | capture (disc census; both wrongs are the port's own parse and upload) |
| What is in PROT 1062? | a single-chunk DATA_FIELD stream carrying a SEQWhyOne chunk, type byte | capture (disc read) |
| Where does a scene's entry BGM come from? | the scene's own script, not the loaderWhyThe BGM request word | disassembly |
What is the dev menu's BGM CALL arm? | it plays a track; cycling is a different routineWhyThe arm at | disassembly |
| What makes a captured SPU voice audible? | the envelope level, not the phase wordWhyMednafen's | capture |
What is BGM id 0x1000? | a park sentinel, shared by both streaming slotsWhy
| disassembly + capture |
| Does a save state's scene decide which BGM it holds? | no - 0x8007BAC8 doesWhyOf six catalogued | capture + disassembly |
| What is retail's reverb depth and work area? | vLOUT = vROUT = 0x3264, mBASE -> 0x79020WhyThe depth pair is what | capture |
| Is slots-per-note a difference between the port and retail? | no - it is a property of the arrangementWhySounding voices over distinct | capture |
| Does a Muscle Dome round load the class-2 bank? | yes - PROT 0869 into slot 2, as a field battle doesWhyFrom the arena's hub into a round, | capture |
What do FUN_8001E54C's chunk types install? | bank and score, no LZSWhyJump table | disassembly + disc scan |
Is every music_01 slot a [VAB][SEQ] pair? | 75 of 81WhyThe installer walk splits 75 of the 81 slots into a bank and a score; slot 72 is a score with no bank, 76..79 are one-sector fills and 80 is a bank with no score, and the old magic hunt agrees on all 81 ( | capture (disc-gated walk) |
What does field-VM op 0x35 sub-5's expiry do? | a scheduled field-BGM pauseWhyThe expiry arm of | disassembly |
| What does the field init's slot-10 load serve? | the credits theme 0x814Why
| disassembly + capture |
Is FUN_80064090 (the sequencer's channel restart) reachable? | noWhyIts only reference is the | disassembly |
What does field op 0x35 sub-op 8 (FUN_80019898) sound? | nothing, on every catalogued stateWhyIt replays once the sequence bound to the second sound-source record | disassembly + capture |
Are the .MAP / .PCH strings in the sound-driver cluster SoundArtist intermediates? | no - they are field files, loaded by FUN_8001F7C0WhyEach is referenced once, by the field-file loader: its dev branch builds | disassembly |
Who writes the 0x75EA CD input volume a battle voice plays at? | the clip player's CD-ready state machineWhy
| disassembly |
Who writes the positional emitter's per-row scale pair at 0x80091658? | the boot-time mode initialiser, onceWhy
| disassembly + capture |
Long write-ups in this area
Op-0x35 sub-op 0xA is the track-swap commit
Status: resolved; ported. Evidence: disassembly (a store-offset writer census over SCUS and every based overlay image, address-reference-scan.md).
Sub-op 0xA (arm 0x801E0264, field overlay 0897) is the commit half of the sub-op 9 / 0xA swap handshake, not a toggle: wait until the incoming track is staged, release the slot's paused occupant, acknowledge with flag bit 4 (which unstalls the poller's install), clear pause bit 1.
- Nothing writes
_DAT_8007B868. Its only store in the static corpus is a read-modify-write clearing bit 1 at0x8001E008in the boot mode-initFUN_8001DCF8; a raw byte sweep over all 1233 PROT entries adds one incidental data word (0392_map03.BIN +0x2bc40, surrounded by non-code). The word never goes non-zero in retail play. It is the dev / dual-mode gate the whole actor-sound family (FUN_800266E0/80026520/26740/26478/26410) checks, so the arm's early return mirrors its callees. _DAT_8007B750bit 3 has one setter:ori v1,v0,0x8at0x800246D0inFUN_800243F0, the BGM resolver / poller's load-settle stage. It is reached only while a track swap is in flight, after the settle countdown atgp+0x768(armed to0x1Eframes at load start,0x3Cwhen master mode is 2) hits zero. The poller then stalls its own install while bit 0 (sub-op 9's script-owned start) is up and bit 4 is not (0x800246E0..E8).FUN_80026520closes whatFUN_800266E0only detaches.800266E0resets the pan state and rewinds / stops the bound sequence (FUN_80064370, theSsSeqRewindwrapper), leaving the source active.80026520VSyncs, clears the source's active flag (+0x8), rewinds and closes the handle (FUN_80061E94, theSsSeqCloseshim). Together they are a full slot release; the poller's own teardown calls both.- Sub-ops 2 / 3 / 4: sub-op 2's pause sets flag bit 1; sub-op 3 also sets it and calls the voice-stop
FUN_80026740; sub-op 4 clears it and calls the re-attachFUN_80026478.
Port: SceneHost::route_bgm_events routes sub-op 10 to BgmDirector::unhalt_pause (release the source only while the pause latch is set, then clear the latch unconditionally), overridden by AudioDirector, the one director both play hosts run; its starts also clear the pause gate, as retail's sub-op 1 arm does. Test: crates/engine-scene-host/tests/bgm_midscene_change_disc.rs (town01's cutscene records carry the op).
Owning pages: audio.md (protocol + flag-word bit map), script-vm.md (the arm). See ghidra/scripts/funcs/800243f0.txt, 800266e0.txt, 80026520.txt, 8001dcf8.txt.
Hyper Arts fanfare selector
Status: resolved - selector pinned in code and capture.
A Hyper art fires no pool shout: its action constant sits below the shout table's lo bound. Instead:
- The staged-animation materialiser
FUN_8004AD80, on the Hyper class byte0x1Aatactor+0x1DA, reads the queued art constant and fires the jingle queue (FUN_8004FCC8->FUN_8003D53C) withjingle_id = rand()%2*3 + base(class testli v0,0x1aat0x8004B71C; one arm's draw at0x8004B91C..0x8004B944, joining thejalat0x8004BB34). - That is a per-(character, art) coin flip between the fixed channel pair
{base, base+3}of the character's stereo fanfare bank (the even clip slots: VahnXA1.XA, NoaXA3.XA, GalaXA5.XA). There is no avoid-repeat memory, unlike the shout pool. - Super and Miracle expansions take a sibling branch to fixed ids
0x101/0x111/0x121(0x8004B7AC..0x8004B7D0), the same bank's generic channel 1. A Miracle's finisher additionally fires its anim cue track (FUN_800508DC, ids0xC8..0xFFrebased+0x38).
All nine per-art Hyper rows are capture-witnessed off the FUN_8003D53C staging globals, and every witnessed duration reproduces the 0x800788B8 table arithmetic against the real SCUS. Unwitnessed: the second pair member for seven of nine arts is rule-derived, as are the Vahn / Noa Miracle finisher cue-track ids and XA1 channel 0.
Engine table: legaia_art::hyper_fanfare::CAPTURED_FANFARES. Owning page: battle-action.md.
XA clip-table writer + (clip_id, chan) cue census
Status: resolved - writer pinned; cue census decoded.
- Table.
0x801C6ED8holds 34[CdlLOC][len]slots =XA1..XA34; a title capture matches the disc files byte for byte. - Writer.
FUN_801CFA78in PROT 0895init.pak(base0x801CE818, recovered from four in-blob string refs) sprintf-generates\XA\XA%d.XA;1per slot and fills[BCD-MSF][size]through the ISO9660 lookupFUN_8005DBB4. It is called once from the init boot tick0x801CF500. The twolui 0x801csites in SCUS (FUN_8003D53C/FUN_8003EAE4) are the readers; the writer is overlay-resident, so no absolute-form scan of SCUS sees it. - Cues (caller census of
FUN_8003D53C/FUN_8003EAE4): menu voiceFUN_8004FCC8; the normal-move grunt (XA30chan 0 / 4 / 6, overlay0x801EEB44); the arts shout (FUN_8004C140->XA2/XA4/XA6per character, per-art channel pool); SM state0x6E(XA9via0x800787AF); slot machineXA1. - Capture.
scripts/recomp/xa_cue_capture.pyreads live-battle fires frame-tagged off theFUN_8003D53Cstaging globals and pins the live table variant and the packed second-half spans; witnessed picks arelegaia_art::arts_voice::CAPTURED_ART_CHANNELS. - Census caveat. A PROT-entry over-read aliases call sites into neighbouring overlays; dedupe by true entry extent (gameover 0902 and world-map 0901 have zero genuine XA calls).
Owning pages: audio.md (full one-shot + streamed cue census), battle-action.md.
SFX cue bank routing - the category byte selects the VAB slot
Status: resolved - a cue names its own bank, and both hosts stage two banks and route by category. Which PROT entry fills each slot is the next entry.
- A descriptor's
+4byte is a category. It selects the 12-byte mixer record at0x80091508 + category*12. - That record's
+8is a VAB slot id, not a level:FUN_80065034hands it toFUN_80068b98, which rejects it unless the per-bank open-state byte_DAT_801CE368[id] == 1(lbu/bneat0x80068BBC..0x80068BC8) and then repoints the current-bank globals at that slot (0x80068C34..0x80068C48) before the program / tone lookup. - Across the catalogued save states record
Nholds+8 == Nand+0== slotN's liveVabHdr, in every record of every state: categoryNselects slotN. - Slot 0 is PROT 0868 (a live field state's 512-byte slot-0
VagAtrprogram-0 page occurs verbatim in that entry at VAB offset+4,ps = 5agreeing). Slot 2 is the class-2 bank PROT 0869, which the battle scene loaderFUN_800520F0loads witha1 = 2. - Histogram over the 100 descriptors: category
0: 16,2: 53,6: 30,11: 1.
A port that fires every cue through one bank neither errors nor goes silent: both banks carry a one-VAG-per-semitone UI key map at program 0, so a category-0 id resolves to a sibling sample (PROT 0869's is roughly twice as long and a fifth lower, because its center bytes are authored higher). Peak, duration and "did a voice key on" all pass; only the source entry of the samples separates the two, which is what the disc-gated oracles assert.
Port: legaia_game_tables::sfx_table carries the law (slot_for_category, prot_index_for_slot, SLOT_BANKS, PINNED_SLOT_BANKS). engine-session's boot and the browser play page each stage the two resident banks out of one SPU allocator over their shared reserved region and resolve every cue through its own slot; categories 6 and 11 fall back to the class-2 bank.
Owning page: sfx-table.md.
Which PROT entries fill SFX VAB slots 1 / 3 / 6 / 11
Status: resolved - slot 6 is PROT 0876 and slot 11 PROT 0889; slots 1 and 3 hold banks that are re-selected at runtime, so neither has a fixed entry. Evidence: disassembly for the bindings, with a capture byte-pin and a structural cross-check.
A bank reaches a slot through one pair of calls. FUN_8001FC00(raw_toc_index, category, buf, append, len) streams the entry in; it ignores its second argument. FUN_8001E54C(category, buf, len) installs it: it indexes the same 12-byte mixer record the descriptors do, takes the header buffer from +0 and the VAB slot from +8, and opens the bank via FUN_8002630C -> SsVabOpenHead (sticky, at the SPU address the per-slot table at 0x800917B0 holds) -> SsVabTransBody. The binding is therefore a0 at each call site of FUN_8001E54C:
| Slot | Filler | Call site |
|---|---|---|
0 | PROT 0868 | resident system bank |
1 | current BGM bank (music_01), variable | FUN_800243F0, raw = *(0x8007BC64) + id - 2000 |
2 | PROT 0869 (raw 0x367) / 0875 | FUN_800520F0, FUN_801CF00C |
3 | a vab_01 side-band bank, variable | FUN_800243F0, raw = *(0x8007BBE4) + id - 2000 from _DAT_8007BABC |
6 | PROT 0876 (raw 0x36E) | field init FUN_801D6704 |
7 / 8 | the two monster.snd banks | FUN_8003E104 from FUN_800520F0 |
11 | PROT 0889 (raw 0x37B) | battle-end reward resolution FUN_8004E568 |
Cross-checks on the two pins:
- PROT 0889 populates exactly one
ProgAtrslot, number 10, and the one category-11descriptor (0x50) names program 10 with 2 voices against that program's 2 tones. The function that loads it is the one that fires the cue. - PROT 0876 holds 30 VAGs for the 30 category-
6descriptors, and its populated programs1..=7cover 29 of the 30. - A catalogued field state's live slot-6 and slot-1 header buffers match extraction 0876 and 0998 byte for byte - unique hits across all 218 VABs on the disc once the runtime-written
ProgAtr +8..0xFwords are excluded.
Two laws from the same read:
FUN_8001D424writes+8 = record indexfor all 16 mixer records, so "category is the slot" is the initialiser's own statement.- It assigns four pairs of records one shared header buffer, which
FUN_800265E8matches with one shared SPU base. Slot 6 and slot 2 are the same physical bank in two modes, so retail needs no extra SPU room for the field cues, and a host that stages once at boot cannot simply add them. The open-state array_DAT_801CE368holds slots0,1,3,6in every field-family state and0,1,2,7in every battle state, never 2 and 6 together.
Owning page: sfx-table.md (map, budget arithmetic, port surface).
The FUN_8006EF18 trio is a BIOS kernel-patch sequence, not an SPU init
Status: resolved-negative - the trio touches no SPU register, voice block or libspu global. Evidence: disassembly (the veneer bodies and the patch payloads are both read out of the executable).
FUN_8006EF68is a bare BIOS stub (li t2,0xb0; jr t2; li t1,0x4c) = B00x4CStopCARD. Its neighbours8006EF48/8006EF58are the same shape with0x4AInitCARDand0x4BStartCARD.FUN_8006F088callsGetB0Table, takes entry0x5B(ChangeClearPAD) as a version-stable anchor, and swaps five words between+0x9C8off it and the static block at0x8006F058. The shipped block is ajalrtrampoline back to0x8006F058, so after the swap the kernel calls a buffer holding its own displaced instructions, falls through into a0xC8-iteration busy-wait at0x8006F070, and returns: a timing delay spliced into a kernel routine. A swap is its own inverse, so install and teardown both call it.FUN_8006F118callsGetC0Table, takes entry6(ExceptionHandler) and copies three words from0x8006F180over+0x70..+0x78. That blanks the immediate pair its install-side siblingFUN_8006EFD0reads to reconstruct a kernel address (and then patches at+0x28with a jump out into SCUS).- Both are bracketed by
EnterCriticalSection(syscall(1)) andFlushCache(A00x44). - The install veneer is
FUN_8006EE8C(pad_enable):ChangeClearPAD(0),InitCARD, then_EFD0+_F088.FUN_8006EF18is its teardown mirror, which the callerFUN_8002035Cruns after closing eight kernel event handles.
Owning page: functions/runtime-libs.md.
_DAT_8007B910 is the live audio level, not screen brightness
Status: resolved - the cell is a volume and _DAT_8008457C is its persistent reference. Evidence: disassembly.
FUN_80062004(a, b, c)tail-callsFUN_80061EDC(a, 0, b, c)(the argument shuffle at0x8006200C..0x80062024) =SsSeqSetVol(slot, channel 0, vol, ...), so the halved cell ((v << 15) >> 16) thatFUN_800267A8passes lands in the volume argument.FUN_80026478handsv >> 1toFUN_8002657C, which writes it as both channels ofFUN_80064890(slot, vol_l, vol_r): a symmetric level, not a directional pan.- The dumped corpus holds 26 read sites of the cell:
SsSeqSetVol(six),SpuSetCommonAttr(FUN_8006BCB4, four, each building anSpuCommonAttron the stack with the cell in the CD-volume pair), the audio-context volume re-applyFUN_8002614C,FUN_8002657C, and arithmetic / tween plumbing. None reaches a draw primitive. - The cold reset
FUN_8001FFA4seeds0xD7(li a0,0xd7at0x8001FFE0) into both_DAT_8008457Cand_DAT_8007B910, then callsFUN_8002614C(0)(0x80020020). A0..255cell halved is libsnd's0..0x7F.
What the ramps are:
- Battle-action states
0x35/0x6F/0x70duck the mix to 75% of the configured level (50% for spell ids>= 0x99);0x51restores it. - The world-map sub-list halves it on open and doubles it on close.
- The field VM's
MENU_CTRLsub-0xDsets it to(input * _DAT_8008457C) >> 12, a percentage of the player's setting.
Not brightness: a summon does dim the screen in step, but through a different scalar, _DAT_8007B440, ramped by FUN_801ED308 and drawn by the wipe / curtain emitter FUN_8003479C (clamped 0xF2).
Port names: BattleActionHost::duck_audio_level, BattleEvent::DuckAudioLevel, SubListEffect::ScaleAudioLevel, PanelActorHost::audio_level (seeded 0xD7). Owning page: battle-action.md.
Key-on pitch: unity on centre
Status: resolved - note == center keys 0x1000, unity, 44.1 kHz.
Both the SFX / direct key-on path (FUN_80065034) and the sequencer note-on path (FUN_80066308) reach the same arithmetic (FUN_80066e50 / FUN_80066d8c) and hand the result to FUN_80067550, which stores it verbatim into the shadow register file at 0x801CE084 + voice*16 (voice +4 = pitch). Nothing rescales it afterwards:
n = note + 60 - center + carry (MIPS div: truncates toward zero)
pitch = PITCH[(n % 12) * 16 + fine] << (n / 12 - 5)
- Checked against the listing of
FUN_80066E50:note + 0x3C - center + carryat0x80066EB4..0x80066EC0, the divide by 12 at0x80066ECC..0x80066EDC, the table index at0x80066EEC..0x80066F04, the load at0x80066F1C, the- 5at0x80066F08and the two shifts at0x80066F2C/0x80066F40.FUN_80067550stores the pitch at0x800678AC. PITCHis the 192-entry table atDAT_8007A940(SCUS file0x6B140). Every entry is exactlyfloor(0x1000 * 2^(k/192))(192 of 192 verified against the disc; first entry0x1000, last0x1fe2): a one-octave table at 1/16-semitone resolution starting at unity, with the octave applied by the shift. The closed form is exact, so no disc bytes are needed to reproduce it.- The retail cue arm passes
fine = 0x40at every tracedFUN_80065034call site, so a cue keys half a semitone above the sequencer for the same tone. The two paths also differ in whether the fine index saturates or carries a whole semitone. - A 22.05 kHz VAG body is authored with
centertwelve semitones high: the sample rate is already encoded incenter. Applying a22050/44100factor on top keys every voice one octave low. - Capture: 126 of 128 voices holding a non-zero staged pitch match this law exactly; the 2 misses are records whose bank was swapped after key-on.
- The recomp PCM oracle cannot check this law, because it mirrors retail's captured pitch into the engine SPU rather than deriving one.
Owning page: audio.md § key-on pitch law.
FUN_80018DB0 is a rumble cadence, not an audio one
Status: resolved - the surrounding cluster is libpad, DAT_800915DA / DB are port 0's actuator bytes, and the kernel plays no sound. There is no retail footstep SFX cue id to pin. The identification is instruction-level: the FUN_8006CE30 and FUN_8001D230 windows are read straight out of extracted/SCUS_942.54 at 0x800 + va - 0x80010000.
FUN_8006E2B4(buf0, buf1)=PadInitDirect.FUN_8001D230bzeros0x44= 2 x0x22bytes at0x800840F8and calls it with(0x800840F8, 0x800840F8 + 0x22)(addiu a1,a0,0x22), the canonical pair of 34-byte direct-mode report buffers. It clears0x1E0= 2 x0xF0at0x801CE628(one context per socket), stores the two buffers at each context+0x30, seeds each buffer[0] = 0xFF/[1] = 0, and fills six bytes at context+0x5Dwith0xFF(PadSetActAlign's unassigned default). The pad pumpFUN_8001822Cdecodes those buffers as[status][type nibble][inverted u16 buttons], port 1 at+0x22/+0x23.FUN_8006CE30(socket, table, len)=PadSetAct. Three arguments in the instructions:a0passes untouched into the context resolverjalr _DAT_801CE564;a1/a2are stashed ins0/s1and forwarded. Ghidra's C dropsparam_1(artifact #1 inghidra.md). The tailFUN_8006D7B4storesctx+0x28 = table,ctx+0x34 = (u8)len.- Siblings.
FUN_8006CA7C=PadGetState(report status byte throughctx+0x30, then normalisesctx+0x49).FUN_8006CB3C=PadInfoMode, whoseterm = 4branch returns the id-table lengthctx+0xE3foroffs < 0and otherwise the bounds-checked((u16 *)ctx[0])[offs].FUN_8006CDB0=PadSetActAlign.FUN_8006D1E0/FUN_8006D2AC=PadStartCom/PadStopCom(ChangeClearRCnt(3, 0)vs(3, 1)).FUN_8006EE8C/FUN_8006EEE0callChangeClearPAD(B00x5B) and wrapInitCARD/StartCARD(B00x4A/0x4B).FUN_80056618=_bu_init. The eightOpenEvent/EnableEventpairs on0xF4000001/0xF0000011are the memory-card event set. - The bytes
FUN_80018DB0writes are the actuator table. It stores to0x800915DA/0x800915DB, andFUN_80018F94registers the same block per port withPadSetAct(socket, block+2, 2), whereblock = 0x800915D8 + (socket>>4)*0x40 + (socket&3)*0x10. The0x40stride matchesFUN_8001D230'ss1+2/s1+0x42, and the0x80-bytebzeromatches 2 x0x40. DAT_8007B79Cis not a footstep-active flag.FUN_80018F94sets it from_DAT_800845A8 == 0 && PadInfoMode(socket, 2, 0) == 0(the pad reports no extended-mode data). It selects between two actuator payload layouts: set ->act[0] = 0x40fixed withact[1]carrying the pulse; clear ->act[0]carries the pulse andact[1]is loaded with the low byte ofgp+0x618every frame.- No audio call in the kernel. Its other branch counts down ~1200 frames and calls
FUN_8005C034(9, 0), the retry wrapper overCdControl(FUN_8005CF80) issuingCdlPause: a CD-drive pause, not a voice stop or rewind.
Consequences:
- The two "per-voice trigger bytes" are one actuator payload: a per-step on / off pulse and an intensity level, transmitted by libpad every poll.
gp+0x614/gp+0x618are vibration-intensity requests, not a locomotion speed (+0x618is written verbatim into an actuator level byte). Withgp = 0x8007B318they are_DAT_8007B92C/_DAT_8007B930, whose writers are the field VM's op0x4Cnibble-4 subs6/7, the encounter handoff, the battle intro, the battle overlay, the summon and cast modules and the options screen's Events commit (audio.md)._DAT_8007B8A4pinned at2across four field and overworld runs means nothing requests vibration while walking, as the game's Vibration options (battles / events / encounters) predict.
Not SsAPI: the 0x8006C000..0x8006F000 band does hold libspu / libsnd code and the stride-0xF0 record array with an 0xFF idle fill resembles a sequence-worker table, but the resolved context's +0x30 is the button report FUN_8001822C decodes, PadInfoMode's id-table branch has no sequencer reading, and the record count is 2 - the number of controller sockets.
Port: legaia_engine_system::rumble carries the // PORT: tag and drives the controller from the motor words, written by the field scripts and every battle-side writer (audio.md). The old engine-audio::footstep copy, fed a stand-in walking speed, is retired. Owning page: audio.md.
See ghidra/scripts/funcs/8006e2b4.txt, 8006ce30.txt, 8006d7b4.txt, 8006ca7c.txt, 8006cb3c.txt, 8006cdb0.txt, 8006d1e0.txt, 8006d2ac.txt, 8001d230.txt, 8001822c.txt, 80018db0.txt, 80018f94.txt, 8005c034.txt.
XA channel map / STR demux SM
Status: resolved - both halves are statically decompiled from PROT 0970 at its base plus the SCUS St library.
- No XA channel selector exists in the STR overlay. FMV playback reads with Setmode
0xE0(Speed|RT|Size1, sector filter off): the drive hardware-plays every ADPCM sector, and eachMOV/MV*.STRinterleaves exactly one XA track at(file 1, chan 0)(raw-subheader-verified across all six movies). The per-cue channel selector is the SCUS XA-clip sequencerFUN_8003D764(CdlSetfilter {file 1, chan}, mode0xC8at0x8003D80C), used for theXA1..XA34voice / music files, not for movies. Not the\DATA\MOV.STRcontainer: that is a dev path in slots 11..=22 of the dispatch table, absent from the disc. See cutscene.md § XA channel selection. - The FMV dispatch table stride is 32 bytes (
sll v0,v0,0x5at0x801CEC9C), not 64. All nine retail slots0..=8resolve, every movie on the disc plays, andMV3.STRcarries four abutting segments. The master dispatchFUN_801CEA3Chands each mid-game FMV off to a return scene (the seven-label table at0x801CE8ACplus a spawn word). Parserlegaia_game_tables::fmv_dispatch(disc-gatedfmv_dispatch_real); the engine resolverlegaia_engine_system::movie::cutscene::fmv_index_to_str_filenamemirrors the nine-slot map and the return scenes. See str-fmv-table.md. - The 24-byte records at
0x801CAE08are libcd's directory cache:CdlFILEstructs ([loc][size][name[16]]). A name-first parse pairs each name with the next record's location, which is what produces an apparent "MV1 points at disc MV2 / MV6 points at XA15" shift. See str-fmv-table.md.
SPU reverb live routing (C7-REVERB)
Status: resolved - retail runs Studio C, master-enabled, globally. There is no selective per-cue reverb-enable source. Evidence: the mednafen save-state corpus, read by psx_mednafen::PsxSpu (CLI mednafen-state spu <state>).
PsxSpu reads the SPU register shadow (Regs block): reverb_master_enabled (SPUCNT bit 7), reverb_registers (the 32 reverb coefficient / address registers at 0x1F801DC0..0x1F801DFF), and voice_reverb_mask (the per-voice EON enable at 0x1F801D98 / 0x9A, which mednafen also mirrors under its Reverb_Mode sub-entry - a byte-for-byte cross-check in every state). Across all 45 mednafen states (field / town / battle / summon / title / minigames):
- Master reverb is always enabled (
SPUCNTbit 7 set everywhere). No scene toggles it. - The preset is
Studio Ceverywhere. The 32-register block is byte-identical in every state and matches theStudioClibspu preset exactly (dAPF1=0x00E3,dAPF2=0x00A9, work area0x6FE0).engine_audio::ReverbMode::identifyresolves the captured block toStudioC. - Per-voice reverb send (
EON) is broad: 15-22 of 24 voices in any state, BGM and SFX alike. Reverb is the default routing, not a per-cue effect ("Spirit Arts / echo cues opt in, everything else dry" is not what the states show).
Port: the engine calls Spu::set_retail_reverb once at SPU init (StreamResampler::new): ReverbMode::StudioC with every voice routed. The PCM oracle's retail side reads the EON mask as a mask. Not modelled: the output-depth setting (SpuSetReverbDepth, vLIN / vROUT); the engine uses a fixed half-scale approximation.
Owning page: audio.md.
bse.dat record columns and the gp+0x678 consumers
Status: resolved - grade disassembly.
gp = 0x8007B318 (lui gp,0x8008; addiu gp,gp,-0x4ce8 at 0x80026CA8), so the record-table pointer FUN_8001FA88 stores at 0x8001FBC0 (sw a0,0x678(gp)) lives at 0x8007B990. Seven readers exist and every one forms the address as lui rX,0x8008 + lw rY,-0x4670(rX) - the pair the five-form address scan does not accept, which is why the consumer read as untraced: 0x8004FFAC / 0x8004FFE0 / 0x80050078 in the battle SFX-cue router FUN_8004FE5C, and 0x801CEE48 / 0x801CEFC0 / 0x801CF038 / 0x801CF0A0 in the overlay-0971 debug sound test. Sweep: scripts/ghidra-analysis/find-gp-relative-refs.py.
The columns are the static SFX-table columns, pinned by shared code rather than analogy: FUN_80016B6C picks its arm at 0x80016C24 (slti v0,s0,0x200), resolves either 0x8006F198 + id*8 or record[id - 0x200] off gp[0x5B8], and then falls into one block of field reads from 0x80016CB0 - +0 program, +1 tone (+i per voice), +2 note level, +3 low-5 voice count / 0x20 sustained, +4 category into the 12-byte mixer record 0x80091508 + cat*12. +4 is a u8: +5..+7 are zero in every retail row. The router's two tinted legs (sb v1,-0x31c(v0) at 0x8004FFEC, sb v1,0x40c(v0) at 0x80050084) both reduce to record[cue_id - 0x200] + 4, and the sound test stores 7 at 0xDC(gp[0x678]) before enqueuing cue 0x21B = 0x200 + 27 - row index is cue id minus 0x200, and the category byte is rewritten per cue. Layout and carriers: bse-dat.md.
No second bse dat record family
Status: resolved (negative) - grade capture
Entry 888's 1,716-byte tail is byte-identical to entries 886 and 1063 at the same file offsets; entry 1062's 1,616-byte tail matches entry 1056 (two bytes differ where its SEQ padding clipped a row). The builder fill C0 00 C1 00 C2 00 C3 00 occurs in 221 entries, always at offset ≡ 0x1C (mod 0x20) (VagAtr + 0x18), and only these two carry it without a pBAV of their own. bse_bank::detect stops on a residue row's structurally-zero vib / por fields
- reliable, but a foreign row's field. 1062 as a whole is a SEQ-only
music_01
entry (sound-test track 72) borrowing another entry's bank. See bse-dat.md.
How a score ends, and what a loop end does
Status: resolved - grade disassembly, capture
FUN_8006139C (CC 99) jumps back only while the count a data entry stored lasts, and FUN_80063AA8 (end-of-track) never reads a Loop Start: it restarts from the score head or, with the play count spent, stops. The field BGM attach plays with a count of 1, so 2050, 2057, 2070 and 2071 play once; a capture of 2050 runs off its end with its flags at 0. A jump touches no voice, and does not wait the delta behind the Loop Start again. See seq.md.
What a track's residual against retail PCM was
Status: resolved - grade capture, disassembly
The -12 to -15 dB the track channel left once voices, volumes and pitches matched was three defects, each read off a per-note fit of the render against mednafen's recording: a key-on's start (seventh output, sample 3, on real history), the flush's cost before its KON write (FUN_8006C048 per staged voice, 1.2 samples a voice against the recording and 1.34 under PCSX-Redux's cycle counter), and a loop jump that re-waited the delta behind its Loop Start (FUN_8006139C). See real retail PCM.
The level of the port's mix against retail's
Status: resolved - grade capture
Against mednafen's recorded output, the SPU model resumed sample for sample reads correlation 1.000 and level within 0.003 dB, and a track from the state's read cursor reads within 0.2 dB, under 0.1 dB per band. The recorder's own 0.75 is divided out. See real retail PCM.
What a monster's strike sounds like against retail's
Status: resolved - grade capture
Dealt retail's hand (--strike-deal: the state's queue, approach terms and ATK, retail's seed word going into each hit), most strike recordings match cue for cue: the swing, the block or flinch voice, the impact pick and their times, which no chance could do across their hits. So the block roll, its draw order and the damage rolls hold, and what the plain score reads is mostly two streams dealing two hands. The rest are not all read: see the oracle's list of what the deal leaves out. The frame driver's discard at 0x80046D2C runs ahead of the hit, so a block roll's words sit one step past the frame's first word. See the oracle.
Why an impact cue rang on a block
FUN_80047430 runs its three walks in the order hit kernel, effect script, cue track (jal at 0x800478A0, 0x800478B8, 0x800478E4), and a blocked hit steps its attacker's cue cursor over the impact cue (0x801EE6EC..0x801EE6F8). Nova's clips 7 and 8 both carry cue 0x04 (ring 0x03) after the swing. On clip 8 it stands on the hit's own keyframe, so a port that walked the cues first sounded it two ticks ahead of every block voice. A --sfx-callers recording of battle_dohaty_nova_v2_strike has the router called with 4 on the one swing that lands a flinch and never on the three blocks. Grade: disassembly + capture.
Which bank Terra's battle keys
FUN_800520F0 loads raw 0x36D (PROT 0875) over slot 2 when DAT_8007BD11, party seat 1's roster id, is 4 (0x80052378..0x800523AC). With 0869 in its place her attack cues 0x40 / 0x41 are silent and eleven art cues sound where 0875 holds nothing. legaia-engine cue-differential --battle reads terra_party_battle alike on every row once the bank is the loader's. Grade: disassembly + capture.
Where a monster's +4 category comes from
FUN_800520F0 stores 7 at node +0x80 of every seated monster (0x8005225C) and then 8 on the seats holding the formation's largest id when the formation mixes ids (0x8005234C); FUN_800513F0 stores 7 on each party node (0x80051548). The summon seat's node has no such store and reads 0 (gimard_summon_visible, battle_rugi_rogue_rogue_thunder_mid_cast, battle_rugi_rogue_rogue_wind_mid_cast). cue-differential --battle prints the bytes of every battle state against monster_sound_category: the only disagreement is v0_1_battle_loading_tetsu, whose loader has not reached the store. Grade: disassembly + capture.
Where Glare's landing cue sits
Status: resolved - grade disassembly (the id and the arm), capture (one landed roll)
PROT 0940's 0x3C arm 2 counts out at 0x801F6F30, rolls the Stone test (0x801F6F9C), calls FUN_8004FE5C(0xF4, ctx[+0x13]) on a landing (0x15B for Zora, 0x801F7018) ahead of the guard tests, and steps to arm 3. Ring id 0x290. The port rolls on that pass, not at the band's closing seam. See the cast module page.
Why son_field_door_hop's records hold two 0x201 voices
Status: resolved - grade capture (the ring writers and the records over time), disassembly of the disc's sample flags (the voice length)
At the state one move-VM part stores cue 0x201 into ring slot 3 (op 0x1D, 0x80023688) every 63 vsyncs, and the drainer keys it on voices 23 and 22 by turns (autorun_keyon_census.lua from the state: stores at 34, 97, 160, key-ons two vsyncs later). The cue keys tone 0 of bank 2038 at note 60 against centre 84, pitch 0x400, and its VAG ends at block 268 with flag 1 (end without repeat), so the voice stops about 40 vsyncs in on hardware and in the engine (38), and only one voice sounds at a time. The records' +0x06 reading 7FFF on both is PCSX-Redux's ENVX, which its SPU thread advances on the host's audio clock (not emulated time): a watch of the two records from the state holds a voice at 7FFF for 30 to 90 vsyncs after its key-on, varying period to period. A second part spawns 175 vsyncs past the state (FUN_80021B04 from FUN_800252EC, its first store at 0x80021DC8) and runs the same loop 12 vsyncs behind the first, after which two voices do overlap; the engine spawns it too, the two streams 11 vsyncs apart, past the sweep's listening window. So the sweep's ambient miss on this state is the capture's ENVX, not the engine.
What sounds at the end of Zenoir's cast?
Status: resolved - grade capture (the ring and the seats frame by frame), disassembly (the show loop)
zenoir_summon_mid_cast stepped frame by frame under mednafen shows the cue ring DAT_8007B6D8 take every kill-phase cue twice - 0x18 in slots 1 and 2 at vsync 778, 0x18 in slots 0 and 3 at 814, 0x27A in slots 1 and 2 at 856 - and two seats react: arm 11 strikes the victim (seat 3, 3469 -> 3268) and seat 4 (3542 -> 3350). Seat 4 is struck because arm 10 showed it: the arm copies the victim's body pair onto the creature ((-751, 968)) and shows every live monster seat 3..6 whose ground pair lies under a Manhattan 0x400 of it (0x801F7A2C..0x801F7A9C, +0x04 = 0x20080200, +0x21C = 0); seat 4 at (0, 750) is 969 away, seat 5 at (600, 825) is 1494. The arm-11 splash skips a hidden seat, so the port, which showed the victim alone, struck one seat and sounded each cue once, 4..5 dB under the recording. It now runs the show loop (zenoir_bump_in_reach); the cast channel's level went from +1.38 to +0.09 dB. The cues land a few vsyncs later than retail's because the module's arms run on the port's frame step 4 where retail's alternates 3 and 4. See the cast module page.
When does a ring cue sound against the frame that pushed it?
Status: resolved - grade disassembly (the frame driver's order) + capture (frame-stepped key-ons)
Answer. A ring cue keys where the frame that pushed it ends - one frame step after the vsync its slot first shows the id - and so one frame step ahead of that frame's picture. The cast channel's per-arm residual is a term of the comparison, not a cue the player hears late. The measurement behind it:
Frame-stepped under mednafen, a battle cue's voices key one game frame after the vsync its ring slot first shows the id: zeto_big_wave_mid_cast at step 4 (pushes at 351, 391, 399, 407, 419, 483, voices keyed at 355, 395, 403, 411, 423, 487) and zenoir_summon_mid_cast at step 3 (778 to 781); son_field_door_hop's field part keys two vsyncs after its store at step 2. Retail's mode handler runs the ager FUN_8001698C, the frame body FUN_80016444, then FUN_80016B6C, which drains at its head and paces the vsync wait after (0x8001702C..0x80017068), so a cue keys where the frame's work ends. The port drains on the tick that opens the frame, which puts the cast channel's cues up to a frame step early (Big Wave's party reactions, the reading the cast channel gave as four vsyncs). The recordings put the onset a whole frame step after the push (Big Wave's first cue 3.9 vsyncs after the engine's, which pushes on retail's vsync), so the lag is the step, not step - 1. Holding the key-on a battle frame step leaves every cast row's median correlation where it was and lowers the tenth percentile on Swordie (0.77 to 0.33), Zenoir (0.68 to 0.33), Theeder and Guilty Cross, raising it only on Big Wave (0.48 to 0.52): the port's battle step is its cost model's, 2 through Big Wave's reactions where retail runs 4, and 4 through Zenoir's kill phase where retail alternates 3 and 4, so a lag on the port's step moves half the cues the wrong way. It is not ported. What closes it: the battle frame step itself on those arms (the cost ring's table), after which the lag is the step. The step byte is not always the frame's length either: on the PCSX-Redux film of Gaza's Neo Star Slash (gaza2_card_pre_neo_star_slash) capture phase 3 holds the step byte 4 and the cost at its 700 clamp while the frames take 6 vsyncs rising to 15 (48 frames in 583 vsyncs, the frame counter gp+0x480), so a frame that overruns its step delays its cues by the overrun as well; the engine runs the same phase in 240 ticks. Held on the port's own step from the arms' current cost rows, the same run reads Big Wave's tenth percentile 0.478 to 0.522, Zenoir's 0.682 to 0.329, Swordie's 0.774 to 0.332 and Theeder's 0.342 to 0.308, the medians unmoved: the table still names a step retail does not run through the kill phases.
The lag is not a sound-against-picture offset the port owes, and holding the cue alone would create one. FUN_80016B6C drains the ring at its head (0x80016BF8), then waits out the frame (jal 0x8005FB84 at 0x80017088 / 0x800171C0), and only after the wait swaps the display and draws the ordering table the frame built (0x80017230, 0x80017254, DrawOTag at 0x80017288), into the buffer the next frame's swap shows. So retail keys frame N's cue at the end of N and shows N's picture at the end of N + 1: the sound leads its picture by one frame step. The port presents a tick's picture and keys its cues on that tick. A cue held one step would trail its picture by a step, two steps from retail's order, where the unheld cue is one step from it. The cast channel's residual is therefore a term of the comparison (its one offset per cast cannot absorb a step that changes arm by arm), not a cue the player hears late. What would still close the number: the cast render delaying each ring cue by retail's step on that arm, in the oracle.
What is left is the oracle's, not the game's: a cast render that delays each ring cue by retail's step on that arm, which needs the battle frame step itself on those arms (the cast channel).
No script changes the BGM word inside a swap's read
The swap poller's stage 6 latches the requested index of its own call (0x800247B4..0x800247C4), so a sub-op 1 that changes the word after stage 2's read plays the old score and reports the new one loaded, where the port starts the newer word. Whether a script reaches that window is a property of the disc's bytecode. scene-formats/tests/bgm_swap_word_census_disc.rs walks every scene MAN record from each sub-op 1 along every control-flow path (both arms of each conditional, the picker's table) and adds up the explicit waits, WAIT_FRAMES targets and one per yield, at one vsync a frame, the shortest a frame can be. Ops that block on other state only lengthen a path, so the sum is a lower bound.
Of 201 sites in 81 scenes, 71 reach another word with no wait between: all in a scene's entry record, where both stores run in one VM frame, before the poller's next call, and stage 2 reads the last one. None reaches a different word a frame or more on inside the 66-vsync read (autorun_bgm_swap_latency.lua: stage 6 at +66 / +67). Sub-op 9 re-runs its own PC while requested and loaded differ (0x801E0238), so it waits the walk out. Grade disassembly (of the disc's scripts). The walk cannot see two records each starting a track as separate contexts; no such pair is known.
What clears the drive word before a clip ends
0x8007BC20 reads 2 while a one-shot plays: the starter FUN_8003D53C stores it (0x8003D65C) and the read callback's stop FUN_8003EE00 zeroes it once the read passes the clip's end sector (0x8003DA90, 0x8003EE2C). The other way to zero it is FUN_8003ED04, which mutes the CD input (FUN_8003EE7C(0)), zeroes the word (0x8003ED28) and pauses the stream: the clip stops. Its callers are the starter itself, the seek FUN_8003EAE4 (0x8003EB18), the stream queue FUN_8003DDA0 (0x8003DE28), the loaders FUN_8003E104 (0x8003E234) and FUN_8003E800 (0x8003E850), the mode resets FUN_80015E90 / FUN_80017714, the debug menu and PROT 0969.
In battle none of them lands on a sounding clip. The seek-ahead selector FUN_8004DA00 runs only while the word is 0 (bne at 0x8004DA14). A summon module seeks on an arm ahead of its head cue. The side-band applier FUN_801F12D0, the battle's one stream loader, opens with slti v0,v0,0x2 on the word (0x801F12E4): while a clip sounds it stores stage 1 and returns, so its read starts over from the base slot once the clip ends; its caller FUN_801F17F8 runs it only while the stage is up (0x801F180C..0x801F181C).
A PCSX-Redux Theeder cast (spell 0x82 from party_basic_attack_vs_gobu_gobu, cast-sequence.py --watch 0x8007BC20) shows the word's life on that drive: the selector's seeks (0x8003EB78 then 0x8004DBA8, vsyncs 278 and 428), the module's arm-1 seek (0x801F6C5C, 433), the voice (0x8003D65C, 553), and the callback's stop (0x8003EE2C, 1386): 833 vsyncs for the clip's dur of 807, with no store between. Mednafen shows the applier's hold itself: stepped frame by frame from theeder_summon_mid_cast (drive-timeline.py), the word goes to 2 at +229 and the cast reaches 0x51 at +1001 with the voice still sounding; the next turn's side-band stage stands at 1 from there until the word drops at +1060, 831 vsyncs after it rose, and only then steps (2 a vsync later, through 6). Both emulators hold the word about 25 vsyncs past the clip's dur; with one clip measured, whether that is a constant start cost or proportional is not separated, and the port holds dur. Grade disassembly + capture. Port: AudioState::battle_xa_busy_frames (the word, dur vsyncs), the applier hold in World::tick_sideband_token.
Title / boot / overlays
# Settled threads: Title / boot / overlays
| Thread | Answer | Grade |
|---|---|---|
| Which routine uploads the system-UI TIMs ahead of extraction entry 0? | FUN_801CECD0 in init.pak, PROT 0895WhyThe TIMs are raw TOC entry 0 (CDNAME | disassembly + capture |
Do MOV15.STR and MV1A.STR ship on the disc? | no - path strings onlyWhy
| capture |
What does FUN_801E0418 draw, and which entry uploads its page at VRAM (512, 256)? | the title's own strips; PROT 0890WhyIt redraws the title TIM's strips behind the Load window when that window is opened from the title: records 0/3/4/2/5 of the descriptor table at | capture + disassembly |
| How many rows does retail's title menu have? | twoWhyThe title tick wraps its row counter with | disassembly |
| How does a slot-B module hand a spawn call its record? | three shapes beyond an adjacent pairWhyThe pair can complete in the | disassembly |
| Which images take the slot-B layout walk? | the seventy at the link base, not the sixty-four in the index bandWhyThe walk's three regions are each recovered by resolving a word against | disassembly |
| What is PROT 0967's residue? | a head table, dumped code, one leaf, a consumed prompt pool and a neighbour's tailWhy408 bytes of head jump table, 2,744 bytes of dumped code, one 92-byte frameless leaf | disassembly |
What is in the PROT 0898 head block below 0xDF8? | a string pool and twenty-two jump tables; two more sit above itWhyEach consumer is | disassembly |
| What is PROT 0970's 131,172-byte zero hole? | the overlay's own uninitialised data regionWhyIt decomposes exactly, in the image's own operands: the | disassembly |
| What is PROT 0974's 10,836-byte run of readable text? | an 81-record 0x84-stride rosterWhyNot a string pool: the loop operands in | disassembly |
What is PROT 0975's undumped 0x801D4138 run? | PROT 0972's code, inherited at the same file offsetWhyThe 1,760 bytes from file | capture (disc byte compare) |
| Which of PROT 0978 / 0979 / 0980 is the dance overlay? | only 0980Why0978 is | disassembly |
| The front-end mode chain, re-derived independently | the six stores, the four-word edge and the INIT hand-offs all holdWhyA three-form opcode scan for stores to the mode word over 84 images finds 53 store sites, and every row of the boot chain, the mode-change edge's four cleared | disassembly |
| Which mode does the retail title screen run under? | card mode 0x17, not 0x10WhyThe front end is a chain of six mode stores, each written by the handler that hands off, not by a table's | disassembly |
What reads the title entry word _DAT_8007BB00? | two readers, both realWhy
| disassembly |
| What does a mode-change edge clear? | four gp words, not threeWhyThe edge at | disassembly |
_DAT_8007B8C2 - how many writers? | exactly oneWhy
| disassembly |
_DAT_8007B8C2 polarity, and its writer | != 0 is retail, == 0 is devWhy | disassembly + capture |
Actor-VM (FUN_801D6628) program source - which carrier, what selects one | menu-overlay-resident program tableWhyThe interpreted programs are data in PROT 0899's own data segment (file | disassembly |
title.pak PROT entry | resolvedWhy | capture |
Does FUN_801CE9C0 pin the per-logo quads? | no - it uploads; the quads are a descriptor table in the same imageWhy | disassembly |
The title menu's law (FUN_801DD35C sub-mode 0x10) | + ported on both hostsWhy | disassembly |
FUN_801D71F0 and its "shared armament placer" 0x801E5AE8 | one routine, FUN_801E5A08, unreferencedWhy | disassembly |
Save-screen info-panel view mode 4 ("Return") | dead - the grid has 15 cellsWhy | disassembly |
| Title screen mode-table PROT | no row is named for it - it runs under the CARD mode pair 22/23; FUN_801DD35C is PROT 0899 +0xEB44Why | disassembly |
| Load-screen panel 9-slice geometry | engine renders byte-perfectWhyRetail composes the 81×29 panel at dst | capture |
Key-item area consumers (0x800859E8..0x80085A40) | narrow negative; the reader enumeration is closedWhy | disassembly |
| XP-table source + reader | + portedWhy | capture |
New-game world-state seed store widths (FUN_80034A6C) | the port matches the disassemblyWhy | disassembly |
| Overlay identity from the disc (static extraction) | pipeline legaia_overlay_images::static_overlayWhy | capture |
| SCUS recomp gap - render/GTE + boot/init clusters | aliases + libgte residue + dev tooling; main() documentedWhy | disassembly |
| Options/menu overlay PROT entry | RAM-verified; PROT 0899 @ 0x801CE818WhyThe options/pause/inventory-equipment-status menu overlay is PROT 0899, not 0896: | capture |
PROT 0896 (bat_back_dat) identity | resolvedWhyThe unique ~ | capture |
| Slot-A scene-overlay family beyond field/battle/menu | in the static mapWhyThe rest of the slot-A ( | disassembly |
| "world-map / save / shop" overlay PROT entries | not separate entriesWhyThe world-map / overworld controller | disassembly |
| PROT 0977 / 0978 extraction + the dump re-key | resolvedWhy | disassembly |
Slot-B capture-module band 0935..0966 per-entry identity | statically derived, capture-corroboratedWhy | disassembly |
| Phantom-VA sweep of the PROT 0897 imports | resolvedWhy | disassembly |
Debug flag 0x8007B98F | the MSB of the debug-mode word _DAT_8007B98CWhy | disassembly + capture |
| New-Game opening chain + narration roller | chain + caption + roller + prologue gold grade; far-geometry residual resolved-negativeWhy | capture + disassembly |
| Overlay-loader index off-by-2 - remaining ripple | slot A reconciled; slot-B per-spell identity capture-pinnedWhy | capture + disassembly |
Slot-B overlay cluster (0900..0969) per-entry identity | for every entryWhy | capture + disassembly |
| PROT 0968 - what it is, who loads it, and how big it really is | identity and extent by disassembly, residency by captureWhy | capture |
0x80010390 - the SCUS word that looked like a lead on 0968 | it is the slot-B overlay destination pointer, shared by every slot-B entryWhy | disassembly |
Who registers the MDECin DMA callback wrapper FUN_801CFE98? | nobody - linked libpress residue; retail hooks only the MDEC-out twinWhyZero references in eight forms over 1234 images (SCUS, 31 based overlays, every raw PROT entry). Its twin | disassembly (the residue reading inference) |
PROT 0895 (init.pak) identity + base | slot A 0x801CE818; mode 16's whole body is its FUN_801CE9C0WhyBase recovered from the image's own | disassembly |
Does anything draw the init.pak WARNING screen? | negative (the TIM is uploaded and never drawn)WhyPROT 0895 uploads the health-warning TIM to VRAM | disassembly + capture |
Which title sub-mode does a cold boot show - 0x02 or 0x10? | 0x10, alwaysWhy | disassembly + capture |
Where does fmv_id 5's trigger op live? | taiku's streamed MAN, P2[28]; every FMV trigger sits in partition 2, none in partition 1WhyEntry 0373 is a DATA_FIELD stream whose type-3 chunk is an uncompressed MAN; at | capture (disc census, asset field-op-census --only '4C E2' --context) |
| How the slot-B module pager turns a request into a PROT entry | extraction entry = a0 + 895, with no upper boundWhy
| disassembly |
| Where does PROT 0896 link? | 0x801D4DF0 - and it calls no entry of this disc's executableWhy
| disassembly |
| What is PROT 0970's run above its one-shot init flag? | two MDEC command packets, then the register-pointer blockWhy
| disassembly |
What does PROT 0970's FUN_801CFCDC do? | the MDEC quant table upload - not an output-rect stagerWhyIt copies sixteen words from | disassembly |
What is PROT 0899's 3,552-byte zero run at file 0x1EB28? | inter-asset fill, not uninitialised dataWhyIt sits between the save-menu atlas's end and the save-slot icon sheet at | disassembly |
| What are PROT 0899's options-screen tables? | four, each bound to a consumerWhyThe display layout | disassembly |
| Where does PROT 0901's own content end? | file 0x252A, donor PROT 0900Why0901's code ends at | disassembly |
Long write-ups in this area
A cold boot always shows title sub-mode 0x10
Status: resolved. Evidence: disassembly + capture.
FUN_801DD35C's Init arm (0x00) writes state[+0x204] = 0x02 and overwrites it with 0x11 whenever the entry word _DAT_8007BB00 reads non-zero. On retail that always happens, so the 0x02 two-row menu (rows y 107 / 120, confirm mask 0x44, advancing to 0x14) is unreachable from a cold boot.
- Entry word is always raised. The boot
init.paksets it unconditionally -li s2,0x1/sw s2,-0x4500(s0)at0x801CEB84withs0 = 0x80080000, in the mode-16 body. The three sites that store zero back are all behind dev-flag or pad-hold arms. - A surviving
0x02is rewritten. TheAttractDelay(0x11) arm writes0x10at0x801DDAC4when its hold has drained; the shared epilogue's exits at0x801DFED8/0x801DFEF8do the same. - Capture. A per-vsync cold-boot poll sees
_DAT_8007BB00go0 -> 1in the frame the master mode steps0x10 -> 0x11; the sub-mode is written0x11the frame after the title mode is entered and0x10about 75 vsyncs later;0x02is never observed. Returning from the attract FMV the word reads2, so the overwrite holds on the second entry too. - The tick. PROT 0899 file
+0xEB44, 12 104 bytes / 3 026 instructions, dispatching0x801F0204through the 24-word jump table at0x801CF244(slot0x11=0x801DDA90), with 56 stores to that word and one shared epilogue at0x801DFC3C._DAT_8007BAB4is its pre-attract hold, not an active-submenu index.
Falsified readings: the sub-mode word's address, the slider clamp. Owning page: boot.md.
_DAT_8007B98F is byte +3 of the debug-mode word _DAT_8007B98C
Status: resolved - no byte-granular reader exists; the 32-bit word is the consumer surface. Evidence: disassembly + capture.
0x8007B98F is the MSB (little-endian byte +3) of the debug-mode word _DAT_8007B98C. Nothing reads or writes the byte on its own, so SELECT+START / a GameShark write of 0x8007B98F = 1 sets the word non-zero and every _DAT_8007B98C != 0 gate reads debug mode active.
- Byte references: zero in SCUS and every overlay dump (
ghidra/scripts/funcs/has no8007b98f). - Word readers, SCUS:
FUN_8001822c(8001822c.txt:500/533), plus80016230/80016444/800173bc/800188c8/8003cbf8/8004ad80/80025cb4. - Word readers, overlays: an aligned word search of the 23 static overlays finds 14
lw ...,-0x4674(reg)reads (base reg0x80080000) in the field overlay 0897. - Sole
swwriter: the shared menu/title/save-init routine (overlay_menu_801de234/overlay_title_801ddccc, internal offset0x4158). It clears the word, so the gate does not survive scene initialisation and has to be held asserted for a session. - Not BIOS-zeroed: the PS-X EXE header carries
b_addr = 0, b_size = 0, so no BSS is cleared for this executable; the same holds for_DAT_8007B8C2. - Runtime (static recomp): asserting the word and pulsing
SELECT + △on controller port 2 opens the game-owned developer menu - port 2 follows from the_DAT_8007B850 &= 0xFFFFmask, which puts every debug binding in the upper half. Forcing game mode 0 loads PROT 0971's full-screen configuration tester instead, as theCONFIG INITrow inboot.mdsays. The developer menu's MAP CHANGE appliers are resident in field overlay 0897, matching the 14 gate reads there.
Owning pages: boot.md § Debug flags; combo table in builds.md § Debug input bindings.
New-Game opening chain + narration roller
Status: resolved - chain, caption, roller and prologue gold grade; the far-geometry brightness gap is resolved-negative. Evidence: capture + disassembly.
The opening is a five-scene chain - opdeene → opstati → opurud → map01 → town01 - all under master mode 3 with zero input. Its narration is a bottom-up scrolling crawl that runs in a child context while the parent timeline keeps cutting the camera, and its gold look is a palette-space collapse, not a depth cue.
- Chain. Each leg's record spawn is an exec-BP hit on
FUN_8003BDE0(exactly 5): field-VM op0x44SPAWN_RECORD in the first three legs' entry scripts, the walk-on tile trigger (FUN_801D1EC4→FUN_801D5630) formap01/town01. TheFUN_801D1344town01packet is the intro skip, not the hand-off gate. Name entry auto-opens from op0x49STATE_RESUME sub-op 3 attown01P2[3] body offset0x02c6(_DAT_8007B450parks there); retail order is establishing pan → name entry → Vahn's walk-out. - Crawl. Roller actor
FUN_80037174, spawned as a child context byCC F8 80 N(N= page count). Retail opens every crawl non-blocking and holds only at the record's terminal0x3FSceneChange while narration is active. A cold-boot capture (scripts/pcsx-redux/autorun_crawl1_capture.lua) shows the eye cutting through the Genesis-grove foliage to the villager tableau while the creation crawl scrolls. The one-caption presenter is the separate4C E1balloon op (FUN_8003C764/FUN_801DA7F0). - Roller config.
CC F8 E8 ...(nibble-Esub-80xE8; handler0x801E3378inoverlay_0897_801e0c3c.txt, reader80037174.txt) reads four signed-16 LE words and stores three into_DAT_801C6EA4(shat0x801E34B0/34B4/34BC):+0x4Cwindow top Y,+0x4Evisible line count,+0x50scroll-cadence divisor.word3selects seed / pause / resume / kill and is never stored. Not op00x88, which writes_DAT_80084628/....RollerSeed::config_op_before(crates/engine-field/src/cutscene_narration.rs) reads it off the scene's timeline bytecode. - Caption. "It was the Seru." is not text: a pre-rendered 112×32 4bpp TIM (two CLUT palettes = the fade steps) in PROT 0749 (
opdeene) at LZS-decoded offset0x01EC30, VRAMfb=(384,0), drawn as a screen-space textured quad. Every UI text/image draw path fires zero times in the caption window and the string is in RAM in no encoding (autorun_text_census.lua,autorun_seru_blit_probe.lua, a full-RAM dump).tim-scan extracted/PROT/0749_opdeene.BINrenders it. - Camera. The per-frame mover is
FUN_801DC0BC(FUN_801DB510is the follow / scroll camera);FUN_801DD310attaches ten(start, end)pairs plus one shared progress / duration / curve to a mover actor, so a glide runs in parallel with the record that staged it and a beat landing mid-tween re-seeds every axis from the live pose. One curve applies to all ten axes - mode 1 is linear on pitch/yaw too (three independent beats, one a 2000+-frame yaw dolly) - and thetown01arrival H glide (P2[3] +0x00C4,apply600, H 412 → 512) is mode 4 ease-in-out (op0 0x13 >> 2; disc pintown01_arrival_camera). Eye-back depth is offset-trio slot 5,0x800840B8(no separate eye-distance scalar); the op-0x45params map to the camera globals and the rotation build isFUN_800172C0->FUN_80026988. - Timing. Record durations count retail display frames (op
0x4Aand the mover both accumulateDAT_1F800393);FUN_8002519Cwalks the actor lists in full every frame, so there is no hidden step parallelism (script-vm.md). - Gold grade. Four CLUT-cell cyclers in the prologue's effect scripts grey and re-tint the CLUT rects
(0, 473, 320, 2)/(0, 476, 320, 34)toL = max(r,g,b) → (L, (L*247)>>8, L>>1)(5-bit, STP preserved; settled) and the scripts collapse authored colour packets to the amber family~(M, 0.94M, 0.43M); neutral0x80ground quads stay neutral. Node+0x78(IR0) is 0 on every render node (0x8007C34C..) at every beat, so there is no per-node depth cue (re-do-not-re-walk.md). - Far-geometry brightness (resolved-negative). Retail spires / wings read
B/R ≈ 0.15..0.16at brightness ~51 against the port's 0.27 at ~80; the tableau ground is identical. No CLUT-rewrite loop exists in overlay 0970 (28 functions, pure MDEC/STR), field 0897 (690) orSCUS_942.54(945): the rewrite is a table/DMA upload. The gap is neutral packets on lit-descriptor prims, which retail draws through the GTE far/back colourFUN_80029888loads (DAT_8007B788 = 0x00202020inopdeene,0x00FFFFFFintown01) - the port's no-field-light-op boundary (Field decoration path).
Port: a non-blocking crawl in the field timeline, paced off the 60 Hz sub-clock (opening_chain_wall_time: within ~4 % of retail wall time, each leg short by the unmodelled scene-load window, so its bands are asymmetric); legaia_engine_camera::camera_mover (2471 of 2480 sampled axis values against a live capture, the rest inside the probe's read skew; camera_mover_recomp_oracle under LEGAIA_RECOMP_TRACE_DIR replays the snap / mode-1 / mode-2 / mode-4 beats bit-exact); Renderer::set_palette_grade (palette_law_word / palette_collapse_prim, staged by play-window when World::scene_color_grade is active; tableau ground G/R 0.890 against retail 0.88); play-window renders through psx_camera_mvp.
Owning page: cutscene.md - crawl roller, roller operands, sepia grade.
Overlay-loader index off-by-2 - remaining ripple
Status: resolved - slot A reconciled; per-spell slot-B identity capture-pinned for every block (player, evolved, flutes, enemy). Evidence: capture + disassembly.
The overlay loaders (FUN_8003EBE4 / FUN_8003EC70 → FUN_8003E8A8(param + 0x381)) resolve against the in-RAM TOC at 0x801C70F0, which is raw PROT.DAT from byte 0 (byte-verified against the door_warp_town01_to_map01 state). Extraction indices slice entry starts two words higher, so the loaded entry is extraction param + 0x37F; a param + 0x381 attribution is 2 high.
Slot A (each content- or prologue-anchored; boot.md): field 0897 = mode 2, battle 0898, menu 0899 = mode 22, STR-path 0969, cutscene 0970, debug menu 0971 = mode 0, the seven 0x3E minigame slots, efect-test 0979 = mode 8.
Slot B per-spell stagers. The player span 0x81..=0xA0 is one unbroken linear run: loader id = spell - 0x79, extraction = loader id + 895 = 903 + (spell - 0x81).
| Spell ids | Loader ids | Extraction | What | Pin |
|---|---|---|---|---|
0x81..=0x8B | 0x08..0x12 | 903..=913 | base Seru summons (0907 = Nighto) | one mid-cast state per spell |
0x8C..0x95 | 0x13..0x1C | 914..923 | evolved Seru: Gola Gola 914, Mushura 915, Aluru 916, Barra 917, Kemaro 918, Spoon 919, Slippery 920, Iota 921, Puera 922, Gilium 923 | eight by mid-cast states (evolved_summon_binding); 0x90 / 0x91 by injected casts |
0x96 / 0x97 | 0x1D / 0x1E | 924 / 925 | rare-Seru flute summons Lippian / Spikefish | flute_lippian_midcast / flute_spikefish_midcast, probe autorun_flute_cast.lua |
0x98 | - | 926 | unused: a one-sector jr ra stub | disc bytes |
0x99 | 0x20 | 927 | Evil Seru Magic (Juggernaut) | mid-cast mednafen state |
0x9A..0x9D | 0x21..0x24 | 928..931 | Sim-Seru Palma / Mule / Horn / Jedo | mid-cast mednafen states |
0x9E..0xA0 | 0x25..0x27 | 932..934 | Ra-Seru Meta / Terra / Ozma (untitled entries head with a pre-linked slot-B pointer table) | mid-cast mednafen states |
- Method. The loader-B current id (
gp+0x934=0x8007BC4C) is read out of catalogued save states (scripts/pcsx-redux/match_prim_groups_to_disc.py::extract_ramwalks the gzipped-protobuf.sstateto the RAM blob), together with the predicted entry byte-resident at slot B0x801F69D8. The id is a last-load tracker - an idle Begin/Run-menu state holds a stale6- so only in-cast states are evidence. - Gimard. All three player cast states (
gimard_summon_start/_visible/_burning_attack) holdid = 8→ 0903 through spawn, steady render and attack; the id never moves off 8, so 0900 does not overwrite the stager mid-cast. The enemy Gimard "Fire Tail" frames (battle_gimard_tail_fire_a/_b) holdid = 5→ 0900: the enemy special pages the move-FX module, not a stager. - Attack-titled heads are display names, not separate uses: 0907 "Hell's Music" (Nighto; the dance overlay has no slot-B loader callsite), 0927 "Dark Eclipse", 0924 "Ultimate Rave" (the failed-kill banner; a landed kill shows "Ultimate Death"), 0925 "Blowfish", 0918 "Canine Fangs", 0919 "Holy Eyes".
- Evolved block. All ten entries trim to move-VM stagers with 4..67 spawn sites (
EVOLVED_SUMMON_STAGER_PROT, disc-gatedsummon_overlay_block). The injected casts (autorun_evolved_cast.lua; statesevolved_0x90_midcast/evolved_0x91_midcast) write the spell into the caster's record spell list and MP into the record and battle-actor+0x150: the battle Magic submenu reads the record spell list live, the MP gate reads actor+0x150. Loader-B flips when the slot-B load is queued, so the probe saves 90 frames after the flip; the image then matches 100 % over the entry's full LBA footprint. The two0x4000render-mode carriers (916 Aluru, 921 Iota) are player casts, so neither seats a live render-mode part. - Flutes. SummonFlute items (effect classes 126 / 127) enqueue the spell id directly, so they ride the same stager mechanism.
- Enemy arm (six catalogued final-boss Cort mid-cast states), id band
0x2B..0x47→938..966: Mystic Circle0x2B→ 938, Mystic Shield0x2D→ 940, Guilty Cross0x31→ 944, evolved-form Final Crisis / Ultra Charge0x42/0x43→ 961 / 962, Cort's Evil Seru Magic0x47→ 966. The player and enemy arms of one spell ship separate stagers (927 vs 966).
The 0977 sub-id-5 minigame. 0977 ("Ronginus") is the mode-24 case-5 door/init slot: the 0x801CEA6C init prologue, the arena monster-name roster and other6 dev paths. The Muscle Dome match SM FUN_801D0748 and all its data live in the battle-action overlay PROT 0898 - the arena is a mode of the battle engine. asset overlay find-sig of the controller prologue (lui v0,0x8008; lw v0,-0x42dc(v0), reading the ctx _DAT_8007bd24) lands on 0898 at base 0x801CE818, file offset 0x1F30, and the deck / sub-draw / victory tables resolve in-overlay (legaia_minigame_formats::muscle_dome::verify_resident); the Duckstation overlay_muscle_dome.bin capture is that overlay's slot.
Port. OVERLAY_PROT_BASE carries the extraction-space 0x37F (the host chain prot_one_shot_load → entry_start_lba_retail consumes extraction indices; its toc array starts at raw dword 2), with a unit test documenting the raw-vs-extraction shift. engine-effects::summon::summon_stager_prot_entry maps spell id to stager entry.
Muscle Dome match shape: an ordinary battle ladder, not a card battle
Status: resolved. Evidence: disassembly.
The arena is a ladder of ordinary battles. The four "cards" are the four d-pad direction commands 0xC..=0xF, each carrying that fighter's own AP cost - the input a normal battle command screen takes, bounded by AP.
- Course descriptor table
0x801D1A08in PROT 0977: three{ i32 rounds; ptr first }records walking 29{ u32 label; u32 monster_id }round records at0x801D1920. FUN_801D1510stores the round's id into formation slot 0 at0x8007BD0C.- Courses are 8 / 8 / 13 rounds, matching the populated rows of the score table at
0x801D1860; the 29 ids resolve against PROT 867 to the curatedcasino.tomlline-ups 29 of 29 in order. - The HP ratio is
x 100: the "out of 108" (0x6C) reading consumes only part of the compiler's shift-add chain at0x801d0f38..0x801d0f4c. 0x8007BD0Cis the formation cell, not a battle-type byte: the strip's gate reads "the first enemy is monster0xB6" - Koru, the one four-turn timed boss - and no dome round fields that id.
Port: engine-world-views::muscle_dome (parse_course_ladder, course_score_cell, resolve_turn playing whole strings per actor, DomeDamageModel - the one retail damage kernel both hosts resolve through). Falsified readings: re-do-not-re-walk.md. Owning page: minigame-muscle-dome.md § Course ladder.
The dome runs two state machines; the outer one is the contest
Status: resolved. Evidence: disassembly.
The battle round driver FUN_801D0748 is the inner machine and has exactly one contest-gated arm (0x801D322C, the flee path). The contest - which (course, round) is staged, whether the run continues, what a cleared leg is worth and what the run pays - is a second machine wholly in PROT 0977.
- Contest machine:
FUN_801CEA6C, re-entered after every leg, and the hubFUN_801CF870dispatchingDAT_801D1A78through a 51-entry jump table at0x801CE990. - Course / round: packed in the low byte of the mode-24 sub-id word
_DAT_8007BAC0(course = ((w-1) & 0xFF) >> 4,round = (w-1) & 0xF); a finished leg isw += 1. - Story flags:
0x536/0x537/0x538pick which course opens; only the Master course's length is clamped, by0x378/0x382/0x471. - Leg survived:
DAT_8007BD60 & 0x80at0x801CEDD8, cleared by the battle's own0x5Aparty-wipe scan - not an arm ofFUN_801D0CD4orFUN_801D0068.settle_contest'scontinuinginput is therefore derived, not prompted. - Tally rows: three of the six are HP recovery (
round*2,min(turns,8),[8,12,4,2][outcome], each× max_hp / 100) draining into the restore accumulatorDAT_801D1AC8; only the(course, round)score cell reaches the coin tally. - A cleared course banks its whole score row: the Master reward is 13830 (the curated table's 13856 is wrong).
Port: engine-minigames::muscle_dome::DomeContest, driven by World::report_muscle_leg / World::settle_muscle_contest and the browser's muscle_contest_* bindings. Owning page: minigame-muscle-dome.md § Two state machines.
Battle arts-input UI decomposition (dome = standard battle input)
Status: resolved. Evidence: capture.
The arts command input (the FUN_801D0748 state-0x50 arm) is decomposed piece by piece from a live dome match in the static recomp (slot-5 savestate + scripted pad), read through the runtime's gpu_frame_dump GP0 ring plus a same-moment full-VRAM dump.
| Piece | Source |
|---|---|
| High / Left / Right / Low chips | widget-page hexagon pieces + baked label strips + diamond ends |
| Input bar | tiled maroon widget bar filling with command pennants at cost-wide pitch |
| AP plate (right) | reads the Spirit gauge; the entry budget's only visible form is the bar |
| Triangle list | 5-row-per-page learned-arts window: system-UI interior tiles under a 0x40..0x88 gouraud; name / arrows / AP columns are the SCUS arts-name table's, drawn through orange sub-palette 15 |
| Green Triangle circle | its own 64x32 gap TIM at PROT.DAT 0x7B00 |
Behaviour: per-press ctx+0x6dc debit + actor+0x1df append; auto-end on exhaustion (0x50 -> 0x5a); the 0x5a -> 0x6e Begin|Reselect chain; Triangle inert at learned-art constant 0.
Port: engine-world-views::muscle_dome (selection_exhausted / reset_selection), web-minigames::muscle_web (arts_input pieces + muscle_arts_list_json). Owning page: minigame-muscle-dome.md § Arts command input.
Slot-B overlay cluster (0900..0969) per-entry identity
Status: resolved for every entry. Evidence: capture + disassembly.
The slot-B buffer (link base 0x801F69D8) timeshares the 0900..0969 blobs. Each is extracted statically at the link base and cross-checked by in-file self-pointer resolution (static_overlay::pointer_resolution, ≥70%). A shape census over the cluster (per-entry lui 0x801F/0x8020; addiu in-file resolution, FUN_80021B04 / FUN_80050ED4 spawn-call counts, damage-wrapper jal words) corroborates slot-B linkage for every entry except slot-A 0902. The CDNAME label is xxx_dat (a dev placeholder) across the cluster and identifies nothing.
| Entries | Identity |
|---|---|
| 0900 / 0901 | the slot-B default render pair: FUN_80025BA0 loads param 5 or 6 by flag DAT_8007B6A8 (0900 field scenes, 0901 world-map scenes) |
| 0902 | GAME OVER - a slot-A image (FUN_8003EBE4(7) in the mode-18 init); a slot-B reading of it is a pointer_resolution false positive |
| 0903..0913 | player summon stagers, spells 0x81..=0x8B |
| 0914..0923 | evolved-Seru stagers, 0x8C..0x95 |
| 0924 / 0925 / 0926 | rare-Seru flutes Lippian 0x96 / Spikefish 0x97, and the unused-0x98 one-sector jr ra stub |
| 0927..0934 | Evil Seru Magic (Juggernaut), the Sim-Seru quartet, the Ra-Seru trio (0x99..0xA0) |
| 0935..0966 | the capture-class cast-module band, extraction = 935 + sub_id - its own section |
| 0967 | the battle sparring-tutorial overlay (capture-pinned, s5 needle sweep); battle-stage id 1 |
| 0968 | the evolved-Cort battle's stage overlay, battle-stage id 2 - details ↓ |
| 0969 | the STR-path table the STR-mode init pages (FUN_8003EC70(0x4A); boot.md), and Cort's form-transition module |
Per-spell pins for 0903..0934: the loader-index section.
0969's battle-side load uses the same gate as 0968's. At 0x801E6D04 the battle SM reads *(u8 *)0x8007BD0C - the first formation monster id - compares it to 0xB5 and pages 0x4A (jal 0x8003ec70 at 0x801E6D14, overlay_battle_action_801e6968). The guard just above is lhu v0, 0x14C(actor) on slot 3 of the actor table 0x801C9370, taken only when that actor's HP has reached zero - so 0969 is paged when a Cort form dies. The 0xB5 is not the Lapis Wave spell id: the byte the branch reads is the formation id, and formation 0xB5 is Cort (monster-archive id 181).
0x80010390 is the slot-B overlay destination pointer
Status: resolved - a slot constant, not a lead on 0968. Evidence: disassembly.
0x80010390 is a SCUS-resident global holding the slot-B overlay load address 0x801F69D8, and 0x8001038C is its slot-A twin. It is the one literal-word hit for 0x801F69D8 outside the overlay band (SCUS_942.54 +0x390) in an address-reference sweep over all 1234 images, and it says nothing about any one slot-B entry.
FUN_8003EBE4reads*(0x8001038C)at0x8003EC24;FUN_8003EC70reads*(0x80010390)at0x8003ECCC. Both then runFUN_8003E8A8(param + 0x381)andFUN_8003E800into that buffer.- The two loaders differ only in the residency tracker they stamp:
gp+0x924(slot A) vsgp+0x934(slot B). - No instruction in
SCUS_942.54stores to either word: a sweep forlui 0x8001paired with a memory op at+0x38C/+0x390finds nine sites, alllw. - In-band hits for the base are equally uninformative: ~70 sibling images share the VA, and every
jal/j/ branch to it is that sibling's own code. When overlays share a load base, a reference to the base is a reference to the slot, not to a tenant.
PROT 0968 - the Cort battle stage overlay
Status: resolved. Evidence: capture (residency), disassembly (loader chain and extent).
PROT 0968 is the stage overlay of the evolved-form Cort fight: a 7-state scripted battle set-piece paged into slot B by battle-stage id 2. Only 2600 bytes of its two sectors are its own.
Loader chain
- Stage overlays are paged by a computed parameter, so no
0x49constant exists anywhere: sub-states0x0E/0x10of the battle loader read the stage-id byte_DAT_8007B64Aand callFUN_8003EC70(stage_id + 0x47)(battle.md). - The selector is a hardcoded override ending the formation fix-up in
FUN_80055B6C, the battle scene initialiser, at0x80055D2C:
lbu v1, -0x42f4(v1) ; v1 = *(u8 *)0x8007BD0C - the first formation monster id
addiu v0, zero, 0xb5
bne v1, v0, 0x80055d48
addiu v0, zero, 2 ; delay slot
sb v0, -0x49b6(at) ; *(u8 *)0x8007B64A = 2 - the battle-stage id
- Formation id
0xB5is monster-archive id 181, Cort's evolved second form (HP 65535). The first-form fight is a separate formation headed0xB4(id 180, HP 50000); both carry the display name "Cort" (asset monster-archive --id 181on PROT 867). - So stage id
2→ param0x49→ extraction 968. The same byte against the same constant pages 0969 mid-battle when a Cort form's HP reaches zero.
Residency
cort_evolved_battle_first_menu(PCSX-Redux; scenejouine, first command-input screen, before any cast; fingerprint inscenarios.toml): loader-B tracker0x8007BC4C = 0x49, entry 968 100% byte-resident at0x801F69D8over its0xA28extent, formation head*(u8 *)0x8007BD0C = 0xB5. Scriptcheck-0968-residency.py. It is the same observation pair that pins 0967 for the Tetsu tutorial.- The bracketing field states
cort_evolved_approach_cutscene/cort_evolved_pre_battleshow the co-resident library 0900 at 100% with tracker0x05, so the page-in is the battle load. - The six
cort_*_mid_castmednafen states read formation head0xB4(four first-form) or0xB5(two evolved-form), and in every one the slot-B page is 100% byte-identical over0x1000bytes to that state's cast stager (0938 / 0940 / 0944 / 0961 / 0962 / 0966) with the tracker reading the stager's id; 0968's window matches at chance (10.5-12.1%). A cast stager is a full slot-B page, so the fight's first special or summon evicts the stage overlay. _DAT_8007B64Areads0x00in all nine mid-fight / bracketing states: it is transient around the load, not a mid-battle marker.
Extent
- Own content is file
0x00..0xA28: a 7-entry dispatch table at offset 0 (every target inside the window) and code from0x1C. Everyjal,jand LUI+ADDIU materialisation in the window resolves inside it, intoSCUS_942.54, or into the co-resident slot-A battle overlay; none reaches past0xA28. The first instruction reads the battle-context pointer_DAT_8007BD24and writesctx[+0x6D6] = 0x100. - The trailing 1496 bytes are byte-identical to PROT 0967 at the same file offsets, cut mid-string at the sector boundary - packer-buffer inheritance from the tutorial overlay. They contain
FUN_801F747C, a text-box placement routine dispatchingjr *(0x801F6B48 + style*4): in 0967 that address is a 10-entry jump table at file0x170, right after 0967's 91-entry step table; in 0968 file0x170is live code. The window also materialises0x801F7C80, a string that exists only in 0967, past 0968's end. - Structural measures taken over all 4096 bytes ("pointer-table head, 10 of 11 self-pointers, 2+8 spawn calls") mix the two modules.
What it calls
| Callee | Role | Calls |
|---|---|---|
FUN_80050ED4 | summon / effect-actor pool allocator | 8 |
FUN_80021B04 | actor spawn | 2 |
FUN_80024E80 | screen-fade spawn | 2 |
FUN_8003541C | text actor | 1 |
FUN_8004FCC8 | cue / streamed-voice dispatch | 1 |
FUN_80058490 | MoveImage VRAM blit | 1 |
FUN_80035F04, FUN_80050E74 | - | 1 each |
0x801D829C | in the co-resident slot-A battle overlay | 4 |
That is the external-call family of the tutorial overlay 0967 minus the tutorial's prompt helpers. Owning page: battle.md.
PROT 0977 / 0978 extraction + the dump re-key
Status: resolved - both entries are in the static overlay map and every overlay_0977_* / overlay_0978_* dump resolves. Evidence: disassembly.
The static map (static-overlays.toml) carries 0977 (arena_init, the Muscle Dome door/init slot-A overlay at 0x801CE818, anchor FUN_801D0F60) and 0978 (field_back_read, slot B 0x801F69D8, pinned by the SCUS FUN_80025358 state-2 call into FUN_801F6B24); asset overlay verify reproduces both fingerprints from the disc. With those images indexed, check-dump-base-integrity.py classifies all 22 dumps in the two families and none is NOT_FOUND:
| Dumps | Verdict | Bytes live in |
|---|---|---|
801d050c 801d08ec 801d1288 801d1308 801d14b0, slotA_801d0f60 | MATCH | 0977 at the printed VA |
other_game_801f6b24 | MATCH | 0978 at the printed VA |
0977 801c085c 801c0f48 801c2748 | SHIFTED +0xE818 | 0977 own code (801C085C→801CF074, 801C0F48→801CF760, 801C2748→801D0F60 - the War God Icon settlement) |
0977 801c614c 801c6268 801c6804 801c6cf8 | SHIFTED +0xA018 | 0979 (801C614C→801D0164, 801C6268→801D0280, 801C6804→801D081C) |
0978 801c2b58 801c3004 801c39b8 | SHIFTED +0xD818 | 0979 (→801D0370 / 801D081C / 801D11D0) |
0978 801c5c58 801c7b40 801c82dc 801c8b04 801c8d0c | SHIFTED +0x9818 | dance 0980 (801C5C58→801CF470 - the documented beat-clock SM) |
Each delta is one wrong base seen through an over-read footprint imported at 0x801C0000:
- 0977's footprint holds its own
0x3800bytes, then 0978 (0x1000), then 0979. Own-content prints re-key at+0xE818(0x801CE818 − 0x801C0000), 0979-stratum prints at0xE818 − 0x4800 = +0xA018. - 0978's footprint holds 0979 from
+0x1000(+0xD818) and the dance overlay from+0x5000(+0x9818). - The two-hit
801c614csignature (a duplicated 10-instruction run inside 0979) is disambiguated by the batch-constant delta: its program siblings resolve single-hit at+0xA018. - A "
baka_fighter_0976at+0x5710" hit is a cross-overlay duplicate of a sequence that MATCHes 0977 at its printed VA. 801c2b58,801c3004,801c39b8,801c614c,801c6804are four distinct routines of the field-battle-intro overlay 0979; two of the dumps are the same routineFUN_801D081Creached through two different wrong bases, which cross-checks the decode.
Slot-B capture-module band 0935..0966 per-entry identity
Status: resolved - the per-entry map is static spell-table data in SCUS_942.54, capture-corroborated. Evidence: disassembly.
A capture-class spell record (class byte 'c' at stats +0) pages its cast module as FUN_8003EC70(record[+1] + 0x28) (lbu a0,0x1(v0) at 0x801E44E4, jal 0x8003EC70 with addiu a0,a0,0x28 in its delay slot at 0x801E44EC, PROT 0898), and the loader resolves raw TOC index param + 0x381 (addiu a0,s1,0x381 at 0x8003ECC4) = extraction param + 0x37F, so the module is extraction entry 935 + record[+1]. Enumerating the 'c'-class records yields the complete map: the sub-id space covers 0935..=0966 exactly, with no orphan entry.
- Parser
legaia_game_tables::spell_names::capture_class_records/capture_module_prot; the disc-gatedspell_names_realtest asserts band coverage and every independently pinned leg (capture-pinned boss stagers 938 / 940 / 944 / 961 / 962 / 966; playtest-pinned Delilas / Xain modules 952 / 953 / 958 / 959 / 960). - Shape census: every band entry resolves its
lui 0x801F/0x8020; addiuself-pointers in-file at the slot-B link base, spawns throughFUN_80021B04/ theFUN_80050ED4pool wrapper, and carries damage-wrapperjals where the battle-formulas wrapper census puts them. - 0957 = the Death Game / Thunder Storm module; its head strings
Dies/Puera/Both/Damage/Recoverare Death Game's roulette outcome labels, not a summon-effect descriptor or debug table. - 0965 = the Doomsday module. It is not a shifted sibling of 0967: the claimed shift
0x5FE8lies wholly past 0965's real0x2000-byte extent, and the two entries share no content.
Owning page: spell-table.md § capture-class module index.
New-game world-state seed store widths
Status: resolved - the port matches the disassembly. Evidence: disassembly.
Every (offset, width, value) entry in legaia_game_tables::new_game::new_game_seed_words matches the stores FUN_80034A6C issues, decoded from SCUS_942.54. Ghidra's DAT_ / _DAT_ naming is a symbol-size heuristic and is not width evidence (ghidra.md).
- The routine holds the save-context base in
$s0(lui $s0, 0x8008/addiu $s0, $s0, 0x4140=0x80084140, at0x80034A74) and issues each seed write as ansborswat$s0 + off: sixsbover0x454..0x45Band nineswover0x45C..0x478/0x590..0x59C, all inside0x80034A88..0x80034AE8. - The C's absolute globals
DAT_80085958/DAT_80085959aresb $v0, 0x1818($s0)/sb $v0, 0x1819($s0)- the starting-item pair atINVENTORY_SC_OFFSET,SC-relative and issued after the template expander, so not part of the pre-expander set. - The starting-item pair is stored at
0x80034B10/0x80034B18, after thejal 0x800560B4at0x80034AFC. - The story-flag clear is a downward walk from
$s0 + 0x1FFoversb $zero, 0x1618($v1)(0x80034B04..0x80034B28, loop onbgez), coveringSC + 0x1618..0x1817-0x200bytes, the port'sSTORY_FLAGS_LEN. - The disc-gated
new_game_seed_disc::world_state_seed_matches_the_routines_storesre-derives the table from the instruction encodings in the user's executable and fails on a wrong offset, value or width.
Owning page: new-game-table.md.
_DAT_8007B8C2 polarity, and its writer
Status: resolved - != 0 is retail, == 0 is dev. Evidence: disassembly + capture.
The halfword at 0x8007B8C2 is the build-mode selector. main() stores 1 into it once at cold boot; the non-zero arm of every reader is the path a retail disc can service.
- Writer:
main()(FUN_80015E90) at0x80015F08,sh v0,0x5aa(gp)withgp = 0x8007B318, storing the return ofFUN_8003F084- a two-instruction leaf (jr ra/addiu v0,zero,0x1) whose sole caller is0x80015F00. A stubbed-out build-mode predicate; the dev build presumably returned0(inference). - Readers: every read is an
lh- 43 sites inSCUS_942.54(40 in the absolutelui 0x8008/lh -0x473eform, three gp-relativelh v0,0x5aa(gp)at0x80015FD4/0x80016038/0x8001631C), 57 across the dump corpus including overlays. != 0arm: resolves assets by PROT-TOC index (FUN_8003E8A8+FUN_8003E800, orFUN_8003EB98).== 0arm: opens a path throughFUN_800608F0, whose entire body isbreak 0x103- a PsyQ dev-station host trap - onh:\paths that do not exist on a retail disc. No site dissents; at0x80016038thebnez v0at0x80016040skips thejal FUN_8003E6BCdev-path call when the flag is non-zero.FUN_8003E6BCdoes no CDNAME name resolution. Its body isstrcpy→break 0x103→ fseek / fread / fclose; thepath_openerlabel is Ghidra's, not evidence.- Capture: the halfword reads
1in 60 of 60 Mednafen save states - field, battle, world-map, stock and randomized discs. - Not zero-initialised: the PS-X EXE header carries
b_addr = 0, b_size = 0, so the BIOS clears no BSS for this executable.
The store and three of the reads are gp-relative, so a sweep for only the absolute lui 0x8008 / -0x473e form reports zero writers and 40 readers: ghidra.md.
Key-item area consumers
Status: resolved on the narrow negative; the reader enumeration is closed. Evidence: disassembly.
The range is inventory slots >= 72 of &DAT_80085958. No consumer treats a key-item byte as an unguarded index, and no instruction on the disc names an address inside the band.
Indexing is bounded by construction. Readers use the id byte as an index into 256-entry, 12-byte-stride item tables: an lbu yields 0..255, so the maximum offset is 3060 against a 3072-byte table. The only signed lb reads (0x8004250C, 0x80042510, in FUN_800423E0) are a compaction move immediately re-stored via sb, with no index use.
The enumeration is closed by three structural facts.
- The array sits
0xA640abovegpand ends atgp + 0xA840, so noimm(gp)instruction can address any byte of it. - Neither
0x80085958nor the block base0x80084140occurs as a literal 32-bit word anywhere in SCUS or the 1233 PROT entries, so no pointer table offers an indirect route. - Every access materialises the block base inline (
lui+addiu 0x4140,sll slot,1,addu,lbu|lb|sb ...,0x1818/0x1819).
Decoding for that displacement pair gives 125 sites over six images:
| Image | Sites | Notes |
|---|---|---|
SCUS_942.54 | 51 | 13 functions, 0x8003004C..0x800430A0 |
| PROT 0899 (menu) | 55 | 19 functions; four hide behind a lui in a branch delay slot |
| PROT 0897 (field) | 5 | |
| PROT 0898 (battle) | 2 | |
| PROT 0941 (Steal) | 7 | no active window; clamps its random slot against a live count (0x801F7828) |
| PROT 0954 (Fatal Decision) | 5 | no window, no clamp: rand & 0xFF over all 256 slots (0x801F81A4..0x801F81B0, retry cap 0x400) - the only reader that reaches the key-item band outside the menu window |
Every other based overlay and PROT entry has zero. A displacement-only match gives 156 hits across 11 files, of which 31 are data-byte coincidences in five scene entries. The two absolute sites are the new-game seed's slot 0 (0x80034B10 / 0x80034B18).
Other facts
- The
& 0x3ffmask belongs to four packed-handle sites, not to readers generally, and it admits slot 1023 -0x5FEpast the array - so it is not a 256-slot bound. - The add / find / consume helpers bound their scans by the live window, but the id store at
0x800422BCis not bounded: when the free-slot loop at0x80042270finds no empty slot the index exits equal to the window limit andsbwrites one slot past the scanned window. Thesltguard at0x800422C0is downstream and gates only the quantity byte. The overflow index derives from the window-limit global, not from an item byte, so it is a bounded one-slot write - the range amplifies to game-state corruption, not a native chain step (reachability). lb $reg,0x5aXX($zero)"hits" in overlay dumps are mis-decoded data tables: 117 occurrences across 74 files, and SCUS's 7 sit at0x80010AE4..0x80010AFCas a stride-0x10progression - a pointer table, not code.
Owning page: inventory.md.
title.pak PROT entry
Status: resolved. Evidence: capture.
There is no single title.pak bundle entry: the dev-tree title.pak content is split across two PROT entries, both confirmed by fingerprinting a title-phase RAM snapshot (the title_screen_new_game save state) - the method that identifies 0895_bat_back_dat as init.pak, with the same CDNAME-mislabel pattern.
- Title wordmark TIM: PROT 0890 at file
0x14228(parserlegaia_ui_formats::title_pak); the big-logo RAM TIM at0x80170DF8fingerprint-matches it. Offsets quoted against 0888 (0x1AA28) or 0889 (0x19A28) are the same absolute bytes reached through over-read entry sizes. - Options / config-menu bundle: PROT 0899 (
xxx_dat). Its indexed payload opens with the config-menu string pool ("Display Off / Gradual / Immediate / Field HP Display / Encounters / Vibration / Dual Shock / Voices / Battle Camera / Monaural / Stereo ...") followed by the small config TIMs (the four RAM TIMs at0x8010FEF0..0x80110130, CLUTs byte-matched at 0899 offsets0x169DC/0x1F91C+). - Title-overlay code: the same entry - the title tick
FUN_801DD35Cis PROT 0899 file+0xEB44(below).
Title screen mode-table PROT
Status: resolved - no row is named for it, but it is a mode: the CARD pair, 22/23. Evidence: disassembly + disc bytes.
The title screen runs under the CARD mode pair. The mode table's 28 x 24-byte records at 0x8007078C carry fourteen even/odd name pairs - CONFIG / MAIN / MONSTER / TMD / EFECT / TEST / MAPDSIP / MAP / READ / GAME OVER / BATTLE / CARD / OTHER / STR - and none says "title".
Chain from main() to the title tick
| Step | Site | Effect |
|---|---|---|
| Seed | FUN_8001D424 at 0x8001D5B8 | _DAT_8007B83C = 0x10 (mode 16 READ) |
| Pre-loop overlay load | 0x8001612C jal 0x8003ebe4, a0 = 0 | extraction 0895, the boot init.pak |
Mode 16 init FUN_8002612C | calls 0x801CE9C0 (0895 +0x1A8) | publisher-logo pass; sets mode 17 at 0x801CEC94 |
| 0895 | 0x801CF4D4 | writes mode 22 |
Mode 22 init FUN_8002574C | 0x800258B4, a0 = 4 | loads PROT 0899, spawns descriptor 0x800706D4, sets mode 23 |
Descriptor handler 0x801E36A0 (0899 +0x14E88) | jal 0x801dd35c at 0899 +0x14E94 | calls the title tick every frame |
Loader param 0 is therefore producible - by main() - so 0895 is statically reachable; 0896 is not (boot.md).
Ownership of FUN_801DD35C
- Its 48-byte prologue occurs exactly once in all of
PROT.DAT, at extraction 0899 file+0xEB44(0x801CE818 + 0xEB44reproduces the VA); it is absent fromSCUS_942.54. - Its own master-mode stores:
0x801DDCF0(0x1A, attract → STR) and0x801DFC00(2, NEW GAME → field). overlay_801dd35c.txtis a different routine: the 436-byteFUN_801DD310from PROT 0897, a VA alias.- The engine carries two ports of the routine (
menu.rs,title_overlay.rs):vm-inventory.md.
XP-table source + reader
Status: resolved + ported. Evidence: capture.
The retail XP curve is the static-SCUS per-level delta table DAT_80076AF4 (u16), and the delta is the closed form delta(n) = ⌊n²/4⌋ + 1.
- Reader: the level-up applier
FUN_801E9504(overlay-resident, called from the reward resolverFUN_8004E568at0x8004F34C). The running sum to the current level is scaled(sum × 9999999) / 0x140FEforlevel < 0x11(elsesum × 0x79) and compared≤ record cumulative XPin a multi-leveldo...whileloop. Checked against the listing: the level test issltiu 0x11at0x801E95D0, thex 9999999shift chain and the0xCC2ABE13reciprocal (>> 48, a divide by0x140FE) run0x801E95D8..0x801E9614, and thex 0x79arm is0x801E9618..0x801E9624. - Not
0x8007123C/0x80070A3C: those are an off-by-0x800file/virtual confusion and a sin-LUT slice. A New Game Status capture shows "Next Level 121", the real L2 threshold, not 50. - Port:
legaia_save::RETAIL_XP_CUMULATIVE/retail_xp_table()ship the derived base curve (121, 365, 730, ..., 9_646_483); the boot-time disc parse (legaia_game_tables::level_up_tables::xp_thresholds_from_scus→BootSession) cross-validates it byte-identically. - Capture: library-wide record sampling (
+0x0XP /+0x4next threshold /+0x130level at0x80084708 + slot×0x414) matches through L37 including the Noa / Gala ± corrections (New Game 121 / 102 / 140; L99 carries 0). The Status menu (FUN_801D33D8) draws+0x0/+0x4verbatim.
Owning page: level-up.md.
Overlay identity from the disc (static extraction)
Status: resolved (pipeline legaia_overlay_images::static_overlay + asset overlay ...). Evidence: capture.
PSX overlays are clean copies of a fixed-VA-linked blob (FlushCache + jump, no per-load relocation), so each runtime overlay is extracted statically from its PROT.DAT entry and disassembled at its load base, with identity taken from the source entry rather than a guessed label. That resolves VA aliasing (0x801DD864 = battle-action in one overlay, muscle-dome in another).
- Proof case: the battle overlay (PROT 0898 @
0x801CE818) is byte-identical to its resident RAM image over the full.text+.rodata(0x28800of0x29800bytes; only the trailing.bssdiverges). - Base recovery: from the overlay's own internal
jalcall graph (static_overlay::recover_base). For a call graph too sparse, the cross-check is a documented function landing on a prologue (anchor_va, slot A) or the fraction of internal absolute self-pointers resolving in-file (static_overlay::pointer_resolution, slot B). - Committed map (
crates/asset/data/static-overlays.toml): the slot-A scene family (field / battle / menu, the cutscene/STR overlay 0970, the minigame overlays 0972 / 0975 / 0976 / 0980) and the slot-B entries (render 0900, the summon stagers including 0903 Gimard, 0905 spell0x83, 0907 Nighto "Hell's Music", 0924, 0927; GAME OVER 0902 is slot A; 0957, labelledsummon_effect_table, is the Death Game cast module). - Reconnaissance:
asset overlay scan(range sweep: base + leading dev string) andasset overlay find-sig(locate a function-head signature → infer the host overlay). - It complements the dynamic captures: runtime values still need live probes.
Owning page: static-overlay-pipeline.md.
PROT 0896 (bat_back_dat) identity
Status: resolved. Evidence: capture (identity and non-residency), disassembly (link base).
The unique ~0x9000-byte head is the vestigial Japanese-build field-menu / config / status overlay - the debug-string sibling of the English retail menu overlay PROT 0899. It links at 0x801D4DF0 against an executable this disc does not carry, and the USA build never loads it.
Identity (decode of the head off extraction entry 896, located by its "FWIN ERR" bytes)
- A Shift-JIS label pool: config toggles, the Item / Summon / Equip / Status / Config / Save top menu, the ATK / UDF / LDF / SPD / INT / AGL + EXP status labels.
- The
"FWIN ERR %d"window-manager debug printf at file0x3D4(FWIN= Field WINdow); nofwin/bat_backreference exists inSCUS_942.54. - Real MIPS (~54 prologues): a status / name-draw routine indexing the
0x414-byte character records, and head function-pointer tables holding ~61 addresses across0x801D81C0..0x801DC700(the window / screen renderers) - the VA family of 0899's0x801D33D8status renderer and0x801DC6B4save SM. Also a large byte-map-like data block (rows of gradually shifting byte values). - 0899 carries the English versions of the identical label set and zero
FWIN. - From file
+0x9000an over-read footprint carries the field overlay's bytes; they are not 0896's.
Link base 0x801D4DF0
- Call-graph recovery over the
0x9000-byte entry: ten corroborating targets of eleven distinct internaljaltargets, ten of themaddiu sp, sp, -Xprologues. - All 218 internal
jinstructions land inside the file there; none does at0x801C5818or0x801C0000. - Three runs of consecutive in-image VA words resolve every word, one holding the base itself.
- It reconciles the corpus's two phantom import programs: the function printed at
0x801C6534and at0x801C0D1Cis one function at file+0xD1C. 0x801C5818(60jalvotes) is an over-read artifact: the field overlay's self-consistent code at0x801CE818fixes a whole-footprint recovery to0x801CE818 − 0x9000by construction. Alui-pair resolution ratio is one-sided and is not a base test (falsified).
Never loaded by the USA build
- Foreign link. Of the image's 322 calls into the SCUS address range, zero land on a function entry of this disc's
SCUS_942.54(the same test scores 0897 at 1203 of 1307 and 0899 at 793 of 884), and no constant shift of the executable within +-0x20000brings more than 7 of its 42 distinct SCUS targets onto an entry. - No loader reaches it. A full-image scan of
SCUS_942.54forjal FUN_8003EBE4/FUN_8003EC70with thea0setup decoded finds 16 sites; every constant param maps to extraction 897..902, 969..981, or the spell- / stage-driven bands (id - 0x79summon stagers,+0x28special-attack,+0x47battle stage). Extraction 0896 needsparam == 1, which no site produces (the three computed-param sites have+0x74/+0x47/5-or-6bases). The raw indices0x381/0x382appear as immediates only in the two loaders' ownparam + 0x381adds, so there is no directFUN_8003E8A8/ file-open path either. The+0x47site reaches only 967 / 968 (battle.md§ Stage-overlay dispatch). - Not resident. A distinctive-signature scan across 140 catalogued RAM states (37 PCSX
.sstate+ 98 gzipped mednafen states, all phases) finds 0896 in none; the positive control, English "Battle Voices" (live 0899 config), is resident in 10 menu-phase states. Thescenarios.tomlsave_select_idlenote "overlay 0896 paged in" uses the extraction-index name for what is the English 0899. - Not the mode-24 OTHER overlay. A live capture of the Baka Fighter entry (
autorun_minigame_overlay_capture.lua, triggered on the0x8007B83C = 0x18write; sub-id0x8007BA34 = 4, confirming the0x3Eoperand−100 model) dumps the overlay window at +0 / +10 / +30 vsyncs, spanning the SCUS-resident OTHER INIT handler's completion (its"other init end"print) and the minigame overlay streaming into slot A. 0896's bytes appear at no offset in any dump, nowhere in main RAM in the pre-transition save, and in none of 45+ parked library states.
The CDNAME label bat_back_dat is not corroborated: no captured battle state holds the data, and under the raw-TOC index shift the #define covering 0896's extraction slot may belong to a neighbour. With the base committed, the image's code region is dumped and accounted - a base buys coverage, not a caller (byte-accounting.md). Owning page: static-overlay-pipeline.md.
SCUS recomp gap - render/GTE + boot/init clusters
Status: resolved, the general-game band included. Evidence: disassembly.
The static recompilation's function inventory lists SCUS entries with no dump, doc or port tag, clustered by VA band. Every member is attributed, mostly as block splits and +4 entry skews of documented functions; disc-coverage.py puts every SCUS_942.54 code byte inside a dumped body.
"COP2 render gap" band (0x43000..0x47000) - not render code
| Inventory entry | Is |
|---|---|
0x800430D4..0x80043134 | interior of FUN_800430AC (party-wide accessory unequip-by-id) |
0x80043238..0x8004325C | interior of FUN_800431FC (knows-spell) |
0x80043290 / 0x800432A8 | interior of FUN_80043264 (accessory-equipped) |
0x80043580 / 0x8004361C | interior blocks of the cluster-A renderer FUN_80043390 (far-colour / ZSF setup + its custom-convention epilogue) |
0x80046498 | FUN_80046494 + 4, the locomotion collision resolver (not a render→overlay draw seam) |
0x8004697C | FUN_80046978 + 4, palette fade |
The 14 gte_execute entries are statically linked libgte per-op wrappers (MulMatrix0, Square12/0, AverageZ3/4, OuterProduct12/0, DCPL / DPCT / INTPL, the RotTransPers3-shaped RTPT projector) with zero static callers and zero runtime hot-profile hits: link residue, since the render paths issue COP2 inline. All ignore-listed; table in functions.md § libgte primitives.
Boot/init cluster - aliases of documented functions
| Inventory entry | Is |
|---|---|
0x80016448 | FUN_80016444 |
0x80016B74 | FUN_80016B6C |
0x800173C0 | FUN_800173BC (dev profiler HUD, ignored) |
0x80016998 | interior of FUN_8001698C |
0x80017914 | FUN_80017910 |
0x80017A04 family | interior of FUN_800179C0 |
0x8001A078 | interior of the dev printf FUN_8001A068 |
0x8001A814 | interior of FUN_8001A78C (RGB→HSV) |
0x8001AA14..0x8001AA60 | the six hue-sextant jump-table arms inside FUN_8001A8DC (HSV→RGB) |
0x80019BC0..0x80019D48 | interior of the atan2 bearing resolver FUN_80019B28 |
0x8005B2A4 / 0x8005B340 | interior of PushMatrix 0x8005B268 / PopMatrix 0x8005B308 |
New identifications
FUN_80015E90=main()(boot.md§ The main loop).- The dev draw cluster:
FUN_8001CE34(3-D line),FUN_8001CAD8(wireframe box - the sole source of8001CE34's in-degree of 12, so it is not a heavily used boot utility),FUN_8001CCFC(2-D line),FUN_8001C7A0(4x8 digit printer). FUN_800430AC, whose Ghidra auto-analysis body is degenerate until force-created.FUN_8004CE2C, the largest SCUS function in the inventory: the per-frame battle actor maintenance pass (battle.md§ Per-frame actor maintenance), not a mode dispatcher.
General-game band
0x8002A9F8is the branch target past the header test ofFUN_8002A9CC, the"ME"channel-delta codec:beq v0,v1,0x8002a9f8at0x8002A9E8takes it when(b0 & 0xC0) == 0x40, and the fall-through is thejr ra; clear v0reject. A block split, hence no static caller (functions.md§ battle).0x80025DA4isFUN_80025DA0+ 4, the Mode 12MAPDSIP INIThandler, split after its leadinglw v0,0x798(gp)(boot.md).0x8004DC68is the near-camera ghost pass (see the Battle area);0x80036D80is the ambient ramp pool's own actor template (see the Animation area).0x80056208is not a libgpu-band bridge: it is a battle side-band tick (three submodes offDAT_8007B64A) at a PsyQ-adjacent address, ported inengine-render.0x8002149Cand0x80059E10both carry full disassembly.- The PsyQ sound-driver cluster is tracked under Audio.
Full-window item-add OOB reachability
Status: resolved - the write primitive is real; normal play cannot reach it. Evidence: disassembly (full window) + inference (the half-window sub-case).
FUN_800421D4 can store an item id one slot past the active inventory window, but the retail add call sites cannot drive it there in normal play.
The write
- The id store
sb t0,0x1818(a0)at0x800422BCis unconditional and precedes theslt/beqguard (0x800422C8/0x800422CC), which gates only the count store at0x80042300. - When the free-slot scan (
0x80042254..0x8004229C) exhausts the window it leaves the index== end, so the id lands atbase + end*2:0x80085A58forend = 128,0x80085B58forend = 256. - The window is installed only by
FUN_8004313C, as[0,256),[0,128)or[128,256)- never a 72-slot span. - No add caller pre-checks room; each loads an item id and
jals the helper directly (shop buy-confirm0x801C38A4loadinga0 = rec+8; battle loot0x8004F380/0x8004F608; the menu / save / fishing / world-map / minigame / equip-refund helpers). The helper's scan is the only backstop.
Reachability
| Window | Installed when | Verdict |
|---|---|---|
[0,256) | any party of >= 2 (live-verified at 3 members) | unreachable: the merge pass keys on the id byte (andi a3,t0,0xff @ 0x800421F4), so each non-zero id occupies at most one slot and 0 is the empty sentinel; at most 255 distinct ids fill 256 slots, a hole always remains |
[0,128) / [128,256) | a single playable member with story flag 20 clear | not capacity-bounded: the static item-name table 0x80074368 carries 250 non-empty names over 256 ids (blank: 0x00, 0x12, 0x1A, 0x52, 0xB9, 0xFD), so 128 distinct live ids is arithmetically possible. The bound is how much of that population a lone character can obtain - a progress bound, unmeasured (the inference) |
- Cast module PROT 0954 (Fatal Decision) reads the whole array with no window and no live-count clamp (
rand & 0xFFover 256 slots). - A non-add path (debug menu, cheat engine, a crafted save seeding duplicate live ids) can still force the exit with an attacker-influenced byte; that is outside normal play.
Port: legaia_save::retail_inventory (ItemWindow::oob_reachability, MAX_DISTINCT_ITEM_IDS, OobReachability). Provenance: ghidra/scripts/funcs/{800421d4,8004313c,8004e568,8003ce64}.txt, overlay_0971_801c36b0.txt. Owning page: inventory.md.
Phantom-VA sweep of the PROT 0897 imports
Status: resolved - the three residues the delta arithmetic leaves open are byte-decided. Evidence: disassembly.
The two measured deltas (0xE818 base error, 0x25000 over-read) re-key most 0897-import prints. The rest are decided by a word-level comparison (resolve-phantom-va.py): each dump is compared against every candidate (image, base) reading at the printed VA, with Ghidra's data-as-instruction renderings (nop, <load> rt,imm(zero)) re-encoded into exact 32-bit words so data regions decide at full strength. Every verdict is word- or token-exact against the corrected-extent images, and each rival reading is excluded by the same comparison.
- Boundary band
0x801E4000..0x801E6000: every dump resolves to exactly one reading, and the strata switch exactly at0x801E5000. The two open addresses are 0897 own-content data - pointer tables at true VAs0x801F3308/0x801F3450(14/14 and 13/13 words; the rival 0898 reading scores 0).0x801E5134is printed by two programs with two owners: one print correct, one a phantom of 08980x801CE94C. 0x8020D05C: 0898 rodata at true VA0x801F6874, a(pointer, count)table into 0898's0x801CF9xxband. Its words include values with no R3000 decoding, matching the dump's zero-instructionhalt_baddata; every rival reading maps the VA to code that would decode. Not a function under any reading.- PROT 0896: the
overlay_0896_*prefix covers two imports of the over-read footprint - untagged at0x801C0000(three strata: own content< 0x9000; field+ 0x5818; battle- 0x1F7E8) and taggedbase=0x801C5818(prints are 0896's own bytes atprinted - 0x801C5818). Every addressed dump resolves under exactly one program, and the header-tag partition agrees with the byte partition dump for dump. The same function printed by both programs pins the pair: file+0x5C90at0x801C5C90/0x801CB4A8, file+0xD1Cat0x801C0D1C/0x801C6534. 0x801FD4C0: its dump starts at printed0x801FD150and is the battle image'sFUN_801E6968; the printed VA is that body's interior at 0898 VA0x801E6CD8, not the field image'sFUN_801E6B34.
Owning page: overlay-va-aliases.md § the byte-level sweep.
The publisher-logo quads
Status: resolved - FUN_801CE9C0 uploads and does not draw. Evidence: disassembly; the descriptor table reproduces from the extracted image.
FUN_801CE9C0 (PROT 0895 +0x1A8) uploads the four init.pak TIMs through FUN_800198E0 after writing their CLUT and pixel VRAM rects, selects the 640x480 display env (FUN_8001DAF8(0x400)) and spawns the two boot actors. It contains no primitive emit: its jal set holds the four uploads (0x801CEA68, 0x801CEA9C, 0x801CEC40, 0x801CEC54), the display-env call (0x801CEB18) and the two spawns (0x801CEB60, 0x801CEB70), and no call to the OT linker FUN_8003D2C4.
- Quads: a six-record, 20-byte sprite-descriptor table at
0x801F369C(file+0x24E84, immediately after the fourth TIM):[u32 scale][u16 tpage][u16 clut][u8 u,v,w,h][rgb top][stp][rgb bottom][tpage adder]. - Emit: opaque
POLY_GT4s fromFUN_801CFBB8, sequenced byFUN_801CEFD4(13-arm jump table at0x801CE8E8). - Binding: each record's
tpage/clutmatches a rect the uploader wrote -0x9A/0x7ED4,0x9C/0x7F54,0x0A/0x7F14,0x0B/0x7E80. - Order: SCEA, Contrail, PROKION.
- Fade: the PSX texture blend
texel * colour / 128over a vertex colour scaled by a0..0x80level, not alpha.
Owning page: boot.md.
FUN_801E5A08, the per-slot equip applier
Status: resolved - one routine, unreferenced. Evidence: disassembly + bytes.
"FUN_801D71F0" and its "shared armament placer 0x801E5AE8" are one 324-byte routine, FUN_801E5A08, and nothing on the disc calls it.
ghidra/scripts/funcs/overlay_0897_801d71f0.txt(and the_801d7210sibling) is mis-based by0xE818: the bytes are PROT 0897 file+0x171F0, true VA0x801E5A08.- Its four class arms end on
j 0x801E5AE8/j 0x801E5AEC- intra-function jumps whose targets print correctly while the body prints low.0x801E5AE8is the routine's own inline placer at+0xE0. - Class -> equipment byte: 0 -> 0, 1 -> 1, 2 -> the weapon index (
_DAT_8007B42C,2/3/2), 3 -> 4 (anaddiu v1,zero,4in the delay slot at0x801E5AB0). The class is bits0x60of equipment-table byte+7(0x801E5A80..0x801E5A8C); a slot argument of 4 or more bypasses the class and stores atslot + 1(0x801E5A4C..0x801E5A50,0x801E5AE4). - No
jal, data word oraddiumaterialisation of0x801E5A08exists in any image. The live equip confirm isFUN_801D9C14's candidate arm.
Port: legaia_engine_dev_menu::dev_equip_commit::commit_equip.
The dead Return view mode
Status: resolved - dead; the grid has 15 cells. Evidence: disassembly + bytes.
FUN_801E3F74's mode 4 ("Return", 0x801CF384) is real and unreachable.
- The selector returns
4only for cell0xF(li v0,0xf/bneat0x801E3F74..0x801E3F84). - Its only caller forms the cell as
col + row*5(0x801E06D0..0x801E06E0). - The shared stepper wraps rather than clamps, and the range is the same:
col < 0becomes4andcol >= 5becomes0(0x801E0170..0x801E019C),row < 0becomes2androw >= 3becomes0(0x801E01A4..0x801E01CC). The cell tops out at14. - The linear seed
_DAT_8007B7CChas three references on the disc, all in PROT 0899; its single writer (0x801DED2C) stores the samecol + row*5.
Source: docs/reference/re-settled-threads/title-boot-overlays.md
Rendering / camera
# Settled threads: Rendering / camera
| Thread | Answer | Grade |
|---|---|---|
| What left V-Windhash's arm-3 trail brighter than retail's? | the trail was seated one tick into its pass, not on the pass's drained focusWhyArm 3 drains the word by the whole pass and seats the blade on the shot's focus ( | disassembly + capture |
How does retail draw Kemaro's arm-17 rings 0x8000 out? | in the far bucket, under the band washWhyOn the shot's trio every corner saturates | capture + disassembly |
| Which frames of Ozma's closing arms lose the creature? | none: the closing arms framed off retail's focusWhyA retail table of arm 23 ( | disassembly + capture |
| Why does Aluru's head tip show through arm 11's ray burst? | the rays' rand() deal, not draw orderWhyA | capture |
Why does the engine's camera.offset_ease stand apart from retail's _DAT_8007BCAC after a door hop? | a timed 4C 49 is a rampWhy
| disassembly + capture |
Which pose does kor_field_route_walk's frame stand on after the walk rewind? | the RAM's pose of the pass two backWhyA film of the deterministic route walk that made the state ( | capture |
What frames Koru's Done tail on its first pass (koru_pre_*)? | the held pass of case 7, pitch includedWhyA retail film of Gala's Lightning Storm into the Done arm (probe pre-trace camera columns from | capture + disassembly |
What draws Koru's ice shards on the koru_pre_* frames? | the stage's own crystals under the held pitchWhyNo source record: the shards stood high because the displayed pitch was the Done arm's held | capture |
What pose does a monster cast's frame display under a one-frame shot it holds on the second 0x2B pass? | the 0x2A terminator's pose on the case-6 walk; the frames measured the instrument, not the gameWhyThe pass count is | capture + disassembly |
| Why did the full-party Caruban Fire Breath stills miss the camera? | the replay seated the breath's dearer step on the pass that had not paid for itWhyThe frame driver waits a frame out on the step that pass ran at and hands the step its cost raised to the next pass ( | disassembly + capture |
| Why does Big Wave's additive sheet draw so few packets? | a raised +0x42 sends the record down FUN_8002735C, whose winding test has no double-sided mask; the floor row clips what staysWhy
| disassembly + capture |
| Why do Ozma's fans read white in retail and dark in a frame built from its packets? | an arm that arms the wash replaces the frame's clear; the packets build over earlier framesWhyA retail ordering table of arm 16 rasterised on black matches the cleared engine frame. The far bucket holds an opaque black rect except on a frame with the armed wash ( | capture + disassembly |
Why did battle_dohaty_nova_v2_scythe_wind_mid_cast read .860 with the camera on retail's? | the displayed pose was walked back on a table built once; arm 4 builds its shot every passWhyNot the tornado ring. Scythe Wind's arms 3 to 5 call | disassembly + capture |
| Why does Scythe Wind's camera stand frozen through the walk? | the approach leg made no case-6 call for itWhyArms 0 and 1 of the walking bodies call | capture + disassembly |
| Where does Koru's 0962 charge shot set out from? | the half-turned walk-in framing, after a walk a capture replay skipsWhyArms 0 and 1 of the | capture + disassembly |
| What rewrites the prologue's CLUT rows to gold? | four mode-3 CLUT-cell cyclers in the prologue effect scriptsWhyRecords 10..13 of the effect-script bundles of | capture + disc data |
| Are camera-relative move-VM parts drawn where retail draws them? | yes, to the pixel, on both armsWhyIn every library state a flagged part's | capture |
| How far is the hosts' f32 camera rotation from retail's q3.12? | sub-pixel almost everywhereWhyMatrix elements differ by at most 2.44/4096. Over 392000 samples spread through the view volumes of 98 states, 0.77% move half a pixel or more; the median per-state worst case is 0.18 px, the worst 2.98 px ( | capture |
What does FUN_80028158 build? | a Legaia TMD object of GT4 packetsWhy
| disassembly + capture |
| Why does map01's kind-4 puff column hold seven nodes in two groups? | one spawner, two children, one driftingWhyOne spawner re-seats itself at | disassembly + capture |
| Which Muscle Dome hub packets blend? | only the row-502 palettes 0/2/6/8, additivelyWhyThe hub marks variant packets semi-transparent (variant 1 | disassembly |
| In what units are a field attached light's extents? | view space, six times world scaleWhy
| capture + disassembly |
Why did vell's fog not show on the native host? | the effect-texture pool has to sit under the scene VRAMWhyThe fog page | capture + disassembly |
| What sets the field-fog cap? | MAN header bits 0 and 2Why
| disassembly |
Why did the port's vell fog run denser than retail's? | a raw LCG state where retail calls BIOS randWhy
| disassembly + capture |
What are FUN_80024EE4's three arguments, the per-frame screen-effect push? | one quad, never a familyWhyThe routine builds exactly one full-display | disassembly |
| What does the scene-entry camera reset write? | six stores, and H is not one of themWhy
| disassembly |
| Is the field follow camera's framing one set of constants? | no - retail derives it per scene and per player tileWhyMeasured across the walkable state population, a single pinned height matches 12 of 19 states, a single pitch 8 of 19 and a single yaw 1 of 19 - so the pins are one state's values. Retail's arrival handler queries the MAN section-3 zone table and hands the hit's camera-region record to the config loader, which writes the angle trio, the height and the eye trio with hold and glide rates. Camera roll is a separate matter and is genuinely zero in 51 of 51 sampled frames. | capture |
Which FUN_8002C69C arm lays the post-battle report's nine-slice? | resolvedWhyThe routine reads the style from | disassembly + capture |
FUN_80058490 - a sound-driver lane? | MoveImageWhyIt moves a VRAM rect to | disassembly |
How does FUN_80043390 unpack an actor's afterimage colour? | resolvedWhyActor | disassembly |
| Which routine draws the world-map / field ground pass? | PROT 0900 ships a depth-cued / flat pair, not one emitter twiceWhy
| disassembly + capture |
| Which ordering-table slot does each screen effect take? | resolvedWhyLetterbox bands at OT | disassembly |
| Does any retail shot author a non-zero camera roll? | yesWhyEight scenes stage a reachable, executing op- | capture + disassembly |
| Does retail stack coincident curved shells? | noWhyField-run display-list reads in | capture |
FUN_80045BB4 - a function entry, or interior residue? | the bank-3 kind-19 textured Gouraud quad handlerWhyIt is word 12 of the bank-3 cluster-A primitive-handler table at | disassembly |
How does PROT 0901's 0x801F7644..0x801F8EB4 band split into routines? | nine: eight tail-call leaves plus a framed ninthWhyThe band has no prologue and no | disassembly |
| Which base does the world-map overlay's per-prim handler table use? | 0x801F8968, with words 0..7 zeroWhy
| disassembly |
Does FUN_8002C69C draw the post-battle report windows? | yes - driven from SCUS, off the retained widget listWhyPROT 0898 contains no | disassembly + capture |
What is FUN_80019D50? | the CLUT-cell HSV cyclerWhyIt rotates hue / saturation / value over one CLUT cell block and pushes the result with a single | disassembly |
What order does FUN_80026988 (RotMatrix) compose? | Rx(vx) * Ry(vy) * Rz(vz)WhyThe sin / cos tables it walks are the pointers at | disassembly |
Who reads the GTE light matrix FUN_8001ADA4 writes inline? | five sites, all in SCUS, all in the world-map NCC handlersWhyA disc-wide GTE command-word census finds exactly five light-matrix consumers: | disassembly |
| How often is the field view matrix built? | three times a vsync, from three different callersWhyAn exec-count probe over a field run puts | capture |
| Is the field view's pre-multiplied matrix a pure scale? | _DAT_8007BF10 = 24576 * I, zero translationWhyRead out of a retail | capture |
Does anything on disc select the light matrix through MVMVA? | nothingWhyOf the 31 | disassembly |
| How many call sites does the field view builder have? | seventeen jal sites disc-wide; three run per field vsyncWhyThe three that run each field vsync are the | disassembly |
| What the dance count-in banner is made of | one textured quad off the hall's own texture pageWhyPROT 1230's member at VRAM | capture |
| The field passive-ability badge column, host against retail | pixel-exactWhyThe panel's origin matches retail with | capture |
What stages the dance widgets' second texture page at VRAM (960, 256)? | boot-resident system UI; no PROT entry stages itWhyThe page is in no PROT entry: it is uploaded from | capture |
| Why does the dance floor hide the dancers' shadows in PSX mode? | a depth tie the port's height lift could not break; retail draws the floor first in one slotWhyIn | capture + disassembly |
| Which of a field frame's view builds does the drawn geometry use? | the first; the last frames none of itWhySplitting every ordering-table link by GPU command code over three runs - including a real | capture |
| Which routine draws a field or world-map actor's mesh? | three arms of one bracket, chosen by actor[+0x42] then +0x7AWhyEach of | disassembly |
| Does any sampled mode enter the table-driven mesh renderers? | noWhyOver 720 vsyncs across four states and three game modes - battle, field, cutscene and the slot machine - the census records 0 entries into | capture |
Does the disc ship writers of actor[+0x42], the mesh-renderer gate? | yes, three families - one dev-gated, two contentWhy
| disassembly |
Is an actor a shipped script raises +0x42 on drawn through a bracket? | yes - field placements, through FUN_8001C204 + FUN_8002735CWhyField-VM | disassembly |
What do move-VM ext sub-ops 0x17..0x1A write? | four different writes to one object-effect rowWhyRow | disassembly |
| How large are Viguro's arm-5 sparks? | the module shrinks the effect pool's sprite scaleWhyPROT 0909's arm 0 stores the pool head's sprite scale | disassembly + capture |
| Which emitter does effect render mode 4 take? | all three - the move VM writes both flag bitsWhy
| disassembly |
| Where does a fade block's delay and hold end, frame for frame? | delay n suppresses n - 1 frames; hold 0 retires the frame after the ramp landsWhy
| disassembly |
Does any asset set an actor's render mode +0x56 to 4? | no - every write of 4 is codeWhyThe four | disassembly |
| Is the field fog half-sheet a world-space quad? | a view-space billboardWhy
| disassembly + capture |
| What does the field drop shadow draw? | four textured quads over a projected 3x3 gridWhy
| disassembly + capture |
| Which field actors cast a drop shadow? | flags with 0x01020000 and without 0x200000Why
| disassembly + capture |
Why did opdeene's jungle plants draw black natively? | the dim ambient belongs to lit rows, not to a vertex colourWhyThe prologue's dim ambient | capture |
| Does the port's battle tint pass match retail? | yes, distance fade includedWhy
| capture |
| What is the party body radius in battle? | 640; 1024 on TerraWhy
| capture + disassembly |
| What camera does the Baka Fighter duel play under? | the overlay's own scripted shot, not a fitted oneWhyRound setup (state | disassembly + capture |
| How is the Baka Fighter arena built? | four placed wall models and the battle ground gridWhy
| disassembly |
| Where do Ozma's middle arms part from retail once the camera matches? | the module band's rand() deal, not the bodyWhyOn the | capture |
| What whites out Ozma's arm 24? | the frame step: arms 22 and 23 at retail's 3, then the dealWhyArm 24's luma runs a 32-vsync pulse on both sides, the engine's a pass ahead on the arm's clock: retail's step falls to | capture |
| How much of Iota's arms 5..9, and of its backdrop through arm 5, is left once the sheets stop crossing? | the rays' rand() dealWhyWith the cycler rows, scrolled pages and part sets agreeing (cast-module), | capture |
| Why are Slippery's storm clouds dimmer than retail's in arm 6? | each record's rand() % 4 pickWhyThe clouds stand where retail's do on the same | capture |
Long write-ups in this area
Does retail stack coincident curved shells?
Status: resolved (no) - Evidence: capture.
Every surface in the live ordering table is submitted exactly once, so retail has no stack of coincident shells to order.
- Measurement: field-run display-list reads in
jouind(1263 packets walked) andjouine(1016) report zero screen-coincident surface groups above a 16 px^2 area floor, with no undecoded polygon link in either state. - Decoder requirement:
POLY_FT3carries seven payload words. The two chains hold 46 and 45 textured flat triangles - the strand meshesjouineis built from - and a walk that skips them is blind to exactly those. - The one coincidence: a single mesh drawn three times in one texture family (
clut=0x7F86 tpage=0x001F). That is the multi-pass semi-transparency idiom, not a mesh stack: its members share a material, and two different env TMDs would not. - Consequence: the coplanar kernel's same-position curved-shell residual is a property of how the port assembles a scene's env TMDs, not something retail resolves by ordering.
- False positive 1: retail double-buffers, so ordering tables come in pairs holding frame N and frame N-1 with near-identical packet counts; merging a pair makes every surface look stacked with itself.
- False positive 2: distant geometry projects to 1-3 pixel slivers that coincide constantly without saying anything about meshes, which is what the area floor is for.
- Coverage limit: each read is one frame and one camera. The library's other
jouine/jouindentries are byte-identical backups of these two (a library filename is the sha256 of its contents), andchitei2is unmeasured and inherits the conclusion. The negative rests on scale: a stacked shell would double many adjacent surfaces at once, and in cave interiors the walls are the dominant on-screen geometry.
Does any retail shot author a non-zero camera roll?
Status: resolved (yes) - Evidence: capture for the census (a disc-derived executing oracle), disassembly for the factor.
Slot 2 of the op-0x45 CONFIGURE mask is the roll angle _DAT_8007B794, the third factor of the camera build FUN_80026988's Rx * Ry * Rz (it reads three angles off its argument, +0 / +2 / +4, at 0x800269F8, 0x80026998, 0x800269A4), and eight scenes stage a reachable, executing non-zero value.
- Factor: the per-node variant
FUN_8001CF50loads_DAT_8007B794at0x8001CFD0..0x8001CFE8(lui a0,0x8008; lh a0,-0x486c(a0)) and calls0x8004638Cunless the render node's+0x52bit0x200is set. That site is not insideFUN_80026988. - Writers: none of
FUN_801DAB90,FUN_801DB8ECorFUN_801DBE9Cwrites_DAT_8007B794; the only zeroing is the scene-entry resetFUN_80025C24(sh zero,-0x486cat0x80025C5C). - Beat shape: every rolling beat carries the full nine-slot mask - pitch, yaw, roll, the eye trio, focus X and Z,
H, and no focus Y (theopdeenereading oncutscene.md). Every operand is an in-range 12-bit angle, and the beats of one shot repeat the same tilt.
| scene | what it is | PROT entry | record | roll (12-bit) | degrees |
|---|---|---|---|---|---|
edstati3 | Ending (station gondola) | 826 | P2[0] | 10, 20 | 0.9, 1.8 |
station3 | Karisto Station | 616 | P2[0] | 30 | 2.6 |
map03 | World map (Karisto Kingdom) | 392 | P2[10] | 60 | 5.3 |
nilboa | Nivora Ravine | 638 | P2[33] | 60 | 5.3 |
taiku | Zora's Floating Castle | 373 | P2[27] | -120 | -10.5 |
korout | Sol Tower (entrance) | 534 | P2[3] | 240 | 21.1 |
juui1 | Ra-Seru scene (1) | 588 | P2[0], P2[3], P2[4] | -400 | -35.2 |
juui2 | Ra-Seru scene (2) | 597 | P2[0] | -660 | -58.0 |
edstati3is a real scene: the ending cutscene block, bundle at extraction entry 826. Its CDNAME label inherits forward as far as the battle-data packs at 863 / 864.- Frequency: of the 371 CONFIGUREs a control-flow walk reaches, 123 set the slot and 15 of those write a non-zero value. Roll is a minority term, not an absent one.
- Port:
Camera::roll, the shell'spsx_camera_mvp/cutscene_view, the cutscene interp's tenth component, and the browser page's orbitcam.roll.
Method. A field-VM record's tail is not linearly decodable, so a decode census measures its own resynchronisation policy. The authored rolls live in partition 2 (cutscene-timeline / walk-on beat records), while a linear census walks partition 1:
| Instrument | CONFIGUREs reached | Non-zero rolls | What it measures |
|---|---|---|---|
| Strict linear (stop at first decode error) | 21 | 0 | its own blindness - it reaches none of the eight |
| Resuming linear (advance one byte on error) | 2182 | 637 roll operands, 7 of them outside the 12-bit angle space | its resynchronisation into data |
| Raw byte scan (decode at every offset) | 4257 | a "2 %" ratio | its own post-hoc credibility filter |
- The resuming sweep's shortlist of "coherent" candidates -
deroa,chitei2,station3,town0b,retona,nilboa,edstati3- holds three of the eight and misses five. - The decider is control flow. Stepping the ported field VM (
legaia_engine_vm::field::step) from each record's real entry PC, under a probe host that answers every branch predicate both ways, gives the PCs control flow can reach; executing the same records in a realWorldgives the subset that runs. Both find the same eight scenes, and neither reaches a roll operand outside the angle space, which marks the resuming sweep's impossible operands (26708) as data. - Oracle:
crates/engine-core/tests/thread_camera_roll_execution.rs; the linear modes are kept as a negative record incrates/scene-formats/tests/thread_camera_roll_census.rs.
Owning page: cutscene.md.
What does field op 4C ED do to a PSX-mode frame?
Answer: it writes the dither word _DAT_8007BA66 (sh at 0x801E351C), which the frame-begin driver FUN_80016B6C copies into the DRAWENV dtd bit. Boot writes 1 at 0x8001D520.
Evidence: disassembly for the writers; capture for the word, which reads 1 in every library state, ending_vignette_fullscreen included. The census (asset field-op-census --only "4C ED" --context) finds 00 only in the ending scenes, immediately ahead of the closing 43 12 / 43 13 wipe of a vignette, with the next vignette's head raising it again. So the word is off for the wipe and the wait that follows it, not for the picture.
Port: World::toggles.draw_dither, read per frame by both hosts into PSX mode's dither gate (Renderer::set_draw_dither, the page's setDrawDither). No channel of the retail-compare corpus can see it, because no state stands inside the window.
Owning page: shading.md.
Source: docs/reference/re-settled-threads/rendering-camera.md
Measurement + corpus
# Settled threads: Measurement + corpus
| Thread | Answer | Grade |
|---|---|---|
Why did the image child draw battle_uru2_nurga_glare_mid_cast's near Nurga opaque? | the child re-derived no ghost bits on a partly aligned poseWhyThe second Nurga holds the near-camera ghost ( | capture |
What pose does battle_garmel_songi_hyper_wave_mid_cast display? | two step-2 passes back along the RAM's tableWhyThe close-up accumulator reads | capture |
| What are the bytes past a DATA_FIELD stream's terminator? | last-sector fillWhyAll 34 clean streams end inside their entry's last | capture (disc census) |
| Does any scene place an actor whose model lives in another PROT entry? | no loadable scene doesWhyOver the 101 CDNAME blocks with a MAN, every placement with a model byte below | capture (disc census) |
What do move-VM ext sub-ops 0x36 / 0x37 compare against? | the live actor count; every 0x37 is a pool-headroom guardWhy
| disassembly + capture |
| Does a shipped move program keep the camera yaw factor on a camera-relative node? | yes, one: urudre1 stager record 14, word 0x0080WhyThe word sits past that record's | capture + disassembly |
Where in the story does the party first enter vell (West Voz Forest)? | between drake_castle_revisited and voz_forestWhyFlag | capture |
Does any code reseed BIOS rand? | no; one kernel streamWhy
| disassembly |
Why do PROT 0896's jal targets miss this disc's SCUS entries? | a foreign-build linkWhyImported at its own base | disassembly |
What is PROT 0970's run at 0x801D0E9C..0x801D199C? | an unreferenced AC VLC tableWhyEvery word is zero or | disassembly |
| What sizes the debug monitor rows and the 0972 / 0977 tables? | their consumersWhy
| disassembly |
| Which catalogued save states carry a patched executable? | measured and tagged per familyWhy
| capture |
| What are PROT 0897's short data tables? | window layouts, sound-test rows, a handler table, window programsWhy27 | disassembly + inference |
| What is PROT 0976's sparse table? | Baka Fighter's per-fighter move tableWhySeventeen per-fighter blocks of nine | disassembly |
What does the indexed form lui; addu; lw lo(at) hide? | 512 accesses, none in 0897 / 0899, no verdict movedWhyA strict register walk completes the form 512 times over SCUS and the mapped overlays (273 SCUS, 223 PROT 0896, 9 PROT 0971). A scanner that never drops an overwritten register reports 23 / 44 phantom sites in 0897 / 0899. Both reference sweeps share the strict walk ( | capture |
| Which live ports cover only part of their routine, and which of those differ from retail? | every residue portedWhyOf 43 partial ports a 316-tag audit found, each one that changed behaviour is ported or closed without a port details ↓. | disassembly + capture |
How often does a live // PORT: tag describe its routine correctly? | about seven in tenWhy316 live, dumped tags in the VM / world / audio crates read against their disassembly: 222 match, 45 misname the routine, 43 port only part of it, 6 name the wrong address. The window VM and anim VM were worst. | capture |
Does the byte account cut an overlay's inherited tail the way disc-coverage.py does? | yes - one rule feeds bothWhy
| capture |
What did the framed-text walk change in the op-0x49 census? | 55 sites lost, 54 gained, the spine-flag negative unmovedWhyDiffed site for site against a walk that does not frame text: every one of the 55 lost sites reads a printable-ASCII base pair (dialogue decoded as an op), and five scenes leave the census entirely; the 54 gained are real instructions behind a record's first line - 48 are the | capture |
| Can a green ladder rung assert its invariant vacuously? | yes - and a canonical one didWhyA rung meant to show that re-applying an inventory Arrange leaves the bag unchanged drove Throw Out first, which emptied the bag; retail's own dispatch then swallowed the confirm, so the assertion compared two empty bags and passed. A rung's order is part of its claim: an assertion whose subject an earlier step can empty has to check the subject non-empty before it compares. | capture |
| Does a "not observable" reach verdict survive the next export? | no - it is a link-time propertyWhyWhether an address is observable at all depends on what the profile links in, so a verdict recorded against one export does not carry: the same addresses come back unobservable on the next run rather than staying converted. Read such a cell as a statement about the export that produced it, and re-run the export before planning work off it - the same law the reach audit states for the prose beside an address. | capture |
| Can a per-vsync save-state capture measure an envelope statistic? | no - the emulator's envelope is not on emulated timeWhyPCSX-Redux runs its SPU on a thread paced by the audio device accepting samples and steps ADSR once per sample that thread produces, so a capture that writes a ~19 MiB state every vsync advances the envelope by an uncontrolled amount per captured frame. Measured: the wall-clock gap between captured vsyncs averages | capture |
Are FUN_801D14B0 and FUN_801D6710 two routines? | one, linked into two overlaysWhyBoth are 24 instructions and 22 of them are identical (words 2..23 of | disassembly |
Can replay-port-coverage.py's PARTIAL UNION line tell a failing ladder from one nobody exported? | resolvedWhyNo. Both leave no | inference |
What does DAT_8007BB38 hold? | the id just issuedWhy
| disassembly |
| Can a mode-seat parity witness sample the mode word? | no - an INIT mode never survives the callWhyAn INIT mode lasts one frame inside the seat's own entry call, which resolves it and hands off to RUN before returning, so no post-call sampler on either host can observe one. The witness that does work is the edge count: five on each host with the hooks in place, three without. A parity check reading a mode word instead is measuring the wrong global as well as the wrong moment. | capture |
| What does a reach-triage cell assert? | nothing - it is prose beside an addressWhyThe page audit checks the addresses a row cites, not the sentence next to them, so a cell can keep describing work a ladder has already done. Nine of eleven "not driven" rows were converted by canonical ladders whose cells were never rewritten. Treat a reach cell as a claim to re-measure, not as a measurement. | inference |
Does a PCSX-Redux .sstate carry the scratchpad? | yesWhyThe state carries a 64 KiB | capture |
| Is the overlay corpus's uncovered-byte residue un-dumped code? | sixteen of twenty runs are not codeWhyFour of the twenty ranked runs on the bytes-derived dump worklist hold real routines. Three are slot-A: | disassembly |
| Why does every extracted overlay image end in another image's bytes? | the build bufferWhyAn image's extracted extent runs past its own content into another module's code at the same file offset - a build-buffer leftover, the | disassembly |
| What is the slot-B modules' unclassified data tail? | the spawn-record bandWhy
| disassembly |
Does FUN_8003E8A8 return an LBA? | no - a sector countWhy
| disassembly |
| Where is the libcd directory cache? | 0x801CB408Why
| disassembly |
The asset descriptor's +0x04 word | Σ of the decompressed sizes; never readWhy105 PROT entries carry a descriptor table, and in all 105 the header word equals the sum of the descriptors' decompressed sizes (re-measured over every extracted entry). It is dead at runtime: the walker | disassembly + capture |
record[0] + 0x5C in the player battle files | no reader existsWhyA sweep of 518,656 words across 84 images finds 8 non- | disassembly |
| Two minigame overlays carry private copies of the two-window tile lookup | resolvedWhyPROT 0972 at | disassembly |
| Do the feature views measure distinct features? | no - twelve measured one blobWhyA BFS from a feature root spills through the title tick into the save UI, the effect spawner and the move VM, so | capture |
What is in the SCUS_942.54 code gap the citation graph never listed | resolvedWhy | disassembly + capture |
Why three plausible-code blocks in SCUS_942.54 can never be a function body | resolvedWhyThe BIOS kernel-patch cluster copies | disassembly |
| Is the unattributable dump-extent residue a re-dumping problem | noWhyThree shapes remain and only one is dump-shaped: short windows no image reproduces at that VA, bytes in no extracted image at any VA (needs an extraction), and extents where two dumps genuinely resolve to different images (an answer). The "repaired by re-dumping" reading is on | capture |
| Can the inner of two nested overlay spans be measured at all | yesWhyAddress ambiguity is total for it by construction; byte attribution places most of its extents anyway and the row reports. What is structural is that it can never be resolved by address - a statement about one method, not about the image. | capture |
| Where does a slot-B image's highest spawn record end? | its own move-VM program bounds itWhyWalk the opcode widths to a terminator - | capture |
When does SCUS's jal 0x801F7B88 run? | ordinary in-battle frames, while PROT 0920's effect budget is non-zeroWhyOne Slippery cast (action | capture |
| Is a VA that prints in two neighbouring slot-B images one routine? | usually, but not always - diff the bytesWhy
| disassembly |
How long is FUN_801DD9D4? | 588 bytes / 147 instructions, 0x801DD9D4..0x801DDC20WhyEvery one of its seven dumps stops at 276 bytes, because the decompiler stops at the | disassembly |
content_bytes vs clean_copy_bytes - which is an overlay's length? | the entry's sector extent is the length; clean_copy_bytes is how much a RAM capture verifiedWhyOn PROT 0899 they differ by | capture |
Does PROT.DAT's TOC leave gaps between entries? | no - it is a gapless partitionWhyThe resolver sizes an entry as the gap: | disassembly + capture |
PROT 0867's trailing 0x14000 slots | raw 4bpp PSX TIMs, not LZS monster blocksWhySlots 187..194 open with the TIM magic | capture |
summon.dat's seven unclassified slots | the big-summon third membersWhyPROT 0893 slots 77 / 81 / 85 / 89 / 93 / 97 / 101 are the raw slot of each big-summon group: id | disassembly |
| Do a MAN's partition offset tables tile its data region? | in order, data-region-relativeWhyRecord offsets are relative to | capture |
| Where does an overlay image's code stop? | at the word after its last jr raWhyEvery complete MIPS body ends in | inference + capture |
| Is a caller-site citation checkable, or only assertable? | checkable - the disc word at the cited address settles itWhyA citation of the form "called from | inference |
Which dumps do the overlay_dance_* prints above 0x801D6818 really come from? | PROT 0972, the fishing overlayWhyPROT 0980's own content is | disassembly |
| Which status effects is each enemy immune to? (community: SPD-down lands, ATK-down never) | no per-monster immunity - it is the scripted-fight boost profileWhySeru-magic stat debuffs are the side-effect | disassembly + capture |
| What is PROT 0981's link base? | slot A, 0x801CE818WhyDecoding the image's own | disassembly |
Which scene bundles carry a type-0x14 FLAG descriptor? | 28 of 105 tables, always the last slotWhyRe-measured over every extracted entry: the FLAG descriptor appears exactly once on 28 of the 105 descriptor tables, whose counts run 4 to 7 (nine count-4, four count-5, three count-6, twelve count-7), and in every one of the 28 it is the last entry. It is not a property of a bundle's descriptor count; the "every count-4 or count-5 bundle" reading pairs two properties that are only correlated. | capture |
| Why do the save sub-screens read as unreached by every ladder? | an entry-context gap, not a wiring gapWhy
| capture |
Do battle_cursor_pose's routines have a caller? | yes, all of them, in retail and in the portWhyEvery routine the module ports is reached by | disassembly |
| Does the browser play page emit audio? | yesWhyIntercepting the | capture |
| Why three big PROT entries carry a third of their bytes as residue | the consumers declare it - the extents are slot fillWhyEntries | disassembly |
| The "64 runs" in the residue reports | an instrument truncation cap, not a disc figureWhyA residue report that prints at most 64 runs per entry gives a floor: the real counts are 194, 92 and 84. No entry's largest run is capped, so the summaries read right while the totals do not. | inference |
Which shipped scenes carry field-VM ops 4C EA and 4C 52? | three and seventyWhy
| capture |
Why do two identically-shaped toc[p + 2] expressions disagree? | they use different pWhy
| disassembly |
Is a resolution ratio over lui pairs a base test? | no - it is one-sidedWhyA base whose two high halves catch few pairs scores perfectly on all of them, so the metric ranks a base by how little of the image it explains. On PROT 0896 it reports 65 of 65 at the refuted slot-A base and 110 of 177 at the base the call graph recovers, and it cannot see the call graph at all - which is where an image's own structure is. | capture |
| When may a dump's filename credit an extent? | only where something else in the same image confirms by bytesWhyAn extent whose printed instructions are outside the encodable grammar is unverifiable, and crediting it on the dump filename alone makes the filename the evidence - exactly the case where the dump program's base is wrong, so the extent lands at an arbitrary offset in a file that never held it (three such extents fall inside | capture |
| Can a measurement tool be stale the way its cache is? | yes, and more quietlyWhyA committed sweep CSV and a "fresh" per-entry run can disagree with the stale side being the binary: a | capture |
| Do two host kernels paired by name do the same work? | not necessarily - and an empty body pairs with anythingWhyThe host-drift gate's tier-11 alias row pairs kernels by name plus a stated reason, so an empty body pairs cleanly - on record, a native | inference |
What is PROT 0976's pointer table at 0x801D7134? | eleven lines of the Baka Fighter help panelWhyThe eleven words run | disassembly |
| What is a scene bundle's last-sector residue? | the packer's buffer - an earlier entry's bytesWhyThe bytes after a bundle's last descriptor's LZS stream are byte | capture |
| Is the packer's buffer run confined to an entry's last sector? | noWhyPROT 0976's last | capture |
| Where does a slot-B record chain stop? | at eight zero bytesWhyNo consumer loop bounds the record band, and a walk that chains past the padding reads an image's zero bytes as a | capture |
Why did PROT 0780 (edteien) rank as scene_event_scripts residue? | its prescript holds two records, under the standalone count floorWhyIts prescript holds two records, under the standalone detector's count floor, so the walker never starts - "walker tail" is the wrong verdict. The class selects the walker instead (the entry is already placed) and reads the records positionally, as the scene loader does. | capture |
| Is a dump that opens on data credited as code? | no - the extent is refusedWhy
| capture |
Is a #[wasm_bindgen] export visible to any cargo gate? | no - it is a property of the impl blockWhyA method in the plain | capture |
| How do you find a host's call sites for a web-only name? | scan [.:]name(, not .name(WhyA method-call scan misses path calls ( | inference |
| Can a probe cross a field door without a pad ladder? | yes - by tile pokeWhyA walk-on door is an exact tile match in the | capture |
| Does any battle routine carry its own generator? | no - one overlay PRNG, for ribbon geometry onlyWhyA scan of SCUS and every based overlay for | disassembly |
Which engine draws hand a raw LCG state, or a private seed, where retail calls BIOS rand? | none; every port draws the world streamWhyEvery draw is shaped through | disassembly |
What completes a tmd_size_prefix entry's truncated TMD at load? | nothing - no retail entry is truncatedWhyAll 182 entries that open with a type- | capture (disc census) |
Long write-ups in this area
Which live ports cover only part of their routine
Status: resolved - Evidence: disassembly + capture. Every behaviour-changing residue of the 316-tag audit's 43 partial ports is ported, or closed without a port.
Ported, on every host that draws or drives the routine:
- Field actors and scripts: the encounter reroll
FUN_801DDF48and its global_DAT_8007B5FC; the actor tick's move-VM step when+0x54 < 0;FUN_801DBF9C's parameter stream at its true byte offset; the entry-context park's release; the actor allocatorFUN_80020DE0, which resets a reused pool slot's handler, reflection link and state words; the model setter's4C 50re-stage throughWorld::npcs.models, which both hosts re-bind from, and the player-mesh rebuild for4C 50aimed atF8;FUN_801D1BA0/FUN_801D1EC4's player CFlag bit 24; the system channel's clip reset on every unlocked tick. - Settle and warp: the settle's clip tail with the timed kind-0 warp; its
4C CEclip override_DAT_8007B6AC; op0x22's clip-base write; the warp's0x801DA7F0tag (the4C E1text balloon, which the warp tears down) and its0x80000same-tile re-poll. - Camera: the follow camera's shake consumption and mode-5 focus;
FUN_801DB510's two hold gates (the reader half - the writers were already ported). - Audio and text: the SFX ring's three producers; the narration roller's config block.
- Tile board: event-cell flag writes and trigger exit (states
3and8), prompt and fade states, step cue, bonk, run and idle clips, octant facing. No retail scene installs a board, so these rest on disassembly. - Menus: Rula and Riremito, reached by the Door of Wind / Door of Light through the pause-menu session as in retail.
- Minigames: Baka Fighter's and the slot machine's face buttons; the strip emitter
FUN_801D31B0and the dome course cardFUN_801D042C, each ported whole; the dome hub's subtractive shade on all three hosts, its two waits and first-visit lines; Baka Fighter's impact pair, cameo and cell blit, and its display clip - the actor's+0x5C/+0x68pair the clip selector plays out past the booked exchange, which all three hosts pose from (minigame-baka-fighter.md). - Battle flow: the initiative sweep's dead-slot refund and tie list; the escape timer's HUD; the "nobody can act"
0x1E->0x6Eround with itsctx[+0x25]skip count; the0x801F696Cswing drift; state0x3E's class-5 arm at0x801E3F2Cwith its(rand % 2) * 2camera draw; the commit's clip-tag ladder; the Seru absorb's kill-check gate. - Battle special word and passes: the special-battle word's readers (the wipe rule
0x801E6578, the arena Run arm0x801D322C, the two result-window calls inFUN_8004E568); the0x801F0518flag write, the ribbon's per-clip caller and the War God Icon's per-stage bump. The shadow's+0x6Aflag is the after-image bracket the engine draws; the ghost pass'sgp+0x330gate passes on every battle state andctx[+6]comes from the flow mirror.ctx[+0x26B]follows a PCSX-Redux capture of the victory load hold - raised for its first 28 of 80 vsyncs (one capture), then idle; the loss window0x42draws on both hosts (battle.md). - Anim:
FUN_800480D8's schedule lives inengine-battle-vm::battle_actor_tick, outsideengine-render, soengine-corereaches it:World::battle_actor_draw_planruns it with the tint pass per body per frame on both play hosts.
Closed without a port:
FUN_801CF8AC: only the+0x10 & 3exempt early-out changes behaviour. The no-class arm is unreachable - bit 17 is never cleared on the disc and all three callers keep only& 1of the result - and the+0x98link is overwritten before anything reads it.FUN_80020F88:REPLACED-BYscene-build mesh binding plus the live model seat.FUN_8004DA00only seeks the drive, andFUN_80064090is unreachable (settled).
Audit rows not named here were not re-read against the fixes; start from the audit's own evidence column before trusting one.
What is in the SCUS_942.54 code gap
Status: resolved - Evidence: disassembly for the identifications, capture for the reachability split.
The gap the citation-denominated instruments cannot see is mostly unreferenced library code. Working the disc-denominated gap list to exhaustion yields about 95 function entries; a five-form reference sweep over each, across SCUS_942.54, the based overlay images and every PROT entry, splits them roughly three quarters / one fifth / three:
- No reference of any form, anywhere (about three quarters). Ghidra builds functions from the call graph, so a routine nothing references gets no function record, no dump and no citation; only a byte-denominated worklist finds it.
- Referenced from
SCUS_942.54(about a fifth). Mostly reached through the entry-stub / init path, or from a body whose own analysis was incomplete. - Referenced only from an overlay (three). Live routines whose only caller is in an image a SCUS-only analysis never sees - "zero static callers is not dead", surfacing as a coverage gap.
Four recovered entries are game logic rather than library material, written up in the function directory: the inventory count-add primitive carrying the 99 stack cap (0x80042FE8; the cap is slti v0,v0,0x64 at 0x8004302C and li a2,0x63 at 0x80043038), the CD-read retry step and subsystem mode toggle (0x8003F210 / 0x8003EDAC), and a camera preset (0x800260DC).
The reachability split rests on find-address-word-refs.py, whose negative covers static references only: a computed target assembled some other way, or a caller in an overlay never extracted, would not appear.
Owning page: disc-coverage.md.
Source: docs/reference/re-settled-threads/measurement-corpus.md
See also
A thread whose verdict no longer matches the disassembly moves back to the open list; the row here is not edited in place.