How this page was made
The base of every section is static analysis of the USA disc: disassembly of SCUS_942.54 and its overlays, and data decoded from the disc. Several sections also rest on emulator captures, and each says which kind. The Mt. Rikuroa capture is on a poked state: a retail cold boot from a memory-card save, with story flags cleared and the player teleported. The Fortress captures are one played memory-card save and one edited copy of it. The bath scene is read against the Japanese disc (SCPS_100.59) as well. Where a step is inferred rather than read or observed, the text says so. PROT numbers are extraction indices; P1[n] and P2[n] are records of a scene's script partitions 1 and 2.

WORK ERR on Mt. Rikuroa

Reported as
The debug text “WORK ERR2” appears in the top-left corner on Mt. Rikuroa right after the Golem fight, as the cave collapses, and stays (community reports; one recorded playthrough shows it, and two viewers of that recording report the same text at the same place). Also reported on a console and in DuckStation, both relayed rather than recorded, so it is not one emulator's artefact
Cause
The rock and dust burst after the Golem fight fills the fixed 143-slot actor pool; the number is how many requests arrived while it was full
Where
SCUS_942.54: print in FUN_80016444, counter 0x8007B8A0, spawn FUN_80020DE0, effect stager FUN_80021B04
Affects
Any scene, minigame or battle stage that runs the pool dry
Status
Partially explained - the message is proven and the event that fills the pool is captured; the failed request itself was never caught, and a played run is several actors heavier than the replay for a reason not yet measured

Everything that moves in a scene - the player, each townsperson, each door and prop, each rock and dust puff of a visual effect, each open dialogue box - is an actor, and all of them come out of one fixed pool of 143 slots. When something asks for a slot and the pool is empty, the request fails, an error counter goes up by one, and from then on the frame loop prints WORK ERR followed by that counter on every frame.

So “WORK ERR2” is not error number two. It says that two actor requests have failed since the current stage was set up. There is no separate “WORK ERR1” message: the digit is a %d.

Mt. Rikuroa uses 97 of the 143 slots just standing still. When the Golem is beaten the game does not rebuild the scene: it restores the mountain exactly as it was before the fight, every actor included, and at once plays the cave collapse, which throws up to 40 rocks and dust puffs at the same moment a dialogue box opens. Replayed from a memory-card save, that moment leaves between four slots and no slots to spare. A few more actors alive at the peak and the next requests fail: the counter reads 2 in every report and stays on screen until the next battle or scene load. Nothing else breaks; a part or two of the collapse never appears.

A recorded playthrough shows the text arrive at the moment the replay's pool is fullest, but with the dialogue box already closed, which the replay survives with three or four slots free. A played run is therefore several actors heavier than the replay, and which actors those are is not known.

Scene loaded: 97 of 143 7 82 props and NPCs 8 46 free Field resumes after the Golem: 110 of 143 110 live: everything from before the fight is restored 33 free Peak of the cave collapse: 143 of 143 7 82 props and NPCs 40 rocks and dust puffs 11 7 list sentinels 3 dialogue-box actors and 11 others; slot 143 is the last: the next request fails
The actor pool at three moments, to scale (143 slots of 0xD8 bytes). The top bar is measured on a loaded save. The other two are captured on a poked state: flags cleared and the player teleported to replay the climb. The pool is full in 7 of 32 replays, and no request was seen to fail.

The chain from text to cause

  1. The printThe frame driver FUN_80016444 loads the signed 16-bit counter at 0x8007B8A0 (lh a1,0x588(gp) at 0x800164B0) and, when it is non-zero, calls the dev text formatter FUN_8001A068 with the string at 0x800100F4. Unlike the two prints above it, it is not behind the development switch gp+0x3B8.
  2. The counterCleared by stage init FUN_8001E1B4 at 0x8001E304; incremented in exactly one place, 0x80020E0C..0x80020E1C.
  3. The failing spawnThat place is the failure arm of the actor spawn FUN_80020DE0: it calls the pop FUN_80020454, and when the pop returns null it bumps the counter, logs to the dev console and returns 0.
  4. The poolFUN_800203EC seeds a free stack of 143 slot pointers, 0x8007C5AC + i * 0xD8. The pop refuses when the top index at 0x8007C348 is negative; free slots = top index + 1. Stage init takes 7 slots as list sentinels, leaving 136.
  5. The battle return keeps the poolStage init always clears the counter, but skips the reseed and the seven pops when the field resumes from a battle: resume word 0x8007B8B8 == 2 and next mode 2 (bne at 0x8001E308, beq at 0x8001E31C to 0x8001E36C). The pool block is parked on the way into the fight and copied back, so every actor alive before the fight is alive after it.
  6. The collapseThe Golem record P2[47] is respawned by the system script when the field returns, and its post-battle arm installs two effect trees (34 30 0D, 34 30 07). Their parts are spawned by FUN_80021B04 (jal 0x80020de0 at 0x80021B78), two to seven per frame at the peak.
The instructions
800164b0  lh    a1,0x588(gp)        ; counter at 0x8007B8A0
800164b8  beq   a1,zero,0x800164cc
800164c0  lui   a0,0x8001
800164c4  jal   0x8001a068          ; dev text formatter
800164c8  _addiu a0,a0,0xf4         ; "WORK ERR%d"

80020df4  jal   0x80020454          ; pop a slot off the free stack
80020dfc  move  s0,v0
80020e00  bne   s0,zero,0x80020e34  ; got one: initialise it
80020e0c  lhu   a1,-0x4760(a1)      ; 0x8007B8A0
80020e14  addiu a1,a1,0x1
80020e1c  sh    a1,-0x4760(at)
80020e2c  j     0x80020f74          ; return 0

8001e304  sh    zero,-0x4760(at)    ; counter = 0, always
8001e308  bne   v1,v0,0x8001e324    ; resume word 0x8007B8B8 != 2 -> reseed
8001e314  lh    v0,-0x47c4(v0)      ; 0x8007B83C, next mode
8001e31c  beq   v0,v1,0x8001e36c    ; == 2 -> skip the reseed and the seven pops
8001e324  jal   0x800203ec

The stage-init branch is read from the instructions. The park of the pool block (in the mode-entry prologue FUN_80016230) and the 0x7B0C-byte copy back are read from the decompiled C and not re-read as instructions; their consequence is what the capture shows.

What the pool holds, measured

The scene is rikuroa: its field map is PROT entry 153, its walk-on triggers PROT entry 154 and its story-state script container the variant MAN in PROT entry 157 (13 / 29 / 64 records in its three partitions). The Golem record P2[47] sits on tiles (69,97) to (69,99): it sets flag 0x285 and starts a scripted battle against formation row 16 (3E FF 10).

Capture pedigree
A retail disc cold-booted in PCSX-Redux, and a memory-card save loaded through the title screen: Vahn alone on Mt. Rikuroa, just after the Golem. No save from before the Golem was available, so this is captured on a poked state: story flags 0x281..0x286, 0x300 and 0x366 were cleared, the player's position was written to stand on each trigger tile in route order, and the Golem's HP was set to 1. The triggers, the battle, the return and the collapse then ran on the game's own code.
At scene entrySlotsRead from
List sentinels7FUN_8001E1B4
Props and NPCs: script placements 1..2828the MAN header count
Props and NPCs: map objects with a script (doors, props)54the 128 x 128 object map of PROT entry 153
The player and the other system actors8measured; among them list-0 ticks 0x801D6058, 0x801DA51C, 0x801E76D4, 0x80036D80, 0x801D84D0
Total97 of 143free-stack top 45: 46 free
Moment (one replay)Free slots
Scene loaded from the card46
After the four trigger records on the climb44
Field resumes after the battle33
First rock burst, 160 vsyncs later15
Second rock burst, 237 vsyncs after the return3
Third burst, 446 vsyncs after the return9
At rest afterwards43

Thirty-two replays of the same battle, differing only in when Cross is pressed after the fight, bottom out at:

Fewest free slots43210
Replays215267

At zero free the pool holds 82 props and NPCs, 40 effect parts (tick 0x80021DF4), 7 sentinels, 3 dialogue-box actors, 4 list-8 actors and 7 others. The spread between replays is whether the three-actor “!” text box is open on the burst's peak frame. In none of the 32 did a request arrive while the stack was empty: the counter stayed zero.

With breakpoints on the spawn, 81 of the 96 spawns between the field's return and the peak come from return address 0x80021B80, the effect-tree stager FUN_80021B04 with template 0x8007062C. The stager tests the result (bnez s0 at 0x80021B84), so a failed part is simply a rock or dust puff that is not drawn.

The recorded run

One community recording shows the event in an ordinary playthrough. It is the only played run in this section, and everything read off it is graded community report: a video of someone else's session, with no access to its RAM. Times are positions in the video.

