Files
gen1recomp/src/import/RomExtractorGen3.lua
T
1jamie 8621d024dc feat(game3): implement roaming legendary beasts, Seagallop ferry, field weather, and overworld fixes
- 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.
2026-09-23 13:46:57 -05:00

708 lines
27 KiB
Lua

local CacheFs = require("src.import.CacheFs")
local LuaWriter = require("src.import.LuaWriter")
local GameVersion = require("src.core.GameVersion")
local CachePaths = require("src.core.game3.cache_paths")
local RomExtractorGen3 = {}
RomExtractorGen3.__index = RomExtractorGen3
local STAGE_COUNT = 5
local GBA_ROOT = CachePaths.CACHE_ROOT
local function canonicalImportId(id)
return id == "leafgreen" and "firered" or id
end
local function importVersion(sha1)
return GameVersion.forSha1(sha1) or "firered"
end
local function hexSha1(data)
local digest = love.data.hash("sha1", data)
if type(digest) == "userdata" and digest.getString then
digest = digest:getString()
end
return love.data.encode("string", "hex", digest)
end
local function writeText(rel, body)
local ok, err = CacheFs.write(rel, body)
if not ok then
error("could not write " .. rel .. ": " .. tostring(err))
end
end
local function writeJson(rel, obj)
local Canon = require("src.import.canonical_json")
writeText(rel, Canon.encode(obj) .. "\n")
end
local function makeImports(romData, sha1, version)
version = version or importVersion(sha1)
return {
info = function(_, id)
if canonicalImportId(id) ~= canonicalImportId(version) then
return nil, "undeclared"
end
return {
id = canonicalImportId(version),
size = #romData,
-- versions.lua looks up by SHA-1 (legacy field name is md5).
md5 = sha1,
file = "memory",
}
end,
read = function(_, id, offset, length)
if canonicalImportId(id) ~= canonicalImportId(version) then
return nil, "undeclared"
end
if offset < 0 or length < 0 or offset + length > #romData then
return nil, "short read"
end
return romData:sub(offset + 1, offset + length)
end,
}
end
local function makeCache()
return {
write = function(_, rel, bytes)
return CacheFs.write(rel, bytes)
end,
read = function(_, rel)
return CacheFs.read(rel)
end,
exists = function(_, rel)
return CacheFs.exists(rel)
end,
info = function(_, rel)
if CacheFs.exists(rel) then return { type = "file" } end
return nil
end,
}
end
local POKEMON_SUBTASKS = {
["pokemon"] = { min = 0.00, max = 0.40, label = "Pokémon Species & Sprites" },
["learnsets"] = { min = 0.40, max = 0.55, label = "Move Learnsets" },
["battle_moves"] = { min = 0.55, max = 0.60, label = "Battle Moves Data" },
["party_chrome"] = { min = 0.60, max = 0.65, label = "Party UI Graphics" },
["battle_chrome"] = { min = 0.65, max = 0.70, label = "Battle UI Graphics" },
["pokedex_entries"] = { min = 0.70, max = 0.73, label = "Pokédex Database" },
["pokedex_categories"]= { min = 0.73, max = 0.75, label = "Pokédex Categories" },
["pokedex_orders"] = { min = 0.75, max = 0.77, label = "Pokédex Sorting" },
["pokedex_done"] = { min = 0.77, max = 0.78, label = "Pokédex Complete" },
["storage_chrome"] = { min = 0.78, max = 0.82, label = "PC Storage Chrome" },
["battle_transition"] = { min = 0.82, max = 0.86, label = "Battle Transitions" },
["summary_chrome"] = { min = 0.86, max = 0.90, label = "Summary Screen Graphics" },
["bag_chrome"] = { min = 0.90, max = 0.94, label = "Bag & Items Graphics" },
["shop_chrome"] = { min = 0.94, max = 0.97, label = "Mart & Shop Graphics" },
["trainers"] = { min = 0.97, max = 0.99, label = "Trainer Data & Parties" },
["map_preview"] = { min = 0.99, max = 1.00, label = "Location Previews" },
}
function RomExtractorGen3.new(romData, manifest, progressCb, romSha1)
local sha1 = romSha1 or (manifest and manifest.romSha1) or hexSha1(romData)
require("src.import.gba.versions").select(sha1)
return setmetatable({
version = assert(require("src.core.GameVersion").forSha1(sha1), "unknown FRLG ROM"),
romData = romData,
manifest = manifest,
progress = progressCb,
romSha1 = romSha1,
stage = 0,
_lastPct = 0,
}, RomExtractorGen3)
end
function RomExtractorGen3:sharedImports(sha1)
if not self._imports then
self._imports = makeImports(self.romData, sha1, importVersion(sha1))
end
return self._imports
end
function RomExtractorGen3:ensureSha1()
if type(self.romSha1) == "string" and self.romSha1 ~= "" then
return self.romSha1
end
self.romSha1 = hexSha1(self.romData)
return self.romSha1
end
function RomExtractorGen3:report(pct, stageName, current, stageTotal)
pct = math.max(self._lastPct or 0, math.min(1.0, pct or 0))
self._lastPct = pct
if self.progress then
self.progress(math.floor(pct * 1000), 1000, stageName or "Extracting", current or 0, stageTotal or 1)
end
end
function RomExtractorGen3:beginStage(name)
self.stage = self.stage + 1
self:report((self.stage - 1) / STAGE_COUNT, name, 0, 1)
end
function RomExtractorGen3:tickPokemon(name, current, total)
local st = POKEMON_SUBTASKS[name]
if st then
local curFrac = (current or 0) / math.max(total or 1, 1)
local frac = st.min + curFrac * (st.max - st.min)
local label = st.label
if total and total > 1 then
label = label .. string.format(" (%d/%d)", current or 0, total)
end
self:report(frac, label, current, total)
else
local frac = (current or 0) / math.max(total or 1, 1)
self:report(frac, name or "Pokémon Data", current, total)
end
end
-- plus semantic module stubs for SEMANTIC_MODULES[3].
function RomExtractorGen3:writeRequiredMarkers(sha1)
local Versions = require("src.import.gba.versions")
local version = importVersion(sha1)
local cache = makeCache()
local metaRaw = cache:read(GBA_ROOT .. "/meta.json")
if not metaRaw or not metaRaw:find('"md5"%s*:%s*"' .. sha1 .. '"') or not metaRaw:find('"cache_version"%s*:%s*' .. tostring(Versions.CACHE_VERSION)) then
writeJson(GBA_ROOT .. "/meta.json", {
romSha1 = sha1,
md5 = sha1,
version = version,
cache_version = Versions.CACHE_VERSION,
native_version = Versions.NATIVE_VERSION or 5,
stub = false,
})
end
if not CacheFs.exists(GBA_ROOT .. "/maps.json") then
writeJson(GBA_ROOT .. "/maps.json", {
maps = {},
stub = true,
})
end
if not CacheFs.exists(GBA_ROOT .. "/intro/meta.json") then
writeJson(GBA_ROOT .. "/intro/meta.json", {
stub = true,
})
end
if not CacheFs.exists(GBA_ROOT .. "/audio/meta.json") then
writeJson(GBA_ROOT .. "/audio/meta.json", {
stub = true,
})
end
if not CacheFs.exists("data/generated/maps.lua") then
LuaWriter.write("data/generated/maps.lua", { stub = true, maps = {} })
end
if not CacheFs.exists("data/generated/intro.lua") then
LuaWriter.write("data/generated/intro.lua", {
stub = true,
generation = 3,
version = version,
})
end
if not CacheFs.exists("data/generated/audio.lua") then
LuaWriter.write("data/generated/audio.lua", { stub = true })
end
end
function RomExtractorGen3:runGbaExtract(sha1)
local Extract = require("src.import.gba.extract_island1")
local prevRoot, prevNative = Extract.CACHE_ROOT, Extract.NATIVE_ROOT
Extract.CACHE_ROOT = GBA_ROOT
Extract.NATIVE_ROOT = GBA_ROOT .. "/native"
local imports = self:sharedImports(sha1)
local cache = makeCache()
local runOk, runDetail = false, "extract did not run"
local callOk, err = pcall(function()
runOk, runDetail = Extract.run(imports, cache, function(stage, n, name, cur, total)
local curFrac = (cur or 0) / math.max(total or 1, 1)
local frac = ((stage or 0) + curFrac) / math.max(n or Extract.STAGE_COUNT or 7, 1)
local label = "World Maps: " .. tostring(name or "Processing")
if total and total > 1 then
label = label .. string.format(" (%d/%d)", cur or 0, total)
end
self:report(math.min(frac, 1.0), label, cur or 0, total or 1)
end)
end)
Extract.CACHE_ROOT = prevRoot
Extract.NATIVE_ROOT = prevNative
if not callOk then
return false, err
end
return runOk, runDetail
end
--- Species pack + party chrome into data/generated/gba/pokemon/.
function RomExtractorGen3:runPokemonExtract(sha1)
local Rom = require("src.import.gba.rom")
local PokemonExtract = require("src.import.gba.pokemon_extract")
local Extract = require("src.import.gba.extract_island1")
local prevRoot = Extract.CACHE_ROOT
Extract.CACHE_ROOT = GBA_ROOT
local cache = makeCache()
if PokemonExtract.ready(cache, GBA_ROOT) then
Extract.CACHE_ROOT = prevRoot
return true, { skipped = true }
end
local imports = self:sharedImports(sha1)
local version = importVersion(sha1)
local rom, openErr = Rom.open(imports, version)
if not rom then
Extract.CACHE_ROOT = prevRoot
return false, openErr or "rom open failed"
end
local ok, detail = pcall(function()
local pRes = PokemonExtract.run(rom, cache, {
cacheRoot = GBA_ROOT,
progress = function(name, cur, total)
self:tickPokemon(name or "pokemon", cur or 0, total or 1)
end,
})
local ItemsExtract = require("src.import.gba.items_extract")
ItemsExtract.run(rom, cache, { cacheRoot = GBA_ROOT })
local PokedexExtract = require("src.import.gba.pokedex_chrome_extract")
PokedexExtract.run(rom, cache, { cacheRoot = GBA_ROOT })
local StorageExtract = require("src.import.gba.storage_chrome_extract")
StorageExtract.run(rom, cache, { cacheRoot = GBA_ROOT })
local TextChromeExtract = require("src.import.gba.text_chrome_extract")
TextChromeExtract.run(rom, cache, { cacheRoot = GBA_ROOT })
local TrainerCardExtract = require("src.import.gba.trainer_card_extract")
TrainerCardExtract.run(rom, cache, { cacheRoot = GBA_ROOT })
local SeagallopExtract = require("src.import.gba.seagallop_extract")
SeagallopExtract.run(rom, cache, { cacheRoot = GBA_ROOT })
require("src.import.gba.cave_transition_extract").run(rom, cache, { cacheRoot = GBA_ROOT })
require("src.import.gba.weather_extract").run(rom, cache, { cacheRoot = GBA_ROOT })
require("src.import.gba.ingame_trades_extract").run(rom, cache, { cacheRoot = GBA_ROOT })
require("src.import.gba.union_room_classes_extract").run(rom, cache, { cacheRoot = GBA_ROOT })
local MapPreviewExtract = require("src.import.gba.map_preview_extract")
local mpOk, mpErr = pcall(MapPreviewExtract.run, rom, cache, {
cacheRoot = GBA_ROOT,
progress = function(name, cur, total)
self:tickPokemon(name or "map_preview", cur or 0, total or 1)
end,
})
if not mpOk then
print("[map_preview] warn: " .. tostring(mpErr))
end
return pRes
end)
Extract.CACHE_ROOT = prevRoot
if not ok then
return false, detail
end
return true, detail
end
function RomExtractorGen3:runAuxExtracts(sha1)
local GameVersion = require("src.core.GameVersion")
local Profile = require("src.core.game3.profile")
local wantedList = Profile.of(GameVersion.get()).extractors
local wanted = nil
if type(wantedList) == "table" and #wantedList > 0 then
wanted = {}
for _, name in ipairs(wantedList) do wanted[name] = true end
end
local function wantedExtractor(name)
return wanted == nil or wanted[name] == true
end
local Rom = require("src.import.gba.rom")
local RegionMapExtract = require("src.import.gba.region_map_extract")
local MapSectionsExtract = require("src.import.gba.map_sections_extract")
local MultichoiceExtract = require("src.import.gba.multichoice_extract")
local HealLocationsExtract = require("src.import.gba.heal_locations_extract")
local DoorAnimExtract = require("src.import.gba.door_anim_extract")
local SlotMachineExtract = require("src.import.gba.slot_machine_extract")
local TradeExtract = require("src.import.gba.trade_extract")
local LinkArtExtract = require("src.import.gba.link_art_extract")
local FameCheckerExtract = require("src.import.gba.fame_checker_extract")
local TeachyTvExtract = require("src.import.gba.teachy_tv_extract")
local MysteryGiftExtract = require("src.import.gba.mystery_gift_extract")
local TrainerTowerExtract = require("src.import.gba.trainer_tower_extract")
local TutorExtract = require("src.import.gba.tutor_extract")
local MuseumExtract = require("src.import.gba.museum_extract")
local MoveRelearnerExtract = require("src.import.gba.move_relearner_extract")
local EggExtract = require("src.import.gba.egg_extract")
local BattleAnimExtract = require("src.import.gba.battle_anim_extract")
local BattleAiExtract = require("src.import.gba.battle_ai_extract")
local CreditsExtract = require("src.import.gba.credits_extract")
local LeagueExtract = require("src.import.gba.league_extract")
local Extract = require("src.import.gba.extract_island1")
local prevRoot = Extract.CACHE_ROOT
Extract.CACHE_ROOT = GBA_ROOT
local cache = makeCache()
local needRegion = wantedExtractor("region_map_extract") and not RegionMapExtract.ready(cache, GBA_ROOT)
local needSections = wantedExtractor("map_sections_extract")
and not CacheFs.exists(GBA_ROOT .. "/region_map/map_sections.lua")
local needChoices = wantedExtractor("multichoice_extract") and not MultichoiceExtract.ready(cache, GBA_ROOT)
local needHeal = wantedExtractor("heal_locations_extract") and not HealLocationsExtract.ready(cache, GBA_ROOT)
local needDoors = wantedExtractor("door_anim_extract") and not DoorAnimExtract.ready(cache, GBA_ROOT)
local needSlots = wantedExtractor("slot_machine_extract") and not SlotMachineExtract.ready(cache, GBA_ROOT)
local needTrade = wantedExtractor("trade_extract") and not TradeExtract.ready(cache, GBA_ROOT)
local needLinkArt = wantedExtractor("link_art_extract") and not LinkArtExtract.ready(cache, GBA_ROOT)
local needFame = wantedExtractor("fame_checker_extract") and not FameCheckerExtract.ready(cache, GBA_ROOT)
local needTeachy = wantedExtractor("teachy_tv_extract") and not TeachyTvExtract.ready(cache, GBA_ROOT)
local needGift = wantedExtractor("mystery_gift_extract") and not MysteryGiftExtract.ready(cache, GBA_ROOT)
local needTower = wantedExtractor("trainer_tower_extract") and not TrainerTowerExtract.ready(cache, GBA_ROOT)
local needTutor = wantedExtractor("tutor_extract") and not TutorExtract.ready(cache, GBA_ROOT)
local needMuseum = wantedExtractor("museum_extract") and not MuseumExtract.ready(cache, GBA_ROOT)
local needRelearner = wantedExtractor("move_relearner_extract") and not MoveRelearnerExtract.ready(cache, GBA_ROOT)
local needEgg = wantedExtractor("egg_extract") and not EggExtract.ready(cache, GBA_ROOT)
local needAnims = wantedExtractor("battle_anim_extract") and not BattleAnimExtract.ready(cache, GBA_ROOT)
local needAi = wantedExtractor("battle_ai_extract") and not BattleAiExtract.ready(cache, GBA_ROOT)
local needCredits = wantedExtractor("credits_extract") and not CreditsExtract.ready(cache, GBA_ROOT)
local needLeague = wantedExtractor("league_extract") and not LeagueExtract.ready(cache, GBA_ROOT)
if not (needRegion or needSections or needChoices or needHeal or needDoors
or needSlots or needTrade or needLinkArt or needFame or needTeachy
or needGift or needTower or needTutor or needMuseum or needRelearner
or needEgg or needAnims or needAi or needCredits or needLeague) then
Extract.CACHE_ROOT = prevRoot
return true, { skipped = true }
end
local rom, openErr = Rom.open(self:sharedImports(sha1), importVersion(sha1))
if not rom then
Extract.CACHE_ROOT = prevRoot
return false, openErr or "rom open failed"
end
local ok, detail = pcall(function()
local out = {}
local step = 0
local totalSteps = 19
local function auxTick(name)
step = step + 1
self:report(step / totalSteps, "Game Data: " .. name, step, totalSteps)
end
if needRegion then
out.regionMap = RegionMapExtract.run(rom, cache, { cacheRoot = GBA_ROOT })
end
auxTick("Town Map")
if needSections then
MapSectionsExtract.run(rom, cache, { cacheRoot = GBA_ROOT })
local rel = GBA_ROOT .. "/region_map/map_sections.lua"
local body = cache:read(rel)
if type(body) ~= "string" or #body < 1024 then
error("map_sections extract wrote " .. tostring(body and #body or 0)
.. " bytes to " .. rel)
end
out.mapSections = #body
end
auxTick("Map Sections")
if needChoices then
out.multichoice = MultichoiceExtract.run(rom, cache, { cacheRoot = GBA_ROOT })
end
auxTick("Dialog Menus")
if needHeal then
local okHl, detailHl = HealLocationsExtract.run(rom, cache, { cacheRoot = GBA_ROOT })
if not okHl then print("[heal_locations] warn: " .. tostring(detailHl)) end
out.healLocations = detailHl
end
auxTick("Heal Locations")
if needDoors then
local okDr, detailDr = pcall(DoorAnimExtract.run, rom, cache, { cacheRoot = GBA_ROOT })
if not okDr then print("[door_extract] warn: " .. tostring(detailDr)) end
out.doors = okDr and detailDr or false
end
auxTick("Door Animations")
if needSlots then
local okSl, detailSl = pcall(SlotMachineExtract.run, rom, cache, { cacheRoot = GBA_ROOT })
if not okSl then print("[slot_machine_extract] warn: " .. tostring(detailSl)) end
out.slotMachine = okSl and detailSl or false
end
auxTick("Slot Machines")
if needTrade then
local okTr, detailTr = pcall(TradeExtract.run, rom, cache, { cacheRoot = GBA_ROOT })
if not okTr then print("[trade_extract] warn: " .. tostring(detailTr)) end
out.trade = okTr and detailTr or false
end
auxTick("Link Trade")
if needLinkArt then
local okLk, detailLk = pcall(LinkArtExtract.run, rom, cache, { cacheRoot = GBA_ROOT })
if not okLk then print("[link_art_extract] warn: " .. tostring(detailLk)) end
out.linkArt = okLk and detailLk or false
end
auxTick("Wireless Union")
if needFame then
local okFc, detailFc = pcall(FameCheckerExtract.run, rom, cache, { cacheRoot = GBA_ROOT })
if not okFc then print("[fame_checker_extract] warn: " .. tostring(detailFc)) end
out.fameChecker = okFc and detailFc or false
end
auxTick("Fame Checker")
if needTeachy then
local okTv, detailTv = pcall(TeachyTvExtract.run, rom, cache, { cacheRoot = GBA_ROOT })
if not okTv then print("[teachy_tv_extract] warn: " .. tostring(detailTv)) end
out.teachyTv = okTv and detailTv or false
end
auxTick("Teachy TV")
if needGift then
local okMg, detailMg = pcall(MysteryGiftExtract.run, rom, cache, { cacheRoot = GBA_ROOT })
if not okMg then print("[mystery_gift_extract] warn: " .. tostring(detailMg)) end
out.mysteryGift = okMg and detailMg or false
end
auxTick("Mystery Gift")
if needTower then
local okTt, detailTt = pcall(TrainerTowerExtract.run, rom, cache, { cacheRoot = GBA_ROOT })
if not okTt then print("[trainer_tower_extract] warn: " .. tostring(detailTt)) end
out.trainerTower = okTt and detailTt or false
end
auxTick("Trainer Tower")
if needTutor then
local okTu, detailTu = pcall(TutorExtract.run, rom, cache, { cacheRoot = GBA_ROOT })
if not okTu then print("[tutor_extract] warn: " .. tostring(detailTu)) end
out.tutor = okTu and detailTu or false
end
auxTick("Move Tutors")
if needMuseum then
local okMu, detailMu = pcall(MuseumExtract.run, rom, cache, { cacheRoot = GBA_ROOT })
if not okMu then print("[museum_extract] warn: " .. tostring(detailMu)) end
out.museum = okMu and detailMu or false
end
auxTick("Museum Exhibits")
if needRelearner then
local okMr, detailMr = pcall(MoveRelearnerExtract.run, rom, cache, { cacheRoot = GBA_ROOT })
if not okMr then print("[move_relearner_extract] warn: " .. tostring(detailMr)) end
out.moveRelearner = okMr and detailMr or false
end
auxTick("Move Relearner")
if needEgg then
local okEg, detailEg = pcall(EggExtract.run, rom, cache, { cacheRoot = GBA_ROOT })
if not okEg then print("[egg_extract] warn: " .. tostring(detailEg)) end
out.egg = okEg and detailEg or false
end
auxTick("Egg & Hatching")
if needAnims then
local okAn, detailAn = pcall(BattleAnimExtract.run, rom, cache, { cacheRoot = GBA_ROOT, force = false })
if not okAn then print("[battle_anim_extract] warn: " .. tostring(detailAn)) end
out.battleAnims = okAn and detailAn or false
end
auxTick("Battle Animations")
if needAi then
out.battleAi = BattleAiExtract.run(rom, cache, { cacheRoot = GBA_ROOT })
end
auxTick("Battle AI Scripts")
if needCredits then
out.credits = CreditsExtract.run(rom, cache, { cacheRoot = GBA_ROOT })
end
auxTick("Staff Credits")
if needLeague then
out.league = LeagueExtract.run(rom, cache, { cacheRoot = GBA_ROOT })
end
auxTick("League Lighting & Diploma")
return out
end)
rom:clearCache()
Extract.CACHE_ROOT = prevRoot
if not ok then return false, detail end
return true, detail
end
function RomExtractorGen3:runIntroAudio(sha1)
self:report(0.05, "Audio & Intro: Initializing", 0, 3)
local cache = makeCache()
if cache:exists(GBA_ROOT .. "/intro/meta.json") and cache:exists(GBA_ROOT .. "/audio/meta.json") then
local im = cache:read(GBA_ROOT .. "/intro/meta.json")
if im and not im:find('"stub"%s*:%s*true') then
self:report(1.00, "Audio Streams Ready", 3, 3)
return true, true
end
end
local RevisionView = require("src.import.gba.revision_view")
local imports = self:sharedImports(sha1)
local version = importVersion(sha1)
local info = imports:info(version)
local romShim = { data = RevisionView.forImports(imports, version, info) or self.romData }
local Intro = require("src.import.gba.extract_intro")
local Naming = require("src.import.gba.extract_naming")
local AudioExt = require("src.import.gba.extract_audio")
self:report(0.20, "Intro: Cutscene Sequence", 1, 3)
local okI, metaI = Intro.run(romShim, cache, { sha1 = sha1, root = GBA_ROOT .. "/intro" })
self:report(0.50, "Intro: Naming Screen Graphics", 2, 3)
local okN = Naming.run(romShim, cache, { sha1 = sha1, root = GBA_ROOT .. "/naming" })
self:report(0.75, "Audio: Music & Sound Streams", 3, 3)
local okA, metaA = AudioExt.run(romShim, cache, { sha1 = sha1, root = GBA_ROOT .. "/audio" })
writeJson(GBA_ROOT .. "/intro/extract_status.json", { ok = okI == true })
writeJson(GBA_ROOT .. "/naming/extract_status.json", { ok = okN == true })
writeJson(GBA_ROOT .. "/audio/extract_status.json", { ok = okA == true })
self:report(1.00, "Audio & Intro Ready", 3, 3)
return okI, okA, metaI, metaA
end
function RomExtractorGen3:runParallel(sha1)
if os.getenv("POKEPORT_NO_THREAD") == "1" then
return false, "POKEPORT_NO_THREAD set"
end
if not (love and love.thread and love.thread.newThread and love.timer) then
return false, "love.thread unavailable"
end
local ch_name = "gba_extract_" .. tostring(love.timer.getTime()):gsub("%.", "") .. "_" .. tostring(math.random(10000, 99999))
local ch = love.thread.getChannel(ch_name)
local tasks = { "gba", "pokemon", "aux", "intro_audio" }
local threads = {}
local prefix = CacheFs.prefix or ""
for _, t in ipairs(tasks) do
local okTh, th = pcall(love.thread.newThread, "src/import/gba/extract_worker.lua")
if not okTh or not th then
return false, "failed to spawn thread for " .. t .. ": " .. tostring(th)
end
threads[t] = th
th:start(t, prefix, self.romData, sha1, ch_name)
end
local done_count = 0
local errors = {}
local task_progress = { gba = 0, pokemon = 0, aux = 0, intro_audio = 0 }
local weights = { gba = 0.40, pokemon = 0.35, aux = 0.15, intro_audio = 0.10 }
while done_count < #tasks do
local msg = ch:pop()
if msg then
if msg.type == "progress" then
task_progress[msg.task] = math.max(task_progress[msg.task] or 0, math.min(1.0, msg.fraction or 0))
local total_pct = 0.03
for k, w in pairs(weights) do
total_pct = total_pct + (task_progress[k] or 0) * w * 0.95
end
self:report(total_pct, msg.stage or "Extracting", msg.current or 0, msg.stageTotal or 1)
elseif msg.type == "done" then
done_count = done_count + 1
task_progress[msg.task] = 1.0
local total_pct = 0.03
for k, w in pairs(weights) do
total_pct = total_pct + (task_progress[k] or 0) * w * 0.95
end
self:report(total_pct, "Finalizing " .. tostring(msg.task), done_count, #tasks)
if not msg.ok then
errors[#errors + 1] = msg.task .. ": " .. tostring(msg.error)
end
end
else
love.timer.sleep(0.005)
end
end
if #errors > 0 then
error("Parallel extraction error:\n" .. table.concat(errors, "\n"))
end
if not CacheFs.exists(GBA_ROOT .. "/maps.json") then
writeJson(GBA_ROOT .. "/maps.json", { maps = {}, from_extract = true })
end
return true
end
function RomExtractorGen3:run()
local sha1 = self:ensureSha1()
self:report(0.01, "Initializing Markers", 0, 1)
self:writeRequiredMarkers(sha1)
self:report(0.03, "Markers Ready", 1, 1)
local okPar, parErr = self:runParallel(sha1)
if okPar then
writeJson(GBA_ROOT .. "/pokemon/extract_status.json", { ok = true, error = nil })
writeJson(GBA_ROOT .. "/region_map/extract_status.json", { ok = true, error = nil })
self:report(1.00, "Ready", 1, 1)
collectgarbage("collect")
return {
romSha1 = sha1,
extractOk = true,
pokemonOk = true,
auxOk = true,
}
end
-- Fallback sequential execution path
local ok, detail = self:runGbaExtract(sha1)
if ok then
if not CacheFs.exists(GBA_ROOT .. "/maps.json") then
writeJson(GBA_ROOT .. "/maps.json", { maps = {}, from_extract = true })
end
else
writeJson(GBA_ROOT .. "/extract_status.json", {
ok = false,
error = tostring(detail),
romSha1 = sha1,
})
error("GBA extract failed: " .. tostring(detail))
end
self:report(0.42, "World Maps Ready", 1, 1)
collectgarbage("collect")
local okPoke, pokeDetail = self:runPokemonExtract(sha1)
writeJson(GBA_ROOT .. "/pokemon/extract_status.json", {
ok = okPoke == true,
error = (not okPoke) and tostring(pokeDetail) or nil,
})
if not okPoke then
error("Pokemon extract failed: " .. tostring(pokeDetail))
end
self:report(0.88, "Game Data & Chrome Ready", 1, 1)
collectgarbage("collect")
local okAux, auxDetail = self:runAuxExtracts(sha1)
writeJson(GBA_ROOT .. "/region_map/extract_status.json", {
ok = okAux == true,
error = (not okAux) and tostring(auxDetail) or nil,
})
if not okAux then
error("Region map / script table extract failed: " .. tostring(auxDetail))
end
collectgarbage("collect")
self:runIntroAudio(sha1)
self:report(0.98, "Finalizing Cache", 1, 1)
collectgarbage("collect")
self:report(1.00, "Ready", 1, 1)
return {
romSha1 = sha1,
extractOk = ok == true,
pokemonOk = okPoke == true,
auxOk = okAux == true,
detail = detail,
}
end
return RomExtractorGen3