Resurrecting unused remnants
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.
- Take a save state;
- Place a breakpoint at
0x80038f88
, then pickup the key, stopping at that address: - Open memory view, go to
obj_vars
address (0x800cdfb0
): - Dereference
obj_vars
: - Derefence offset
0xC
: - 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:
- Run up to the address after calling
load_vlo_after()
: - Set the program counter back to
load_res()
: - Set the next program counter to the next instruction:
- Step up to the delay slot, and set the first parameter to the VLO index:
- Run up to the delay slot of
read_res_after()
, and set the VLO index again: - Run up to the delay slot of
load_vlo_after()
, and set the VLO index again:
For a WAD:
- Run up to the address after the 2nd call to
read_res_after()
: - 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:
- Patch the 2 instructions after calling
load_vlo_after()
with a jump to our subroutine (in this case, I picked address0x801ff000
as a code cave) + anop
for the delay slot; - Check if the VLO being loaded is for level 19 (
VLO=0x16f
); - If it is, then call the 3 functions for
VLO=0x220
, otherwise skip these calls; - Run the 2 instructions we patched in the original function;
- 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 missingVLO=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 missingVLO=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:
- Add some test cheat with “Alt+c”;
- Exit Mednafen;
- 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
- Remove the test cheat entry while keeping the added ones;
- Reopen Mednafen;
- Wait for the PS1 logo sequence to finish;
- 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