Video timeWhat is on screen
3:08 to 5:19The Golem fight: Noa alone at level 5, Terra beside her; Noa reaches level 6
5:26.5The field returns; the “Terra: !” box is already open
5:29.7The second box (“the cave is about to collapse! Follow me!”) closes
5:30.7WORK ERR2 appears, reading 2 on its first frame; Terra has run off and Noa is running
5:31.3The scripted fade starts; the mountainside fades in at about 5:32.5 with the text still up
6:48Noa walks through a cave mouth, a short fade; the text is still up
6:52.3A random battle starts; the text is gone from the first battle frame
9:12The field returns after that battle; the text does not come back
  • When. About four seconds, some 240 vsyncs, after the field returns. That is the replay's second burst (237 vsyncs after the return), the moment the replay measures the pool at its fullest. It is not the return from the battle and not the fade to the mountainside, which starts about 0.6 seconds later.
  • The digit. It reads 2 from the first frame it is drawn and never changes: both requests fail within one frame of the recording. No frame reads 1.
  • How long. About 82 seconds, across the fade to the mountainside and a walk through a cave mouth, until the next battle. That matches the counter's one clear, in stage init. The “transition to the next area” in the reports is not a scene change: the Golem record P2[47] fades the screen (34 01 FF FF FF 3C 00 at +0x131), moves the player and Terra to tile (0x5D,0x5D) (A3 F8 5D 5D at +0x13D) and plays the mountainside dialogue from the same record, so no stage init runs.
  • The run. Noa alone with Terra following, in Noa's opening chapter, after ordinary random battles and a menu visit on the climb. The player does nothing unusual, and two commenters report the same text at the same place: in a played run this is the common outcome.
  • The platform. An emulator at a raised internal resolution, by inference from the picture; the uploader names none.

The recording contradicts the replay on one point, the dialogue box. In the replay the pool only fills when the three-actor box is open on the peak frame; with the box closed, three or four slots stay free. In the recording the box has been closed for about a second when two requests fail. A played run therefore holds at least five more actors at the peak than the replay. The replay is a different party on a poked state (Vahn alone, teleported between trigger tiles); the reported runs are Noa with Terra, walked. Which of those differences supplies the actors is not measured.

The number on screen is then a derivation, and it is inference: the counter goes up once per request made while the stack is empty, the stager asks for two to seven slots a frame at the peak, and parts free themselves shortly after. A run that reaches the peak with the pool already full fails the requests of one frame and recovers. Every report reads 2; nothing in the instructions fixes that number, so its constancy says a played run is the same few actors over the limit each time. The failing caller is most likely the stager creating a part of the collapse, since the dialogue box is closed when the text appears. Three sources of extra actors are tested and ruled out on the replay's set-up: finished cutscene records do not accumulate, ordinary random battles do not leak slots, and nothing survives a real scene change.

Addresses

AddressImageWhatEvidence
0x800100F4SCUS_942.54the WORK ERR%d stringdisc data
0x800164B0..0x800164C8SCUS_942.54the print, in FUN_80016444disassembly
0x8007B8A0RAMfailed-spawn counter, signed 16-bitdisassembly
0x8001E304SCUS_942.54counter clear, in stage init FUN_8001E1B4disassembly
0x8001E308..0x8001E31CSCUS_942.54battle-return test: skips the reseed, to 0x8001E36Cdisassembly
0x8007B8B8, 0x8007B83CRAMresume word; next modedisassembly
0x80020E00..0x80020E30SCUS_942.54failure arm of the spawn FUN_80020DE0disassembly
0x80020454SCUS_942.54free-stack pop; null when the top index is negativedisassembly
0x800203ECSCUS_942.54seed: top 0x8E, 143 slots of 0xD8 bytesdisassembly
0x8007C348RAMpool control block: +0x00 top index, +0x28 free stackdisassembly
0x8007C5ACRAMslot 0; slot i at + i * 0xD8, last slot 0x80083D7Cdisassembly
0x80016230SCUS_942.54mode-entry prologue: parks the pool block when the field is left for a battleinference (decompiled C)
0x80021B78SCUS_942.54the spawn call in the effect-tree stager FUN_80021B04; result tested at 0x80021B84disassembly
return address 0x80021B80SCUS_942.5481 of 96 spawns between the battle return and the peak; template 0x8007062Ccapture (poked state)
PROT entry 153discMt. Rikuroa field map: 54 bound objectsdisc data
PROT entry 154discwalk-on triggers; the Golem record on tiles (69,97)..(69,99)disc data
PROT entry 157discvariant MAN: 13 / 29 / 64 records; P2[47] is the Golem record, effect installs at +0x23disc data
97 of 143 at scene entryRAM82 props and NPCs, 7 sentinels, 8 system actorscapture
143 of 143 at the collapse peakRAMzero free in 7 of 32 replays; no failed spawncapture (poked state)

The same routine prints PRG ERR%d (0x80010100, counter 0x8007B828) and READ ERR%d (0x80010134, counter gp+0x556) the same way.

Mt. Rikuroa is singled out by the burst, not by its standing load: read from catalogued save states at rest it has 44 to 46 slots free, and the scenes ropeway (15), garmel (20 to 27), koin3 (21) and dolk (25) are tighter.

Verifying it

Put an execute breakpoint on 0x80020E0C and play the climb from the mountain entrance through the Golem, from a save made before the Golem tile (flag 0x284 clear) in Noa's opening chapter, with Terra following. A hit is a failed spawn: the caller's return address is at sp+0x38 and s1 is the template of the actor that did not get a slot. The word at 0x8007C348, logged every frame, shows the load directly: 0x8E is an empty pool, -1 is exhaustion. That played, unpoked run is the one measurement still missing. Without a debugger, the recording gives the check: beat the Golem, dismiss both of Terra's boxes, and the text appears about a second later, before the screen fades; it is gone after the next battle.

Open questions

  • The failed spawn is not captured. The recording shows that two requests fail and when, not which caller made them.
  • What makes a played run at least five actors heavier than the replay at the peak: Terra's own actors, parts left by trigger records the replay skips, and actors a walked route leaves behind are all untested.
  • Why the count is 2 in every report and never 1 or 3.
  • The dev text formatter FUN_8001A068 is not traced for a gate of its own. The recording shows it drawing in an emulator; that it draws on a retail console rests on a relayed report.

No encounters in Jette's Absolute Fortress

Reported as
In some files Jette's Absolute Fortress has no random battles at all, before Jette is fought; it survives a reset and a memory-card load, and Incense and the encounter accessories make no difference (community reports, one first-hand: Reddit, r/LegendofLegaia, “Jette's fortress no random encounter”)
Cause
Escaping Zora's Floating Castle with under a minute on the clock leaves its “no battles” flag set for good, and the Fortress's encounter tables still obey it
Where
Clock actor 0x801D2EBC and roll FUN_801D9E1C in PROT entry 0897; scripts in PROT entry 373; tables in PROT entries 606 and 706
Affects
Both Fortress scenes, in any file that had less than 1:00 left during a successful castle escape
Status
Explained - a real defect; the flag write and the silent Fortress are captured on an edited save, and the castle escape itself was not played to its last minute here. A player's own test confirms the outcome: leaving the castle with under a minute left gives a Fortress with no encounters and no Door of Light (community report, first-hand)

The cause is in Zora's Floating Castle, hours earlier. When the castle is falling the game gives six minutes to get out, and it switches random battles off for the last minute by raising a story flag. The record that ends a successful escape stops the clock but never lowers the flag, so a file that reached the exit with less than a minute showing keeps it for good.

In the castle that no longer matters. But the Fortress's two encounter tables were built from the castle's and still begin with the same “this flag means no battles” group. In that file the whole Fortress is silent, and the Door of Light is greyed out there as well. No item, accessory or reset changes it, because the flag is saved with the file. A file that escaped with more than a minute to spare is unaffected.

Floating Castle escape clock Jette's Absolute Fortress random battles on no battles 6:00 5:00 4:00 3:00 2:00 1:00 0:00 0:20, to scale: lost at every scene entry File A: reaches the exit with 2:00 left flag 0x15 clear exit rate 18: battles default group File B: reaches the exit with 0:30 left flag 0x15 clear set at 1:00 exit rate 0: no battles group 0, the whole map hours of play later The escape record stops the clock and never clears the flag; nothing after the castle sets or clears it.
The castle clock to scale, and two files. The clock raises flag 0x15 whenever less than 1:00 remains; the exit times shown are examples. The flag write is captured on an edited save; a played escape ending under 1:00 was not observed end to end.

Check and fix

How
Check, no toolsIn the Fortress, open Items: the Door of Light is greyed out. In a healthy file it is usable there
Check, RAMbyte 0x8008575A, bit 0x04 set (system flag 0x15)
Check, memory cardbyte +0x161A of the save block, bit 0x04
Fix, RAMclear bit 0x04 of 0x8008575A once, then save. It stays clear: nothing sets it after the castle
Fix, memory cardclear bit 0x04 at save-block +0x161A and re-sum the block (+0x1FFC is the wrapping sum of the first 0x7FF little-endian words)
Fix, discpatch the disc with --fortress-encounter-fix (below): the Fortress stops reading the flag, so an affected save needs no edit
Avoidreach the Floating Castle's exit with more than 1:00 on the clock

The clock is not gentle: the scene loader takes 20 seconds off a running clock on every scene entry, and the castle is two scenes, so each crossing between them costs 20 seconds on top of the walking.

The fix

