StadiumRom.lua
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
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
-- STADIUM battles: getting at the Pokemon Stadium ROM. -- -- Byte order, the archive the battle models are packed into, the Yay0 -- decompressor that unwraps each one, and the per-species battle tables. It -- is a port of model_extract/pipeline/rom.py, function for function, and the -- Python remains the reference: tools/stadium_pack.py drives that side and -- tests/stadium_extract_test.lua diffs this side's finished packs against it -- byte for byte. -- -- ------- why this exists in Lua at all -- -- The mod cannot ship the models. They are ROM data, so what ships is the -- READER, and the player supplies the ROM -- exactly the arrangement the -- engine itself already has for the Game Boy ROM it is a recompilation of -- (src/import/RomImporter.lua). Everything from `baserom.z64` to -- `assets/stadium/NNN.dsm` therefore has to happen here, on the machine, in -- Lua, with no Python and no build step. -- -- ------- what makes that tractable -- -- Three steps, and none of them needs a decompilation toolchain: -- -- 1. BYTE ORDER. The three N64 dump conventions differ by a swap that is -- detected from the magic word and undone once, on load. -- 2. THE ARCHIVE. The segment at 0x920000 is a count and a table of -- (offset, size) records. No compression at that level, no names. -- 3. Yay0. Nintendo's LZ variant: a bitstream where a 1 copies a literal -- byte and a 0 pulls a (distance, length) pair out of a side table. -- Thirty lines, and the same thirty lines the Python has. -- -- Verified in the Python by decompressing all 215 entries and diffing against -- what the decompilation's own `make init` produces: 215/215 identical. -- the mod namespace (see main.lua): V.require loads a sibling module local V = ... local StadiumRom = {} local byte = string.byte local char = string.char local concat = table.concat local sub = string.sub local floor = math.floor -- ROM offsets, from pokestadium-us.yaml by way of pipeline/rom.py. StadiumRom.POKEMON_MODELS = 0x920000 -- archive of the 215 battle models StadiumRom.BATTLE_DATA = 0x70D3A0 -- per-species battle tables StadiumRom.MAIN_ROM = 0x1000 -- main code segment ... StadiumRom.MAIN_VRAM = 0x80000400 -- ... and where it lands in RAM StadiumRom.PTR_TABLE_VRAM = 0x80075BD0 -- D_80075BD0[species - 1] -- The revision every offset above is keyed to. A different ROM still runs -- -- it may well be a regional variant with the same layout -- but the caller is -- told, because "the models came out as garbage" and "that is not the ROM -- this was written against" are the same fact and only one of them is useful. StadiumRom.US_MD5 = "ed1378bc12115f71209a77844965ba50" -- The battle table's shape: 0xB90 bytes a species, as 0x10-byte entries. -- Entries 0..164 are the moves (entry n drives move n + 1) and 165 up are the -- fixed battle contexts. StadiumRom.STRIDE = 0xB90 StadiumRom.ENTRY = 0x10 StadiumRom.N_MOVES = 165 -- How many of the archive's 215 models are the battle Pokemon. The rest are -- props and trophies with no battle table. StadiumRom.N_POKEMON = 151 -- ------- byte order -- -- .z64 is big-endian and native; .v64 has each pair of bytes swapped; .n64 -- has each word reversed. `gsub` with a capture-reversing replacement does -- either in one call through C rather than a Lua loop over 33 million bytes. local MAGIC_Z64 = "\128\055\018\064" local MAGIC_V64 = "\055\128\064\018" local MAGIC_N64 = "\064\018\055\128" -- Normalise a dump to .z64 order, or nil when it is not an N64 ROM at all. function StadiumRom.normalise(bytes) if type(bytes) ~= "string" or #bytes < 0x1000 then return nil end local magic = sub(bytes, 1, 4) if magic == MAGIC_Z64 then return bytes end if magic == MAGIC_V64 then return (bytes:gsub("(.)(.)", "%2%1")) end if magic == MAGIC_N64 then return (bytes:gsub("(.)(.)(.)(.)", "%4%3%2%1")) end return nil end -- ------- Yay0 -- -- The output has to be RANDOM ACCESS while it is being written -- a back -- reference copies from what has already been produced, and overlapping runs -- are legal and common -- so it is built in a flat table of byte values and -- turned into a string at the end. -- -- The string.char conversion is the part that wants care: it is variadic and -- has an argument limit, so the table is walked in blocks and the blocks -- concatenated. Blocks of 4096 keep the call count and the intermediate -- string count both low; the whole 151-model set converts in well under a -- second on LuaJIT, which is what made an FFI buffer unnecessary here and -- kept this module portable to any Lua the engine runs on. local CHUNK = 4096 -- LuaJIT keeps `unpack` global; 5.2+ moved it onto table. local unpack = unpack or table.unpack local function bytesToString(out, n) if n == 0 then return "" end local parts, np = {}, 0 local i = 1 while i <= n do local j = i + CHUNK - 1 if j > n then j = n end np = np + 1 parts[np] = char(unpack(out, i, j)) i = j + 1 end return concat(parts) end -- Nintendo Yay0. Header: magic, decompressed size, link table offset, chunk -- offset; then a bitstream read a word at a time. function StadiumRom.yay0(src, base) base = base or 0 if sub(src, base + 1, base + 4) ~= "Yay0" then return nil, "not Yay0" end local function be32(o) local a, b, c, d = byte(src, base + o + 1, base + o + 4) return ((a * 256 + b) * 256 + c) * 256 + d end local size = be32(4) -- all three cursors are 1-based indices into `src`; the mask stream starts -- immediately after the 16-byte header local maskP = base + 0x10 + 1 local linkP = base + be32(8) + 1 local chunkP = base + be32(12) + 1 local out = {} local pos = 0 -- bytes produced so far local mask, bits = 0, 0 while pos < size do if bits == 0 then local a, b, c, d = byte(src, maskP, maskP + 3) mask = ((a * 256 + b) * 256 + c) * 256 + d maskP = maskP + 4 bits = 32 end if mask >= 0x80000000 then pos = pos + 1 out[pos] = byte(src, chunkP) chunkP = chunkP + 1 else local a, b = byte(src, linkP, linkP + 1) linkP = linkP + 2 local link = a * 256 + b local dist = link % 0x1000 local count = floor(link / 0x1000) if count == 0 then count = byte(src, chunkP) + 0x12 chunkP = chunkP + 1 else count = count + 2 end -- overlapping runs are legal: copying one byte at a time from the -- output as it grows is the behaviour, not a naive version of it local copy = pos - dist for _ = 1, count do pos = pos + 1 out[pos] = out[copy] copy = copy + 1 end end mask = (mask * 2) % 0x100000000 bits = bits - 1 end return bytesToString(out, size) end -- Unwrap whatever container an asset arrived in. The model archive's entries -- are PERS-SZP: an eight-byte magic plus a header size, wrapping a Yay0 -- stream. function StadiumRom.decompress(blob) if sub(blob, 1, 8) == "PERS-SZP" then local a, b, c, d = byte(blob, 9, 12) local header = ((a * 256 + b) * 256 + c) * 256 + d return StadiumRom.yay0(blob, header) end if sub(blob, 1, 4) == "Yay0" then return StadiumRom.yay0(blob, 0) end return blob end -- ------- the ROM local Rom = {} Rom.__index = Rom -- `bytes` is the whole file. Returns the ROM, or nil plus why. function StadiumRom.open(bytes) local data = StadiumRom.normalise(bytes) if not data then return nil, "not an N64 ROM (bad magic)" end return setmetatable({ data = data }, Rom) end function Rom:u8(o) return byte(self.data, o + 1) end function Rom:u32(o) local a, b, c, d = byte(self.data, o + 1, o + 4) if not d then return 0 end return ((a * 256 + b) * 256 + c) * 256 + d end function Rom:vramToRom(vram) return StadiumRom.MAIN_ROM + (vram - StadiumRom.MAIN_VRAM) end -- The md5 of the normalised image, or nil where LOVE's hash is not there -- (the headless suite). Only ever used to tell the player which ROM they -- gave us, never to refuse one. function Rom:md5() if self.hash ~= nil then return self.hash or nil end local ok, hex = pcall(function() local digest = love.data.hash("md5", self.data) if type(digest) == "userdata" and digest.getString then digest = digest:getString() end return love.data.encode("string", "hex", digest) end) self.hash = (ok and hex) or false return self.hash or nil end function Rom:isExpectedUS() local hex = self:md5() return hex == nil or hex == StadiumRom.US_MD5 end -- ------- the archive -- -- Segments that hold many files start with -- u32 tag, u32 0, u32 totalSize, u32 fileCount -- followed by fileCount { u32 offset, u32 size, u32 pad[2] } records, all -- relative to the start of the segment. -- -- Only the top three bytes of the first word are reliably zero: the model -- archive puts a nonzero value in the low byte, which is the same quirk the -- decompilation's own tools/unpack_asset.py works around. -- -- Returns a list of { start, size } rather than the bytes, so nothing is -- copied until a caller actually wants a file. function Rom:archive(off) local tag = self:u32(off) if (tag - tag % 256) ~= 0 or self:u32(off + 4) ~= 0 then return nil end local count = self:u32(off + 12) if count <= 0 or count >= 4096 then return nil end local out = {} for i = 0, count - 1 do local rec = off + 0x10 + i * 0x10 out[i + 1] = { start = off + self:u32(rec), size = self:u32(rec + 4) } end return out end -- The entries of the battle-model archive, uncopied. function Rom:models() if not self.modelDir then self.modelDir = self:archive(StadiumRom.POKEMON_MODELS) or {} end return self.modelDir end function Rom:modelCount() return #self:models() end -- One model fragment, decompressed. `fileno` is 0-based, as in the Python and -- in the source-file names: `N.bin` holds species N + 1. function Rom:model(fileno) local rec = self:models()[fileno + 1] if not rec then return nil end return StadiumRom.decompress(sub(self.data, rec.start + 1, rec.start + rec.size)) end -- ------- the per-species battle tables -- -- func_84302658 in src/fragments/62 DMAs 0xB90 bytes a species out of the -- 0x70D3A0 segment, addressed through the D_80075BD0 pointer table. Byte 0 of -- each 0x10-byte entry indexes that Pokemon's animation list and byte 1 its -- auxiliary (texture) animation list. -- -- Returns a 0-based array-like table of { anim, aux }, aux 0xFF meaning none -- and coming back as -1 -- the shape the packer writes. function Rom:battleRows(species) local ptrTable = self:vramToRom(StadiumRom.PTR_TABLE_VRAM) local raw = self:u32(ptrTable + (species - 1) * 4) local o = StadiumRom.BATTLE_DATA + raw % 0x1000000 local rows = {} local n = StadiumRom.STRIDE / StadiumRom.ENTRY for e = 0, n - 1 do local anim = self:u8(o + e * StadiumRom.ENTRY) local aux = self:u8(o + e * StadiumRom.ENTRY + 1) rows[e] = { anim, aux == 0xFF and -1 or aux } end rows.n = n return rows end return StadiumRom