MediEvil, a 1998 PS1 game, had a rolling demo that could be modified to be interactive, allowing the player (Dan) to freely explore levels, alongside some debugging features. Some years ago, SolidSnake11 published a video of that demo, showcasing patched-in unused items.

Since then, the ECTS demo was released to the public. Let’s see what changed between builds, rediscovering these items, and maybe other unused assets…

We begin without any clue on how items are represented in memory, since there’s no specific strings to lookup, as you usually have with e.g. debug menus.

Tooling

Similar to my previous PS1 writeup, but this time I patched Mednafen’s debugger and wrote some scripts to simplify processing of trace logs, covered on the next sections.

As an alternative, I could have used PCSX-Redux’s gdb remote stub, allowing me to run gdb command files. However, I found value in extending Mednafen, since it could be reused for other platforms besides PS1, which may not have emulators with support for gdb.

Filter #1: Only log unique addresses hit

We can easily generate GBs of trace logs without doing much, so let’s start by reducing the logged instructions to only the first time each are executed.

I decided to try a generic approach that could be adapted to other emulators without having to mess with their codebases. We read from a FIFO file named “f”, to avoid wasting disk space. A script reads from this file and stores the address in a set:

#!/usr/bin/env python3

import os

fifo = "f"
if os.path.exists(fifo):
    os.remove(fifo)
os.mkfifo(fifo)

addresses = set()
with open(fifo) as f:
    for line in f:
        addresses.add(line[:8])  # Program counter value

out = "trace.out"
if os.path.exists(out):
    os.remove(out)
with open(out, "w") as f:
    for a in addresses:
        f.write(a)
        f.write('\n')

We run the script first, then Mednafen:

./mednafen \
    -debugger.autostepmode 1 \
    -force_module psx -psx.region_autodetect 0 -psx.region_default eu \
    MEDIEVIL.cue

We start the trace log by pressing “Alt+d” to enter the debugger, then “l” and inputting f ffffffff, so that it keeps logging until we exit the emulator (normal execution never hits address 0xffffffff). At that point, the script also exits, outputting the final set of unique instructions.

Due to how Mednafen opens the trace log file, there’s a small patch that needs to be done in “src/drivers/debugger.cpp”, due to FIFOs not supporting seek operations:

/*
TraceLog->seek(0, SEEK_END);
if(TraceLog->tell() != 0)
 TraceLog->print_format("\n\n\n");

TraceLog->print_format("Tracing began: %s\n", Time::StrTime().c_str());
TraceLog->print_format("[ADDRESS]: [INSTRUCTION]   [REGISTERS(before instruction exec)]");
*/

We will take 2 distinct trace logs: one where we do a bunch of actions that aren’t item pickup (trace.0), and another where we pickup a shield (trace.1.shield).

Filter #2: Pick addresses that start blocks

We can further reduce the unique addresses to those that belong to the start of a basic block, since the other instructions in the block don’t affect control-flow. These are delimited by e.g. if/then/else statements, branches… The reasoning is that any conditional logic related to item pickups should reach different blocks. This information is readily available from Ghidra’s disassembly, so we can extract the start addresses with a script:

from java.awt import Color
from ghidra.program.model.block import BasicBlockModel
from ghidra.util.task import TaskMonitor, ConsoleTaskMonitor
from ghidra.app.plugin.core.colorizer import ColorizingService
from docking.options.editor import GhidraColorChooser
from ghidra.program.model.address import AddressSet

bbm = BasicBlockModel(currentProgram)
monitor = ConsoleTaskMonitor()
lastBlock = None

entries = set()
input_file = askFile("Select Basic Block Address List", "whatever")
input_file = str(input_file)
with open(input_file, 'r') as f:
    for line in f:
        data = str(line.strip())
        if not data:
            continue
        offset = "0x" + data
        address = currentProgram.getAddressFactory().getDefaultAddressSpace().getAddress(offset)
        disassemble(address)

        block = bbm.getFirstCodeBlockContaining(address, monitor)
        if block:
            entry = block.getFirstStartAddress()
        else:
            entry = address

        print("{} {}".format(address, entry))

	entries.add(entry)

entries = sorted(entries)
output_file = askFile("Select Basic Block Address List", "whatever")
output_file = str(output_file)
with open(output_file, 'w') as f:
    for entry in entries:
        f.write("{}\n".format(entry))

Now you might ask why we don’t use function start addresses, which would look something like this:

-        block = bbm.getFirstCodeBlockContaining(address, monitor)
-        if block:
-            entry = block.getFirstStartAddress()
+        func = getFunctionContaining(address)
+        if func:
+            entry = func.getEntryPoint()
         else:
             entry = address

Unfortunately, that would cause us to miss any new blocks in a function reached by both trace logs. So, let’s keep it at block-level.

We run the script for both trace logs, resulting in trace.bb.0 and trace.bb.1.shield.

Now, let’s take the address that are unique to the item pickup, storing them in trace.bb.1.uniq:

diff -u trace.bb.0 trace.bb.1.shield \
    | grep '^+[^+]' \
    | cut -c2- > trace.bb.1.uniq

Filter #3: Add temporary breakpoints on exclusive hits

It can happen that some of the block start addresses are not exclusive to item pickup logic. After all, there’s a lot more happening in the game state: Enemies might decide to come towards you, particle effects from a torch, etc…