The patcher's --fortress-encounter-fix (a checkbox in the browser patcher; reference) closes it on the disc, and it heals a save that already carries the flag. In both Fortress scenes it rewrites the leading group of the encounter table - 14 bytes of each scene's data, same size - into a duplicate of the scene's own collapse group: the first condition tests flag 0x521 instead of 0x15, and its row becomes a copy of that group's whole-map row. The Fortress then never reads flag 0x15. A healthy file sees the same rooms and rates as before, and once the Fortress collapses the same quiet one.

The castle is not touched: its last minute stays quiet and the flag is still left set afterwards, with nothing left to read it. The missing op itself, a 60 15 in the escape-success record, is not added - the castle's script ships as a raw chunk of a streamed file, where a two-byte insertion cannot be made in place.

The cause, step by step

  1. The countdown opField-script op 4C D3 arms a countdown kept in the saved game state: two flag ids, a duration and a threshold (handler 0x801E2BDC..0x801E2C38 in PROT entry 0897), and spawns a clock actor.
  2. The clock actor0x801D2EBC takes the play-clock delta off the remaining time each tick. On expiry it sets the first flag (0x801D2F60); whenever the remaining time is below the threshold it sets the second (0x801D2F84). The flag ids come from RAM, not from constants in the code.
  3. The castle arms ittaiku P2[29] (PROT entry 373), the Mist Generator scene, clears flag 0x15 (60 15 at +0x1508) and then arms the clock at +0x1533: expiry flag 0x396, warning flag 0x15, 21600 ticks (6:00), threshold 3600 ticks (1:00). The castle's encounter table opens with a flag-0x15 group that is one whole-map rate-0 row: the last minute has no battles by design.
  4. Nothing clears itThe success record taiku P2[16] issues the same op with duration 0, which stops the clock, and never issues 60 15. The whole disc has one script clear of flag 0x15 - the one that runs before the clock is armed - and no native call clears it by constant id.
  5. The Fortress reads itFlag 0x15 heads the condition list of exactly three scenes: the second castle scene, and the two Fortress scenes deroa and chitei2. Their first row is the castle's row carried over: whole map, rate 0, both Doors blocked.
The instructions
801e2bdc  jal 0x8003ce9c ; addiu a0,s6,1      ; u16 operand 1
801e2be8  jal 0x8003ce9c ; move  s1,v0        ; u16 operand 2
801e2bfc  sll s1,s1,0x10 ; or s1,s1,v0
801e2c08  sw  s1,0x480(s0)                    ; +0x482 = operand 1, +0x480 = operand 2
801e2c14  sw  v0,0x478(s0)                    ; duration (non-zero = running)
801e2c1c  sw  v0,0x47c(s0)                    ; threshold
801e2c20  sw  v1,0x460(s0)                    ; remaining = duration
801e2c2c  jal 0x801d596c                      ; spawn the clock actor

801d2f44  lw   v0,0x460(s1) ; subu v0,v0,v1 ; sw v0,0x460(s1)
801d2f50  bgtz v0,0x801d2f68
801d2f58  lh   a0,0x482(s1) ; sw zero,0x478(s1)
801d2f60  jal  0x8003ce08   ; andi a0,a0,0xfff     ; expired: set flag "operand 1"
801d2f68  lw   v0,0x460(s1) ; lw v1,0x47c(s1)
801d2f74  slt  v0,v0,v1     ; beqz v0,0x801d2f8c
801d2f80  lw   a0,0x480(s1)
801d2f84  jal  0x8003ce08   ; andi a0,a0,0xfff     ; remaining < threshold: set flag "operand 2"

The 20-second charge is at 0x8003B62C..0x8003B648 in the scene loader (addiu v0,v0,-0x4b0 on the remaining time when a clock is running). Loading a save while a clock is running restores it to its full duration (0x8003B96C..0x8003B98C).

What the report describes

ReportExplanation
No encounters anywhere in the Fortressthe flag-0x15 group: one whole-map rate-0 row in both scenes
Jette and Cort still therethe file is before Jette; the post-collapse flag 0x521 is not involved
Survives a full restart and a memory-card loadthe flag bank is in the saved game state, save-block +0x1618
Incense and both Bells (the encounter-rate accessories) change nothingall three scale or suspend a rate that is already 0
The locked Fortress was visited before Conkramcoincidence: read from the scripts, that visit changes none of the three condition flags; the flag was set in the castle
“Sometimes”, across the communityit depends on how much time was left on the castle clock

What was captured

Capture pedigree
PCSX-Redux on the retail disc: cold boot, Continue, card load, then the pad walks up and down the interior's main hall. The save is a played one from inside the Fortress, before Jette. The key run is captured on a poked state: the castle's clock words were written into a copy of that save (an edited save), because the castle escape itself was not played to its last minute. The remaining runs poke one flag byte in RAM.
RunStateResult
Playedthe save as playedrate 18 on all 55 steps; counter 974 down to 2; battle rolled
Edited saveclock words in the save block (flag pair 0x03960015 at +0x480, threshold 3600, duration 3900), checksum restampedrate 18 for 23 steps; then the clock actor sets flag 0x15 from return address 0x801D2F8C at 3598 ticks left; the next 104 steps drain 0; the Door of Light is blocked
RAM pokebyte 0x8008575A bit 0x04 setrow 0, rate 0: 128 steps, counter fixed at 974, no battle
RAM pokebyte 0x800857FC bit 0x40 set (flag 0x521)the same, with the Door of Light still allowed
RAM pokeflag 0x15 set and the step counter written to 0battle on the first step: a counter resting at zero is not the fault

Played saves support the reading without any poke: the saves of one full playthrough between the castle and the Fortress's collapse hold the castle's stop-op flag pair 0x03960382 at save-block +0x480, exactly as the handler stores it. All of that playthrough's post-castle saves have flag 0x15 clear, and it has normal Fortress battles: a file that left the castle with more than a minute to spare.

The quiet states that are intended

  • The collapse flag, 0x521. This is not a “Jette is beaten” flag: beating Jette changes nothing about encounters, and the walk from his room to the Mist Generator rolls exactly as the walk in did. The flag is set later, in the Mist Generator room after the Cort fight, as the Fortress starts to collapse (chitei2 P2[15] and P2[19]), and it is never cleared. Both scenes then select an all-zero group. This is the designers switching the dungeon off; the steps below show how little of it a player can see.
  • Safe rooms. In the interior's default table each zero-rate box outlines one room: Jette's room, the antechamber before it, the council chamber and Mist Generator room, the two elevator shafts and the bridge. Every ordinary room and corridor rolls at rate 18. Measured over enclosed floor tiles, 39% of the interior and 8% of the entrance is rate 0.

The interior's story runs in this order, each step a chitei2 record:

StepRecordWhat happensFlags it sets
1P2[11], Jette's roomJette's speech, then the fight (formation row 12)0x470
2-the walk to the council chamber, through rooms that roll at rate 18-
3P2[13], Mist Generator room“We must destroy the Mist Generator!”, Cort appears, the fight (row 13)0x4C4
4P2[14]Cort falls; the scene cuts to the entrance scene and back-
5P2[15] or P2[19]“Why did you do it?”; the room starts to shake0x521
6P2[18], or the end of P2[19]“The Fortress is collapsing”; a 40-second clock is armed-
7P2[17], tile (18, 94)a boulder seals the room's exit0x4C9
8P2[16]about 25 seconds in, Cara arrives and the scene changes to the entrance0x1B

From step 5 to step 8 the player has control in the Mist Generator room, which is rate 0 in every group, with the boulder trigger between the party and the way out. The clock is the castle's op again, 4C D3 with flag 0x4C7 in both slots, 2400 ticks and threshold 910; the scene's entry script runs P2[16] when the flag comes up. So inside the interior the flag has about 25 seconds to matter and a sealed zero-rate room to matter in: a player does not get to stand in a room that used to roll. That the boulder cannot be walked around is read from its wall paint over tiles (15..19, 92), not tested.

Where the flag does apply is the entrance scene, deroa. Its group for flag 0x521 is one whole-map rate-0 row, where its normal groups have five rate-18 rooms. P2[16] ends there, in a long cutscene with Cara outside the Fortress. deroa's entry script has a branch for the collapsed state (flag 0x4C8): it paints a solid wall over tiles (55..59, 41..42), shows two props that are otherwise parked off the map, and changes the music. The overworld entrance record has no gate, so the scene reads as open to a later visit, with one passage sealed and no random battles. That is read from the scripts; the ground was not walked after the collapse.

Jette council MistGen. Entrance (deroa), default group Interior (chitei2), default group 128 x 128 tiles8% of the enclosed floor is rate 0 128 x 128 tiles39% of the enclosed floor is rate 0 rate 18: steps count rate 0 box: a safe room rate 0 catch-all Boss, cutscene and transport rooms never roll. Flag 0x521 or 0x15 set: both maps are all rate 0.
Default-state encounter regions of the two Fortress scenes, to scale, stacked in the order the game searches them (earlier records on top). The boxes are drawn by tile area; the percentages count enclosed floor tiles of the wall grid.

The tables

SceneOrderFlagRegions in the groupResult
deroa00x0015one, whole map, rate 0no encounters
10x0521one, whole map, rate 0no encounters
20x046Deight: two rate-0 pockets, five rate-18 rooms, catch-allnormal
3defaultseven: one rate-0 pocket, five rate-18 rooms, catch-allnormal
chitei200x0015one, whole map, rate 0no encounters
10x0521two, both rate 0no encounters
2defaultsixteen: seven rate-0 boxes, eight rate-18 boxes, catch-allmixed

