stadium_extract_test.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
-- The Lua ROM extractor, against the Python packer that is its oracle. -- -- luajit mods/DramaticShapeVoxelMod/tests/stadium_extract_test.lua \ -- [--rom=PATH] [--oracle=DIR] [--only=25,6] [--out=DIR] [--mod=DIR] -- -- Run from the PROJECT ROOT. Defaults: the ROM under -- model_extract/baseroms/, the oracle in assets/stadium (whatever -- tools/stadium_pack.py last wrote there). -- -- --mod points at the mod directory whose lib/ is under test. It exists for -- worktrees: the ROM and the packs are both gitignored, so a worktree has -- neither, and without this the test would load the SHARED checkout's -- modules while claiming to test the branch's. -- -- ------- what this is for -- -- lib/StadiumRom, StadiumFragment, StadiumFx and StadiumBuild are a port of -- roughly fifteen hundred lines of dense numeric Python -- an F3DEX2 -- interpreter, six texture codecs, a bit-packed animation sampler, a noise -- generator and a binary writer. Reading a port of that twice does not -- establish that it is right. Producing the same thirty-four megabytes, -- byte for byte, does. -- -- It is also the guard on the two of them DRIFTING. The packer and the -- extractor have to keep agreeing about the format forever, and a change to -- one that is not made to the other shows up here as a differing offset -- rather than as a Pokemon that renders as noise three months later. -- -- Headless: no LOVE, no graphics. Everything these four modules do is -- arithmetic on strings. local args = {} for _, a in ipairs({ ... }) do local k, v = a:match("^%-%-([%w_]+)=(.*)$") if k then args[k] = v else args[a:gsub("^%-%-", "")] = true end end local MOD = args.mod or "mods/DramaticShapeVoxelMod" local ROM = args.rom or (MOD .. "/model_extract/baseroms/us/baserom.z64") local ORACLE = args.oracle or (MOD .. "/assets/stadium") -- ------- the mod namespace, enough of it to load four modules local loaded = {} local V = {} function V.require(name) if loaded[name] == nil then local chunk = assert(loadfile(MOD .. "/lib/" .. name .. ".lua")) loaded[name] = chunk(V) end return loaded[name] end V.mod = { log = { warn = function() end, info = function() end } } -- the mod's own directory, so a module that loads a data file finds it -- relative to the MOD rather than to wherever this was run from V.path = MOD local StadiumRom = V.require("StadiumRom") local StadiumBuild = V.require("StadiumBuild") -- ------- run local function readFile(path) local fp = io.open(path, "rb") if not fp then return nil end local data = fp:read("*a") fp:close() return data end local romBytes = readFile(ROM) if not romBytes then io.stderr:write("no ROM at " .. ROM .. "\n") os.exit(2) end local rom, err = StadiumRom.open(romBytes) if not rom then io.stderr:write("could not open ROM: " .. tostring(err) .. "\n") os.exit(2) end local only = nil if args.only then only = {} for n in args.only:gmatch("%d+") do only[tonumber(n)] = true end end local checked, matched, missing, failed = 0, 0, 0, 0 local shinyOk, shinyMissing, shinyBad = 0, 0, 0 local firstBad = nil local t0 = os.clock() for fileno = 0, StadiumRom.N_POKEMON - 1 do local ok, res = pcall(StadiumBuild.species, rom, fileno) if not ok or not res then -- res carries the message on the error path io.write(("file %d: EXTRACT FAILED: %s\n"):format(fileno, tostring(res))) failed = failed + 1 elseif not only or only[res.species] then checked = checked + 1 if args.out then local fp = io.open(("%s/%03d.dsm"):format(args.out, res.species), "wb") if fp then fp:write(res.bytes) fp:close() end -- the shiny variant too, so the pair can be diffed out of process if res.shinyBytes then local sp = io.open(("%s/%03ds.dsm"):format(args.out, res.species), "wb") if sp then sp:write(res.shinyBytes) sp:close() end end end -- The shiny pack is the same DSM3 with recoloured texels, so it must be -- exactly as long and must actually differ. A species that produced none -- is counted rather than failed: it ships without a recolour and the -- runtime falls back to its normal model. if not res.shinyBytes then shinyMissing = shinyMissing + 1 elseif #res.shinyBytes ~= #res.bytes then shinyBad = shinyBad + 1 io.write(("species %d: shiny pack is %d bytes, normal is %d\n") :format(res.species, #res.shinyBytes, #res.bytes)) elseif res.shinyBytes == res.bytes then shinyBad = shinyBad + 1 io.write(("species %d: shiny pack is identical to normal\n") :format(res.species)) else shinyOk = shinyOk + 1 end local want = readFile(("%s/%03d.dsm"):format(ORACLE, res.species)) if not want then missing = missing + 1 elseif want == res.bytes then matched = matched + 1 else -- where, exactly: the first differing byte localises a format mistake -- far faster than "the file is wrong" does local at = nil local n = math.min(#want, #res.bytes) for i = 1, n do if want:sub(i, i) ~= res.bytes:sub(i, i) then at = i break end end io.write(("%03d.dsm DIFFERS: %d bytes vs %d, first at %s\n") :format(res.species, #res.bytes, #want, at and ("offset " .. at) or "(one is a prefix)")) if not firstBad then firstBad = res.species end end end end io.write(("\n%d checked, %d identical, %d differ, %d oracle files missing, " .. "%d extractions failed (%.1fs)\n") :format(checked, matched, checked - matched - missing, missing, failed, os.clock() - t0)) io.write(("shiny: %d recoloured, %d without a variant, %d malformed\n") :format(shinyOk, shinyMissing, shinyBad)) -- NONE recoloured is a failure, not a quiet zero. It is what a missing or -- unfindable data/shiny_colors.lua looks like, and the first version of this -- test reported PASS through exactly that: 151 species built, every one of -- them without a shiny variant, and nothing in the output that read as -- wrong. A count of zero is now as loud as a malformed pack. if checked > 0 and shinyOk == 0 then io.write("NO SPECIES RECOLOURED -- data/shiny_colors.lua was not found\n") shinyBad = shinyBad + 1 end -- The oracle diff is the load-bearing assertion and is unchanged: the shiny -- pass must not have moved a single byte of the normal packs. The shiny -- counters are additional, and a malformed variant fails the run -- a pack -- of the wrong length would be read as a corrupt model at runtime. if matched == checked and failed == 0 and missing == 0 and shinyBad == 0 then io.write("PASS -- the Lua extractor reproduces the packer exactly\n") os.exit(0) end io.write("FAIL\n") os.exit(1)