Modding Wii Sports : Part I : Identifying files and creating a debug output
2020-04-21 16:00 +0200A few months ago I saw someone playing Wii Sports doing some Golf. This reminded me I always wanted to create custom golf tracks. After a little bit of search, I found out that nobody really did it. Some people were asking if someone did it and they were a few attempts on Wii Sport Resort (here, here or here) but I found no real public source code or walkthrough of how to do your own custom golf track on the original Wii Sports.
After struggling for multiples weeks now I will show you my current (slow) progress and I hope I will be able to continue this series of blog posts up until a complete usable mod. The best would be an easy to use tool that allow a conversion of any 3D models into a golf course and a user interface on the Wii that allow loading custom tracks from the SD card. For the moment I’m not skilled enough nor I have enough time but maybe writing blog posts will encourage me to continue…
I - Identifying existing files
The first easy step was to rip the original disc. I own an original Wii Sports European disc, it is the second revision that have some bug patched. I used USBLoaderGX, it’s a backup loader that allow to copy discs to an USB drive. It produces a WBFS file, it is a custom file format that only contain useful part of the ISO, by removing all the padding an image can shrink from 4GiB to a few hundreds of MiB (it, of course, depends of the game).
To extract and rebuild WBFS images I used the Wiimms ISO Tools suite.
$ # We can easily extract the content of the original image
$ wit X RSPP01.wbfs RSPP01/
***** wit: Wiimms ISO Tool v3.02a r0 x86_64 - Dirk Clemens - 2020-03-07 *****
wit: EXTRACT 1/1 WBFS:RSPP01.wbfs/#0 -> RSPP01/
$ # And rebuild the modded one after some work
$ wit CP RSPP01.modded/ RSPP01.modded.wbfs
***** wit: Wiimms ISO Tool v3.02a r0 x86_64 - Dirk Clemens - 2020-03-07 *****
* COPY/SCRUB 1/1 FST:RSPP01.modded/ -> WBFS:RSPP01.modded.wbfs
After a little bit of search, we can identify two important things:
- The
sys/main.dol
file, it is the main game binary in the DOL format (the executable format for the Wii and the GameCube) - The
files/Stage/RPGolScene/
folder, it contains a file per golf track. The name of most of the file is in the formglf_course_fcX.carc
wherefc
is for Family Computer or Famicom (the Japanese version of the NES) and the number identify the number of the track it corresponds to in the 1984 Golf NES game.
total 17M
1,8M glf_course_E3.carc
258K glf_course_angle.carc
1,6M glf_course_fc1.carc
1,3M glf_course_fc11.carc
1,1M glf_course_fc12.carc
1,8M glf_course_fc13.carc
1,3M glf_course_fc14.carc
1,8M glf_course_fc16.carc
367K glf_course_fc18.carc
1,1M glf_course_fc3.carc
1,6M glf_course_fc5.carc
1,9M glf_course_fc8.carc
1,3M glf_course_fc9.carc
212K glf_course_survey.carc
These carc
files are in fact Yaz0 compressed U8 archives. Another Wiimms tool suite can be used to extract these files: the Wiimms SZS Toolset.
Here is the content of glf_course_fc1.carc
:
$ wszst LL glf_course_fc1.carc
* Files of YAZ0.U8:glf_course_fc1.carc
size/dec magic file or directory
-------------------------------------------------------------------------------
687202 ...< glf_course_fc1.kcl
10600 PMPF glf_course_fc1.pmp
- - G3D/
999424 bres G3D/glf_course_fc1.brres
179712 bres G3D/glf_map_fc1.brres
- - glf_scene_fc1/
164 PBLM glf_scene_fc1/glf_scene_fc1.pblm
1352 LGHT glf_scene_fc1/glf_scene_fc1.plight
408 LMAP glf_scene_fc1/glf_scene_fc1.plmap
1 - The glf_course_fc1.kcl
file
The kcl
file format is the same used in the Mario Kart Wii game to describe the collision of a the track. We can suppose this one also describe the collision of the golf track. Using wkclt
from the Wiimms SZS Toolset, we can convert the kcl
into a simple Wavefront obj
file
$ wkclt DEC glf_course_fc1.kcl
DECODE KCL:glf_course_fc1.kcl -> KCLTXT:./glf_course_fc1.obj
* CHECK KCL:glf_course_fc1.kcl
- HINT: 1 of 8398 drivable triangles is face down => --kcl=RM-FACEDOWN
- HINT: 1 of 8398 drivable triangles is face down (>30°).
=> No warnings and 2 hints for KCL:glf_course_fc1.kcl
=> see https://szs.wiimm.de/cmd/wkclt/check#desc for more info.
$ ll glf_course_fc1.*
-rw-r--r-- 1 redoste redoste 672K 1970-01-01 00:00 glf_course_fc1.kcl
-rw-r--r-- 1 redoste redoste 19K 1970-01-01 00:00 glf_course_fc1.mtl
-rw-r--r-- 1 redoste redoste 911K 1970-01-01 00:00 glf_course_fc1.obj
And of course the opposite is possible
$ wkclt ENC glf_course_fc1.obj
ENCODE KCLTXT:glf_course_fc1.obj -> KCL:./glf_course_fc1.kcl
* CHECK KCLTXT:glf_course_fc1.obj
- HINT: 1 of 8398 drivable triangles is face down => --kcl=RM-FACEDOWN
- HINT: 1 of 8398 drivable triangles is face down (>30°).
=> No warnings and 2 hints for KCLTXT:glf_course_fc1.obj
=> see https://szs.wiimm.de/cmd/wkclt/check#desc for more info.
- create octree: rshift=10, n_bcube=256, cube_size=512..1048576, blow=400, max_tri=30, max_depth=10, fast=0
Here is what glf_course_fc1.obj
looks like imported into Blender :
Since wkclt
have been thought for Mario Kart Wii the objects are not correctly named but they correspond to the different kind of ground available in the game (Green, Bunker, etc.) :
2 - The G3D/*.brres
files
The brres
files are some sort of archives that describe a 3D model. This archive is split in sections each one represents a specific part of the object (Model, Texture, Animations…). Since brres
files are common to Mario Kart Wii and Super Smash Bros. Brawl, we can use the BrawlBox tool.
BrawlBox is a huge Windows tool that allow easy manipulation of brres
archives and its different sections. Because I use GNU/Linux I had to do a little bit of tinkering to run BrawlBox with Wine. Installing dotnet48
using winetricks
seems to do the job.
Here is what G3D/glf_course_fc1.brres
looks like in BrawlBox :
The other brres
file: G3D/glf_map_fc1.brres
corresponds to the minimap visible in game in the bottom left corner. The map in it self is at the exact same scale as the original, it is only scaled down at the final rendering, making the creation of the map from the original course really easy.
Here is what G3D/glf_map_fc1.brres
looks like in BrawlBox :
3 - The glf_scene_fc1/*.p*
files
These three files seem to be used to polish the rendering of the map, but I was able to identify only one of them. The plight
file seems to match the BLIGHT format since its magic number is the same (LGHT
). However leaving the folder empty seems to do the trick since the map loads without any problem.
4 - The glf_course_fc1.pmp
file
I was unable to clearly identify the format of the pmp
file but I think it contains things such as the starting point, the ending point of the course and the position of trees. Its format should be similar to the KMP format of Mario Kart Wii since it is its purpose.
5 - Demo
The first easy demo I can do is making the map flat. For this I converted the KCL file to an OBJ file and set the Y value of every vertices to 0.
Then I used the scripting feature of BrawlBox to export every objects vertices from the model. The script is based on the builtin one made to export textures.
# Script to export or import objects vertices from brres files
from BrawlBox.API import bboxapi
from BrawlLib.SSBB.ResourceNodes import *
def search(node):
if isinstance(node, MDL0VertexNode):
return [node]
list = []
for child in node.Children:
list += search(child)
return list
if bboxapi.RootNode != None:
root = bboxapi.RootNode
for item in search(root):
print item.Name
# Use Replace to import and Export to export
item.Export("C:\\inp\\vec\\" + item.Name + ".vec")
#item.Replace("C:\\inp\\vec\\" + item.Name + ".vec")
print("Done!")
else:
bboxapi.ShowMessage('Cannot find Root Node (is a file open?)','Error')
Then I made a (extremely ugly and unreadable) Python script to flatten the object before reimporting them to the brres
.
import struct
import sys
wo_offset = 0
def wo(b):
global wo_offset
wo_offset += len(b)
sys.stderr.buffer.write(b)
inf = open(sys.argv[1], "rb")
file_length = struct.unpack(">I", inf.read(4))[0]
mdl0_offset = struct.unpack(">I", inf.read(4))[0]
data_offset = struct.unpack(">I", inf.read(4))[0]
name_offset = struct.unpack(">I", inf.read(4))[0]
index = struct.unpack(">I", inf.read(4))[0]
comp_count = struct.unpack(">I", inf.read(4))[0]
vec_format = struct.unpack(">I", inf.read(4))[0]
divisor = struct.unpack(">B", inf.read(1))[0]
stride = struct.unpack(">B", inf.read(1))[0]
n_vec = struct.unpack(">H", inf.read(2))[0]
min_x, min_y, min_z = struct.unpack(">fff", inf.read(12))
max_x, max_y, max_z = struct.unpack(">fff", inf.read(12))
# Check and write the header
wo(struct.pack(">I", file_length))
wo(struct.pack(">I", mdl0_offset))
wo(struct.pack(">I", data_offset))
wo(struct.pack(">I", name_offset))
wo(struct.pack(">I", index))
if comp_count != 0x1:
print("comp_count != 1")
sys.exit(1)
wo(struct.pack(">I", comp_count))
if vec_format != 0x4:
print("vec_format != 4")
sys.exit(1)
wo(struct.pack(">I", vec_format))
if divisor != 0:
print("divisor != 0")
sys.exit(1)
wo(struct.pack(">B", divisor))
if stride != 0xc:
print("stride != 0xc")
sys.exit(1)
wo(struct.pack(">B", stride))
print("n_vec = {}".format(n_vec))
wo(struct.pack(">H", n_vec))
print("min = {},{},{}".format(min_x, min_y, min_z))
print("max = {},{},{}".format(max_x, max_y, max_z))
min_y = 0
max_y = 0
print("min = {},{},{}".format(min_x, min_y, min_z))
print("max = {},{},{}".format(max_x, max_y, max_z))
wo(struct.pack(">fff", min_x, min_y, min_z))
wo(struct.pack(">fff", max_x, max_y, max_z))
for _ in range(8):
wo(b"\x00")
inf.seek(data_offset)
for n in range(n_vec):
x, y, z = struct.unpack(">fff", inf.read(12))
print("n = {} : {},{},{}".format(n, x, y, z))
y = 0
wo(struct.pack(">fff", x, y, z))
for _ in range(file_length - wo_offset):
wo(b"\x00")
sys.stderr.buffer.flush()
After packing everything back up, we can rebuild the game image and admire this amazing flat golf course with flying trees, starting point and ending point !
II - Reverse-engineering the binary
The main game binary is in the DOL format, it’s a pretty simple format and was able to open it in Ghidra pretty easily. I’m far from being skilled enough to completely reverse-engineer the binary but using simple string searches and X-refs I was able to identify important functions : reading files, reading archives, loading maps and I think I even identified the one responsible of parsing the unknown pmp
file.
Here is the list of function identified (for the second European version I own : sha1sum main.dol : 0328a87d999995f95592f91c8d948d9995bb06bd
)
crash
:0x8010ab58
get_lang_code
:0x80186410
golf_get_fc_string
:0x8029db44
golf_load_kcl_pmp
:0x80293d5c
golf_load_stage_common_carc
:0x8028eb84
golf_process_kcl?
:0x802a7414
golf_process_pmp?
:0x801bf824
golf_process_pmp?2
:0x801bf890
heap_alloc
:0x800a2e38
heap_alloc_wraper
:0x800a3250
load_from_carc
:0x80187a44
load_from_carc_in_filelist
:0x8028eb68
load_locales
:0x801877d0
print_serial
:0x801840dc
sprintf
:0x802aaf00
strcat
:0x800b8e40
What made the process really hard and sometimes impossible for me is that I don’t know a lot PowerPC assembly so I generally blindly trusted Ghidra decompiler and only looked at the manual when required but most importantly this is C++ code, so we have to deal with all the C++ annoying stuff. To make this thing even more hard, Nintendo should use some weird custom compiler because it uses r13
to store the this
pointer instead of using the first function argument like any other compilers but most importantly r13
point to the end of the structure ! Ghidra doesn’t seem to support looking at structure from the end and having to subtract offsets from the pointer so it just decompiles it to unreadable garbage pointer arithmetic.
Here is my favorite one (from golf_get_fc_string
) :
return (&PTR_s_fc1_803e1fe0)[*(int *)(*(int *)((int)local_r13_-1 + -0x5abc) + 0x98) * 9];
Edit : 2020-04-22 23:20 +0200 : u/Leseratte10 mentioned on reddit that r13
is used for the Small Data Area. Because PowerPC is a RISC architecture there is a really small number of instructions, something as simple as accessing a global variable can take 2 instructions. To compensate, the compiler put all frequently accessed globals in this Small Data Area (here it is 64KiB large) and makes r13
constant by initialising it in the entry point function. Now globals in the Small Data Area can be accessed with only one instruction.
This problem was already discussed in a Ghidra Github issue. After installing a custom language definition for the Gekko and Broadway CPUs and reanalysing the whole binary, the r13
register is now considered constant. Using the Register Manager of Ghidra, we can set the value of r13
(here it is 0x804df900
) and now decompilation makes way more sense.
Here is the previous snippet of golf_get_fc_string
correctly decompiled :
return (&PTR_DAT_803e1fe0)[*(int *)(DAT_804d9e44 + 0x98) * 9];
The this
pointer is correctly passed as the first argument of functions (via r3
).
To finish this part on a positive note, some of the code is shared with Mario Kart Wii (yes, again) so here is this amazing decompilation project of Mario Kart Wii by riidefi that helped me a lot : https://github.com/riidefi/MKWDecompilation
III - Adding a custom debug output
While working on custom maps, the game crashed, a lot. So to understand why it crashed I generally enabled the Dolphin debugger and followed the backtrace, looking at what functions it corresponds in Ghidra. A lot of this crashes where due to failed assert
s and the assert
s called print_serial
before calling crash
. This print_serial
just seems to backup some registers to locals before returning. I think they removed the debug output in the final release.
print_serial
stwu r1,local_70(r1)
bne cr1,LAB_80184104
stfd f1,local_48(r1)
stfd f2,local_40(r1)
stfd f3,local_38(r1)
stfd f4,local_30(r1)
stfd f5,local_28(r1)
stfd f6,local_20(r1)
stfd f7,local_18(r1)
stfd f8,local_10(r1)
LAB_80184104
stw r3,local_68(r1)
stw r4,local_64(r1)
stw r5,local_60(r1)
stw r6,local_5c(r1)
stw r7,local_58(r1)
stw r8,local_54(r1)
stw r9,local_50(r1)
stw r10,local_4c(r1)
addi r1,r1,0x70
blr
To get this debug output working I didn’t want to patch the binary since I don’t know how to easily output the strings so I just modified the code of the emulator instead !
Since Dolphin is open source, it was really easy. I edited the code of the branch instruction to print strings when the destination address is the one of print_serial
. Because print_serial
should behave like printf
and that the memory of the emulated console is only available via functions emulating the memory bus, the easiest thing to do was to create a simple and incomplete printf
implementation.
// In Source/Core/Core/PowerPC/Interpreter/Interpreter_Branch.cpp
void Interpreter::bx(UGeckoInstruction inst)
{
if (inst.LK)
LR = PC + 4;
if (inst.AA)
NPC = SignExt26(inst.LI << 2);
else
NPC = PC + SignExt26(inst.LI << 2);
// Here is my incomplete ugly printf implementation
if (NPC == 0x801840dc) {
uint32_t gpr3 = PowerPC::ppcState.gpr[3];
int r = 4;
char c, t;
do {
c = PowerPC::Read_U8(gpr3);
gpr3++;
if(c != '%'){
putc(c, stdout);
continue;
}
t = PowerPC::Read_U8(gpr3);
gpr3++;
if(t == 's') {
uint32_t ptr = PowerPC::ppcState.gpr[r];
r++;
char cBis;
do {
cBis = PowerPC::Read_U8(ptr);
ptr++;
putc(cBis, stdout);
} while (cBis != 0);
}
else if (t == '%'){
putc('%', stdout);
}
else if (t == '0'){
// Padded format strings : "%08x"
char formatStr[] = {'%', t, PowerPC::Read_U8(gpr3), PowerPC::Read_U8(gpr3+1)};
gpr3 += 2;
printf(formatStr, PowerPC::ppcState.gpr[r]);
r++;
}
else {
char formatStr[] = {'%', t};
printf(formatStr, PowerPC::ppcState.gpr[r]);
r++;
}
} while(c != 0);
}
m_end_block = true;
}
Because I edited the PowerPC interpreter, I had to disable the JIT but the Wii is a pretty modern console, my 7-year-old Intel CPU was pretty slow while trying to interpret the 729 Mhz PowerPC CPU of the Wii. It was unusable. I was not confident while trying to understand the JIT code so I just added a line to disable JIT on branch instruction to print_serial
:
// In Source/Core/Core/PowerPC/Jit64/Jit_Branch.cpp
void Jit64::bx(UGeckoInstruction inst)
{
//...
FALLBACK_IF(js.op->branchTo == 0x801840dc);
//...
}
There are some slowdowns when the game tries to print a lot of stuff but at least it works !
<< RVL_SDK - EXI release build: Nov 30 2006 03:26:56 (0x4199_60831) >>
<< RVL_SDK - SI release build: Nov 30 2006 03:31:44 (0x4199_60831) >>
Revolution OS
Kernel built : Apr 24 2007 11:50:47
Console Type : NDEV 2.1
Firmware : 21.4.15 (3/3/2010)
Memory 88 MB
MEM1 Arena : 0x804f0fa0 - 0x817fcda0
MEM2 Arena : 0x90000800 - 0x933e0000
<< RVL_SDK - OS release build: Apr 24 2007 11:50:47 (0x4199_60831) >>
<< RVL_SDK - SC release build: Nov 30 2006 03:33:00 (0x4199_60831) >>
<< RVL_SDK - NAND release build: Nov 30 2006 03:32:57 (0x4199_60831) >>
<< RVL_SDK - NWC24 release build: May 10 2007 17:58:59 (0x4199_60831) >>
<< RVL_SDK - DVD release build: Apr 24 2007 11:44:29 (0x4199_60831) >>
<< NW4R - EF final build: Jun 8 2007 11:16:29 (0x4199_60831) >>
<< RVL_SDK - GX release build: Nov 30 2006 03:30:39 (0x4199_60831) >>
<< RVL_SDK - VI release build: Nov 30 2006 03:31:49 (0x4199_60831) >>
<< RVL_SDK - WPAD release build: May 17 2007 01:52:03 (0x4199_60831) >>
<< RVL_SDK - KPAD release build: Jun 5 2007 11:27:45 (0x4199_60831) >>
<< NW4R - G3D final build: Jun 8 2007 11:16:25 (0x4199_60831) >>
<< NW4R - LYT final build: Jun 8 2007 11:17:26 (0x4199_60831) >>
<< RVL_SDK - AI release build: Nov 30 2006 03:26:11 (0x4199_60831) >>
<< RVL_SDK - AX release build: Dec 18 2006 15:43:48 (0x4199_60831) >>
<< RVL_SDK - DSP release build: Nov 30 2006 03:26:46 (0x4199_60831) >>
<< NW4R - SND final build: Jun 8 2007 11:17:15 (0x4199_60831) >>
<< RVL_SDK - RFL release build: Jun 9 2007 17:25:33 (0x4199_60831) >>
eggAudioArcPlayerMgr:Sound Archive is already opened
IV - Conclusion
This blog post summarize how far I have been able to mod Wii Sports, I hope it will be useful to someone else but a least it useful for me to note my progress and maybe, one day, later, try to do something more complete.