-
Notifications
You must be signed in to change notification settings - Fork 0
/
patchgame.py
163 lines (135 loc) · 5.44 KB
/
patchgame.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
#!/usr/bin/env python3
import subprocess
import sys
import os
import argparse
import shutil
import shlex
import pathlib
import lzdec
import patcher
def run(*cmd, workdir='.', stdout=None):
print("> " + shlex.join(cmd))
subprocess.run(cmd, cwd=workdir, stdout=stdout, check=True)
def get_program(f, path):
if path is None: return f
else: return os.path.join(path, f)
def assemble(asm_file, devkitppc_path=None):
base, ext = os.path.splitext(asm_file)
wd = 'asm'
run(get_program('powerpc-eabi-as', devkitppc_path),
'-mbroadway', '-a', asm_file, '-o', base + '.o', workdir=wd)
run(get_program('powerpc-eabi-ld', devkitppc_path),
'-T', 'ldscript.ld', base + '.o', '-o', base + '.elf', workdir=wd)
run(get_program('powerpc-eabi-objcopy', devkitppc_path),
'--dump-section', '.text=%s.bin' % base, base + '.elf', workdir=wd)
with open(os.path.join(wd, base + '.syms'), 'w') as f:
run(get_program('powerpc-eabi-readelf', devkitppc_path),
'--syms', base+'.elf', workdir=wd, stdout=f)
def main():
# arguments for advanced use:
# -a | --assemble : assemble (needs devkitPPC)
# -d | --dkp-path <path> : look for devkitPPC binaries in this location
# -w | --wit-path <path> : look fof WIT in this location
# -i | --iso <file> : path to the input ISO
# -o | --output <file> : output ISO/WBFS
parser = argparse.ArgumentParser()
parser.add_argument('-a', '--assemble', action='store_true', dest='assemble',
help='Assemble the patch binary. Requires devkitPPC')
parser.add_argument('-d', '--dkp-path', dest='dkp_path', metavar='path',
help='Path to devkitPPC binaries for the --assemble option')
parser.add_argument('-w', '--wit-path', dest='wit_path', metavar='path',
help='Path to WIT binaries')
parser.add_argument('-i', '--iso', dest='iso_path', metavar='path',
help='Path to the input (clean) ISO file')
parser.add_argument('-o', '--output', dest='output_path', metavar='path', required=True,
help='Path to the output ISO/WBFS file')
args = parser.parse_args()
if args.assemble:
assemble('main.s', args.dkp_path)
pathlib.Path('game').mkdir(parents=True, exist_ok=True)
if not os.path.exists('game/data'):
# extract the game if it isn't already
print()
print("Extracting game files...")
run(get_program('wit', args.wit_path),
'--psel', 'DATA', 'extract', os.path.abspath(args.iso_path), 'data', workdir='game')
# decompress the REL file if necessary
decomp_rel = 'd_basesNP.rel.orig'
if not os.path.exists(decomp_rel):
lzfile = 'game/data/files/rels/d_basesNP.rel.LZ'
rename = True
if not os.path.exists(lzfile):
# use the renamed file
lzfile += '_'
rename = False
with open(lzfile, 'rb') as f:
in_data = f.read()
data = lzdec.lzdec(in_data)
with open(decomp_rel, 'wb') as of:
of.write(data)
# explicitly clear out these huge data blocks
del in_data
del data
if rename:
# rename the LZ file so that the game doesn't pick it up
os.rename(lzfile, lzfile + '_')
# throw the output directly into the game files
# The game's read function will transparently open this file if the LZ file doesn't exist
out_file = "game/data/files/rels/d_basesNP.rel"
patch_spec = {
"rel": "d_basesNP.rel.orig",
"output": out_file,
"sections": [
{
# define the section for our patch data
# this goes into an empty section in the REL
"ref": "main",
"file": "asm/main.bin",
"symtab": "asm/main.syms"
}
],
"patches": [
# patches into the REL file itself
{
# patch the penalty calculation
"type": "branch_section",
"section": 1,
"addr": 0x37d68, # where to put the branch
"target": "calc_penalties", # function to jump to
"target_section": "main"
},
{
# override the sort data
"type": "branch_section",
"section": 1,
"addr": 0x386a0, # where to put the branch
"target": "modify_ranking_coins", # function to jump to
"target_section": "main"
},
{
# patch the prolog to add a init lives patcher
# The init lives calculation is in main.dol (which we don't have a patcher for)
# so we have a runtime patcher
"type": "branch_section",
"section": 1,
"addr": 0x10c,
"target": "patch_init_lives",
"target_section": "main"
}
]
}
# Run the patcher
patcher.main(patch_spec)
# re-pack the game
print()
print("Re-packing game...")
run(get_program('wit', args.wit_path),
'copy', 'data/', os.path.abspath(args.output_path), workdir='game')
print()
print("Done!")
if __name__ == "__main__":
try:
main()
except subprocess.CalledProcessError as e:
print("Error: process returned %d" % e.returncode)