mirror of
https://github.com/bryanthaboi/gen1recomp
synced 2026-10-02 15:50:39 -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.
708 lines
27 KiB
Lua
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
|