For this step, I use the temporary breakpoint method: Again, we will do a bunch of actions that aren’t item pickup, and remove any hit breakpoints in the process. Mednafen doesn’t have this concept, so we still have to press “Space” to clear the breakpoint when hit. What about adding them? You can end up with dozens of them, which is a chore to place with the UI. Therefore, I added a new command to Mednafen that parses the breakpoints from a file and places them automatically:

else if(InPrompt == BreakCommandsPrompt)
{
 if(pstring != BreakCommandsSpec || !TraceLog)
 {
  BreakCommandsSpec = pstring;
  char tmpfn[256];
  int num = trio_sscanf(tmp_c_str, "%255s", tmpfn);
  if(num >= 1)
  {
    FileStream* fs = new FileStream(tmpfn, FileStream::MODE_READ);
    std::string line;
    while(fs->get_line(line) >= 0) {
      if(line.size() >= 1) {
        unsigned long long addr = std::stol(line, nullptr, 16);
        std::cout << "bp @ " << std::hex << addr << std::endl;
        DisAddr = addr;
                DisAddr &= ((1ULL << CurGame->Debugger->LogAddrBits) - 1);
            DisAddr &= ~(CurGame->Debugger->InstructionAlignment - 1);
        DisCOffs = 0xFFFFFFFF;
        TogglePCBreakPoint(addr);
      }
    }
  }
 }
}

This command is then binded to “Shift+d”:

case SDLK_d:
   if(!InPrompt)
   {
    if(event->key.keysym.mod & KMOD_SHIFT) {
    InPrompt = BreakCommandsPrompt;
    myprompt = new DebuggerPrompt("BreakCommands(filename)", BreakCommandsSpec);
    PromptTAKC = event->key.keysym.sym;
    }
   }
   break;

Finally, we also print whenever a breakpoint is hit (done just for the PSX debugger):

diff --git a/src/psx/debug.cpp b/src/psx/debug.cpp
index 2afeb2e..7c97859 100644
--- a/src/psx/debug.cpp
+++ b/src/psx/debug.cpp
@@ -23,6 +23,7 @@
 #include "timer.h"
 #include "cdc.h"
 #include "spu.h"
+#include <unordered_set>

 namespace MDFN_IEN_PSX
 {
@@ -142,6 +143,8 @@ void CheckCPUBPCallB(bool write, uint32 address, unsigned int len)
  }
 }

+static std::unordered_set<uint32> hitBPs;
+
 static void CPUHandler(const pscpu_timestamp_t timestamp, uint32 PC)
 {
  if(LogFunc)
@@ -170,6 +173,10 @@ static void CPUHandler(const pscpu_timestamp_t timestamp, uint32 PC)
   if(PC >= bpit->A[0] && PC <= bpit->A[1])
   {
    FoundBPoint = true;
+   if (hitBPs.find(PC) == hitBPs.end()) {
+        hitBPs.insert(PC);
+        printf("hit @ %08X\n", PC);
+   }
    break;
   }
  }

We run BreakCommands with trace.bb.1.uniq, and now go through each hit breakpoint while we do actions that aren’t item pickup.

Then, let’s collect all those printed breakpoints in trace.bb.1.bphits.sort and reduce the start addresses to those that were not hit, which should be close to the exclusive set related to item pickup logic:

diff -u trace.bb.1.bphits.sort trace.bb.1.uniq \
    | grep '^+[^+]' \
    | cut -c2- > trace.bb.1.bpleft

Finally, we run BreakCommands again but with trace.bb.1.bpleft, and analyze whatever we hit during item pickup. I still got some other unrelated hits, further reducing this set to trace.bb.1.bpitem.

Patching bytes at runtime

I’ve also added a PatchCommands to read a file containing a sequence of address bytes_to_replace, since there were some cases where I didn’t want to patch the executable.

The code itself isn’t very interesting, but feel free to check the full diff in the Mednafen fork.

Replacing items

At some point, objects must be compared against the player for collision detection. We can expect some loop that iterates through these objects. Since different objects have different payloads, distinct code paths must be taken. This could happen in some ways:

  • A huge if-else or switch-case statement, that tests each type and branches to a block / calls a function with the corresponding behaviour;
  • A more object-oriented implementation, where each instance contains not only the type but a function pointer to the corresponding behaviour, which is simply accessed with an offset and indirectly called;
  • Other messy conditional logic?

Finding item pickup functions

Let’s start by writting down a list of hit breakpoints and their corresponding actions:

# pickup money bag
38f88
# pickup red rune key
38f88
# pickup broad sword
38f88 7524c 75350 50734 12f64       36e08 33458 4917c
# pickup shield
38f88 7524c 75350 50734 5ed40 5f6ac 36e08 33458 75400 3b5cc 36ff8 5f768 75630
# opening chest
7517c

Let’s focus on object pickups for weapons vs. shields: the last common address before different breakpoints are hit is 0x80050734. Likely the object type is being checked here, although we don’t know at which address yet. Here’s the function body:

undefined4 FUN_80050734(int param_1) {
  short sVar1;
  int iVar2;

  iVar2 = DAT_800cdfa4;
  if (param_1 < 0xe) {
    *(int *)(DAT_800cdfa4 + 0x10) = param_1;
  }
  else {
    *(int *)(DAT_800cdfa4 + 0x14) = param_1 + -0xe;
    sVar1 = *(short *)(&DAT_800cdc18 + (param_1 + -0xe) * 2);
    *(undefined4 *)(iVar2 + 0x44) = 0xffffffff;
    *(int *)(iVar2 + 0x18) = (int)sVar1;
  }
  return 0;
}

There’s conditional logic over the parameter. We can break at the last instruction:

8005077c 08 00 e0 03     jr         ra

Then step into the caller, which passes the field obj_vars[0xC][0x6]:

undefined4 UndefinedFunction_80075350(void) {
  FUN_80050734(*(undefined *)(*(int *)(obj_vars + 0xc) + 6));
  *(uint *)(obj_vars + 0x58) = *(uint *)(obj_vars + 0x58) | 0x20;
  FUN_800380d4(0x2b,0xffffffff);
  FUN_80032b24(obj_vars,0xfffffffe);
  return 0;
}

At this point we could also check other hit functions and try modifying referenced offsets from obj_vars in memory to figure out what is affected ingame, but we were already lucky, since obj_vars[0xC][0x6] is relevant to which object gets spawned.

But hold on, we actually want to replace an existing item we aren’t interacting with, not an item we are already picking up. Nevertheless, we can still use the pickup functions to figure out the address of an object in memory.

Here’s an example with the rune key.

  1. Take a save state;
  2. Place a breakpoint at 0x80038f88, then pickup the key, stopping at that address:
  3. Open memory view, go to obj_vars address (0x800cdfb0):
  4. Dereference obj_vars:
  5. Derefence offset 0xC:
  6. Lookup offset 0x6:

We get obj_vars[0xC][0x6] at 0x80121742. Now, if we load the previous save state, this address still contains the same values (if it didn’t, then we would place a memory write breakpoint to see where it’s hit).

But let’s set obj_vars[0xC][0x4] = 0x7. Walk back enough to retrigger model loading, then when we return…

An unused potion! obj_vars[0xC][0x4] is the object type (e.g. weapon, potion, enemy…), and obj_vars[0xC][0x6] is the subtype (e.g. dragon potion, serpent potion…), which gives us this little table for the potion type:

Subtype Description
0x0 Dragon (No effect)
0x1 Twisted (Spawns serpent)
0x2 Skull (Power-up)
0x3 Red (No effect)
0x4 Green (Dan grows)
0x5 Transparent (No effect)
0x6 Purple (Enemies shrink)
0x7 Yellow (No effect)

As you can tell from the descriptions, the less exciting news: All these potions behave identical to the earlier Rolling Demo build. Well, now we know. 🥲

Replacing models

On the following sections, I’ll use some terms specific to the MWAD file format, which contains this game’s resources.

There’s a lot of unused models viewable with the FrogLord tool. Let’s try to load some ingame.

We already know how the object type is represented, and there’s code where they get written, or maybe even read to decide which model to render. I got better insight from opening a chest: a new item gets spawned, so there’s an address where obj_vars[0xC][0x4] gets written, which means we can set both read and write breakpoints at this address. Figuring out the address can be done with the pickup steps described in the previous section.

We get a memory read breakpoint hit on function 0x80018f50, which has an interesting table being accessed:

uVar1 = *(uint *)((&DAT_800bb744)[(uint)DAT_800d452c * 2] + 0xc) & *puVar2;

Let’s check what’s stored in DAT_800bb744:

; entry 0x0 (offset 0x0 * 4 * 2)
800bb744 70 8b 0b 80     addr       PTR_s_Dan_800b8b70
800bb748 ff ff ff ff     ddw        FFFFFFFFh
; entry 0x1 (offset 0x1 * 4 * 2)
800bb74c 58 8d 0b 80     addr       PTR_s_Zombie1_800b8d58
800bb750 ff ff ff ff     ddw        FFFFFFFFh
; ...
; entry 0x7 (offset 0x7 * 4 * 2)
800bb77c 54 94 0b 80     addr       PTR_s_Potion_800b9454
800bb780 ff ff ff ff     ddw        FFFFFFFFh
; ...
; entry 0x10 (offset 0x10 * 4 * 2)
800bb7c4 38 8c 0b 80     addr       PTR_s_DragonDan_800b8c38
800bb7c8 ff ff ff ff     ddw        FFFFFFFFh
; entry 0x11 (offset 0x11 * 4 * 2)
800bb7cc 7c a1 0b 80     addr       PTR_s_Runekey_800ba17c
800bb7d0 ff ff ff ff     ddw        FFFFFFFFh

It’s interesting to see that not only a descriptive label exists for each entry, but the entry offset is calculated using the same value as the object type we saw stored at obj_vars[0xC][0x4] (e.g. the rune key used value 0x11).

Each entry references a structure with size 0xc8. We can set breakpoints at addresses that point to code, or modify some literals to see what happens ingame when we approach an object of the corresponding type. Here’s a snippet near the end of the potion entry:

800b94dc 04 04 04 04     ddw        4040404h
800b94e0 10 10 10 10     ddw        10101010h
800b94e4 04 bc 06 80     addr       near_potion
800b94e8 4c f1 02 80     addr       FUN_8002f14c
800b94ec 00 00 00 00     ddw        0h
800b94f0 00 00 00 00     ddw        0h
800b94f4 00 00 00 00     ddw        0h
800b94f8 28 c5 06 80     addr       LAB_8006c528
800b94fc 00 00 00 00     ddw        0h
800b9500 00 00 00 00     ddw        0h
800b9504 00 00 00 00     ddw        0h
800b9508 b0 c5 06 80     addr       LAB_8006c5b0
800b950c 08 c6 06 80     addr       LAB_8006c608
800b9510 00 00 00 00     ddw        0h
800b9514 14 94 0b 80     addr       QWORD_800b9414                                   = 19h
800b9518 00 00 00 00     ddw        0h

