mirror of
https://github.com/bryanthaboi/gen1recomp
synced 2026-09-26 05:32:07 -04:00
8621d024dc
- Implement roaming legendary beast system (Raikou, Entei, Suicune) matching pokefirered (src/roamer.c), including 31-cap IV fix, Roar despawn fix, battle flee logic, and trapping interactions (Mean Look, Shadow Tag, Arena Trap, Wrap). - Implement Seagallop ferry crossing cutscene and transit between Sevii Islands and Kanto. - Implement field weather engine, weather extraction, and particle rendering. - Implement renewable hidden items and step events. - Fix poison step sound effect (SE_FIELD_POISON). - Fix encounter cooldown lookup for grass/cave terrain. - Add comprehensive test suites for roamers, weather, Seagallop animations, and ladders.
399 lines
17 KiB
Lua
399 lines
17 KiB
Lua
#!/usr/bin/env luajit
|
|
|
|
package.path = "./?.lua;./?/init.lua;" .. package.path
|
|
|
|
local failed = 0
|
|
local function check(cond, msg)
|
|
if cond then
|
|
print("[ok] " .. msg)
|
|
else
|
|
failed = failed + 1
|
|
print("[FAIL] " .. msg)
|
|
end
|
|
end
|
|
|
|
local function contains(haystack, needle, msg)
|
|
check(type(haystack) == "string" and haystack:find(needle, 1, true) ~= nil,
|
|
msg .. " (" .. tostring(haystack) .. ")")
|
|
end
|
|
|
|
print("[test] 1. refused sprite write stops PokemonExtract.run")
|
|
local PokemonExtract = require("src.import.gba.pokemon_extract")
|
|
|
|
local zeroRom = {}
|
|
function zeroRom:get() return 0 end
|
|
function zeroRom:u16() return 0 end
|
|
function zeroRom:u32() return 0 end
|
|
function zeroRom:readBytes(_, len)
|
|
local t = {}
|
|
for i = 1, (len or 0) do t[i] = 0 end
|
|
return t
|
|
end
|
|
function zeroRom:clearCache() end
|
|
|
|
local refused = {}
|
|
local refusingCache = {
|
|
write = function(_, rel)
|
|
if rel:find("/icons/", 1, true) then
|
|
refused[#refused + 1] = rel
|
|
return false, "disk full"
|
|
end
|
|
return true
|
|
end,
|
|
read = function() return nil end,
|
|
exists = function() return false end,
|
|
}
|
|
|
|
local ok, err = pcall(PokemonExtract.run, zeroRom, refusingCache, {
|
|
cacheRoot = "data/generated/gba",
|
|
numSpecies = 1,
|
|
})
|
|
check(ok == false, "PokemonExtract.run raises instead of returning")
|
|
contains(tostring(err), "could not write", "the error names the refused write")
|
|
contains(tostring(err), "icons/0.rgba", "the error names the file")
|
|
check(#refused == 1, "the refusal happened on the first species (" .. #refused .. ")")
|
|
|
|
local pointerRom = {}
|
|
function pointerRom:get() return 0 end
|
|
function pointerRom:u16() return 0 end
|
|
function pointerRom:u32() return 0x08000100 end
|
|
function pointerRom:ptrOffset(p) return p - 0x08000000 end
|
|
function pointerRom:readBytes(_, len)
|
|
local t = {}
|
|
for i = 1, (len or 0) do t[i] = 0 end
|
|
return t
|
|
end
|
|
function pointerRom:clearCache() end
|
|
|
|
local written = 0
|
|
local acceptingCache = {
|
|
write = function() written = written + 1; return true end,
|
|
read = function() return nil end,
|
|
exists = function() return false end,
|
|
}
|
|
local picsOk, picsErr = pcall(PokemonExtract.run, pointerRom, acceptingCache, {
|
|
cacheRoot = "data/generated/gba",
|
|
numSpecies = 1,
|
|
})
|
|
check(picsOk == false, "a species whose pic never decodes stops PokemonExtract.run")
|
|
contains(tostring(picsErr), "produced no file",
|
|
"the error names the species with a valid pointer and no sprite")
|
|
contains(tostring(picsErr), "front/0", "the error lists the missing front sprite")
|
|
|
|
print("[test] 2. RomExtractorGen3 stage gating")
|
|
local Json = require("src.link.Json")
|
|
local SHA1 = "41cb23d8dccc8ebd7c649cd8fbb58eeace6e2fdc"
|
|
|
|
local function loadExtractorWithStubs(pokeRun, sectionsRun)
|
|
for name in pairs(package.loaded) do
|
|
if name:find("src.import.RomExtractorGen3", 1, true) then
|
|
package.loaded[name] = nil
|
|
end
|
|
end
|
|
local files = {}
|
|
local seenImports = {}
|
|
package.loaded["src.import.CacheFs"] = {
|
|
prefix = "",
|
|
write = function(rel, data) files[rel] = data; return true end,
|
|
read = function(rel) return files[rel] end,
|
|
exists = function(rel) return files[rel] ~= nil end,
|
|
remove = function(rel) files[rel] = nil; return true end,
|
|
}
|
|
package.loaded["src.import.LuaWriter"] = {
|
|
write = function(rel) files[rel] = "return {}"; return true end,
|
|
}
|
|
package.loaded["src.import.gba.extract_island1"] = {
|
|
CACHE_ROOT = "data/generated/gba",
|
|
NATIVE_ROOT = "data/generated/gba/native",
|
|
STAGE_COUNT = 7,
|
|
run = function() return true, { stub = true } end,
|
|
}
|
|
package.loaded["src.import.gba.rom"] = {
|
|
open = function(imports)
|
|
seenImports[#seenImports + 1] = imports
|
|
return { clearCache = function() end }
|
|
end,
|
|
}
|
|
package.loaded["src.import.gba.pokemon_extract"] = {
|
|
ready = function() return false end,
|
|
run = pokeRun,
|
|
}
|
|
for _, sibling in ipairs({
|
|
"items_extract", "pokedex_chrome_extract", "storage_chrome_extract",
|
|
"text_chrome_extract", "trainer_card_extract", "seagallop_extract",
|
|
"cave_transition_extract", "weather_extract", "ingame_trades_extract",
|
|
"union_room_classes_extract", "map_preview_extract",
|
|
}) do
|
|
package.loaded["src.import.gba." .. sibling] = { run = function() return true end }
|
|
end
|
|
package.loaded["src.import.gba.region_map_extract"] = {
|
|
ready = function() return true end,
|
|
run = function() return {} end,
|
|
}
|
|
package.loaded["src.import.gba.multichoice_extract"] = {
|
|
ready = function() return true end,
|
|
run = function() return {} end,
|
|
}
|
|
for _, sibling in ipairs({ "credits_extract", "league_extract", "battle_ai_extract" }) do
|
|
package.loaded["src.import.gba." .. sibling] = {
|
|
ready = function() return true end,
|
|
run = function() return {} end,
|
|
}
|
|
end
|
|
package.loaded["src.import.gba.map_sections_extract"] = {
|
|
run = sectionsRun or function(_, cache, opts)
|
|
cache:write((opts and opts.cacheRoot or "data/generated/gba")
|
|
.. "/region_map/map_sections.lua", string.rep("-", 2048))
|
|
return true
|
|
end,
|
|
}
|
|
package.loaded["src.import.gba.revision_view"] = {
|
|
forImports = function(imports)
|
|
seenImports[#seenImports + 1] = imports
|
|
return nil
|
|
end,
|
|
apply = function(data) return data end,
|
|
}
|
|
package.loaded["src.import.gba.extract_intro"] = { run = function() return true, {} end }
|
|
package.loaded["src.import.gba.extract_naming"] = { run = function() return true end }
|
|
package.loaded["src.import.gba.extract_audio"] = { run = function() return true, {} end }
|
|
local Extractor = require("src.import.RomExtractorGen3")
|
|
return Extractor.new("ROM", {}, nil, SHA1), files, seenImports
|
|
end
|
|
|
|
local STATUS = "data/generated/gba/pokemon/extract_status.json"
|
|
|
|
local throwing, throwFiles = loadExtractorWithStubs(function()
|
|
error("species sprite pass refused a write")
|
|
end)
|
|
local runOk, runErr = pcall(throwing.run, throwing)
|
|
check(runOk == false, "a throwing PokemonExtract fails the import")
|
|
contains(tostring(runErr), "Pokemon extract failed", "the import error names the stage")
|
|
local throwStatus = throwFiles[STATUS] and Json.decode(throwFiles[STATUS])
|
|
check(throwStatus ~= nil, "pokemon/extract_status.json was written")
|
|
check(throwStatus and throwStatus.ok == false, "the failed status says ok = false")
|
|
contains(throwStatus and tostring(throwStatus.error), "refused a write",
|
|
"the failed status carries the real error")
|
|
|
|
local passing, passFiles, passImports = loadExtractorWithStubs(function()
|
|
return { root = "data/generated/gba/pokemon", picsWritten = { icons = 412 } }
|
|
end)
|
|
local passOk, passErr = pcall(passing.run, passing)
|
|
check(passOk == true, "a successful PokemonExtract completes the import (" .. tostring(passErr) .. ")")
|
|
local passStatus = passFiles[STATUS] and Json.decode(passFiles[STATUS])
|
|
check(passStatus ~= nil, "pokemon/extract_status.json was written on success")
|
|
check(passStatus and passStatus.ok == true, "the successful status says ok = true")
|
|
check(passStatus and passStatus.error == nil,
|
|
"a successful stage records no error (" .. tostring(passStatus and passStatus.error) .. ")")
|
|
|
|
local AUX_STATUS = "data/generated/gba/region_map/extract_status.json"
|
|
local silent, silentFiles = loadExtractorWithStubs(function()
|
|
return { root = "data/generated/gba/pokemon", picsWritten = { icons = 412 } }
|
|
end, function() return true end)
|
|
local silentOk, silentErr = pcall(silent.run, silent)
|
|
check(silentOk == false, "a map_sections pass that writes nothing fails the import")
|
|
contains(tostring(silentErr), "map_sections extract wrote 0 bytes",
|
|
"the import error names the empty mapsec write")
|
|
local auxStatus = silentFiles[AUX_STATUS] and Json.decode(silentFiles[AUX_STATUS])
|
|
check(auxStatus and auxStatus.ok == false, "region_map/extract_status.json says ok = false")
|
|
|
|
print("[test] 3. shared imports object")
|
|
check(#passImports >= 3, "the run opened the ROM for at least 3 stages (" .. #passImports .. ")")
|
|
local sameTable = true
|
|
for i = 2, #passImports do
|
|
if passImports[i] ~= passImports[1] then sameTable = false end
|
|
end
|
|
check(sameTable, "every stage got the same imports table, so one revision view is built")
|
|
|
|
local shared = loadExtractorWithStubs(function() return {} end)
|
|
local a = shared:sharedImports(SHA1)
|
|
local b = shared:sharedImports(SHA1)
|
|
check(a == b, "sharedImports returns the same table on every stage")
|
|
|
|
package.loaded["src.import.CacheFs"] = nil
|
|
package.loaded["src.import.LuaWriter"] = nil
|
|
package.loaded["src.import.gba.extract_island1"] = nil
|
|
package.loaded["src.import.gba.rom"] = nil
|
|
package.loaded["src.import.gba.pokemon_extract"] = nil
|
|
package.loaded["src.import.gba.region_map_extract"] = nil
|
|
package.loaded["src.import.gba.multichoice_extract"] = nil
|
|
package.loaded["src.import.gba.credits_extract"] = nil
|
|
package.loaded["src.import.gba.league_extract"] = nil
|
|
package.loaded["src.import.gba.battle_ai_extract"] = nil
|
|
package.loaded["src.import.gba.map_sections_extract"] = nil
|
|
package.loaded["src.import.gba.revision_view"] = nil
|
|
package.loaded["src.import.gba.extract_intro"] = nil
|
|
package.loaded["src.import.gba.extract_naming"] = nil
|
|
package.loaded["src.import.gba.extract_audio"] = nil
|
|
package.loaded["src.import.RomExtractorGen3"] = nil
|
|
for _, sibling in ipairs({
|
|
"items_extract", "pokedex_chrome_extract", "storage_chrome_extract",
|
|
"text_chrome_extract", "trainer_card_extract", "seagallop_extract",
|
|
"cave_transition_extract", "ingame_trades_extract", "union_room_classes_extract",
|
|
"map_preview_extract",
|
|
}) do
|
|
package.loaded["src.import.gba." .. sibling] = nil
|
|
end
|
|
|
|
print("[test] 4. firered required files")
|
|
local CacheContract = require("src.import.CacheContract")
|
|
local required = {}
|
|
for _, path in ipairs(CacheContract.VERSION_REQUIRED_FILES_OVERRIDE.firered) do
|
|
required[path] = true
|
|
end
|
|
for _, path in ipairs({
|
|
"data/generated/gba/pokemon/front/1.rgba",
|
|
"data/generated/gba/pokemon/front/200.rgba",
|
|
"data/generated/gba/pokemon/front/411.rgba",
|
|
"data/generated/gba/pokemon/back/1.rgba",
|
|
"data/generated/gba/pokemon/back/200.rgba",
|
|
"data/generated/gba/pokemon/back/411.rgba",
|
|
"data/generated/gba/pokemon/icons/1.rgba",
|
|
"data/generated/gba/pokemon/icons/200.rgba",
|
|
"data/generated/gba/pokemon/icons/411.rgba",
|
|
"data/generated/gba/region_map/kanto_map.png",
|
|
"data/generated/gba/region_map/cursor.png",
|
|
"data/generated/gba/region_map/player_red.png",
|
|
"data/generated/gba/region_map/map_sections.lua",
|
|
"data/generated/gba/scripts/multichoice.lua",
|
|
}) do
|
|
check(required[path] == true, "required: " .. path)
|
|
end
|
|
|
|
local fakeFs = { prefix = "" }
|
|
local present = {}
|
|
for path in pairs(required) do present[path] = true end
|
|
function fakeFs.exists(rel) return present[rel] == true end
|
|
local complete = CacheContract.allRequiredFilesExist("firered", fakeFs)
|
|
check(complete == true, "a cache holding every required file is complete")
|
|
present["data/generated/gba/pokemon/front/411.rgba"] = nil
|
|
local incomplete, missing = CacheContract.allRequiredFilesExist("firered", fakeFs)
|
|
check(incomplete == false, "a cache without the last species front sprite is incomplete")
|
|
check(missing == "data/generated/gba/pokemon/front/411.rgba",
|
|
"the contract names the missing sprite (" .. tostring(missing) .. ")")
|
|
|
|
local GbaVersions = require("src.import.gba.versions")
|
|
local META = "data/generated/gba/meta.json"
|
|
present["data/generated/gba/pokemon/front/411.rgba"] = true
|
|
local stamp = tostring(GbaVersions.CACHE_VERSION)
|
|
local stampFs = { prefix = "" }
|
|
function stampFs.exists(rel) return present[rel] == true end
|
|
function stampFs.read(rel)
|
|
if rel == CacheContract.MARKER_PATH then return CacheContract.markerFor("firered") end
|
|
if rel == META then
|
|
return '{"md5":"x","cache_version":' .. stamp .. ',"native_version":6}'
|
|
end
|
|
return nil
|
|
end
|
|
check(CacheContract.isReady("firered", stampFs) == true,
|
|
"a complete cache stamped at the current CACHE_VERSION is ready")
|
|
stamp = tostring(GbaVersions.CACHE_VERSION - 1)
|
|
check(CacheContract.isReady("firered", stampFs) == false,
|
|
"a complete cache stamped at an older CACHE_VERSION is not ready, so it re-imports")
|
|
stamp = tostring(GbaVersions.CACHE_VERSION)
|
|
|
|
print("[test] 5. PokemonExtract.ready")
|
|
local Versions = require("src.import.gba.versions")
|
|
local last = (Versions.NUM_SPECIES or 412) - 1
|
|
local iconBytes = (Versions.MON_ICON_W or 32) * (Versions.MON_ICON_H or 32) * 2 * 4
|
|
local ROOT = "data/generated/gba"
|
|
local function readyCacheWith(skip, format, numSpecies)
|
|
local manifest = string.format(
|
|
"return { magic = \"SVPK\", format = %d, numSpecies = %d }%s\n",
|
|
format or PokemonExtract.FORMAT_VERSION,
|
|
numSpecies or Versions.NUM_SPECIES or 412,
|
|
string.rep(" ", 40))
|
|
local sizes = {
|
|
[ROOT .. "/pokemon/manifest.lua"] = 64,
|
|
[ROOT .. "/pokemon/names.lua"] = 64,
|
|
[ROOT .. "/pokemon/stats.lua"] = 64,
|
|
[ROOT .. "/pokemon/learnsets.lua"] = 64,
|
|
[ROOT .. "/pokemon/egg_moves.lua"] = 64,
|
|
[ROOT .. "/pokemon/move_names.lua"] = 64,
|
|
[ROOT .. "/pokemon/party/slot_main.rgba"] = 80 * 56 * 4,
|
|
[ROOT .. "/pokemon/summary/page_info.rgba"] = 240 * 160 * 4,
|
|
[ROOT .. "/pokemon/storage/manifest.lua"] = 64,
|
|
[ROOT .. "/chrome/menu_message_rgba.rgba"] = 64,
|
|
[ROOT .. "/trainer_card/bg.rgba"] = 240 * 160 * 4,
|
|
[ROOT .. "/items/pack.lua"] = 64,
|
|
[ROOT .. "/pokemon/front/1.rgba"] = 64 * 64 * 4,
|
|
[ROOT .. "/pokemon/back/1.rgba"] = 64 * 64 * 4,
|
|
[ROOT .. "/pokemon/icons/1.rgba"] = iconBytes,
|
|
[ROOT .. "/pokemon/front/" .. last .. ".rgba"] = 64 * 64 * 4,
|
|
[ROOT .. "/pokemon/back/" .. last .. ".rgba"] = 64 * 64 * 4,
|
|
[ROOT .. "/pokemon/icons/" .. last .. ".rgba"] = iconBytes,
|
|
[ROOT .. "/chrome/fonts/braille.lua"] = 64,
|
|
[ROOT .. "/seagallop/manifest.lua"] = 64,
|
|
[ROOT .. "/seagallop/wb.rgba"] = 32 * 8 * 32 * 8 * 4,
|
|
[ROOT .. "/pokemon/pokedex/paper_bg.rgba"] = 240 * 160 * 4,
|
|
[ROOT .. "/pokemon/pokedex/footprints/1.rgba"] = 16 * 16 * 4,
|
|
[ROOT .. "/pokemon/pokedex/footprints/question_mark.rgba"] = 16 * 16 * 4,
|
|
[ROOT .. "/pokemon/battle/terrain_cave.rgba"] = 256 * 256 * 4,
|
|
[ROOT .. "/pokemon/battle/terrain_water.rgba"] = 256 * 256 * 4,
|
|
[ROOT .. "/pokemon/battle/terrain_champion.rgba"] = 256 * 256 * 4,
|
|
}
|
|
if skip then sizes[skip] = nil end
|
|
return {
|
|
read = function(_, rel)
|
|
local n = sizes[rel]
|
|
if not n then return nil end
|
|
if rel == ROOT .. "/pokemon/manifest.lua" then return manifest end
|
|
return string.rep("x", n)
|
|
end,
|
|
}
|
|
end
|
|
check(PokemonExtract.ready(readyCacheWith(nil), ROOT) == true,
|
|
"a complete species cache is ready")
|
|
check(PokemonExtract.ready(readyCacheWith(ROOT .. "/pokemon/icons/1.rgba"), ROOT) == false,
|
|
"a cache with no first-species icon is not ready")
|
|
check(PokemonExtract.ready(readyCacheWith(ROOT .. "/pokemon/front/" .. last .. ".rgba"), ROOT) == false,
|
|
"a cache with no last-species front sprite is not ready")
|
|
check(PokemonExtract.ready(readyCacheWith(ROOT .. "/pokemon/icons/" .. last .. ".rgba"), ROOT) == false,
|
|
"a cache with no last-species icon is not ready")
|
|
check(PokemonExtract.ready(readyCacheWith(nil, PokemonExtract.FORMAT_VERSION - 1), ROOT) == false,
|
|
"a cache written by an older species format is not ready")
|
|
check(PokemonExtract.ready(readyCacheWith(nil, nil, 1), ROOT) == false,
|
|
"a cache whose manifest counts fewer species than the ROM is not ready")
|
|
|
|
local siblings = {
|
|
ROOT .. "/chrome/fonts/braille.lua",
|
|
ROOT .. "/seagallop/manifest.lua",
|
|
ROOT .. "/seagallop/wb.rgba",
|
|
ROOT .. "/pokemon/pokedex/paper_bg.rgba",
|
|
ROOT .. "/pokemon/pokedex/footprints/1.rgba",
|
|
ROOT .. "/pokemon/pokedex/footprints/question_mark.rgba",
|
|
ROOT .. "/pokemon/battle/terrain_cave.rgba",
|
|
ROOT .. "/pokemon/battle/terrain_water.rgba",
|
|
ROOT .. "/pokemon/battle/terrain_champion.rgba",
|
|
}
|
|
for _, rel in ipairs(siblings) do
|
|
check(PokemonExtract.ready(readyCacheWith(rel), ROOT) == false,
|
|
"an import interrupted before " .. rel .. " re-runs the pokemon block")
|
|
end
|
|
local requiredSet = {}
|
|
for _, key in ipairs(CacheContract.requiredFiles("firered")) do requiredSet[key] = true end
|
|
for _, rel in ipairs(siblings) do
|
|
check(requiredSet[rel] == true,
|
|
rel .. " is both a ready() sentinel and a contract-required key")
|
|
end
|
|
|
|
print("[test] 4. the parallel success path writes the stage markers")
|
|
local par, parFiles = loadExtractorWithStubs(function()
|
|
return { root = "data/generated/gba/pokemon", picsWritten = { icons = 412 } }
|
|
end)
|
|
par.runParallel = function() return true end
|
|
local parOk, parErr = pcall(par.run, par)
|
|
check(parOk == true, "the parallel path completes the import (" .. tostring(parErr) .. ")")
|
|
local parPoke = parFiles[STATUS] and Json.decode(parFiles[STATUS])
|
|
check(parPoke ~= nil and parPoke.ok == true,
|
|
"the parallel path writes pokemon/extract_status.json with ok = true")
|
|
local parAux = parFiles[AUX_STATUS] and Json.decode(parFiles[AUX_STATUS])
|
|
check(parAux ~= nil and parAux.ok == true,
|
|
"the parallel path writes region_map/extract_status.json with ok = true")
|
|
|
|
if failed > 0 then
|
|
print("[test] FAILED " .. failed)
|
|
os.exit(1)
|
|
end
|
|
print("[test] all passed")
|