Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
MorsGames
GitHub Repository: MorsGames/sm64plus
Path: blob/master/extract_assets.py
7853 views
1
#!/usr/bin/env python3
2
import sys
3
import os
4
import json
5
6
7
def read_asset_map():
8
with open("assets.json") as f:
9
ret = json.load(f)
10
return ret
11
12
13
def read_local_asset_list(f):
14
if f is None:
15
return []
16
ret = []
17
for line in f:
18
ret.append(line.strip())
19
return ret
20
21
22
def asset_needs_update(asset, version):
23
if version <= 6 and asset in ["actors/king_bobomb/king_bob-omb_eyes.rgba16.png", "actors/king_bobomb/king_bob-omb_hand.rgba16.png"]:
24
return True
25
if version <= 5 and asset == "textures/spooky/bbh_textures.00800.rgba16.png":
26
return True
27
if version <= 4 and asset in ["textures/mountain/ttm_textures.01800.rgba16.png", "textures/mountain/ttm_textures.05800.rgba16.png"]:
28
return True
29
if version <= 3 and asset == "textures/cave/hmc_textures.01800.rgba16.png":
30
return True
31
if version <= 2 and asset == "textures/inside/inside_castle_textures.09000.rgba16.png":
32
return True
33
if version <= 1 and asset.endswith(".m64"):
34
return True
35
if version <= 0 and asset.endswith(".aiff"):
36
return True
37
return False
38
39
40
def remove_file(fname):
41
os.remove(fname)
42
print("deleting", fname)
43
try:
44
os.removedirs(os.path.dirname(fname))
45
except OSError:
46
pass
47
48
49
def clean_assets(local_asset_file):
50
assets = set(read_asset_map().keys())
51
assets.update(read_local_asset_list(local_asset_file))
52
for fname in list(assets) + [".assets-local.txt"]:
53
if fname.startswith("@"):
54
continue
55
try:
56
remove_file(fname)
57
except FileNotFoundError:
58
pass
59
60
def get_baserom_path(lang):
61
return os.environ.get("SM64PLUS_BASEROM_" + lang) or "baserom." + lang + ".z64"
62
63
def main():
64
# In case we ever need to change formats of generated files, we keep a
65
# revision ID in the local asset file.
66
new_version = 7
67
68
try:
69
local_asset_file = open(".assets-local.txt")
70
local_asset_file.readline()
71
local_version = int(local_asset_file.readline().strip())
72
except Exception:
73
local_asset_file = None
74
local_version = -1
75
76
langs = sys.argv[1:]
77
if langs == ["--clean"]:
78
clean_assets(local_asset_file)
79
sys.exit(0)
80
81
all_langs = ["jp", "us", "eu", "sh"]
82
if not langs or not all(a in all_langs for a in langs):
83
langs_str = " ".join("[" + lang + "]" for lang in all_langs)
84
print("Usage: " + sys.argv[0] + " " + langs_str)
85
print("For each version, baserom.<version>.z64 must exist")
86
sys.exit(1)
87
88
asset_map = read_asset_map()
89
all_assets = []
90
any_missing_assets = False
91
for asset, data in asset_map.items():
92
if asset.startswith("@"):
93
continue
94
if os.path.isfile(asset):
95
all_assets.append((asset, data, True))
96
else:
97
all_assets.append((asset, data, False))
98
if not any_missing_assets and any(lang in data[-1] for lang in langs):
99
any_missing_assets = True
100
101
if not any_missing_assets and local_version == new_version:
102
# Nothing to do, no need to read a ROM. For efficiency we don't check
103
# the list of old assets either.
104
return
105
106
# Late imports (to optimize startup perf)
107
import subprocess
108
import hashlib
109
import tempfile
110
from collections import defaultdict
111
112
new_assets = {a[0] for a in all_assets}
113
114
previous_assets = read_local_asset_list(local_asset_file)
115
if local_version == -1:
116
# If we have no local asset file, we assume that files are version
117
# controlled and thus up to date.
118
local_version = new_version
119
120
# Create work list
121
todo = defaultdict(lambda: [])
122
for (asset, data, exists) in all_assets:
123
# Leave existing assets alone if they have a compatible version.
124
if exists and not asset_needs_update(asset, local_version):
125
continue
126
127
meta = data[:-2]
128
size, positions = data[-2:]
129
for lang, pos in positions.items():
130
mio0 = None if len(pos) == 1 else pos[0]
131
pos = pos[-1]
132
if lang in langs:
133
todo[(lang, mio0)].append((asset, pos, size, meta))
134
break
135
136
# Load ROMs
137
roms = {}
138
for lang in langs:
139
fname = get_baserom_path(lang)
140
try:
141
with open(fname, "rb") as f:
142
roms[lang] = f.read()
143
except Exception as e:
144
print("Failed to open " + fname + "! " + str(e))
145
sys.exit(1)
146
sha1 = hashlib.sha1(roms[lang]).hexdigest()
147
with open("sm64." + lang + ".sha1", "r") as f:
148
expected_sha1 = f.read().split()[0]
149
if sha1 != expected_sha1:
150
print(
151
fname
152
+ " has the wrong hash! Found "
153
+ sha1
154
+ ", expected "
155
+ expected_sha1
156
)
157
sys.exit(1)
158
159
make = "make"
160
161
for path in os.environ["PATH"].split(os.pathsep):
162
if os.path.isfile(os.path.join(path, "gmake")):
163
make = "gmake"
164
165
# Make sure tools exist
166
subprocess.check_call(
167
[make, "-s", "-C", "tools/", "n64graphics", "skyconv", "mio0", "aifc_decode"]
168
)
169
170
# Go through the assets in roughly alphabetical order (but assets in the same
171
# mio0 file still go together).
172
keys = sorted(list(todo.keys()), key=lambda k: todo[k][0][0])
173
174
# Import new assets
175
for key in keys:
176
assets = todo[key]
177
lang, mio0 = key
178
if mio0 == "@sound":
179
rom = roms[lang]
180
args = [
181
"python3",
182
"tools/disassemble_sound.py",
183
"baserom." + lang + ".z64",
184
]
185
def append_args(key):
186
size, locs = asset_map["@sound " + key + " " + lang]
187
offset = locs[lang][0]
188
args.append(str(offset))
189
args.append(str(size))
190
append_args("ctl")
191
append_args("tbl")
192
if lang == "sh":
193
args.append("--shindou-headers")
194
append_args("ctl header")
195
append_args("tbl header")
196
args.append("--only-samples")
197
for (asset, pos, size, meta) in assets:
198
print("extracting", asset)
199
args.append(asset + ":" + str(pos))
200
subprocess.run(args, check=True)
201
continue
202
203
if mio0 is not None:
204
image = subprocess.run(
205
[
206
"./tools/mio0",
207
"-d",
208
"-o",
209
str(mio0),
210
get_baserom_path(lang),
211
"-",
212
],
213
check=True,
214
stdout=subprocess.PIPE,
215
).stdout
216
else:
217
image = roms[lang]
218
219
for (asset, pos, size, meta) in assets:
220
print("extracting", asset)
221
input = image[pos : pos + size]
222
os.makedirs(os.path.dirname(asset), exist_ok=True)
223
if asset.endswith(".png"):
224
png_file = tempfile.NamedTemporaryFile(prefix="asset", delete=False)
225
try:
226
png_file.write(input)
227
png_file.flush()
228
png_file.close()
229
if asset.startswith("textures/skyboxes/") or asset.startswith("levels/ending/cake"):
230
if asset.startswith("textures/skyboxes/"):
231
imagetype = "sky"
232
else:
233
imagetype = "cake" + ("-eu" if "eu" in asset else "")
234
subprocess.run(
235
[
236
"./tools/skyconv",
237
"--type",
238
imagetype,
239
"--combine",
240
png_file.name,
241
asset,
242
],
243
check=True,
244
)
245
else:
246
w, h = meta
247
fmt = asset.split(".")[-2]
248
subprocess.run(
249
[
250
"./tools/n64graphics",
251
"-e",
252
png_file.name,
253
"-g",
254
asset,
255
"-f",
256
fmt,
257
"-w",
258
str(w),
259
"-h",
260
str(h),
261
],
262
check=True,
263
)
264
finally:
265
png_file.close()
266
os.remove(png_file.name)
267
else:
268
with open(asset, "wb") as f:
269
f.write(input)
270
271
# Remove old assets
272
for asset in previous_assets:
273
if asset not in new_assets:
274
try:
275
remove_file(asset)
276
except FileNotFoundError:
277
pass
278
279
# Replace the asset list
280
output = "\n".join(
281
[
282
"# This file tracks the assets currently extracted by extract_assets.py.",
283
str(new_version),
284
*sorted(list(new_assets)),
285
"",
286
]
287
)
288
with open(".assets-local.txt", "w") as f:
289
f.write(output)
290
291
292
main()
293
294