At offset 0x90 there’s near_potion(), called whenever the player is near an object of this type. In the case of the potion, it seems related to how the model gets animated (some potions shake and release fumes). Then we have another table at offset 0xC0, with these entries:

                     QWORD_800b9414
                     XREF[1]:     800b9514(*)
800b9414 19 00 00        dq         19h
         00 00 00
         00 00
800b941c 1d 00 00        dq         1Dh
         00 00 00
         00 00
800b9424 20 00 00        dq         20h
         00 00 00
         00 00
800b942c 1e 00 00        dq         1Eh
         00 00 00
         00 00
; ...

These happen to be the model (MOF) indexes. We get a little help from FrogLord in figuring this out. MOFs are stored in WAD files. In the case of potions, these are part of the first WAD. Here’s the WAD entry for the dragon potion:

Value 25 is 0x19, which is the first entry in the potion MOF index table, which maps to the dragon potion object subtype 0.

See where this is going? Let’s try replacing the rune key’s first MOF index, which is at this address:

                     QWORD_800ba154
                     XREF[1]:     800ba23c(*)
800ba154 08 00 00        dq         8h
         00 00 00
         00 00

If we set it to value 0x19, then approach the rune key:

Or value 0x31, which maps to one of the unused test models:

Or value 0x175, which maps to DragonDan’s unused model:

Of course, it couldn’t be that simple… In fact, even value 0x9, which maps to Dan’s model, just freezes the game when we get close enough to start loading the model:

But we can try something different: let’s replace the reference at entry 0x11 in the object type table with Dan’s entry, setting bytes 0x800bb7cc = 70 8b 0b 80:

Cool, we can even push it around. But if we try with DragonDan’s entry, setting bytes 0x800bb7cc = 38 8c 0b 80, we also freeze the game…

Loading object resources

Let’s look at some of the addresses in DragonDan’s entry structure at 0x800b8c38. These are actually mapped outside of the .text section used by the main executable, which starts at 0x80017c7c:

800b8cc8 18 06 01 80     addr       DAT_80010618
800b8ccc 58 06 01 80     addr       DAT_80010658
800b8cd0 00 00 00 00     ddw        0h
800b8cd4 00 00 00 00     ddw        0h
800b8cd8 00 00 00 00     ddw        0h
800b8cdc 00 08 01 80     addr       DAT_80010800
800b8ce0 00 00 00 00     ddw        0h
800b8ce4 00 00 00 00     ddw        0h
800b8ce8 00 00 00 00     ddw        0h
800b8cec 2c 08 01 80     addr       DAT_8001082c
800b8cf0 34 08 01 80     addr       DAT_80010834

These must be mapped by a specific overlay. Is it during level loading? We can confirm this e.g. in level 8 (The Crystal Caves). It spawns a few enemies, including this one:

At first, I thought it was the FlyDemon entry at 0x800bb934, but replacing it didn’t affect this enemy.

Some entries in the object type table seem like they are “reserved” for levels. Common entries such as potions are followed by 0xffffffff, but others appear to reference a level number:

800bb7f4 14 31 01 80     addr       DAT_80013114
800bb7f8 05 00 00 00     ddw        5h

There are some with value 8, the level we are loading, could one of the entries be for the enemy in question?

Let’s see, we can take a memory dump of the region allocated to the overlay:

Then import in Ghidra with “File > Add to Program…” and these options:

And look at that, there’s an object type structure with our enemy, even the MOF index at 0x80015060 matches the model seen in FrogLord (File 101 = 0x65).

CC::80014fa0 34 00 01 80     addr       s_ScoutDemon_CC__80010034
CC::80014fa4 02 00 be 00     ddw        BE0002h
CC::80014fa8 04 00 96 10     ddw        10960004h
; ...
CC::80015060 65 00 00 00     ddw        65h
CC::80015064 02 00 00 00     ddw        2h

The structure’s address is referenced in this object type table entry:

800bba34 a0 4f 01 80     addr       DAT_80014fa0
800bba38 08 00 00 00     ddw        8h

Great, so we confirmed that these structures relate to overlays. What about DragonDan? Is there any overlay that matches its addresses?

We can check how each overlay gets loaded, by placing a memory write breakpoint at one of the overlay addresses, e.g. 0x80014fa0. We get a hit at 0x8008ed04, and we can follow the reference to the caller function at 0x8004aec4, which has exactly what we need:

void FUN_8004aec4(int param_1) {
  FUN_8004bda4();
  param_1 = param_1 * 0x58;
  FUN_80087db4(*(undefined4 *)(&DAT_800bc540 + param_1));
  FUN_8008817c(*(undefined4 *)(&DAT_800bc540 + param_1));
  FUN_800882fc(*(undefined4 *)(&DAT_800bc540 + param_1));
  if (DAT_800d79d8 != 0) {
    FUN_80087db4();
    FUN_8008817c(DAT_800d79d8);
    DAT_800d79dc = FUN_80088678(DAT_800d79d8);
  }
  FUN_80087db4(*(undefined4 *)(&DAT_800bc53c + param_1));
  FUN_8008817c(*(undefined4 *)(&DAT_800bc53c + param_1));
  FUN_8008ebd4(*(undefined4 *)(&DAT_800bc590 + param_1));
  return;
}

A table of structures at 0x800bc53c is being read in 0x58 sized chunks, here’s the first two entries:

; entry 1
800bc53c 33 00 00 00     ddw        33h
800bc540 32 00 00 00     ddw        32h
; ...
; entry 2
800bc594 3c 00 00 00     ddw        3Ch
800bc598 3b 00 00 00     ddw        3Bh

