mirror of
https://github.com/bryanthaboi/gen1recomp
synced 2026-09-26 13:33:27 -04:00
349 lines
12 KiB
Lua
349 lines
12 KiB
Lua
-- FireRed Gen 3 extractor: GBA ROM → firered/ cache under CacheFs.prefix.
|
|
-- Parallel to RomExtractor / RomExtractorGen2. Full Island-1 demake lives in
|
|
-- src/import/gba/extract_island1.lua; this module publishes the CacheContract
|
|
-- override files and, when possible, runs that extract under
|
|
-- data/generated/gba/.
|
|
|
|
local CacheFs = require("src.import.CacheFs")
|
|
local LuaWriter = require("src.import.LuaWriter")
|
|
|
|
local RomExtractorGen3 = {}
|
|
RomExtractorGen3.__index = RomExtractorGen3
|
|
|
|
local STAGE_COUNT = 5
|
|
local GBA_ROOT = "data/generated/gba"
|
|
|
|
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 Json = require("src.link.Json")
|
|
writeText(rel, Json.encode(obj) .. "\n")
|
|
end
|
|
|
|
local function makeImports(romData, sha1)
|
|
return {
|
|
info = function(_, id)
|
|
if id ~= "firered" and id ~= "leafgreen" then
|
|
return nil, "undeclared"
|
|
end
|
|
return {
|
|
id = "firered",
|
|
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 id ~= "firered" and id ~= "leafgreen" 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 SUBTASK_RANGES = {
|
|
["pokemon"] = { min = 0.42, max = 0.52, label = "Pokémon Species & Icons" },
|
|
["learnsets"] = { min = 0.52, max = 0.57, label = "Move Learnsets" },
|
|
["battle_moves"] = { min = 0.57, max = 0.60, label = "Battle Moves Data" },
|
|
["party_chrome"] = { min = 0.60, max = 0.63, label = "Party UI Graphics" },
|
|
["battle_chrome"] = { min = 0.63, max = 0.66, label = "Battle UI Graphics" },
|
|
["pokedex_entries"] = { min = 0.66, max = 0.68, label = "Pokédex Database" },
|
|
["pokedex_categories"]= { min = 0.68, max = 0.69, label = "Pokédex Categories" },
|
|
["pokedex_orders"] = { min = 0.69, max = 0.70, label = "Pokédex Sorting" },
|
|
["pokedex_done"] = { min = 0.70, max = 0.71, label = "Pokédex Complete" },
|
|
["storage_chrome"] = { min = 0.71, max = 0.74, label = "PC Storage Chrome" },
|
|
["battle_transition"] = { min = 0.74, max = 0.77, label = "Battle Transitions" },
|
|
["summary_chrome"] = { min = 0.77, max = 0.80, label = "Summary Screen Graphics" },
|
|
["bag_chrome"] = { min = 0.80, max = 0.83, label = "Bag & Items Graphics" },
|
|
["shop_chrome"] = { min = 0.83, max = 0.85, label = "Mart & Shop Graphics" },
|
|
["trainers"] = { min = 0.85, max = 0.86, label = "Trainer Data & Parties" },
|
|
["battle_ai"] = { min = 0.86, max = 0.875, label = "Battle AI Scripts" },
|
|
["map_preview"] = { min = 0.875, max = 0.88, label = "Location Previews" },
|
|
}
|
|
|
|
function RomExtractorGen3.new(romData, manifest, progressCb, romSha1)
|
|
return setmetatable({
|
|
romData = romData,
|
|
manifest = manifest,
|
|
progress = progressCb,
|
|
romSha1 = romSha1,
|
|
stage = 0,
|
|
_lastPct = 0,
|
|
}, RomExtractorGen3)
|
|
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:tick(name, current, total)
|
|
local st = SUBTASK_RANGES[name]
|
|
if st then
|
|
local frac = (current or 0) / math.max(total or 1, 1)
|
|
local pct = st.min + frac * (st.max - st.min)
|
|
self:report(pct, st.label, current, total)
|
|
else
|
|
local frac = (current or 0) / math.max(total or 1, 1)
|
|
self:report((self.stage - 1 + frac) / STAGE_COUNT, name, current, total)
|
|
end
|
|
end
|
|
|
|
-- Minimal trees CacheContract.VERSION_REQUIRED_FILES_OVERRIDE.firered needs,
|
|
-- plus semantic module stubs for SEMANTIC_MODULES[3].
|
|
function RomExtractorGen3:writeRequiredMarkers(sha1)
|
|
local Versions = require("src.import.gba.versions")
|
|
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 = "firered",
|
|
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 = "firered",
|
|
})
|
|
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 = makeImports(self.romData, 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 pct = 0.03 + math.min(frac, 1.0) * 0.39
|
|
self:report(pct, name or "World & Maps", 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 = makeImports(self.romData, sha1)
|
|
local rom, openErr = Rom.open(imports, "firered")
|
|
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:tick(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 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:tick(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:runIntroAudio(sha1)
|
|
self:report(0.88, "Intro Cutscene & Audio", 0, 1)
|
|
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(0.98, "Audio Streams Ready", 1, 1)
|
|
return true, true
|
|
end
|
|
end
|
|
|
|
local romShim = { data = require("src.import.gba.revision_view").apply(self.romData, sha1) }
|
|
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.90, "Intro Cutscene", 0, 1)
|
|
local okI, metaI = Intro.run(romShim, cache, { sha1 = sha1, root = GBA_ROOT .. "/intro" })
|
|
self:report(0.93, "Naming Screen", 0, 1)
|
|
local okN = Naming.run(romShim, cache, { sha1 = sha1, root = GBA_ROOT .. "/naming" })
|
|
self:report(0.95, "Audio Sequences & Streams", 0, 1)
|
|
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(0.98, "Audio Streams Ready", 1, 1)
|
|
return okI, okA, metaI, metaA
|
|
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 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)
|
|
|
|
local okPoke, pokeDetail = self:runPokemonExtract(sha1)
|
|
writeJson(GBA_ROOT .. "/pokemon/extract_status.json", {
|
|
ok = okPoke == true,
|
|
error = okPoke and nil or tostring(pokeDetail),
|
|
})
|
|
if not okPoke then
|
|
error("Pokemon extract failed: " .. tostring(pokeDetail))
|
|
end
|
|
self:report(0.88, "Game Data & Chrome Ready", 1, 1)
|
|
|
|
self:runIntroAudio(sha1)
|
|
self:report(0.98, "Finalizing Cache", 1, 1)
|
|
|
|
self:report(1.00, "Ready", 1, 1)
|
|
return {
|
|
romSha1 = sha1,
|
|
extractOk = ok == true,
|
|
pokemonOk = okPoke == true,
|
|
detail = detail,
|
|
}
|
|
end
|
|
|
|
return RomExtractorGen3
|