Row 0 of both scenes blocks both Doors and returns to tile (0, 0), like the castle's row; every other Fortress row allows the Door of Light and returns to tile (66, 113).

How the roll reads them, step by step
  1. A step is a change of the player's 128-unit tile by at most one on each axis (0x801D9ED4..0x801D9F0C).
  2. The condition list is walked in order (0x801D9F30..0x801D9FD8): the first record whose flag is 0xFFFF or set wins.
  3. The first region of that group whose box contains the tile wins (0x801D9FE8..0x801DA054).
  4. The step does nothing while the player's engaged bit is up, while 0x8007B6B4 is non-zero, while the warp timer 0x8007B6B0 is positive, or while the Incense window 0x8007B600 is non-zero (0x801DA130..0x801DA180). The roll is not reached at all while a menu or scripted submode is open (bit 0x8000 of 0x1F800394, raised at 0x801F1360, dropped at 0x801F1684).
  5. The rate is scaled by the setting byte 0x8007B5F8, the High and Low Encounter accessories, and system flags 0x1D (doubled) and 0x1E (halved) (0x801DA198..0x801DA204).
  6. The result is subtracted from the counter 0x8007B5FC; at zero or below a battle starts (bgtz at 0x801DA218) and the counter is reset to between 488 and 1460. The counter is not saved: after power-on it holds 974, so the first battle after a cold load in a rate-18 room comes on step 55.

Things that thin battles without stopping them: a Low Encounter accessory makes rate 18 into 9, and into 4 if system flag 0x1E is also set. Flags 0x1D and 0x1E come from the two springs in Rogue's Tower, which is reached after the Fortress, so in a normal file neither is set here; they have their own section. A scene load with the counter below 487 adds half of a fresh roll (0x8003AC7C..0x8003AC90); Incense suspends the roll while its window is open. None of them produces a silent Fortress.

Addresses

AddressImageWhatEvidence
0x801D9E1CPROT 0897per-step region encounter rolldisassembly
0x801D9F30..0x801D9FD8PROT 0897condition-list walk; first set flag winsdisassembly
0x801D9FE8..0x801DA054PROT 0897region search; first matching box winsdisassembly
0x801DA130..0x801DA180PROT 0897the four gates that hold a stepdisassembly
0x801DA198..0x801DA204PROT 0897rate scalingdisassembly
0x801DA208..0x801DA21CPROT 0897counter subtract and testdisassembly
0x801E2BDC..0x801E2C38PROT 0897op 4C D3 handler: arms the countdowndisassembly
0x801D2EBCPROT 0897clock actor; flag sets at 0x801D2F60 (expiry) and 0x801D2F84 (below threshold)disassembly; capture (edited save)
0x801F1360, 0x801F1684PROT 0897set and clear of bit 0x8000 of 0x1F800394 while a menu or submode is opendisassembly
0x8003CE08, 0x8003CE64SCUS_942.54flag set; flag test: byte 0x80085758 + (id >> 3), mask 0x80 >> (id & 7)disassembly
0x8003B62C..0x8003B648SCUS_942.54scene loader: 1200 ticks off a running clock per scene entrydisassembly
0x8003B96C..0x8003B98CSCUS_942.54card load restores a running clock to its full durationdisassembly; capture (edited save)
0x8003AC7C..0x8003AC90SCUS_942.54scene-entry counter top-updisassembly
0x800845A0, 0x800845B8, 0x800845BC, 0x800845C0RAMclock: remaining, duration, threshold, flag pair; save-block +0x460, +0x478, +0x47C, +0x480disassembly; capture (played saves)
0x8007B5FCRAMencounter step counter, signed word; not saved, 974 after power-ondisassembly; capture
0x8007B600RAMIncense windowdisassembly
0x8008575A mask 0x04RAMsystem flag 0x15; save-block +0x161Adisassembly
0x800857FC mask 0x40RAMsystem flag 0x521disassembly
PROT entry 373, P2[29] +0x1508, +0x1533disctaiku: the one clear of flag 0x15, then the arming opdisc data
PROT entry 373, P2[16] +0x13disctaiku: escape success; stops the clock, no clear of 0x15disc data
PROT entry 373, P2[12]disctaiku: the script-side set of flag 0x15 (50 15)disc data
PROT entry 606, MAN offset 0x5B4Bdiscderoa encounter section, 0x152 bytesdisc data
PROT entry 706, MAN offset 0x6A1Adiscchitei2 encounter section, 0x166 bytesdisc data
PROT entry 706, P2[15], P2[19]discsetters of flag 0x521: the scene after the Cort fightdisc data
PROT entry 706, P2[11], P2[13]discthe Jette fight (row 12, sets 0x470) and the Cort fight (row 13, sets 0x4C4)disc data
PROT entry 706, P2[18] +0x3DD, P2[19] +0x42Ddiscthe collapse clock: 4C D3, flag 0x4C7 twice, 2400 ticks, threshold 910disc data
PROT entry 706, P2[17], P2[16]discthe boulder at tile (18, 94); Cara's arrival and the change to deroadisc data
PROT entry 606, P1[0] +0x58..+0x72discderoa entry script, collapsed state: wall paint over (55..59, 41..42), musicdisc data
PROT entries 703, 603discwall grids of chitei2 and deroa: 1287 of 3304 and 274 of 3341 enclosed tiles are rate 0disc data
a real escape under 1:00 leaves the flag set-the flag read is not observed; the silent Fortress and blocked Door of Light after such an escape areinference; community report
the entrance scene is silent after the collapse-read from the table and the scripts, not walkedinference

The encounter format itself is on the encounter page.

Verifying it

On an affected file, read byte 0x8008575A (or save-block +0x161A): bit 0x04 is set. In the Fortress, break at 0x801DA208: register s0 holds the final per-step rate and s2 points at the region record that won; the branch at 0x801D9FC8 shows which group was selected. To see the cause itself, play the Floating Castle escape with a breakpoint at 0x801D2F84, let the clock fall below 1:00, reach the exit, and read the byte on the overworld. A player's test confirms the outcome of such an escape (no encounters, no Door of Light); the flag bit itself in such a file has not been read. Also not measured: whether returning from a battle charges the clock the same 20 seconds as a scene change.

Rogue's Tower water changes the encounter rate

Reported as
Not a glitch report. The roll traced for the Fortress reads two more flags, and the question came back from the community: which water does what, does it last, and why is no difference visible in an old save. ZetaPhoenix then observed that of the three bits the springs set, only the shared one survives a random battle (community report, first-hand), which the capture below confirms
Cause
Green water doubles the random-battle rate and blue water halves it until the next battle ends; on the return to the field the game clears both flags again
Where
Rate scaling at 0x801DA1E0..0x801DA204 in FUN_801D9E1C, PROT entry 0897; clean-up at 0x8003B544..0x8003B568 in FUN_8003AEB0, SCUS_942.54; the springs are P1[29] and P1[30] of rugi, PROT entry 697
Affects
Every scene with random battles and the overworld, from the drink to the end of the next battle
Status
Explained - an intended mechanic, read from the disassembly and the scripts; the clean-up is captured on a poked state

Rogue's Tower has two springs side by side. Examining one shows “It's strange green water!” or “It's strange blue water!” and asks whether to drink. Either one fully restores the party's HP and MP. Each also sets a flag that the random-battle roll reads on every step, until the next random battle is over.

WaterFlagEffect on the rollSteps to the next battle, tower's rate-20 rooms
neither-rate as written25 to 73, 49 on average
green0x1Drate doubled: the next battle comes in half the steps13 to 37, 25 on average
blue0x1Erate halved: the next battle takes twice the steps49 to 146, 98 on average
bothbothdoubled, then halved: exactly the written rateas with neither

When the field comes back after a battle, the game clears both water flags. The drink is a one-battle charm: green pulls the next fight closer, blue pushes it away, and from the fight after that the rate is back to normal. Drinking again re-arms it for one more battle; the springs work on every visit. That is why an old save shows no difference: whatever was drunk wore off at its next battle.

Both springs also set a third flag, 0x1C, which nothing ever clears. It is the switch for the clean-up: the battle-return code clears the two water flags only when 0x1C is set, and 0x1C is only ever set together with one of them. A file that has drunk once keeps 0x1C for good, so the byte reads 0x08 after any later battle.

The effect is real but easy to miss even inside its one battle. The step count between battles is re-rolled over a three-to-one range after every fight, so the bands for “normal” and “green” overlap, and so do “normal” and “blue”. A fair test is to drink, count the steps to the next battle, and repeat many times, or to read the rate in a debugger.

Check

How
Check, RAMbyte 0x8008575B: bit 0x04 is the green flag, 0x02 the blue one, 0x08 flag 0x1C. Test the bits, not the whole byte: bit 0x01 is an unrelated flag that scene scripts toggle all the time
Check, memory cardbyte +0x161B of the save block, the same bits
Check, the rate itselfbreak at 0x801DA208 while walking a room that rolls: s0 is the per-step rate. In the tower it reads 20 with neither or both, 40 with green only, 10 with blue only
Check, the clean-upwin the next battle and read the byte again: 0x04 and 0x02 are gone, 0x08 stays. A write watch on the byte stops at 0x8003B568

