This repository was archived by the owner on Aug 14, 2026. It is now read-only.
forked from TeJota1337/DramaticShapeVoxelMod
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathStadiumRom.lua
More file actions
314 lines (281 loc) · 10.9 KB
/
Copy pathStadiumRom.lua
File metadata and controls
314 lines (281 loc) · 10.9 KB
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