WORK ERR, missing encounters and script slips
Five community-reported oddities that live in the field and battle runtime rather than in one attack or item: a debug counter that reaches the screen on Mt. Rikuroa, the random battles that stop in Jette's Absolute Fortress, the freeze after a Spikefish Flute escape, the camera orbit that ends the Gaza rematch, and three small slips in the English script. One section is a mechanic rather than a fault: the two springs in Rogue's Tower that change the encounter rate. One is a freeze that needs an emulator to trigger it: the heap running out in Conkram's mirror scene when the emulated CPU is too fast. Each is explained from the game's own code and data, with the addresses to check.
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 inFUN_80016444, counter0x8007B8A0, spawnFUN_80020DE0, effect stagerFUN_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.
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
- The printThe frame driver
FUN_80016444loads the signed 16-bit counter at0x8007B8A0(lh a1,0x588(gp)at0x800164B0) and, when it is non-zero, calls the dev text formatterFUN_8001A068with the string at0x800100F4. Unlike the two prints above it, it is not behind the development switchgp+0x3B8. - The counterCleared by stage init
FUN_8001E1B4at0x8001E304; incremented in exactly one place,0x80020E0C..0x80020E1C. - The failing spawnThat place is the failure arm of the actor spawn
FUN_80020DE0: it calls the popFUN_80020454, and when the pop returns null it bumps the counter, logs to the dev console and returns 0. - The pool
FUN_800203ECseeds a free stack of 143 slot pointers,0x8007C5AC + i * 0xD8. The pop refuses when the top index at0x8007C348is negative; free slots = top index + 1. Stage init takes 7 slots as list sentinels, leaving 136. - 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 == 2and next mode 2 (bneat0x8001E308,beqat0x8001E31Cto0x8001E36C). 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. - 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 byFUN_80021B04(jal 0x80020de0at0x80021B78), 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).
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 entry | Slots | Read from |
|---|---|---|
| List sentinels | 7 | FUN_8001E1B4 |
| Props and NPCs: script placements 1..28 | 28 | the MAN header count |
| Props and NPCs: map objects with a script (doors, props) | 54 | the 128 x 128 object map of PROT entry 153 |
| The player and the other system actors | 8 | measured; among them list-0 ticks 0x801D6058, 0x801DA51C, 0x801E76D4, 0x80036D80, 0x801D84D0 |
| Total | 97 of 143 | free-stack top 45: 46 free |
| Moment (one replay) | Free slots |
|---|---|
| Scene loaded from the card | 46 |
| After the four trigger records on the climb | 44 |
| Field resumes after the battle | 33 |
| First rock burst, 160 vsyncs later | 15 |
| Second rock burst, 237 vsyncs after the return | 3 |
| Third burst, 446 vsyncs after the return | 9 |
| At rest afterwards | 43 |
Thirty-two replays of the same battle, differing only in when Cross is pressed after the fight, bottom out at:
| Fewest free slots | 4 | 3 | 2 | 1 | 0 |
|---|---|---|---|---|---|
| Replays | 2 | 15 | 2 | 6 | 7 |
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 time | What is on screen |
|---|---|
| 3:08 to 5:19 | The Golem fight: Noa alone at level 5, Terra beside her; Noa reaches level 6 |
| 5:26.5 | The field returns; the “Terra: !” box is already open |
| 5:29.7 | The second box (“the cave is about to collapse! Follow me!”) closes |
| 5:30.7 | WORK ERR2 appears, reading 2 on its first frame; Terra has run off and Noa is running |
| 5:31.3 | The scripted fade starts; the mountainside fades in at about 5:32.5 with the text still up |
| 6:48 | Noa walks through a cave mouth, a short fade; the text is still up |
| 6:52.3 | A random battle starts; the text is gone from the first battle frame |
| 9:12 | The 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 00at+0x131), moves the player and Terra to tile(0x5D,0x5D)(A3 F8 5D 5Dat+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
| Address | Image | What | Evidence |
|---|---|---|---|
0x800100F4 | SCUS_942.54 | the WORK ERR%d string | disc data |
0x800164B0..0x800164C8 | SCUS_942.54 | the print, in FUN_80016444 | disassembly |
0x8007B8A0 | RAM | failed-spawn counter, signed 16-bit | disassembly |
0x8001E304 | SCUS_942.54 | counter clear, in stage init FUN_8001E1B4 | disassembly |
0x8001E308..0x8001E31C | SCUS_942.54 | battle-return test: skips the reseed, to 0x8001E36C | disassembly |
0x8007B8B8, 0x8007B83C | RAM | resume word; next mode | disassembly |
0x80020E00..0x80020E30 | SCUS_942.54 | failure arm of the spawn FUN_80020DE0 | disassembly |
0x80020454 | SCUS_942.54 | free-stack pop; null when the top index is negative | disassembly |
0x800203EC | SCUS_942.54 | seed: top 0x8E, 143 slots of 0xD8 bytes | disassembly |
0x8007C348 | RAM | pool control block: +0x00 top index, +0x28 free stack | disassembly |
0x8007C5AC | RAM | slot 0; slot i at + i * 0xD8, last slot 0x80083D7C | disassembly |
0x80016230 | SCUS_942.54 | mode-entry prologue: parks the pool block when the field is left for a battle | inference (decompiled C) |
0x80021B78 | SCUS_942.54 | the spawn call in the effect-tree stager FUN_80021B04; result tested at 0x80021B84 | disassembly |
return address 0x80021B80 | SCUS_942.54 | 81 of 96 spawns between the battle return and the peak; template 0x8007062C | capture (poked state) |
| PROT entry 153 | disc | Mt. Rikuroa field map: 54 bound objects | disc data |
| PROT entry 154 | disc | walk-on triggers; the Golem record on tiles (69,97)..(69,99) | disc data |
| PROT entry 157 | disc | variant MAN: 13 / 29 / 64 records; P2[47] is the Golem record, effect installs at +0x23 | disc data |
| 97 of 143 at scene entry | RAM | 82 props and NPCs, 7 sentinels, 8 system actors | capture |
| 143 of 143 at the collapse peak | RAM | zero free in 7 of 32 replays; no failed spawn | capture (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_8001A068is 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
0x801D2EBCand rollFUN_801D9E1Cin 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.
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 tools | In the Fortress, open Items: the Door of Light is greyed out. In a healthy file it is usable there |
| Check, RAM | byte 0x8008575A, bit 0x04 set (system flag 0x15) |
| Check, memory card | byte +0x161A of the save block, bit 0x04 |
| Fix, RAM | clear bit 0x04 of 0x8008575A once, then save. It stays clear: nothing sets it after the castle |
| Fix, memory card | clear bit 0x04 at save-block +0x161A and re-sum the block (+0x1FFC is the wrapping sum of the first 0x7FF little-endian words) |
| Fix, disc | patch the disc with --fortress-encounter-fix (below): the Fortress stops reading the flag, so an affected save needs no edit |
| Avoid | reach 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
- The countdown opField-script op
4C D3arms a countdown kept in the saved game state: two flag ids, a duration and a threshold (handler0x801E2BDC..0x801E2C38in PROT entry 0897), and spawns a clock actor. - The clock actor
0x801D2EBCtakes 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. - The castle arms it
taikuP2[29](PROT entry 373), the Mist Generator scene, clears flag0x15(60 15at+0x1508) and then arms the clock at+0x1533: expiry flag0x396, warning flag0x15, 21600 ticks (6:00), threshold 3600 ticks (1:00). The castle's encounter table opens with a flag-0x15group that is one whole-map rate-0 row: the last minute has no battles by design. - Nothing clears itThe success record
taikuP2[16]issues the same op with duration 0, which stops the clock, and never issues60 15. The whole disc has one script clear of flag0x15- the one that runs before the clock is armed - and no native call clears it by constant id. - The Fortress reads itFlag
0x15heads the condition list of exactly three scenes: the second castle scene, and the two Fortress scenesderoaandchitei2. 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
| Report | Explanation |
|---|---|
| No encounters anywhere in the Fortress | the flag-0x15 group: one whole-map rate-0 row in both scenes |
| Jette and Cort still there | the file is before Jette; the post-collapse flag 0x521 is not involved |
| Survives a full restart and a memory-card load | the flag bank is in the saved game state, save-block +0x1618 |
| Incense and both Bells (the encounter-rate accessories) change nothing | all three scale or suspend a rate that is already 0 |
| The locked Fortress was visited before Conkram | coincidence: read from the scripts, that visit changes none of the three condition flags; the flag was set in the castle |
| “Sometimes”, across the community | it depends on how much time was left on the castle clock |
What was captured
| Run | State | Result |
|---|---|---|
| Played | the save as played | rate 18 on all 55 steps; counter 974 down to 2; battle rolled |
| Edited save | clock words in the save block (flag pair 0x03960015 at +0x480, threshold 3600, duration 3900), checksum restamped | rate 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 poke | byte 0x8008575A bit 0x04 set | row 0, rate 0: 128 steps, counter fixed at 974, no battle |
| RAM poke | byte 0x800857FC bit 0x40 set (flag 0x521) | the same, with the Door of Light still allowed |
| RAM poke | flag 0x15 set and the step counter written to 0 | battle 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 (chitei2P2[15]andP2[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:
| Step | Record | What happens | Flags it sets |
|---|---|---|---|
| 1 | P2[11], Jette's room | Jette's speech, then the fight (formation row 12) | 0x470 |
| 2 | - | the walk to the council chamber, through rooms that roll at rate 18 | - |
| 3 | P2[13], Mist Generator room | “We must destroy the Mist Generator!”, Cort appears, the fight (row 13) | 0x4C4 |
| 4 | P2[14] | Cort falls; the scene cuts to the entrance scene and back | - |
| 5 | P2[15] or P2[19] | “Why did you do it?”; the room starts to shake | 0x521 |
| 6 | P2[18], or the end of P2[19] | “The Fortress is collapsing”; a 40-second clock is armed | - |
| 7 | P2[17], tile (18, 94) | a boulder seals the room's exit | 0x4C9 |
| 8 | P2[16] | about 25 seconds in, Cara arrives and the scene changes to the entrance | 0x1B |
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.
The tables
| Scene | Order | Flag | Regions in the group | Result |
|---|---|---|---|---|
deroa | 0 | 0x0015 | one, whole map, rate 0 | no encounters |
| 1 | 0x0521 | one, whole map, rate 0 | no encounters | |
| 2 | 0x046D | eight: two rate-0 pockets, five rate-18 rooms, catch-all | normal | |
| 3 | default | seven: one rate-0 pocket, five rate-18 rooms, catch-all | normal | |
chitei2 | 0 | 0x0015 | one, whole map, rate 0 | no encounters |
| 1 | 0x0521 | two, both rate 0 | no encounters | |
| 2 | default | sixteen: seven rate-0 boxes, eight rate-18 boxes, catch-all | mixed |
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
- A step is a change of the player's 128-unit tile by at most one on each axis (
0x801D9ED4..0x801D9F0C). - The condition list is walked in order (
0x801D9F30..0x801D9FD8): the first record whose flag is0xFFFFor set wins. - The first region of that group whose box contains the tile wins (
0x801D9FE8..0x801DA054). - The step does nothing while the player's engaged bit is up, while
0x8007B6B4is non-zero, while the warp timer0x8007B6B0is positive, or while the Incense window0x8007B600is non-zero (0x801DA130..0x801DA180). The roll is not reached at all while a menu or scripted submode is open (bit0x8000of0x1F800394, raised at0x801F1360, dropped at0x801F1684). - The rate is scaled by the setting byte
0x8007B5F8, the High and Low Encounter accessories, and system flags0x1D(doubled) and0x1E(halved) (0x801DA198..0x801DA204). - The result is subtracted from the counter
0x8007B5FC; at zero or below a battle starts (bgtzat0x801DA218) 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
| Address | Image | What | Evidence |
|---|---|---|---|
0x801D9E1C | PROT 0897 | per-step region encounter roll | disassembly |
0x801D9F30..0x801D9FD8 | PROT 0897 | condition-list walk; first set flag wins | disassembly |
0x801D9FE8..0x801DA054 | PROT 0897 | region search; first matching box wins | disassembly |
0x801DA130..0x801DA180 | PROT 0897 | the four gates that hold a step | disassembly |
0x801DA198..0x801DA204 | PROT 0897 | rate scaling | disassembly |
0x801DA208..0x801DA21C | PROT 0897 | counter subtract and test | disassembly |
0x801E2BDC..0x801E2C38 | PROT 0897 | op 4C D3 handler: arms the countdown | disassembly |
0x801D2EBC | PROT 0897 | clock actor; flag sets at 0x801D2F60 (expiry) and 0x801D2F84 (below threshold) | disassembly; capture (edited save) |
0x801F1360, 0x801F1684 | PROT 0897 | set and clear of bit 0x8000 of 0x1F800394 while a menu or submode is open | disassembly |
0x8003CE08, 0x8003CE64 | SCUS_942.54 | flag set; flag test: byte 0x80085758 + (id >> 3), mask 0x80 >> (id & 7) | disassembly |
0x8003B62C..0x8003B648 | SCUS_942.54 | scene loader: 1200 ticks off a running clock per scene entry | disassembly |
0x8003B96C..0x8003B98C | SCUS_942.54 | card load restores a running clock to its full duration | disassembly; capture (edited save) |
0x8003AC7C..0x8003AC90 | SCUS_942.54 | scene-entry counter top-up | disassembly |
0x800845A0, 0x800845B8, 0x800845BC, 0x800845C0 | RAM | clock: remaining, duration, threshold, flag pair; save-block +0x460, +0x478, +0x47C, +0x480 | disassembly; capture (played saves) |
0x8007B5FC | RAM | encounter step counter, signed word; not saved, 974 after power-on | disassembly; capture |
0x8007B600 | RAM | Incense window | disassembly |
0x8008575A mask 0x04 | RAM | system flag 0x15; save-block +0x161A | disassembly |
0x800857FC mask 0x40 | RAM | system flag 0x521 | disassembly |
PROT entry 373, P2[29] +0x1508, +0x1533 | disc | taiku: the one clear of flag 0x15, then the arming op | disc data |
PROT entry 373, P2[16] +0x13 | disc | taiku: escape success; stops the clock, no clear of 0x15 | disc data |
PROT entry 373, P2[12] | disc | taiku: the script-side set of flag 0x15 (50 15) | disc data |
PROT entry 606, MAN offset 0x5B4B | disc | deroa encounter section, 0x152 bytes | disc data |
PROT entry 706, MAN offset 0x6A1A | disc | chitei2 encounter section, 0x166 bytes | disc data |
PROT entry 706, P2[15], P2[19] | disc | setters of flag 0x521: the scene after the Cort fight | disc data |
PROT entry 706, P2[11], P2[13] | disc | the Jette fight (row 12, sets 0x470) and the Cort fight (row 13, sets 0x4C4) | disc data |
PROT entry 706, P2[18] +0x3DD, P2[19] +0x42D | disc | the collapse clock: 4C D3, flag 0x4C7 twice, 2400 ticks, threshold 910 | disc data |
PROT entry 706, P2[17], P2[16] | disc | the boulder at tile (18, 94); Cara's arrival and the change to deroa | disc data |
PROT entry 606, P1[0] +0x58..+0x72 | disc | deroa entry script, collapsed state: wall paint over (55..59, 41..42), music | disc data |
| PROT entries 703, 603 | disc | wall grids of chitei2 and deroa: 1287 of 3304 and 274 of 3341 enclosed tiles are rate 0 | disc 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 are | inference; community report |
| the entrance scene is silent after the collapse | - | read from the table and the scripts, not walked | inference |
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..0x801DA204inFUN_801D9E1C, PROT entry 0897; clean-up at0x8003B544..0x8003B568inFUN_8003AEB0,SCUS_942.54; the springs areP1[29]andP1[30]ofrugi, 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.
| Water | Flag | Effect on the roll | Steps to the next battle, tower's rate-20 rooms |
|---|---|---|---|
| neither | - | rate as written | 25 to 73, 49 on average |
| green | 0x1D | rate doubled: the next battle comes in half the steps | 13 to 37, 25 on average |
| blue | 0x1E | rate halved: the next battle takes twice the steps | 49 to 146, 98 on average |
| both | both | doubled, then halved: exactly the written rate | as 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, RAM | byte 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 card | byte +0x161B of the save block, the same bits |
| Check, the rate itself | break 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-up | win 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.
| Moment | Byte 0x8008575B | Game mode |
|---|---|---|
| after the poke, walking | 0x0E | 0x03 field |
| battle entry and the whole fight | 0x0E | 0x09, then 0x15 battle |
| return to the field, before the arm | 0x0E | 0x02 field init |
after the store at 0x8003B568 | 0x08 | 0x02 |
| field resumed, to the end of the run | 0x08 | 0x03 |
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 battle | Won | Escaped | Escaped with the Spikefish Flute |
|---|---|---|---|
0x08 and 0x02 set (0x0A) | 0x02 cleared | 0x02 cleared | the game freezes |
0x02 alone | 0x02 stays | 0x02 stays | 0x02 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!” |
| Placement | tile (108, 89) | tile (105.5, 89), two and a half tiles west of it |
| Flag writes after “Yes” | 50 1C, 50 1D at +0xF1 | 50 1C, 50 1E at +0xF0 |
| Other effect | 4C 82 00, 4C 82 01, 4C 82 02: full HP and MP for party slots 0 to 2 | the 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
| Address | Image | What | Evidence |
|---|---|---|---|
0x801DA1B8..0x801DA1DC | PROT 0897 | High Encounter passive 0x3B (rate * 4), Low Encounter passive 0x3C (rate / 2) | disassembly |
0x801DA1E0..0x801DA204 | PROT 0897 | flag 0x1D test, rate * 2; flag 0x1E test, rate / 2 | disassembly |
0x801DA208..0x801DA21C | PROT 0897 | counter subtract; s0 is the final rate | disassembly |
0x8003B504..0x8003B510 | SCUS_942.54 | same-scene re-entry arm of FUN_8003AEB0, _DAT_8007B8B8 == 2 | disassembly |
0x8003B544..0x8003B568 | SCUS_942.54 | clears flags 0x1D and 0x1E when flag 0x1C is set | disassembly, capture |
0x8008575B masks 0x08, 0x04, 0x02 | RAM | system flags 0x1C, 0x1D, 0x1E; save-block +0x161B | disassembly, capture |
PROT entry 697, P1[29] +0xF1, P1[30] +0xF0 | disc | the springs' flag writes after three 4C 82 restores | disc data |
| PROT entry 697, encounter section | disc | tower regions: (0..58, 0..72) rate 0, the rest rate 20 | disc 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 at0x800321E0; runaway copy at0x800367E4inFUN_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.
| Scene | Byte | Escape | Result |
|---|---|---|---|
jouind | 0x0A | Spikefish Flute | stops 165 vsyncs after the teardown, still in battle mode; two runs |
jouind | 0x02 | Spikefish Flute | the same |
jouind | 0x08 | Spikefish Flute | the same |
jouind | 0x0C | Spikefish Flute | the same |
jouind | 0x00 | Spikefish Flute | the same |
jouind | 0x0A | Run | back in the field; the byte reads 0x08 |
rugi | 0x0A | Spikefish Flute | back 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 jouind | Field-back read | Predicted | Result |
|---|---|---|---|
| three Gillium Lv3 (161), the scene's own row | 127 sectors | freeze | freezes |
| three Puera Lv3 (158), the scene's other row | 115 | survive | back in the field |
| one Gimard Lv3 (12) | 112 | survive | back in the field |
| three Gimard Lv3 (12) | 112 | survive | back in the field |
| three Gizam Lv3 (57) | 115 | survive | back in the field |
| three Barra Lv3 (143) | 116 | freeze | freezes |
| Aluru (80) + Orb Lv2 (84) | 134 | freeze | freezes |
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
- The caption outlives the attack (capture). While the flute's attack runs, the retained widget list
FUN_80031D00draws a text widget every frame:jal 0x80036888at0x800321E0, string pointer at widget+0x18. In this fight the string is"Blowfish"at heap address0x800B995C. Arm 8 ofFUN_801F6A00in PROT entry 0925 moves the action state machine to the run wait0x65and never removes that widget. The caption is still drawn every frame through states0x65,0x66and0x67, beside the “successfully escaped” banner. - Its text is overwritten (capture, disassembly). At state
0x67the teardown inFUN_8004E568frees the enemy record buffers and the side-band stream buffer, and the loader-B id0x8007BC4Cgoes from0x1E(PROT 0925) to0x53(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 at0x800B995Creadsf8 f2 f2 ff fa ef da ..., which isjouind's PROT entry 681 at offset0xE4BC, with no zero byte nearby. - The text routine never comes back (capture, interpreter core). The renderer
FUN_80036888hands that “string” to the substitution expanderFUN_80036514. The expander's first pass rewrites each0xFFin the source to0xCFin place, so it also corrupts the freed memory it reads. The copy loop at0x800367E4then follows a substitution code whose source pointert0lands in the expander's own output buffer (from0x800740EC). The read cursor trails the write cursor by0x1FEbytes and never meets a zero. The copy runs on through0x80074723,0x80075E93and0x80079543(logged every 2000 passes), overwriting everything after the buffer. Under the fast core the emulator stops with “Unrecoverable error while running recompiler” at pc0x80036868, 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:
- It takes the largest free heap block (
FUN_8002B790, at0x801F6F08), counts whole sectors in it, and caps the count at0xC5. - It allocates that many sectors less 4 bytes (
FUN_80017888, at0x801F6F74). The best-fit allocatorFUN_8002B468cuts the allocation from the top of the block, so the buffer ends at0x800E7C9C, where the freed stager buffer ended. - 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
0x20draws nothing; - otherwise it follows the normalisation pass, the token count, the
C1..C7substitution table at0x80010EC0and 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:
| Scene | Formation | Sectors | Failure |
|---|---|---|---|
jouind, Bio Castle (D) | three Gillium Lv3 | 127 | runaway copy (captured) |
jouinc, Bio Castle (C) | three Barra Lv3 | 116 | bus error (the same fight froze when forced in jouind) |
jouina, Bio Castle (A) | three Zenoir Lv3 | 114 | runaway copy |
jouina, Bio Castle (A) | three Gola Gola Lv3 | 110 | bus error |
cave01, Snowdrift Cave | one Mushroom; one Blue Piura | 110 | runaway copy |
map01, world map (Drake Kingdom) | one or two Gobu Gobu | 117 | runaway copy |
dohaty, Dohati's Castle | Aluru + Orb Lv2; Nova Lv2 + Gola Gola | 134 | runaway copy |
tunnelb, Fire Path | Nova + Gel Frog | 139 | bus error |
tunnelb, Fire Path | Stone Lizard + two Warman | 134 | runaway copy |
nilboa, Nivora Ravine | two Amethyst + Neo Grude | 131 | bus error |
nilboa, Nivora Ravine | one Amethyst | 102 | runaway 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
koin2Spoon + 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
| Address | Image | What | Evidence |
|---|---|---|---|
0x801F7400, 0x801F78C4..0x801F78F8 | PROT 0925 | Spikefish Flute flee: outcome, survived latch, escape counter, ctx[7] = 0x65 | disassembly |
0x801E7B5C..0x801E7B84, 0x801E8028..0x801E802C | PROT 0898 | the run command's matching success stores in FUN_801E791C | disassembly |
0x80046DD8..0x80046E60 | SCUS_942.54 | battle-exit mode choice in FUN_80046A20; reads no escape kind | disassembly |
0x800321E0 | SCUS_942.54 | FUN_80031D00: the retained text widget's draw, string at widget +0x18 | disassembly, capture |
0x80036514, loop 0x800367E4..0x80036868 | SCUS_942.54 | substitution expander; the runaway copy into and past 0x800740EC | disassembly, 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
0xA8slack 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, state0x19 - 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.
- FlinchThe hit-reaction stager in
FUN_800402F4queues the flinch clip and raises bit 2 ofactor+0x1DC(ori v0,v0,0x4at0x80042140). - Attack setup, state
0x14Out of reach, no tag-0x20walk action (jal 0x80050e2cat0x801E3268, miss tested at0x801E327C): the Move clip is staged at0x801E32B0and state0x19stored at0x801E32D0. - Mid-clip commitThe Move clip is installed by the path that clears bits 0..1 only (
andi v0,v0,0xfcat0x80047A38). Bit 2 survives. - First natural end
FUN_80047430tests bit 2 (0x80047B38) and queues idle over the Move clip (sb zero,0x1da(s2)at0x80047B44). - ParkedState
0x19re-checks the range every frame at0x801E3594and falls to the stall label0x801E35D0, forever.
Addresses
| Address | Image | What | Evidence |
|---|---|---|---|
0x801E31F4..0x801E32DC | PROT 0898 | state 0x14 walk-tag fallback | disassembly |
0x801E3568..0x801E3588 | PROT 0898 | nine redundant facing words in state 0x19; the patch site, overlay offset +0x14D50 | disassembly |
0x80047B38..0x80047B50 | SCUS_942.54 | natural-end test of bit 2 and the idle install | disassembly |
0x80047A38 | SCUS_942.54 | mid-clip commit mask 0xFC | disassembly |
0x80042124..0x80042170 | SCUS_942.54 | flinch stager: bit 2, then bit 0 | disassembly |
0x8004E2F0, 0x80050E2C | SCUS_942.54 | range check; action-tag scan | repo doc |
actor +0x1D9, +0x1DA, +0x1DC | RAM | playing clip, queued clip, anim event flags | disassembly |
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.
| Spelling | Where |
|---|---|
Van | the battle name of monster id 137, in the monster archive (PROT entry 867) |
Bon | the 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 offset | Speaker | Word |
|---|---|---|
0x6D86 | King Drake | moustache |
0x6DB6 | Noa | Whiskers, twice |
0x6E39 | King Drake | moustache |
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”.
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.
| Beat | USA | Japanese | What the script does |
|---|---|---|---|
| Noa turns to the player | +0x4F4 | +0x562 | CC 1F 85 0F 00 F8: channel 0x1F (Noa) faces channel 0xF8 (the player) |
| The player turns to Noa | +0x516 | +0x584 | CC F8 85 0F 00 1F |
| Noa's question | +0x55B | +0x5DB | spoken to Vahn. Japanese: about “this person” (kono hito), the researcher. English: “You don't like me?” |
| The choice box | +0x57D | +0x600 | two 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 | +0x678 | Noa turns to the researcher (CC 1F 85 14 00 25) and calls him odd |
| After option 2 | +0x66E | +0x71B | the 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
| Address | Image | What | Evidence |
|---|---|---|---|
| monster id 137 | PROT entry 867 | the Van spelling | disc data |
MAN offset 0x380C | PROT entry 146 | the Bon spelling | disc data |
MAN offsets 0x6D86, 0x6DB6, 0x6E39 | PROT entry 70 | moustache, Whiskers, moustache | disc data |
MAN offset 0x923E | PROT entry 401, USA | bath scene, P2[4], 2451 bytes | disc data |
MAN offset 0x9BC7 | PROT entry 401, Japanese disc | the same record, 2708 bytes | disc data |
MAN offsets 0x9732, 0x9754 | PROT entry 401, USA | the two face-turn ops before the question | disc data |
MAN offset 0x97BB | PROT entry 401, USA | the two-option picker 27 32 00 D2 00; Japanese 27 4C 00 FC 00 at record +0x600 | disc data |
MAN offset 0x9819 | PROT entry 401, USA | Noa turns to the researcher after option 1 | disc data |
0x800857D7 mask 0x10 | RAM | system flag 0x3FB, the scene's one-shot latch | disassembly |
| option 1 means “he is too shy” | - | the reading of the subjectless Japanese line | inference |
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 opcodein 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
concnowrecordsP2[17](the walk-on at the mirror) andP2[18](the crossing); the unchecked allocation in the move VM's keyframe-pose seat (op0x3C) at0x80023CD0..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
- 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.
- Heap nearly fullAbout 16.6 KB free in
concnow; the allocatorFUN_8002B468is best-fit over one free list. - Battle data pre-stagedAfter the music swap the poller
FUN_800243F0(0x80024BB0..0x80024C64) reads 8 sectors throughFUN_8003E360, thenFUN_80052770loads the party battle filesPLAYER1..3, about 316 KiB in twenty chunks. - Allocation failsAn effect part's keyframe buffer (
FUN_80017888(0, count*32|8), 0x88 bytes for four lanes) returns NULL; the failure arm only adds0x20000to thePRG ERRcounter0x8007B828. - NULL writeMove-VM op
0x3Cstores the pointer with no test and copies through it (shat0x80023D30,0x80023D3C); its retarget op0x3Dwrites through it too (0x80023EF4). - Exception loopCause
0x428(reserved instruction), EPC0x80000084, 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.
| Emulator | Setting | Result |
|---|---|---|
| PCSX-Redux | interpreter or dynarec, several timing settings, Cross every 4 to 300 vsyncs | passes |
| SwanStation (DuckStation core) | 100 % CPU; 50 % | passes |
| SwanStation | 125 %, 150 %, 200 %, 400 % CPU | freezes (125 % already, with the played party) |
| PCSX-ReARMed | its default for this game (a faster CPU setting it applies to Legaia only) | freezes |
| PCSX-ReARMed | CPU clock 30, 45, 50 | passes |
| PCSX-ReARMed | CPU clock 62, 80, 100 | freezes |
| ePSXe 2.0.5 (Linux x64, under box64) | defaults | passes |
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
| Address | Image | What | Evidence |
|---|---|---|---|
0x80023CD0..0x80023CE0 | SCUS_942.54 | move-VM op 0x3C: unchecked malloc | disassembly, capture |
0x80017888, 0x800178B8 | SCUS_942.54 | malloc wrapper and its failure arm | disassembly |
0x80021184..0x80021190, 0x8001B098 | SCUS_942.54 | render-node malloc stores NULL; the draw path writes through it | disassembly, capture |
0x80024BB0..0x80024C64 | SCUS_942.54 | post-swap battle pre-stage | disassembly, capture |
0x80000080, 0x8007B828 | RAM | exception vector; PRG ERR counter | capture |
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 Flute | Spikefish Flute | |
|---|---|---|
| Item id | 0x98 | 0x99 |
| Item record | 0x80074A88, subtype 0x27 | 0x80074A94, subtype 0x28 |
| Effect descriptor | 0x8007535C: 7E 00 C4 41 | 0x80075360: 7F 00 E4 41 |
| Class, target | 126, one enemy, battle only | 127, all enemies, battle only |
| Action id | 0x96 | 0x97 |
Tick table 0x801CF4EC | row 21: jal 0x801F6A18 | row 22: jal 0x801F6A00 |
Stager table 0x801F6734 | row 21: 0x801F7820 | row 22: 0x801F7AE8 |
| Cast module | PROT entry 0924 | PROT 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.