The scaling code

The rate is what one step takes off the step counter, so a larger rate means more battles. The counter restarts between 488 and 1460 after each battle, 974 on average, and a battle comes on step ceil(counter / rate).

801da1e0  jal  0x8003ce64 ; li a0,0x1d      ; test system flag 0x1D
801da1e8  beqz v0,0x801da1f4
801da1f0  sll  s0,s0,0x1                    ; set: rate * 2
801da1f4  jal  0x8003ce64 ; li a0,0x1e      ; test system flag 0x1E
801da1fc  beqz v0,0x801da208 ; andi v0,s0,0xffff
801da204  srl  s0,v0,0x1                    ; set: rate / 2
801da208  lw   v1,-0x4a04(a0)               ; the step counter 0x8007B5FC
801da214  subu v1,v1,v0                     ; counter -= rate
801da218  bgtz v1,0x801da360                ; still positive: no battle

The two tests are independent and run in that order. With both flags set the rate is shifted left and then right by one bit, which returns the same number for every rate, odd or even. There is no cap and no floor other than the integer shift.

The same block applies the accessories first: High Encounter (Bad Luck Bell, Nemesis Gem) multiplies by four and Low Encounter (Good Luck Bell, Evil Talisman) halves. Everything stacks by plain shifts. The roll is the same function for every field scene and for the overworld, so while a flag is set the scaling is not local to the tower. It does nothing in rooms whose rate is 0, to scripted fights, or while Incense holds the roll.

The clean-up after a battle

The scene-entry map init FUN_8003AEB0 has an arm for a field re-entry that stays in the current scene (_DAT_8007B8B8 == 2, tested at 0x8003B504..0x8003B510), which is how the field resumes after a battle. With a0 = 0x80084140, the base of the flag bank at +0x1618:

8003b528  li   a3,-0x3                      ; mask 0xFD
8003b538  addiu a2,a0,0x3                   ; flag byte 3: flags 0x18..0x1F
8003b544  lbu  v1,0x1618(a2)
8003b54c  andi v0,v1,0x8                    ; flag 0x1C set?
8003b55c  beq  a1,zero,0x8003b56c           ; no: leave the byte alone
8003b560  _andi v0,v1,0xfb                  ; clear 0x04, flag 0x1D (green)
8003b564  and  v0,v0,a3                     ; clear 0x02, flag 0x1E (blue)
8003b568  sb   v0,0x1618(a2)

The same arm clears flag 0x0E one byte earlier with no condition. It runs on every re-entry of that kind, not only after a random battle; a scene change through a door takes the other side of the branch and leaves the water flags alone (read from the branch, not captured). The clean-up is an inline byte access through a base register pointed at byte 3, so a search for calls to the flag helpers, or for the byte's absolute address, does not find it.

What was captured

Probe scripts/pcsx-redux/autorun_water_flag_clear.lua on the catalogued Rogue's Tower state rugi_field_card_boot, a card-loaded endgame file whose byte reads 0x08. Poked: the byte raised to 0x0E and the step counter set to 1 once the state runs. The walk, the battle entry, the fight and the return are the game's own code; the fight was won by holding the monsters at 1 HP while Cross was pressed.

MomentByte 0x8008575BGame mode
after the poke, walking0x0E0x03 field
battle entry and the whole fight0x0E0x09, then 0x15 battle
return to the field, before the arm0x0E0x02 field init
after the store at 0x8003B5680x080x02
field resumed, to the end of the run0x080x03

ZetaPhoenix's in-game tests in Bio Castle agree with the gate and add the escape case (community report, first-hand; bits named by mask). The tests used the blue-water bit; the green bit is expected to behave the same but was not tested:

Byte before the battleWonEscapedEscaped with the Spikefish Flute
0x08 and 0x02 set (0x0A)0x02 cleared0x02 clearedthe game freezes
0x02 alone0x02 stays0x02 stays0x02 stays

A plain escape returns through the same clean-up as a win, and so does a flute escape that gets that far. The freeze is real, but it is not caused by the flag byte: it is a separate glitch of the Spikefish Flute, traced in its own section.

Every late-game save state in the project's library reads 0x08 in this byte, never 0x0C or 0x0A: a file that drank, after its next battle.

The two springs