Now cross-reference these with FrogLord:

That’s right, the first field is the WAD index, the second field is the textures (VLO) index, and the table index matches the level. With these insights, we can define structures and add labels:

void load_level_res(int level) {
  load_map_if();
  read_res(level_res_ARRAY_800bc53c[level].vlo);
  read_res_after(level_res_ARRAY_800bc53c[level].vlo);
  load_vlo_after(level_res_ARRAY_800bc53c[level].vlo);
  if (DAT_800d79d8 != 0) {
    read_res(DAT_800d79d8);
    read_res_after(DAT_800d79d8);
    DAT_800d79dc = FUN_80088678(DAT_800d79d8);
  }
  read_res(level_res_ARRAY_800bc53c[level].wad);
  read_res_after(level_res_ARRAY_800bc53c[level].wad);
  load_overlay(level_res_ARRAY_800bc53c[level].overlay);
  return;
}

Here’s a little script to get the values for all levels:

#!/usr/bin/env python3

import sys
import struct

with open(sys.argv[1], "rb") as f:
    f.seek(0xa50c0)  # mapped at 0x800bc53c
    data = f.read()

p = 0x58
lvl = 0
for i in range(32):
    lvl = i + 1
    if lvl == 32:
        p = 0
    wad = int(struct.unpack("<I", data[p + 0 : p + 4])[0])
    vlo = int(struct.unpack("<I", data[p + 4 : p + 8])[0])
    p += 0x58
    print(f"level {lvl:02d}: vlo={vlo:03d} (0x{vlo:03x}), wad={wad:03d} (0x{wad:03x})")

Output:

level 01: vlo=059 (0x03b), wad=060 (0x03c)
level 02: vlo=085 (0x055), wad=086 (0x056)
level 03: vlo=095 (0x05f), wad=096 (0x060)
level 04: vlo=105 (0x069), wad=106 (0x06a)
level 05: vlo=123 (0x07b), wad=124 (0x07c)
level 06: vlo=144 (0x090), wad=145 (0x091)
level 07: vlo=154 (0x09a), wad=155 (0x09b)
level 08: vlo=167 (0x0a7), wad=168 (0x0a8)
level 09: vlo=191 (0x0bf), wad=192 (0x0c0)
level 10: vlo=213 (0x0d5), wad=214 (0x0d6)
level 11: vlo=222 (0x0de), wad=223 (0x0df)
level 12: vlo=277 (0x115), wad=278 (0x116)
level 13: vlo=288 (0x120), wad=289 (0x121)
level 14: vlo=295 (0x127), wad=296 (0x128)
level 15: vlo=301 (0x12d), wad=302 (0x12e)
level 16: vlo=312 (0x138), wad=313 (0x139)
level 17: vlo=343 (0x157), wad=344 (0x158)
level 18: vlo=349 (0x15d), wad=350 (0x15e)
level 19: vlo=367 (0x16f), wad=368 (0x170)
level 20: vlo=376 (0x178), wad=377 (0x179)
level 21: vlo=398 (0x18e), wad=399 (0x18f)
level 22: vlo=405 (0x195), wad=406 (0x196)
level 23: vlo=415 (0x19f), wad=416 (0x1a0)
level 24: vlo=459 (0x1cb), wad=460 (0x1cc)
level 25: vlo=466 (0x1d2), wad=467 (0x1d3)
level 26: vlo=474 (0x1da), wad=475 (0x1db)
level 27: vlo=480 (0x1e0), wad=481 (0x1e1)
level 28: vlo=503 (0x1f7), wad=504 (0x1f8)
level 29: vlo=520 (0x208), wad=521 (0x209)
level 30: vlo=531 (0x213), wad=532 (0x214)
level 31: vlo=539 (0x21b), wad=540 (0x21c)
level 32: vlo=050 (0x032), wad=051 (0x033)

Option #1: Loading resources via Mednafen’s debugger

Let’s do an experiment on level 19, where we try to load more resources via the debugger.

For a VLO:

  1. Run up to the address after calling load_vlo_after():
  2. Set the program counter back to load_res():
  3. Set the next program counter to the next instruction:
  4. Step up to the delay slot, and set the first parameter to the VLO index:
  5. Run up to the delay slot of read_res_after(), and set the VLO index again:
  6. Run up to the delay slot of load_vlo_after(), and set the VLO index again:

For a WAD:

  1. Run up to the address after the 2nd call to read_res_after():
  2. Repeat steps 2..5 but for this second set of breakpoints: we go back to 0x8004af78 and set the parameter for the 2 calls;

Unfortunately, trying to load a second group of MOFs usually freezes the game, since any new resource overwrites entries from the previously loaded resource. However, the game is more tolerant of mismatching textures, here’s an example in level 19, where we load VLO=0x220 (the one that contains DragonDan’s textures):

Option #2: Loading resources via trampoline

As an alternative to doing this manually in a debugger, we can accomplish the same logic by rolling our own trampoline.

For a VLO:

  1. Patch the 2 instructions after calling load_vlo_after() with a jump to our subroutine (in this case, I picked address 0x801ff000 as a code cave) + a nop for the delay slot;
  2. Check if the VLO being loaded is for level 19 (VLO=0x16f);
  3. If it is, then call the 3 functions for VLO=0x220, otherwise skip these calls;
  4. Run the 2 instructions we patched in the original function;
  5. Jump back to the original function;

