Moon: Remix RPG Adventure, a 1997 PS1 game, contains strings for a debug menu, up until now without a way to be activated. Furthermore, some logic related to showing menu text happens to be missing, requiring patching-in some workarounds.

Tooling

There’s pretty decent support for the PS1:

  • Disassembler / Decompiler: Ghidra + PSX executables loader (Identifies all the different code and data sections in the memory map, recognizes SDK functions…)
  • Debugger: PCSX-Redux or Mednafen (Read/Write/Execute breakpoints, live disassembly of edited memory…)

At the time of writing, PCSX-Redux is under a GUI rewrite, so some functionality isn’t available. In that case, Mednafen can be used as a fallback, e.g. testing out GameShark codes.

Finding menu functions

We start by following the cross-reference (xref) for a pointer table containing the addresses of the menu labels, which are iterated and buffered for printing to the screen using FntPrint():

// draw_menu_labels
undefined4 FUN_801367b4(undefined **param_1,char *******param_2,char *******param_3,undefined4 param_4) {
   // ...

   iVar5 = 0;
   ppuVar6 = &PTR_s_flg_mode_80161600;
   do {
     pbVar4 = &DAT_801135a8;
     if (DAT_80172fc0[1] == iVar5) {
       pbVar4 = &DAT_801135a4;
     }
     iVar5 = iVar5 + 1; // increment processed labels counter
     FntPrint(pbVar4,(byte *)param_2,(int)param_3);
     param_2 = (char *******)*ppuVar6;
     ppuVar6 = (undefined **)((char *******)ppuVar6 + 1); // move to next label
     FntPrint(&DAT_801135ac,(byte *)param_2,(int)param_3);
     puVar2 = DAT_80172fc0;
   } while (iVar5 < 10);

   // ...
}

Following xrefs to draw_menu_labels(), we see no direct calls, but instead a callback variable set with the function address, carried over by previous callers up to the main game loop. Such is the case with draw_debug_menu(), the last followable function, since no xrefs were found:

// draw_debug_menu
undefined4 FUN_80138574(undefined **param_1,undefined4 param_2,undefined4 param_3,int param_4) {
  undefined4 uVar1;

  draw_debug_vars();
  if (is_skip_wrap_menu_labels != 0) {
    FUN_80138628();
  }
  uVar1 = 0;
  if ((*(uint *)(DAT_8017efe4 + 0x1124) & 0x100) != 0) {
    DrawSync(0);
    FntLoad(0x3c0,0x100);
    FntOpen(0x10,0x18,0x120,0xd0,0,0x200);
    uVar1 = 0xffffffff;
    is_skip_wrap_menu_labels = 1;
    *param_1 = wrap_menu_labels; // Pass callback to main loop
  }
  return uVar1;
}

I didn’t check the main loop closely, but by setting breakpoints on each callback and on the main game loop (main() is automatically identified by the Ghidra loader), we can verify that they are called in order on each iteration of the loop.

Ok, so no direct calls to this function. We already know that param_1 is used for callbacks, so we need to find some function where similar calls are done. The other parameters are unused.

Before we get to that, what exactly needs to be called to draw text on the screen? After looking up an example (accompanied by the library reference), we see the same calls done in draw_debug_menu(), and additionally a FntFlush(-1) called after FntPrint(). Ok, let’s check the xrefs to FntFlush(), surely finding another piece of the puzzle… Except there aren’t any! Also, see that FUN_80138628()? It’s a no-op… Clearly some logic has been stripped out from the executable.

Finding user input callback functions

At this point, we also don’t know how user input gets parsed, which should be required to activate the debug menu. Assuming that some of those input handlers also use the same callback mechanism, let’s identify where callbacks are passed, by searching for the corresponding byte patterns. Then, we can place breakpoints in those addresses and try some joypad input in hopes of hitting one of those breakpoints.

Let’s check how draw_debug_menu() passes wrap_menu_labels() in assembly (btw, the CPU architecture is MIPS):

80138608 13 80 03 3c   lui   v1,0x8013 ; upper part
8013860c 50 67 63 24   addiu v1,v1,0x6750 ; lower part
80138610 00 00 03 ae   sw    v1=>wrap_menu_labels,0x0(s0) ; *param_1 = 80136750

Addresses are separated by lower and upper parts, loaded with 2 instructions. Searching for instructions matching bytes 00 00 03 ae gives us around 14 results, which is already manageable. We would probably need to try some other registers, but after setting breakpoints on these already found addresses, we hit one of them when pressing the triangle (upper button), which opens an in-game menu:

Seems like a good candidate to replace with the address of draw_debug_menu():

// wrap_ingame_menu
undefined4 FUN_8012743c(undefined **param_1) {
  DAT_80171878 = &DAT_8017187c;
  bzero((undefined *)&DAT_8017187c,300);
  set_action_mode(0,0x100);
  *DAT_80171878 = 0;
  *param_1 = ingame_menu;
  return 0;
}

We apply this patch:

-80127480 12 80 03 3c   lui   v1,0x8012
-80127484 a0 74 63 24   addiu v1,v1,0x74a0 ; address of `ingame_menu()`
+80127480 14 80 03 3c   lui   v1,0x8013
+80127484 74 85 63 24   addiu v1,v1,0x8574 ; address of `draw_debug_menu()`
 80127488 00 00 03 ae   sw    v1,0x0(s0)

Which is equivalent to these GameShark codes:

80127480 8014
80127482 3c03
80127484 8574
80127486 2463

Filling the gaps

Now we can reach draw_debug_menu(), but we need to pass this condition:

if ((*(uint *)(DAT_8017efe4 + 0x1124) & 0x100) != 0)