Both are placement records of rugi (Rogue's Tower, PROT entry 697). The developer name in each record's header is Shift-JIS for “green water” and “blue water”, matching the English line.

P1[29]P1[30]
First line“It's strange green water!”“It's strange blue water!”
Placementtile (108, 89)tile (105.5, 89), two and a half tiles west of it
Flag writes after “Yes”50 1C, 50 1D at +0xF150 1C, 50 1E at +0xF0
Other effect4C 82 00, 4C 82 01, 4C 82 02: full HP and MP for party slots 0 to 2the same

The records are otherwise the same script: the prompt, a two-way picker, the drinking animation, the three restores, the two flag writes, a fade, “What an amazing taste!” and “HP and MP are completely restored!”, then a jump back to the prompt's entry so the spring can be used again. Answering “No” writes nothing. Neither record tests a flag anywhere, so nothing gates a spring and it works on every visit, any number of times.

Both springs stand in the part of the tower that rolls at rate 20. The colour is taken from the script text and the record names; the water's texture was not examined.

Addresses

AddressImageWhatEvidence
0x801DA1B8..0x801DA1DCPROT 0897High Encounter passive 0x3B (rate * 4), Low Encounter passive 0x3C (rate / 2)disassembly
0x801DA1E0..0x801DA204PROT 0897flag 0x1D test, rate * 2; flag 0x1E test, rate / 2disassembly
0x801DA208..0x801DA21CPROT 0897counter subtract; s0 is the final ratedisassembly
0x8003B504..0x8003B510SCUS_942.54same-scene re-entry arm of FUN_8003AEB0, _DAT_8007B8B8 == 2disassembly
0x8003B544..0x8003B568SCUS_942.54clears flags 0x1D and 0x1E when flag 0x1C is setdisassembly, capture
0x8008575B masks 0x08, 0x04, 0x02RAMsystem flags 0x1C, 0x1D, 0x1E; save-block +0x161Bdisassembly, capture
PROT entry 697, P1[29] +0xF1, P1[30] +0xF0discthe springs' flag writes after three 4C 82 restoresdisc data
PROT entry 697, encounter sectiondisctower regions: (0..58, 0..72) rate 0, the rest rate 20disc data

Still open

  • Which other same-scene re-entries besides a battle return reach the clean-up, for example a scripted fight or a cutscene that resumes in place.
  • Which spring is on which side on screen: the placements give tiles, and the camera decides left and right.
  • Whether the colour is also visible in the water's texture.

Spikefish Flute escape can freeze the game

Reported as
Escaping a battle in Bio Castle with the Spikefish Flute freezes the game, first noticed while testing the Rogue's Tower water flags ; narrowed down further, escaping three Gillium Lv3 with the flute always freezes, right after the escape sequence (community report, first-hand, ZetaPhoenix). The captured fight is exactly that formation
Cause
The flute's escape skips the end of its attack, so its “Blowfish” caption stays up after the battle frees the caption's text; once field data is read back over it, drawing it can send the text routine into a copy that never ends
Where
Arm 8 of FUN_801F6A00, PROT entry 0925 (the flee); text widget draw at 0x800321E0; runaway copy at 0x800367E4 in FUN_80036514, SCUS_942.54
Affects
Spikefish Flute escapes from a fixed set of scene-and-fight pairs (14 of 406 random formation rows, predicted by simulation; three Bio Castle freezes captured). Three Gillium Lv3 in Bio Castle (D) always freezes. A plain Run is safe
Status
Explained - captured on a poked state; the water flags play no part

The flute's attack shows a caption with the attack's name, “Blowfish”. The flute's escape skips the end of its own attack, so the caption is never taken down. It stays on screen through the escape and keeps pointing at its text after the battle has freed that memory. When the game reads the field's data back into the freed memory, the caption's “text” becomes picture data. Drawing it as text can send the text routine into a copy that never ends, and the game locks up on the black screen after the escape. Whether a given fight hits that depends on where the caption's text landed and on what overwrites it, not on the water.

The runs

State: a cold boot of the retail disc plus a memory-card load of the Bio Castle save (playthrough-endgame-7saves.mcr block 6, scene jouind), checkpointed in the field by autorun_boot_continue.lua. Poked once the field runs: bits 0x0E of byte 0x8008575B set to the value below (bit 0x01 kept), bag slot 0 set to the Spikefish Flute (0x99) x9, and the step counter 0x8007B5FC set to 1. The walk, the battle (three Gillium Lv3, monster id 161 = 0xA1), the commands and the return are the game's own code, driven by the pad. Probe scripts/pcsx-redux/autorun_spikefish_flee_return.lua on the fast core.

SceneByteEscapeResult
jouind0x0ASpikefish Flutestops 165 vsyncs after the teardown, still in battle mode; two runs
jouind0x02Spikefish Flutethe same
jouind0x08Spikefish Flutethe same
jouind0x0CSpikefish Flutethe same
jouind0x00Spikefish Flutethe same
jouind0x0ARunback in the field; the byte reads 0x08
rugi0x0ASpikefish Fluteback in the field; the byte reads 0x08

Six more jouind runs had the formation rows rewritten in RAM before the forced battle (a seventh poke), to test the prediction below. The byte was 0x0A in each:

Formation forced in jouindField-back readPredictedResult
three Gillium Lv3 (161), the scene's own row127 sectorsfreezefreezes
three Puera Lv3 (158), the scene's other row115surviveback in the field
one Gimard Lv3 (12)112surviveback in the field
three Gimard Lv3 (12)112surviveback in the field
three Gizam Lv3 (57)115surviveback in the field
three Barra Lv3 (143)116freezefreezes
Aluru (80) + Orb Lv2 (84)134freezefreezes

The rugi row uses the catalogued rugi_field_card_boot state with the same pokes. It confirms that a flute escape which survives reaches the same clean-up as a plain one. Every Bio Castle flute run takes the same frames, so the flute's flee always lands at action state 0x65 on frame 2732 or 2733.

The runs disagree with one row of the community table: the blue bit alone also freezes here. The table's runs were each a different fight, so a fight that lays out memory differently can survive.

The mechanism

  1. The caption outlives the attack (capture). While the flute's attack runs, the retained widget list FUN_80031D00 draws a text widget every frame: jal 0x80036888 at 0x800321E0, string pointer at widget +0x18. In this fight the string is "Blowfish" at heap address 0x800B995C. Arm 8 of FUN_801F6A00 in PROT entry 0925 moves the action state machine to the run wait 0x65 and never removes that widget. The caption is still drawn every frame through states 0x65, 0x66 and 0x67, beside the “successfully escaped” banner.
  2. Its text is overwritten (capture, disassembly). At state 0x67 the teardown in FUN_8004E568 frees the enemy record buffers and the side-band stream buffer, and the loader-B id 0x8007BC4C goes from 0x1E (PROT 0925) to 0x53 (PROT 0978, the field-back-read module). PROT 0978 re-reads the scene's own data from disc into the largest free heap block, which now covers the caption. From then on the widget's string at 0x800B995C reads f8 f2 f2 ff fa ef da ..., which is jouind's PROT entry 681 at offset 0xE4BC, with no zero byte nearby.
  3. The text routine never comes back (capture, interpreter core). The renderer FUN_80036888 hands that “string” to the substitution expander FUN_80036514. The expander's first pass rewrites each 0xFF in the source to 0xCF in place, so it also corrupts the freed memory it reads. The copy loop at 0x800367E4 then follows a substitution code whose source pointer t0 lands in the expander's own output buffer (from 0x800740EC). The read cursor trails the write cursor by 0x1FE bytes and never meets a zero. The copy runs on through 0x80074723, 0x80075E93 and 0x80079543 (logged every 2000 passes), overwriting everything after the buffer. Under the fast core the emulator stops with “Unrecoverable error while running recompiler” at pc 0x80036868, the loop's tail. On hardware the game hangs.

A plain escape never installs the caption, so it cannot hit this. The water flags are read only after the battle has ended, by which time the copy has already locked up.

Why one fight and not another

The outcome is a fixed function of the scene and the formation's distinct monster species. Three links make it so.

Where the caption text sits (capture). The caption string is the attack name inside the Spikefish's actor record. That record is streamed into the side-band slot at the bottom of the enemy stager buffer, so the text sits at 0x800B990C + 0x50 = 0x800B995C. The stager buffer (0x2E390 bytes) sits at 0x800B9900 in every captured fight, in both scenes.

How much the field-back read loads (disassembly, capture). PROT 0978's routine 0x801F6B24 works in three steps:

  1. It takes the largest free heap block (FUN_8002B790, at 0x801F6F08), counts whole sectors in it, and caps the count at 0xC5.
  2. It allocates that many sectors less 4 bytes (FUN_80017888, at 0x801F6F74). The best-fit allocator FUN_8002B468 cuts the allocation from the top of the block, so the buffer ends at 0x800E7C9C, where the freed stager buffer ended.
  3. It reads that many sectors from 32 sectors into the scene's first PROT entry (0x801F726C..0x801F7298), the scene's own field data.

After the teardown the largest free block is the stager plus the monster blocks allocated directly below it. There is one monster block per distinct species, each the species' battle-heap cost (block[+0x08] of its PROT 867 record) plus a 12-byte header:

sectors = min((0x2E390 + 0xA8 + sum(cost + 12 over distinct species)) / 0x800, 0xC5)
caption reads scene_data[sectors * 0x800 - 0x2E344]   (reached only from 93 sectors up)

The 0xA8 is fitted: it is one more freed 0x9C node, and without it Gizam Lv3 predicts 114 sectors where the capture reads 115. The formula gives the measured count for all eight captured fights, in both scenes.

What the text routine does with those bytes (simulation). The script scripts/asset-investigation/spikefish_caption_predict.py places the scene's disc bytes at the caption and runs the renderer and expander logic from the disassembly over them:

  • a first byte below 0x20 draws nothing;
  • otherwise it follows the normalisation pass, the token count, the C1..C7 substitution table at 0x80010EC0 and the nested name copy.

A copy that runs past 64 KiB, or touches an address outside RAM and its mirrors (a bus error on hardware), is called a freeze. It needs a battle RAM image as a backdrop for the tables it dereferences; two different backdrops give identical results. The script agrees with all eight captured outcomes, among them the three Barra Lv3 and Aluru + Orb Lv2 freezes, which were predicted before those runs.

Predicted freezes

The script was run over every random formation row (encounter regions with a non-zero rate) of the bundle MAN of every scene in the scene table: 406 rows in 51 scenes. It predicts a freeze for 14 rows; every other row survives:

SceneFormationSectorsFailure
jouind, Bio Castle (D)three Gillium Lv3127runaway copy (captured)
jouinc, Bio Castle (C)three Barra Lv3116bus error (the same fight froze when forced in jouind)
jouina, Bio Castle (A)three Zenoir Lv3114runaway copy
jouina, Bio Castle (A)three Gola Gola Lv3110bus error
cave01, Snowdrift Caveone Mushroom; one Blue Piura110runaway copy
map01, world map (Drake Kingdom)one or two Gobu Gobu117runaway copy
dohaty, Dohati's CastleAluru + Orb Lv2; Nova Lv2 + Gola Gola134runaway copy
tunnelb, Fire PathNova + Gel Frog139bus error
tunnelb, Fire PathStone Lizard + two Warman134runaway copy
nilboa, Nivora Ravinetwo Amethyst + Neo Grude131bus error
nilboa, Nivora Ravineone Amethyst102runaway copy

Three rows sit within the fit's error of a sector boundary, where the neighbouring count gives the other answer:

  • Fire Path two Nova (117; 116 would freeze);
  • Sol Tower koin2 Spoon + Shade (141; 142 would freeze);
  • Fire Path Stone Lizard + two Warman (134; 135 survives).

The same formation can differ between rooms, because the bytes come from each room's own data: three Gillium Lv3 freezes in Bio Castle (D) but is predicted to survive in Bio Castle (E) (jouine).

That fits both community reports. The three-Gillium-Lv3 freeze is first-hand. A Bio Castle flute escape that did not freeze is consistent with three Puera Lv3, the room's other formation, which survives here.

The first two rows of the community table are first-hand play. The freeze rows above are captured on a poked state (the byte, the bag slot and the step counter, plus the formation row for the forced formations). Probe scripts/pcsx-redux/autorun_spikefish_text_hang.lua replays the end of a run on the interpreter from the escape-wait checkpoint, logging the renderer calls and the runaway loop.

Addresses

AddressImageWhatEvidence
0x801F7400, 0x801F78C4..0x801F78F8PROT 0925Spikefish Flute flee: outcome, survived latch, escape counter, ctx[7] = 0x65disassembly
0x801E7B5C..0x801E7B84, 0x801E8028..0x801E802CPROT 0898the run command's matching success stores in FUN_801E791Cdisassembly
0x80046DD8..0x80046E60SCUS_942.54battle-exit mode choice in FUN_80046A20; reads no escape kinddisassembly
0x800321E0SCUS_942.54FUN_80031D00: the retained text widget's draw, string at widget +0x18disassembly, capture
0x80036514, loop 0x800367E4..0x80036868SCUS_942.54substitution expander; the runaway copy into and past 0x800740ECdisassembly, capture

Verifying it

In a battle in Bio Castle, escape with the Spikefish Flute. Before the freeze, the “Blowfish” caption is still on screen next to the “successfully escaped” banner. In a debugger, break at 0x800367E4 once the action state reaches 0x67: the loop keeps running, its write cursor climbing past 0x800740EC. Escaping the same fight with Run returns to the field.

Still open

  • Whether the party (members, equipment) moves the stager or the free block. Every capture used one party.
  • The 0xA8 slack is fitted, not traced to the node it stands for.
  • The world-map rows (Drake Kingdom, Gobu Gobu) assume the world map's field-back read is the same as a field scene's; no world-map flute escape was captured.
  • Formations in the streamed MAN variants are not evaluated.
  • Whether other summon-flute or item captions survive an escape the same way.

Gaza 2 softlock

Reported as
In the Gaza rematch the battle stops and the camera circles the party forever (community report)
Cause
A monster walking to its target is carried only by its Move animation; a leftover flinch flag swaps that animation for idle and the wait never ends
Where
Battle action state machine FUN_801E295C, PROT entry 0898, state 0x19
Affects
Nearly every monster, bosses included, when a melee directly follows a flinch
Status
Explained - traced in full on its own write-up; the key addresses re-read here

This one has a full write-up of its own, with the evidence behind it and the patch. In short: a monster that starts a melee attack out of reach has to walk to its target. Almost no monster has a dedicated approach animation, so the game plays the ordinary Move loop and waits in a state that only re-measures the distance. That state has no movement code and no timeout.

If the monster is still flinching from a hit when its attack begins - a summon is the usual cause - a leftover “go back to idle when this clip ends” flag is still set. The Move loop plays one cycle, the flag swaps it for the idle pose, the monster stops short, and the wait never ends. The camera's idle sweep runs on its own, so the screen keeps orbiting.

  1. FlinchThe hit-reaction stager in FUN_800402F4 queues the flinch clip and raises bit 2 of actor+0x1DC (ori v0,v0,0x4 at 0x80042140).
  2. Attack setup, state 0x14Out of reach, no tag-0x20 walk action (jal 0x80050e2c at 0x801E3268, miss tested at 0x801E327C): the Move clip is staged at 0x801E32B0 and state 0x19 stored at 0x801E32D0.
  3. Mid-clip commitThe Move clip is installed by the path that clears bits 0..1 only (andi v0,v0,0xfc at 0x80047A38). Bit 2 survives.
  4. First natural endFUN_80047430 tests bit 2 (0x80047B38) and queues idle over the Move clip (sb zero,0x1da(s2) at 0x80047B44).
  5. ParkedState 0x19 re-checks the range every frame at 0x801E3594 and falls to the stall label 0x801E35D0, forever.

Addresses

AddressImageWhatEvidence
0x801E31F4..0x801E32DCPROT 0898state 0x14 walk-tag fallbackdisassembly
0x801E3568..0x801E3588PROT 0898nine redundant facing words in state 0x19; the patch site, overlay offset +0x14D50disassembly
0x80047B38..0x80047B50SCUS_942.54natural-end test of bit 2 and the idle installdisassembly
0x80047A38SCUS_942.54mid-clip commit mask 0xFCdisassembly
0x80042124..0x80042170SCUS_942.54flinch stager: bit 2, then bit 0disassembly
0x8004E2F0, 0x80050E2CSCUS_942.54range check; action-tag scanrepo doc
actor +0x1D9, +0x1DA, +0x1DCRAMplaying clip, queued clip, anim event flagsdisassembly

Verifying it

In a stalled battle the context state byte (*0x8007BD24 + 0x07) reads 0x19, the attacker's +0x1D9 and +0x1DA are both zero and its position does not change. To provoke it, set bit 2 of a monster's +0x1DC while it is in state 0x14. The fix is the Approach-softlock fix toggle in the ROM patcher.

Dialogue inconsistencies

Reported as
“Van Saryu” against “Bon Saryu”, “Moustache” against “Whiskers”, and the bath scene in Usha's Research Center, where Noa turns to Vahn and the answer on offer reads “I'm too shy” where it should be “He's too shy” (community report)
Cause
The English script renders one thing two ways; in the bath scene it turns a question and answer about the researcher into ones about Vahn
Where
Scene text in PROT entries 146, 70 and 401; one monster name in PROT entry 867
Affects
Text only - no flag, item or branch changes
Status
Explained - the two word pairs are read from the disc text; the bath scene is a pronoun error, settled against the Japanese disc

Van Saryu and Bon Saryu

The lord of Ratayu has a title in front of his name exactly twice on the disc, spelled differently each time. Every other mention is the bare name. Neither spelling is the established one.

SpellingWhere
Vanthe battle name of monster id 137, in the monster archive (PROT entry 867)
Bonthe signature on a letter in Ratayu: scene retock, PROT entry 146, MAN offset 0x380C

Moustache and Whiskers

Both words occur in one exchange and nowhere else: the audience with King Drake in dolk2 (the variant MAN in PROT entry 70). Drake names the thing, and Noa repeats it back as a different word.

MAN offsetSpeakerWord
0x6D86King Drakemoustache
0x6DB6NoaWhiskers, twice
0x6E39King Drakemoustache

The bath scene in Usha's Research Center

A researcher refuses to share the bath with Noa. Noa turns to Vahn and, in the Japanese script, asks him whether “this person” dislikes her; the player picks Vahn's answer, either that the man is embarrassed or that the bath is too small. The English changed her question to “You don't like me?” and the first answer to “I'm too shy”, so it reads as if Vahn were the one refusing. The rest of the scene is translated straight, which is why Noa's next line - said while turning back to the researcher - and Gala's scolding no longer fit. The line should be “He's too shy”.

Noa turns to Vahn +0x4F4, then he to her Noa asks Vahn +0x55B Vahn answers choice box, +0x57D Noa turns to him the researcher, +0x5DB Japanese script about "this person" third person "(he is) too shy" no subject in the line fits she calls the researcher odd English script "You don't like me?" second person: Vahn "I'm too shy" first person: Vahn no longer fits Vahn never refused anything
The exchange around the choice box. The script structure is identical on the USA and Japanese discs; the offsets are relative to the start of the USA record. Only the pronouns of the question and of option 1 differ.

The scene is P2[4] of doman (the variant MAN in PROT entry 401): MAN offset 0x923E on the USA disc, 0x9BC7 in the same entry of the Japanese disc (SCPS_100.59). It plays once: its header blocks it when system flag 0x3FB is set, and the record ends by setting that flag (53 FB at 0x9BCB). The structure is the same on both discs: the same nineteen face-turn ops in the same order, one choice box, the same rejoin, and no branch on who is in the party.

BeatUSAJapaneseWhat the script does
Noa turns to the player+0x4F4+0x562CC 1F 85 0F 00 F8: channel 0x1F (Noa) faces channel 0xF8 (the player)
The player turns to Noa+0x516+0x584CC F8 85 0F 00 1F
Noa's question+0x55B+0x5DBspoken to Vahn. Japanese: about “this person” (kono hito), the researcher. English: “You don't like me?”
The choice box+0x57D+0x600two options, Vahn's replies. Option 1 in Japanese has no subject: an explanation that someone is embarrassed. English: “I'm too shy”
After option 1+0x5DB+0x678Noa turns to the researcher (CC 1F 85 14 00 25) and calls him odd
After option 2+0x66E+0x71Bthe same turn to the researcher before her reply
The researcher's own excuse+0x8C3-in English, the same words as option 1, now from the right speaker

At the choice Vahn is the speaker and Noa the listener, as in every choice box; the two face-turn ops just before the question are the evidence of who is addressed. Read from the English alone, it is tempting to conclude that the choice box speaks for the researcher. It does not: the box is in the same place and belongs to the player on both discs, and only the pronouns are wrong. Reading the subjectless Japanese line as “he” is inference, but it is forced by the question it answers and by Noa's reply.

Three other things in the English scene look odd and are not errors, because the Japanese script has them too: the researcher's lines carry no name tag; Noa calls the place a hot spring where Gala and the researcher call it a bath; and Gala's “floating” place renders a Japanese phrase for a building up in the air.

Addresses

AddressImageWhatEvidence
monster id 137PROT entry 867the Van spellingdisc data
MAN offset 0x380CPROT entry 146the Bon spellingdisc data
MAN offsets 0x6D86, 0x6DB6, 0x6E39PROT entry 70moustache, Whiskers, moustachedisc data
MAN offset 0x923EPROT entry 401, USAbath scene, P2[4], 2451 bytesdisc data
MAN offset 0x9BC7PROT entry 401, Japanese discthe same record, 2708 bytesdisc data
MAN offsets 0x9732, 0x9754PROT entry 401, USAthe two face-turn ops before the questiondisc data
MAN offset 0x97BBPROT entry 401, USAthe two-option picker 27 32 00 D2 00; Japanese 27 4C 00 FC 00 at record +0x600disc data
MAN offset 0x9819PROT entry 401, USANoa turns to the researcher after option 1disc data
0x800857D7 mask 0x10RAMsystem flag 0x3FB, the scene's one-shot latchdisassembly
option 1 means “he is too shy”-the reading of the subjectless Japanese lineinference

Verifying it

Export the disc text with legaia-patcher translate export and search for the words above; each exported key carries the PROT entry and MAN offset. The bath record decodes on either disc with legaia-engine man-scripts --variant 401 --disasm-partition 2 --disasm-record 4. To replay the scene on a save that has seen it, clear flag 0x3FB and step onto the record's trigger tile.

Conkram mirror freeze

Reported as
The game freezes in present-day Conkram, in the Room of Mirrors scene that sends the party into the past, on many emulators; pressing Cross very fast through the scene's text avoids it (community reports: pSX 1.13 logging r3000: executed illegal opcode in a pSX forum thread, which also reports the PAL release unaffected; ePSXe versions other than 1.7.0; the PS3's PS1 emulation every time; a play guide that moves DuckStation saves to ePSXe for this scene). Nobody reports it on a PS1 or PS2
Cause
The scene runs with almost no free heap. An emulated CPU faster than a console's gives the game more frames per second and keeps more effect parts alive; an allocation fails, the move VM writes animation data through the NULL pointer over the bottom of RAM, and the CPU's exception vector is gone
Where
concnow records P2[17] (the walk-on at the mirror) and P2[18] (the crossing); the unchecked allocation in the move VM's keyframe-pose seat (op 0x3C) at 0x80023CD0..0x80023CE0, SCUS_942.54
Affects
This scene on any emulator that runs the CPU fast enough; how fast depends on the party
Status
Explained - reproduced on two emulators by changing only the emulated CPU speed; why a faster CPU keeps more parts alive is inference
In short
  • What you see. The picture stops during the mirror scene, on some emulators; nobody reports it on a PS1 or PS2.
  • Why. The room leaves the game's heap almost full. An emulated CPU faster than a console's draws more frames and keeps more effect parts alive, and an allocation fails. The code that asked for it never checks, and copies animation data to address zero - over the CPU's exception vector. The next interrupt loops forever.
  • Why some emulators. It depends on CPU speed alone: SwanStation passes at 100 % and froze from 125 % with one save; PCSX-ReARMed freezes at the faster clock it applies to this game by default and passed at the lower clocks tried (30 to 50).
  • What to do. Patch the disc with --conkram-freeze-fix (a checkbox in the browser patcher). Without it, set the emulator's CPU speed to 100 % (on PCSX-ReARMed, a lower CPU clock) for this scene, then put it back.

