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 Plans = require("src.import.gba.plans.registry") 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) local version = GameVersion.forSha1(sha1) if not version or GameVersion.generation(version) ~= 3 then error("unknown gen 3 ROM sha1 " .. tostring(sha1), 2) end return version 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.50, label = "Pokémon Species & Sprites" }, ["learnsets"] = { min = 0.50, max = 0.58, label = "Move Learnsets" }, ["battle_moves"] = { min = 0.58, max = 0.62, label = "Battle Moves Data" }, ["party_chrome"] = { min = 0.62, max = 0.68, label = "Party UI Graphics" }, ["battle_chrome"] = { min = 0.68, max = 0.74, label = "Battle UI Graphics" }, ["pokedex_entries"] = { min = 0.74, max = 0.78, label = "Pokédex Database" }, ["pokedex_categories"]= { min = 0.78, max = 0.80, label = "Pokédex Categories" }, ["pokedex_orders"] = { min = 0.80, max = 0.82, label = "Pokédex Sorting" }, ["pokedex_done"] = { min = 0.82, max = 0.84, label = "Pokédex Complete" }, ["storage_chrome"] = { min = 0.84, max = 0.88, label = "PC Storage Chrome" }, ["battle_transition"] = { min = 0.88, max = 0.92, label = "Battle Transitions" }, ["summary_chrome"] = { min = 0.92, max = 0.96, label = "Summary Screen Graphics" }, ["bag_chrome"] = { min = 0.96, max = 0.98, label = "Bag & Items Graphics" }, ["shop_chrome"] = { min = 0.98, max = 1.00, label = "Mart & Shop Graphics" }, } function RomExtractorGen3.new(romData, manifest, progressCb, romSha1) local sha1 = romSha1 or (manifest and manifest.romSha1) or hexSha1(romData) local version = importVersion(sha1) require("src.import.gba.versions").select(sha1) return setmetatable({ version = version, plan = Plans.of(version), 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 for _, d in ipairs(self.plan.dirs or {}) do if love and love.filesystem and love.filesystem.createDirectory then local p = (CacheFs.prefix or "") .. GBA_ROOT .. d pcall(love.filesystem.createDirectory, p) end end end function RomExtractorGen3:runGbaExtract(sha1, skipScriptsAndOw) 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, { skipScriptsAndOw = skipScriptsAndOw == true }) end) Extract.CACHE_ROOT = prevRoot Extract.NATIVE_ROOT = prevNative if not callOk then return false, err end return runOk, runDetail end function RomExtractorGen3:runScriptsAndOwExtract(sha1) local Extract = require("src.import.gba.extract_island1") local prevRoot = Extract.CACHE_ROOT Extract.CACHE_ROOT = GBA_ROOT local imports = self:sharedImports(sha1) local cache = makeCache() local runOk, runDetail = false, "scripts_ow did not run" local callOk, err = pcall(function() runOk = Extract.runScriptsAndOw(imports, cache, function(cur, total, stageName) local frac = (cur or 0) / math.max(total or 4, 1) self:report(frac, "Scripts & OW: " .. tostring(stageName or "Processing"), cur or 0, total or 4) end) return runOk end) Extract.CACHE_ROOT = prevRoot 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, spMin, spMax) 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 not spMin and 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, spMin = spMin or 0, spMax = spMax, progress = function(name, cur, total) self:tickPokemon(name or "pokemon", cur or 0, total or 1) end, }) for _, module in ipairs(self.plan.pokemonAfter or {}) do require(Plans.moduleFor(module)).run(rom, cache, { cacheRoot = GBA_ROOT }) end return pRes end) Extract.CACHE_ROOT = prevRoot if not ok then return false, detail end return true, detail end function RomExtractorGen3:runPokemonGfxExtract(sha1, spMin, spMax) 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 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 cache = makeCache() local ok, detail = pcall(function() return PokemonExtract.run(rom, cache, { cacheRoot = GBA_ROOT, spMin = spMin or 0, spMax = spMax or 200, onlySpeciesGfx = true, progress = function(name, cur, total) self:tickPokemon(name or "pokemon", cur or 0, total or 1) end, }) end) Extract.CACHE_ROOT = prevRoot if not ok then return false, detail end return true, detail end function RomExtractorGen3:auxWanted() local Profile = require("src.core.game3.profile") local ok, row = pcall(Profile.of, self.version) if not ok or type(row) ~= "table" or row.id ~= self.version then return nil end local list = row.extractors if type(list) ~= "table" or #list == 0 then return nil end local wanted = {} for _, name in ipairs(list) do wanted[name] = true end return wanted end function RomExtractorGen3:runAuxExtracts(sha1) local wanted = self:auxWanted() local Rom = require("src.import.gba.rom") local entries = {} for _, entry in ipairs(self.plan.aux or {}) do entries[#entries + 1] = { entry = entry, mod = require(Plans.moduleFor(entry.name)) } end local Extract = require("src.import.gba.extract_island1") local prevRoot = Extract.CACHE_ROOT Extract.CACHE_ROOT = GBA_ROOT local cache = makeCache() local any = false for _, e in ipairs(entries) do local name, mod = e.entry.name, e.mod if wanted ~= nil and not wanted[name] then e.need = false elseif e.entry.mode == "sections" then e.need = not CacheFs.exists(GBA_ROOT .. "/region_map/map_sections.lua") else e.need = not (mod.ready and mod.ready(cache, GBA_ROOT)) end any = any or e.need end if not any 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 = self.plan.auxSteps or #entries local function auxTick(name) step = step + 1 self:report(step / totalSteps, "Game Data: " .. name, step, totalSteps) end for _, e in ipairs(entries) do local entry, mod = e.entry, e.mod if e.need then local opts = { cacheRoot = GBA_ROOT } for k, v in pairs(entry.opts or {}) do opts[k] = v end local tag = entry.tag or entry.name if entry.mode == "sections" then mod.run(rom, cache, opts) 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[entry.key] = #body elseif entry.mode == "result" then local okR, detailR = mod.run(rom, cache, opts) if not okR then print("[" .. tag .. "] warn: " .. tostring(detailR)) end out[entry.key] = detailR elseif entry.mode == "warn" then local okW, detailW = pcall(mod.run, rom, cache, opts) if not okW then print("[" .. tag .. "] warn: " .. tostring(detailW)) end out[entry.key] = okW and detailW or false else out[entry.key] = mod.run(rom, cache, opts) end end auxTick(entry.label or entry.name) end return out end) rom:clearCache() Extract.CACHE_ROOT = prevRoot if not ok then return false, detail end return true, detail end function RomExtractorGen3:runStepsTask(sha1, task) local Rom = require("src.import.gba.rom") local cache = makeCache() local steps = task.steps or {} local rom, openErr local ok, detail = pcall(function() local out = {} for i, step in ipairs(steps) do local mod = require(Plans.moduleFor(step.name)) if not (mod.ready and mod.ready(cache, GBA_ROOT)) then if not rom then rom, openErr = Rom.open(self:sharedImports(sha1), importVersion(sha1)) if not rom then error(openErr or "rom open failed") end end local opts = { cacheRoot = GBA_ROOT } for k, v in pairs(step.opts or {}) do opts[k] = v end out[step.name] = mod.run(rom, cache, opts) end self:report(i / math.max(#steps, 1), task.id .. ": " .. (step.label or step.name), i, #steps) end return out end) if rom then rom:clearCache() end if not ok then return false, detail end return true, detail end function RomExtractorGen3:speciesRanges() local Versions = require("src.import.gba.versions") local last = (Versions.NUM_SPECIES or 412) - 1 local split = self.plan.speciesSplit or math.floor(last / 2) return split, last end function RomExtractorGen3:runTask(task, sha1) local spec for _, t in ipairs(self.plan.tasks or {}) do if t.id == task then spec = t end end if not spec then error("unknown extract task: " .. tostring(task)) end local run = spec.run or "steps" if run == "gba" then return self:runGbaExtract(sha1, true) elseif run == "scripts_ow" then return self:runScriptsAndOwExtract(sha1) elseif run == "pokemon" then local split, last = self:speciesRanges() return self:runPokemonExtract(sha1, split + 1, last) elseif run == "pokemon_gfx" then local split = self:speciesRanges() return self:runPokemonGfxExtract(sha1, 0, split) elseif run == "aux" then return self:runAuxExtracts(sha1) elseif run == "intro_audio" then return self:runIntroAudio(sha1) elseif run == "steps" then return self:runStepsTask(sha1, spec) end error("unknown extract runner: " .. tostring(run)) 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 local function maxWorkers(taskCount) local override = tonumber(os.getenv("POKEPORT_EXTRACT_WORKERS") or "") if override and override >= 1 then return math.min(taskCount, math.floor(override)) end if os.getenv("HANDHELD") == "1" or os.getenv("POKEPORT_HANDHELD") == "1" or os.getenv("PORTMASTER") == "1" then return math.min(taskCount, 1) end local osName = love.system and love.system.getOS and love.system.getOS() or "" if osName == "iOS" or osName == "Android" or osName == "NX" then return math.min(taskCount, 2) end local cores = love.system and love.system.getProcessorCount and love.system.getProcessorCount() or 4 return math.max(1, math.min(taskCount, cores - 1, 6)) 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, task_progress, weights = {}, {}, {} for _, t in ipairs(self.plan.tasks or {}) do tasks[#tasks + 1] = t.id task_progress[t.id] = 0 weights[t.id] = t.weight or (1 / math.max(#self.plan.tasks, 1)) end local prefix = CacheFs.prefix or "" local workerCode = nil if love and love.filesystem and love.filesystem.read then workerCode = love.filesystem.read("src/import/gba/extract_worker.lua") end local running, nextTask = {}, 1 local function spawnUpTo(limit) while nextTask <= #tasks and #running < limit do local t = tasks[nextTask] local okTh, th = pcall(love.thread.newThread, workerCode or "src/import/gba/extract_worker.lua") if not okTh or not th then return false, "failed to spawn thread for " .. t .. ": " .. tostring(th) end th:start(t, prefix, self.romData, sha1, ch_name) running[#running + 1] = { task = t, thread = th } nextTask = nextTask + 1 end return true end local function retire(task) for i, r in ipairs(running) do if r.task == task then table.remove(running, i) return end end end local limit = maxWorkers(#tasks) local okSpawn, spawnErr = spawnUpTo(limit) if not okSpawn then return false, spawnErr end local done_count = 0 local errors = {} 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 retire(msg.task) okSpawn, spawnErr = spawnUpTo(limit) if not okSpawn then return false, spawnErr end end else for i = #running, 1, -1 do local err = running[i].thread:getError() if err then errors[#errors + 1] = running[i].task .. ": " .. tostring(err) table.remove(running, i) done_count = done_count + 1 end end okSpawn, spawnErr = spawnUpTo(limit) if not okSpawn then return false, spawnErr end love.timer.sleep(0.005) end end if #errors > 0 then return false, "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:hasTask(id) for _, t in ipairs(self.plan.tasks or {}) do if t.id == id then return true end end return false 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, parRes, parErr = pcall(function() return self:runParallel(sha1) end) if okPar and parRes then if self:hasTask("pokemon") then writeJson(GBA_ROOT .. "/pokemon/extract_status.json", { ok = true, error = nil }) end if self:hasTask("aux") then writeJson(GBA_ROOT .. "/region_map/extract_status.json", { ok = true, error = nil }) end self:report(1.00, "Ready", 1, 1) collectgarbage("collect") return { romSha1 = sha1, extractOk = true, pokemonOk = true, auxOk = true, } end print("[RomExtractorGen3] Parallel extraction fell back to sequential: " .. tostring(parErr or parRes)) local ok, detail, okPoke, okAux = true, nil, true, true for _, stage in ipairs(self.plan.sequential or {}) do if stage == "gba" then 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") elseif stage == "pokemon" then local pokeDetail 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") elseif stage == "aux" then local auxDetail 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") elseif stage == "intro_audio" then self:runIntroAudio(sha1) self:report(0.98, "Finalizing Cache", 1, 1) collectgarbage("collect") else local okS, detailS = self:runTask(stage, sha1) if not okS then error(tostring(stage) .. " extract failed: " .. tostring(detailS)) end collectgarbage("collect") end end if not CacheFs.exists(GBA_ROOT .. "/maps.json") then writeJson(GBA_ROOT .. "/maps.json", { maps = {}, from_extract = true }) end self:report(1.00, "Ready", 1, 1) return { romSha1 = sha1, extractOk = ok == true, pokemonOk = okPoke == true, auxOk = okAux == true, detail = detail, } end return RomExtractorGen3