We write an assembler script (load_vlo_dragondan.asm), along with a linker script (load_vlo_dragondan.ld), so that each of the 3 called functions is placed in its own subsection, just for starting at the same addresses as in the original executable. These were assembled as follows:

# Install dependencies on a Debian-based Linux distro
sudo apt install binutils-mipsel-linux-gnu

# Assemble executable
mipsel-linux-gnu-as -o load_vlo_dragondan.o load_vlo_dragondan.asm \
    && mipsel-linux-gnu-ld -o linked.o load_vlo_dragondan.o -T load_vlo_dragondan.ld

# Get hex bytes for instructions under subsection `.text:cave`
mipsel-linux-gnu-objdump -D linked.o

Finally, a PatchCommands file (load_vlo_dragondan.patch) was prepared from the objdump output. This file can be applied by pressing “d” in the debugger and inputting the filename.

DragonDan

Here’s the model we will use to replace Dan’s model ingame, along with the required resources:

  • MOF=373 (0x175)
    • VLO=368, WAD=369 (0x170, 0x171): Level 19 (The Gallows Gauntlet), textures missing
    • VLO=544, WAD=545 (0x220, 0x221): Unused, textures included

We already saw how to load VLO=544. What about the MOF? Recall when we were replacing items, we couldn’t just set this value without crashing the game, and the same happens here if we just replace Dan’s MOF index.

At this point, I went back to the object type structure. There’s quite a few fields there that happen to be zeroed for some objects. Furthermore, some don’t have any animations at all (e.g. torch holder). The more fields we have defined, the more constrained our model needs to be to avoid crashes. What if we reduce Dan’s entry to the minimal necessary to just load a model without animation sequences?

If we remove something we need, the game either freezes or gives us a blank screen, so that’s our boolean test. After some experimentation, this is what I ended up with:

--- obj_type_dan
+++ obj_type_dan.zeroed
@@ -1,50 +1,50 @@
 800b8b70 f8 d7 0c 80     addr       s_Dan_800cd7f8
 800b8b74 01 00 7e 00     ddw        7E0001h
 800b8b78 04 02 96 00     ddw        960204h
 800b8b7c 16 00 00 00     ddw        16h
 800b8b80 40 00 00 00     ddw        40h
-800b8b84 04 11 00 00     ddw        1104h
-800b8b88 20 00 00 00     ddw        20h
+800b8b84 00 00 00 00     ddw        0h
+800b8b88 00 00 00 00     ddw        0h
 800b8b8c 88 13 00 00     ddw        1388h
 800b8b90 00 01 00 00     ddw        100h
 800b8b94 90 01 00 00     ddw        190h
 800b8b98 64 00 00 00     ddw        64h
 800b8b9c 00 01 c0 00     ddw        C00100h
 800b8ba0 80 00 00 00     ddw        80h
 800b8ba4 00 1c 00 00     ddw        1C00h
 800b8ba8 7d 00 00 00     ddw        7Dh
 800b8bac 00 08 00 00     ddw        800h
-800b8bb0 00 00 2c 00     ddw        2C0000h
-800b8bb4 00 01 00 00     ddw        100h
-800b8bb8 00 00 02 00     ddw        20000h
-800b8bbc 44 8b 0b 80     addr       DAT_800b8b44
+800b8bb0 00 00 00 00     ddw        0h
+800b8bb4 00 00 00 00     ddw        0h
+800b8bb8 00 00 00 00     ddw        0h
+800b8bbc 00 00 00 00     ddw        0h
 800b8bc0 00 00 00 00     ddw        0h
 800b8bc4 3c f8 0b 80     addr       PTR_DAT_800bf83c
 800b8bc8 32 00 28 00     ddw        280032h
 800b8bcc 24 f4 00 00     ddw        F424h
 800b8bd0 c0 00 00 00     ddw        C0h
 800b8bd4 ff 00 00 00     ddw        FFh
 800b8bd8 00 00 00 00     ddw        0h
 800b8bdc 00 00 00 00     ddw        0h
 800b8be0 00 00 00 00     ddw        0h
 800b8be4 00 00 00 00     ddw        0h
 800b8be8 00 00 00 00     ddw        0h
 800b8bec 00 00 00 00     ddw        0h
 800b8bf0 00 00 00 00     ddw        0h
 800b8bf4 00 00 00 00     ddw        0h
 800b8bf8 04 04 04 04     ddw        4040404h
 800b8bfc 10 10 10 10     ddw        10101010h
 800b8c00 64 e6 05 80     addr       LAB_8005e664
-800b8c04 6c e7 05 80     addr       LAB_8005e76c
-800b8c08 2c ec 05 80     addr       LAB_8005ec2c
-800b8c0c 78 f2 02 80     addr       LAB_8002f278
-800b8c10 3c f1 05 80     addr       LAB_8005f13c
-800b8c14 40 f2 05 80     addr       LAB_8005f240
+800b8c04 f4 fb 05 80     addr       LAB_8005fbf4
+800b8c08 00 00 00 00     ddw        0h
+800b8c0c 00 00 00 00     ddw        0h
+800b8c10 00 00 00 00     ddw        0h
+800b8c14 00 00 00 00     ddw        0h
 800b8c18 00 00 00 00     ddw        0h