The players call it an FMV, but no movie plays here: it is an ordinary in-engine cutscene. After the party sets the Seru Flame down before the mirror (“That mirror is the door to the past”), the crossing changes the music, lifts the party off the floor, lights a glow on the mirror, shows three pages of text spoken by Terra, fades to white and loads past Conkram.

The game shares one small heap between the scene, its effects and anything it loads. In this room only about 16.6 KB of it is free before the crossing starts. During the crossing the game also loads the party's battle files ahead of time, and the scene's effect parts each take two blocks: a render node and an animation buffer. On a console-speed emulator the free space bottoms out at about 3.7 KB and the scene finishes. On an emulator whose CPU runs faster, the game draws more frames per second (20 per 30 vsyncs instead of 15 in this room in one capture), more parts are alive at once, and the heap runs out.

The routine that sets up a part's animation never checks whether it got its memory. Handed a NULL pointer, it copies the animation data to address zero. On the PlayStation that is real RAM, and the copy covers 0x80000028..0x8000008C, including the CPU's exception vector at 0x80000080. The next interrupt runs the copied data as code, hits a reserved instruction at the vector itself and raises the same exception again, forever. The picture stops; pSX logs exactly that as a stream of illegal opcodes.

1 Free heap in the Room of Mirrors (to scale) before the crossing about 16.6 KB console speed, low point 5,152 bytes in all; largest block 3,708 faster emulated CPU runs out: a 0x88-byte keyframe buffer comes back NULL What eats it: the party battle files PLAYER1..3, pre-loaded after the music swap, and the effect parts, each a 0x9C render node plus a keyframe buffer. A faster CPU draws more frames (20 per 30 vsyncs instead of 15) and keeps more parts alive. 2 Where the NULL copy lands: the bottom of RAM copied through NULL 0x80000000 0x80000028 0x8000008C 0x80000100 exception vector 0x80000080 every interrupt next interrupt jumps to the vector and runs the copied data: reserved instruction (Cause 0x428) at 0x80000084, which enters the same vector again - forever. The picture stops. Before: 00001A3C 800C5A27 08004003 (the BIOS jump to its handler).
Top: free heap in this room as the docs measure it, drawn to scale; the console-speed row is PCSX-Redux at console speed, which passes. The fast-CPU row is where SwanStation at 125 % and above and PCSX-ReARMed at its default end up. At 200 % SwanStation the allocation fails about 70 vsyncs into the crossing, before any text page; on PCSX-ReARMed's default about 560 vsyncs in, around the first page. Bottom: the 0x88-byte, four-lane copy through the NULL pointer, to scale over the first 256 bytes of RAM.
  1. Heap nearly fullAbout 16.6 KB free in concnow; the allocator FUN_8002B468 is best-fit over one free list.
  2. Battle data pre-stagedAfter the music swap the poller FUN_800243F0 (0x80024BB0..0x80024C64) reads 8 sectors through FUN_8003E360, then FUN_80052770 loads the party battle files PLAYER1..3, about 316 KiB in twenty chunks.
  3. Allocation failsAn effect part's keyframe buffer (FUN_80017888(0, count*32|8), 0x88 bytes for four lanes) returns NULL; the failure arm only adds 0x20000 to the PRG ERR counter 0x8007B828.
  4. NULL writeMove-VM op 0x3C stores the pointer with no test and copies through it (sh at 0x80023D30, 0x80023D3C); its retarget op 0x3D writes through it too (0x80023EF4).
  5. Exception loopCause 0x428 (reserved instruction), EPC 0x80000084, on every interrupt.