This dereference resolves to 0x80110000 + 0x1124 = 0x80111124, where we can place a memory write breakpoint. Oh, hit on every main loop iteration, even if it’s null bytes… Alright, let’s just eyeball it then:

Pressing the select button leads to 0x100 being written in little endian at 80111124, which passes the condition.

Moving on to the missing FntFlush(-1) call. Let’s do a quick and dirty attempt: When the debug menu is active, we constantly hit function draw_menu_labels(), the one we decompiled right at the beginning. What if we place the breakpoint at the first instruction of that function, then manually call FntFlush(-1)? For that, we can manually edit registers: set the first argument with -1 in two’s complement ($a0 = 0xffffffff), then the program counter with the address of FntFlush() ($pc = 80143a60). After continuing in the debugger a few times, we do get a glimpse of the menu for a few frames:

Note that entries appear repeated, most likely due to calling FntPrint() multiple times without flushing.

It’s very tempting to just flush at the end of this function… After all, everything to be displayed has already been printed. Without more context on how the debug menu was actually handled, this seems like the most direct approach. We can implement it in the following steps:

  1. Find a code cave to place a subroutine that calls FntPrint(-1) (check sections with the executable flag set in Ghidra’s memory map);
  2. Ensure we can return back from the subroutine to the patched functions’ caller address;
  3. Patch the last instruction of menu handling functions that should jump to that subroutine;
  4. Ensure we can exit cleanly from the debug menu, by clearing state bits set by wrap_ingame_menu() (otherwise, some in-game actions aren’t executed on user input).

There’s a few MIPS idiosyncrasies to take into account:

  • Delay Slot Instructions (represented in Ghidra with an underscore before the mnemonic): When a jump instruction is about to be taken, the next defined instruction will be executed concurrently. If we don’t want that to happen (as it may depend on some return result), we need to place a nop instruction after the jump;
  • The caller address to return to is stored in register $ra. Since our subroutine is calling some other function, we need to store the original value of $ra to a temporary register (e.g. $t0), then use that register as the return value.
  • Jump offsets are absolute, encoded as offset_value / 4 (a nice benefit of having a fixed-size instruction set!)

Translating all this to assembly, in addition to the in-game menu patch, we add our code cave subroutine (step 1), ending with a jump back to the caller (step 2):

-801a0000 00 00 00 00   nop
-801a0004 00 00 00 00   nop
-801a0008 00 00 00 00   nop
-801a000c 00 00 00 00   nop
-801a0010 00 00 00 00   nop
-801a0014 00 00 00 00   nop
+801a0000 00 00 e8 23   addi t0,ra,0 ; save original return address
+801a0004 00 00 04 3c   lui a0,0
+801a0008 ff ff 84 24   addiu a0,a0,-1
+801a000c 98 0e 05 0c   jal 0x80143a60 ; call FntFlush(-1)
+801a0010 00 00 00 00   _nop
+801a0014 08 00 00 01   jr t0 ; jump to original return address

We patch the last instructions of draw_menu_labels(), and another function for submenus, wrap_menu_labels_2() (step 3):

-80136958 08 00 e0 03   jr ra
+80136958 00 80 06 08   j 0x801a0000
  
-801369d8 08 00 e0 03   jr ra
+801369d8 00 80 06 08   j 0x801a0000

The latter function is called by the former function when the circle (right button) is pressed, activating the selected submenu:

if ((*(uint *)(DAT_8017efe4 + 0x1124) & 0x140) == 0) { // Is select or cancel button not pressed?
  if ((*(uint *)(DAT_8017efe4 + 0x1124) & 0x20) != 0) { // Is circle button pressed?
    piVar1 = DAT_80172fc0 + 1;
    *DAT_80172fc0 = 0;
    puVar2[8] = (&PTR_debug_menu_entry1_selected_801615d8)[*piVar1];
    *param_1 = wrap_menu_labels_2;
  }
}

Finally, we clear the state bits 0x100 (step 4), which are set in wrap_ingame_menu(), but wouldn’t be cleared due to us overriding the callback containing that logic:

-801368ec 00 80 05 24   _li param_2,-0x8000
+801368ec 00 81 05 24   _li param_2,-0x8100 ; clear in-game menu bits in addition to debug menu bits

These bits are checked in pick_next_action(), the function where the callback for the in-game menu is set, which does an early return when the bits are set (presumably to avoid parsing input unintentionally, e.g. moving the player’s character in-game when pressing arrow buttons to select entries in a menu):

bVar2 = check_action_mode(0,0x100);
if ((CONCAT31(extraout_var,bVar2) != 0) && ((DAT_801785d0[1] & 0x80) != 0)) {
  *DAT_801785d0 = 0;
  *param_1 = FUN_80131fac;
  return 0;
}

Here’s the full listing of GameShark codes:

80127480 8014
80127482 3c03
80127484 8574
80127486 2463
801a0000 0000
801a0002 23e8
801a0004 0000
801a0006 3c04
801a0008 ffff
801a000a 2484
801a000c 0e98
801a000e 0c05
801a0010 0000
801a0012 0000
801a0014 0008
801a0016 0100
801368ec 8100
801368ee 2405
80136958 8000
8013695a 0806
801369d8 8000
801369da 0806

To sum it up, by pressing the triangle button, then the select button, we now get a properly rendered debug menu:

There’s some fun features to explore, such as this palette test:

TODO

Replacing an in-game menu isn’t ideal, so we could look into patching our own pressed button condition in pick_next_action() (perhaps checking for a less used button, such as L1/L2). We would jump to a new subroutine containing that condition, similar to what we did before.

This would involve copying over the variables setup done in wrap_ingame_menu(), making sure to set the callback to the address of draw_debug_menu(). It’s doable, even though you end up with an even larger list of GameShark codes, maybe patch the executable and rebuild the disc image at that point.