-800b8c1c fc fb 05 80     addr       LAB_8005fbfc
-800b8c20 6c f2 05 80     addr       LAB_8005f26c
-800b8c24 74 f2 05 80     addr       LAB_8005f274
-800b8c28 d0 f2 05 80     addr       LAB_8005f2d0
+800b8c1c 00 00 00 00     ddw        0h
+800b8c20 00 00 00 00     ddw        0h
+800b8c24 00 00 00 00     ddw        0h
+800b8c28 00 00 00 00     ddw        0h
 800b8c2c 00 00 00 00     ddw        0h
 800b8c30 09 00 00 00     ddw        9h
-800b8c34 5c 8b 0b 80     addr       DAT_800b8b5c
+800b8c34 00 00 00 00     ddw        0h

Where 0x8005fbf4 is a no-op function:

8005fbf4 08 00 e0 03     jr         ra
8005fbf8 00 00 00 00     _nop

Also, all entries of the animation table at 0x800bf83c are patched to point at address 0x800b8c1c, which is just an address with null bytes.

A PatchCommands file (obj_type_dan.patch) includes all these changes.

Note that we have to exit the current level before applying the patch, otherwise we get glitchy animations. Afterwards, we set Dan’s MOF index to DragonDan’s (800b8c30 = 75 01 00 00), then load the level again…

We’re getting somewhere! Let’s also load the VLO, which needs to be loaded after the level’s VLO, to avoid overwritting texture indexes for DragonDan.

And a view without the glitched background:

Finally, load_dragondan.patch contains the MOF index update in addition to the changes in load_vlo_dragondan.patch. Use it along with obj_type_dan.patch to get this final result.


Now to actually answer the question: is there an overlay for DragonDan?

Well, it’s not “The Gallows Gauntlet”. If we add its overlay to Ghidra, and take some of the addresses in DragonDan’s object type entry, they all land in the middle of basic blocks (e.g. 0x80010618), or worse, in delay slots (e.g. 0x80010658):

GG::80010610 01 04 02 24     li         v0,0x401
GG::80010614 10 00 a2 af     sw         v0,0x10(sp)
GG::80010618 78 00 65 8c     lw         a1,0x78(v1)
GG::8001061c ac e7 00 0c     jal        FUN_80039eb0
GG::80010620 1c 00 e7 24     _addiu     a3,a3,0x1c
; ...
GG::80010654 4f da 00 0c     jal        FUN_8003693c
GG::80010658 ff ff 05 24     _li        a1,-0x1

Ok, and the other overlays? Here’s a shell script to enumerate those that even have code at these addresses, using radare2’s command line tool rasm2 to get a quick disassembly:

for i in ./overlays/*; do
    echo "$i"
    xxd -s $((0x618)) -l 24 -p "$i" \
        | xargs -I{} rasm2 -a mips -b 32 -d {}
done

There are a few results, but they seem to also hit the middle of basic blocks. Besides, none of them either load textures or models for DragonDan, so it would be hard to try out without crashes. Finally, another oddity in the object type entry: the MOF index is the same as Dan’s:

800b8cf8 09 00 00 00     ddw        9h

This suggests an earlier overlay that didn’t get included in this build, which used a WAD where the model mapped to this index.

Jabberwocky

  • MOF=411 (0x19b)
    • VLO=405, WAD=406 (0x195, 0x196): Level 22 (The Silver Wood), empty overlay

All we need is loaded in this level, we just have to update Dan’s MOF index. Here’s the dragon, as seen on TV:

Morten the Worm

  • MOF=562 (0x232)
    • VLO=459, WAD=460 (0x1cb, 0x1cc): Level 24 (The Entrance Hall), empty overlay, textures included, model missing
    • VLO=560, WAD=561 (0x230, 0x231): Unused, textures missing, model included

Luckly, there’s only one model that is included in this level’s WAD, and loading an extra WAD doesn’t crash the game:

Similar to the VLO trampoline but done for the WAD, we write an assembler script (load_wad_morten.asm), along with a linker script (load_wad_morten.ld), resulting in a PatchCommands file (load_morten.patch) that also includes updating Dan’s MOF index.

AngelDan / DevilDan

  • MOF=472, MOF=473 (0x1d8, 0x1d9)
    • VLO=466, WAD=467 (0x1d2, 0x1d3): Level 25 (The Halls Of Illusion), empty overlay

All we need is loaded in this level, we just have to update Dan’s MOF index:

TODO

  • Apply model loading to other MediEvil builds (textures for the unused models shown here also appear in the Rolling Demo, and there’s an unused potion model that appears in the Timed Demo);
  • Try to load models with some animation logic;
  • Figure out how to place new objects in the map, to see e.g. how some enemies interact with the player. Might be worth tracing the chest opening logic and placing memory write breakpoints on the position (obj_vars[0x24..0x2c]);
  • Improve MediEvil support in FrogLord;

TL;DR

Here are some GameShark codes to try out these findings. These are quite lengthy to input manually, so here’s a workaround to add them to Mednafen:

  1. Add some test cheat with “Alt+c”;
  2. Exit Mednafen;
  3. Generate cheat entries via shell script:
     for i in obj_type_dan.cheat load_dragondan.cheat; do
         < "$i" awk '{
             print "R A 2 L 0 " $0 " '$i'" i "\n";
             i++;
         }' >> ~/.mednafen/cheats/psx.cht
     done
    
  4. Remove the test cheat entry while keeping the added ones;
  5. Reopen Mednafen;
  6. Wait for the PS1 logo sequence to finish;
  7. Activate cheats with “Alt+t”;

Replace chests with potions

This cheat overwrites the object type entry and sets a trampoline to overwrite the subtype in near_potion().

  • chest_as_potion.cheat
      800bb764 9454
      800b9514 00XX
      800b9516 0000
      8006bd24 fc40
      8006bd26 0807
      801ff100 00YY
      801ff102 3403
      801ff104 0006
      801ff106 a043
      801ff108 af4b
      801ff10a 0801
      801ff10c 0000
      801ff10e 0000
      801ff110 0000
      801ff112 0000
    

Replace XX and YY with one of the following values:

XX = MOF Index YY = Subtype Description
0x19 0x0 Dragon (No effect)
0x1d 0x1 Twisted (Spawns serpent)
0x20 0x2 Skull (Power-up)
0x1e 0x3 Red (No effect)
0x1a 0x4 Green (Dan grows)
0x1b 0x5 Transparent (No effect)
0x1f 0x6 Purple (Enemies shrink)
0x1c 0x7 Yellow (No effect)

Reduce Dan’s Object Entry

  • obj_type_dan.cheat
      800b8b84 0000
      800b8b86 0000
      800b8b88 0000
      800b8b8a 0000
      800b8bb8 0000
      800b8bba 0000
      800b8bbc 0000
      800b8bbe 0000
      800b8c04 fbf4
      800b8c06 8005
      800b8c08 0000
      800b8c0a 0000
      800b8c0c 0000
      800b8c0e 0000
      800b8c10 0000
      800b8c12 0000
      800b8c14 0000
      800b8c16 0000
      800b8c1c 0000
      800b8c1e 0000
      800b8c20 0000
      800b8c22 0000
      800b8c24 0000
      800b8c26 0000
      800b8c28 0000
      800b8c2a 0000
      800b8c34 0000
      800b8c36 0000
      800bf83c 8c1c
      800bf83e 800b
      800bf840 8c1c
      800bf842 800b
      800bf844 8c1c
      800bf846 800b
      800bf848 8c1c
      800bf84a 800b
      800bf84c 8c1c
      800bf84e 800b
      800bf850 8c1c
      800bf852 800b
      800bf854 8c1c
      800bf856 800b
      800bf858 8c1c
      800bf85a 800b
      800bf85c 8c1c
      800bf85e 800b
      800bf860 8c1c
      800bf862 800b
      800bf864 8c1c
      800bf866 800b
      800bf868 8c1c
      800bf86a 800b
      800bf86c 8c1c
      800bf86e 800b
      800bf870 8c1c
      800bf872 800b
      800bf874 8c1c
      800bf876 800b
      800bf878 8c1c
      800bf87a 800b
      800bf87c 8c1c
      800bf87e 800b
      800bf880 8c1c
      800bf882 800b
      800bf884 8c1c
      800bf886 800b
      800bf888 8c1c
      800bf88a 800b
      800bf88c 8c1c
      800bf88e 800b
      800bf890 8c1c
      800bf892 800b
      800bf894 8c1c
      800bf896 800b
    

DragonDan in Level 19

Requires obj_type_dan.cheat

  • load_dragondan.cheat
      800b8c30 0175
      800b8c32 0000
      8004af2c fc00
      8004af2e 0807
      8004af30 0000
      8004af32 0000
      801ff000 800c
      801ff002 3c01
      801ff004 0821
      801ff006 0030
      801ff008 c540
      801ff00a 8c31
      801ff00c 016f
      801ff00e 3404
      801ff010 000a
      801ff012 1491
      801ff014 0000
      801ff016 0000
      801ff018 0220
      801ff01a 3404
      801ff01c 1f6d
      801ff01e 0c02
      801ff020 0000
      801ff022 0000
      801ff024 0220
      801ff026 3404
      801ff028 205f
      801ff02a 0c02
      801ff02c 0000
      801ff02e 0000
      801ff030 0220
      801ff032 3404
      801ff034 20bf
      801ff036 0c02
      801ff038 0000
      801ff03a 0000
      801ff03c 800d
      801ff03e 3c11
      801ff040 79d8
      801ff042 2631
      801ff044 2bcd
      801ff046 0801
      801ff048 0000
      801ff04a 0000
      801ff04c 0000
      801ff04e 0000
    

Jabberwocky in Level 22

Requires obj_type_dan.cheat

  • Jabberwocky MOF
      800b8c30 019b
      800b8c32 0000
    

Morten the Worm in Level 24

Requires obj_type_dan.cheat

  • load_morten.cheat
      800b8c30 0232
      800b8c32 0000
      8004af94 fc80
      8004af96 0807
      8004af98 0000
      8004af9a 0000
      801ff200 800c
      801ff202 3c01
      801ff204 0821
      801ff206 0030
      801ff208 c53c
      801ff20a 8c31
      801ff20c 01cc
      801ff20e 3404
      801ff210 0007
      801ff212 1491
      801ff214 0000
      801ff216 0000
      801ff218 0231
      801ff21a 3404
      801ff21c 1f6d
      801ff21e 0c02
      801ff220 0000
      801ff222 0000
      801ff224 0231
      801ff226 3404
      801ff228 205f
      801ff22a 0c02
      801ff22c 0000
      801ff22e 0000
      801ff230 800c
      801ff232 3c01
      801ff234 0821
      801ff236 0030
      801ff238 2be7
      801ff23a 0801
      801ff23c 0000
      801ff23e 0000
      801ff240 0000
      801ff242 0000
    

AngelDan / DevilDan in Level 25

Requires obj_type_dan.cheat

  • AngelDan MOF
      800b8c30 01d8
      800b8c32 0000
    
  • DevilDan MOF
      800b8c30 01d9
      800b8c32 0000