The fix

The patcher's --conkram-freeze-fix (details) makes both of a particle's memory requests safe, with four same-size edits of SCUS_942.54. Nine words of the move VM's op-0x3C handler are rearranged so a NULL animation block branches to the VM's own halt: the particle ends the way a script ends one and its render node goes back to the heap. A failed render node, which retail leaves as a NULL pointer for later code to write through, gets a shared stand-in node kept in free RAM instead. Both requests go through a quiet copy of the game's allocation routine that has no error counter, so a failure the fix absorbs leaves the PRG ERR readout off the screen. Every allocation that succeeds runs the retail instructions.

Run on the settings that froze, the patched disc passes: both saves reach past Conkram on SwanStation at 200 % and on PCSX-ReARMed's default, with PRG ERR at 0. A write watch on the bottom 256 bytes of RAM in PCSX-ReARMed's interpreter sees no store at all through the scene, where the retail disc makes 72. At console speed nothing fails, and past Conkram loads on the same vsync as with the retail disc.

What was captured

All runs use the retail disc, staged away from the .ppf beside it, entered through a poked state (PCSX-Redux) or one of two edited memory-card saves booted cold: one with a level-37 late-game party, and the played save from Zora's Castle just before Conkram (level 27), edited only to start on the mirror walk-on.

EmulatorSettingResult
PCSX-Reduxinterpreter or dynarec, several timing settings, Cross every 4 to 300 vsyncspasses
SwanStation (DuckStation core)100 % CPU; 50 %passes
SwanStation125 %, 150 %, 200 %, 400 % CPUfreezes (125 % already, with the played party)
PCSX-ReARMedits default for this game (a faster CPU setting it applies to Legaia only)freezes
PCSX-ReARMedCPU clock 30, 45, 50passes
PCSX-ReARMedCPU clock 62, 80, 100freezes
ePSXe 2.0.5 (Linux x64, under box64)defaultspasses

Pressing Cross faster did not rescue a freezing setting: on those the allocation fails before or around the first page. One fast-pressing run limped into past Conkram with the vector already overwritten. That fast pressing saves an emulator that is only slightly too fast is inference. ePSXe 1.6.0 and 1.9.0, pSX and the PS3 could not be run here. The port does not freeze: the full-game ladder walks the scene, and it does not allocate from the game's heap.

Addresses

AddressImageWhatEvidence
0x80023CD0..0x80023CE0SCUS_942.54move-VM op 0x3C: unchecked mallocdisassembly, capture
0x80017888, 0x800178B8SCUS_942.54malloc wrapper and its failure armdisassembly
0x80021184..0x80021190, 0x8001B098SCUS_942.54render-node malloc stores NULL; the draw path writes through itdisassembly, capture
0x80024BB0..0x80024C64SCUS_942.54post-swap battle pre-stagedisassembly, capture
0x80000080, 0x8007B828RAMexception vector; PRG ERR countercapture

Verifying it

On an emulator with a CPU-speed setting, load a save in this room and raise the speed: the freeze comes back on demand, and lowering it clears the freeze. In a debugger, break at 0x800178B8 during the crossing and watch 0x80000080 change from 00001A3C 800C5A27 08004003 to animation data; PRG ERR (0x8007B828) reads 0x20000 or more.

Appendix: the two summon flutes

Support data for the flute pages: which item is which action, and which cast module it loads. Read from the item-effect table in SCUS_942.54 and the dispatch tables in PROT entry 0898.

Lippian FluteSpikefish Flute
Item id0x980x99
Item record0x80074A88, subtype 0x270x80074A94, subtype 0x28
Effect descriptor0x8007535C: 7E 00 C4 410x80075360: 7F 00 E4 41
Class, target126, one enemy, battle only127, all enemies, battle only
Action id0x960x97
Tick table 0x801CF4ECrow 21: jal 0x801F6A18row 22: jal 0x801F6A00
Stager table 0x801F6734row 21: 0x801F7820row 22: 0x801F7AE8
Cast modulePROT entry 0924PROT entry 0925

Item id 0x98 and action id 0x98 are different things: the Lippian Flute casts action 0x96, and action 0x98 is a null row whose module, PROT entry 0926, is a stub. The step from effect class to action id is taken from the project's cast-module reference, not re-read from the enqueue code.