Files
gen1recomp/src/mods/Gen3Compat.lua
T
2026-09-30 15:47:06 -04:00

2338 lines
76 KiB
Lua

-- Gen 1 module facades for a FireRed boot: the Gen 1 API backed by src/core/game3.
-- Contract mirrors src/mods/Gen2Compat.lua; docs/mod-api-gen3-compat.md.
local Logger = require("src.core.Logger")
local Runtime = require("src.mods.Runtime")
local GameVersion = require("src.core.GameVersion")
local Gen3Compat = {}
local rawRequire = require
local resolveGame = nil
local lastGame = nil
local built = {}
local claimants = {}
local warned = {}
local function warnOnce(key, fmt, ...)
if warned[key] then return end
warned[key] = true
Logger.warn(fmt, ...)
end
local function who(name)
local ids = claimants[name]
if not ids or #ids == 0 then return "a gen3 mod" end
return table.concat(ids, ", ")
end
local function live()
local g = resolveGame and resolveGame() or nil
return g or lastGame
end
local function unbacked(module, member, why)
return function()
warnOnce(module .. "." .. member,
"[%s] %s.%s has no Gen 3 backing: %s", who(module), module, member, why)
return nil
end
end
local function g3(name)
local ok, module = pcall(rawRequire, "src.core.game3." .. name)
if ok then return module end
return nil
end
local function ui3(name)
local ok, module = pcall(rawRequire, "src.ui.game3." .. name)
if ok then return module end
return nil
end
local function session()
local R = package.loaded["src.core.game3.runtime"]
local s = R and R.getSession and R.getSession()
if s then return s end
local g = live()
return g and g.session or nil
end
local function space()
return package.loaded["src.core.game3.scripting.space"]
end
local function flagStore()
local S = space()
return S and S.store or nil
end
local function inField()
local g = live()
if g and g.phase ~= nil and g.phase ~= "field" then return false end
return session() ~= nil
end
local COVERAGE = {}
Gen3Compat.COVERAGE_VERSION = 1
Gen3Compat.STATUS = { BACKED = "backed", WARNED = "warned", ABSENT = "absent" }
local function words(s)
local out = {}
for w in tostring(s or ""):gmatch("%S+") do out[#out + 1] = w end
return out
end
-- ------- ids
local MAP_PREFIX = "FR_"
Gen3Compat.FAMILIES = { frlg = true }
function Gen3Compat.appliesTo(version)
if type(version) ~= "string" then return true end
if not GameVersion.VERSIONS[version] then return true end
return Gen3Compat.FAMILIES[GameVersion.layout(version)] == true
end
function Gen3Compat.gen1MapId(id)
if type(id) ~= "string" then return id end
if id:sub(1, #MAP_PREFIX) == MAP_PREFIX then return id:sub(#MAP_PREFIX + 1) end
return id
end
function Gen3Compat.gen3MapId(id)
if type(id) ~= "string" then return id end
local g = live()
local maps = g and g.data and g.data.maps
if maps then
if maps[id] then return id end
if maps[MAP_PREFIX .. id] then return MAP_PREFIX .. id end
return nil
end
if id:sub(1, #MAP_PREFIX) == MAP_PREFIX then return id end
return MAP_PREFIX .. id
end
local function canonicalName(display)
if type(display) ~= "string" then return nil end
local s = display:upper()
s = s:gsub("♂", "_M"):gsub("♀", "_F")
s = s:gsub("[%.']", ""):gsub("[%s%-]+", "_")
s = s:gsub("[^%w_]", ""):gsub("_+", "_"):gsub("^_", ""):gsub("_$", "")
return s
end
function Gen3Compat.speciesId(ref)
if type(ref) == "number" then return ref end
if type(ref) ~= "string" then return nil end
local n = tonumber(ref)
if n then return n end
local P = g3("pokemon")
return P and P.speciesFromName and P.speciesFromName(ref) or nil
end
function Gen3Compat.speciesName(sp)
if type(sp) == "string" and not tonumber(sp) then return sp end
sp = tonumber(sp)
if not sp then return nil end
local P = g3("pokemon")
if P and P.keyName then
local key = P.keyName(sp)
if key then return key end
end
if not (P and P.name) then return nil end
return canonicalName(P.name(sp))
end
function Gen3Compat.itemId(ref)
if type(ref) == "number" then return ref end
local D = g3("items_data")
local num = D and D.toNumericId and D.toNumericId(ref)
if num then return num end
if type(ref) == "string" and D and D.toNumericId then
return D.toNumericId((ref:gsub("_", " ")))
end
return nil
end
function Gen3Compat.moveId(ref)
if type(ref) == "number" then return ref end
if type(ref) ~= "string" then return nil end
local n = tonumber(ref)
if n then return n end
local M = g3("battle.moves")
if not M then return nil end
local norm = M.normalizeId and M.normalizeId(ref) or ref
local num = M.numForName and M.numForName(norm) or nil
if num then return num end
local okS, Schemas = pcall(rawRequire, "src.mods.Schemas")
if okS and Schemas and Schemas.gen3View then
local n = Schemas.gen3View.moveNum(M, ref)
if type(n) == "number" then return n end
end
return nil
end
local function schemaId(display)
if type(display) ~= "string" or display == "" then return nil end
local okS, Schemas = pcall(rawRequire, "src.mods.Schemas")
if okS and Schemas and Schemas.gen3View then
return Schemas.gen3View.idOf(display)
end
return canonicalName(display)
end
function Gen3Compat.moveName(ref)
if type(ref) == "table" then ref = ref.numId or ref.id or ref.move end
if type(ref) == "string" and not tonumber(ref) then return ref end
local num = tonumber(ref)
if not num or num < 1 then return nil end
local P = g3("pokemon")
local name = P and P.moveName and P.moveName(num)
if type(name) ~= "string" or name == "" or name:match("^MOVE ") then
local M = g3("battle.moves")
name = M and M.BY_NUM and M.BY_NUM[num] or nil
end
return schemaId(name) or tostring(num)
end
function Gen3Compat.itemName(ref)
if ref == nil or ref == 0 then return nil end
if type(ref) == "string" and not tonumber(ref) then return ref end
local num = tonumber(ref)
if not num or num < 1 then return nil end
local D = g3("items_data")
local name = D and D.displayName and D.displayName(num)
return schemaId(name) or tostring(num)
end
local function typeName(t)
local okS, Schemas = pcall(rawRequire, "src.mods.Schemas")
local types = okS and Schemas and Schemas.gen3View and Schemas.gen3View.TYPES
local n = tonumber(t)
return (types and n and types[n]) or t
end
function Gen3Compat.moveView(ref)
local M = g3("battle.moves")
if not M then return nil end
local def = type(ref) == "table" and ref.effect ~= nil and ref or M.get(ref)
if not def then return nil end
local num = tonumber(def.numId) or Gen3Compat.moveId(type(ref) == "table" and def.id or ref)
return {
id = Gen3Compat.moveName(num) or def.id, index = num, num = num,
name = M.displayName and M.displayName(num or def.id) or def.id,
type = typeName(def.type), gen3Type = tonumber(def.type),
power = def.power, accuracy = def.accuracy, pp = def.pp,
priority = def.priority, category = def.category, effect = def.effect,
target = def.target, secondaryChance = def.secondaryChance,
}
end
function Gen3Compat.speciesView(sp)
sp = Gen3Compat.speciesId(sp)
if not sp then return nil end
local P = g3("pokemon")
local meta = P and P.speciesMeta and P.speciesMeta(sp) or {}
return {
id = Gen3Compat.speciesName(sp), index = sp,
catchRate = tonumber(meta.catchRate), expYield = tonumber(meta.expYield),
growthRate = tonumber(meta.growthRate),
}
end
local function nameList(list, toName)
if type(list) ~= "table" then return list end
local out = {}
for _, v in ipairs(list) do
local name = (tonumber(v) or 0) ~= 0 and toName(v) or nil
if name then out[#out + 1] = name end
end
return out
end
function Gen3Compat.partyNames(party)
local out = {}
for i, mon in ipairs(party or {}) do
local row = {}
for k, v in pairs(mon) do row[k] = v end
local sp = tonumber(mon.species or mon.speciesId)
row.species = Gen3Compat.speciesName(sp) or mon.species
row.speciesId = sp
if type(mon.moves) == "table" then
row.moves = nameList(mon.moves, Gen3Compat.moveName)
row.moveIds = {}
for j, v in ipairs(mon.moves) do row.moveIds[j] = v end
end
if mon.heldItem ~= nil then
row.heldItem = Gen3Compat.itemName(mon.heldItem)
row.heldItemId = tonumber(mon.heldItem)
end
out[i] = row
end
return out
end
function Gen3Compat.partyNums(party)
local out = {}
for i, mon in ipairs(party or {}) do
local row = {}
for k, v in pairs(mon) do row[k] = v end
row.species = Gen3Compat.speciesId(mon.species) or tonumber(mon.speciesId) or 0
row.speciesId = nil
if type(mon.moves) == "table" then
local list = {}
for j, v in ipairs(mon.moves) do list[j] = Gen3Compat.moveId(v) or 0 end
row.moves = list
end
row.moveIds = nil
if mon.heldItem ~= nil then
row.heldItem = Gen3Compat.itemId(mon.heldItem)
end
row.heldItemId = nil
out[i] = row
end
return out
end
-- ------- live views over the session
local function flagId(name)
if type(name) == "number" then return name end
if type(name) ~= "string" then return nil end
local n = tonumber(name)
if n then return n end
local Flags = g3("scripting.flags")
if not Flags then return nil end
local ok, t = pcall(Flags.active, session())
local ids = ok and t and t.IDS or Flags.IDS
return ids and ids[name] or nil
end
local function varId(name)
if type(name) == "number" then return name end
if type(name) ~= "string" then return nil end
local n = tonumber(name)
if n then return n end
local Flags = g3("scripting.flags")
if not Flags then return nil end
local ok, t = pcall(Flags.active, session())
local ids = ok and t and t.VAR_IDS or Flags.VAR_IDS
return ids and ids[name] or nil
end
function Gen3Compat.getFlag(name)
local id = flagId(name)
if not id then return nil end
local Flags = g3("scripting.flags")
local store = flagStore()
if store and Flags then
return Flags.getFlag(store, nil, id) and true or nil
end
local s = session()
local flags = s and s.flags
if not flags then return nil end
return (flags[id] or flags[tostring(id)]) and true or nil
end
function Gen3Compat.setFlag(name, value)
local id = flagId(name)
if not id then return nil, "unknown FireRed flag: " .. tostring(name) end
local Flags = g3("scripting.flags")
local store = flagStore()
if store and Flags then
Flags.setFlag(store, nil, id, value and true or false)
local Objects = package.loaded["src.core.game3.objects"]
if Objects and Objects.syncFlagVisibility then
Objects.syncFlagVisibility(id, value and true or false, true)
end
return true
end
local s = session()
if not s then return nil, "no session" end
s.flags = s.flags or {}
s.flags[tostring(id)] = value and true or nil
return true
end
function Gen3Compat.getVar(name)
local id = varId(name)
if not id then return nil end
local Flags = g3("scripting.flags")
local S = space()
if S and S.store and Flags then
return Flags.getVar(S.store, S.vm and S.vm.ctx, id)
end
local s = session()
local vars = s and s.vars
return vars and tonumber(vars[id] or vars[tostring(id)]) or 0
end
function Gen3Compat.setVar(name, value)
local id = varId(name)
if not id then return nil, "unknown FireRed var: " .. tostring(name) end
local Flags = g3("scripting.flags")
local S = space()
if S and S.store and Flags then
Flags.setVar(S.store, S.vm and S.vm.ctx, id, value)
return true
end
local s = session()
if not s then return nil, "no session" end
s.vars = s.vars or {}
s.vars[tostring(id)] = tonumber(value) or 0
return true
end
local FLAGS_VIEW = setmetatable({}, {
__index = function(_, key) return Gen3Compat.getFlag(key) end,
__newindex = function(_, key, value)
local ok, err = Gen3Compat.setFlag(key, value)
if not ok then
warnOnce("save.flags." .. tostring(key),
"[%s] save.flags.%s: %s", who("src.core.Game"), tostring(key),
tostring(err))
end
end,
})
local VARS_VIEW = setmetatable({}, {
__index = function(_, key) return Gen3Compat.getVar(key) end,
__newindex = function(_, key, value) Gen3Compat.setVar(key, value) end,
})
local INVENTORY_VIEW = setmetatable({}, {
__index = function(_, key)
local s = session()
local Bag = g3("bag")
if not (s and s.bag and Bag) then return nil end
local id = Gen3Compat.itemId(key) or key
local n = Bag.get(s.bag, id)
if not n or n <= 0 then return nil end
return n
end,
__newindex = function(_, key, value)
local s = session()
local Bag = g3("bag")
if not (s and s.bag and Bag) then return end
local id = Gen3Compat.itemId(key)
if not id then
warnOnce("save.inventory." .. tostring(key),
"[%s] save.inventory.%s: no FireRed item of that name",
who("src.core.Game"), tostring(key))
return
end
Bag.set(s.bag, id, tonumber(value) or 0)
end,
})
local function dexSide(field)
return setmetatable({}, {
__index = function(_, key)
local s = session()
local dex = s and s.dex and s.dex[field]
local sp = Gen3Compat.speciesId(key)
if not (dex and sp) then return nil end
return (dex[sp] or dex[tostring(sp)]) and true or nil
end,
__newindex = function(_, key, value)
local s = session()
local sp = Gen3Compat.speciesId(key)
if not (s and sp) then return end
s.dex = s.dex or {}
s.dex[field] = s.dex[field] or {}
s.dex[field][sp] = value and true or nil
end,
})
end
local POKEDEX_VIEW = { seen = dexSide("seen"), caught = dexSide("owned") }
local PLAYER_VIEW = setmetatable({}, {
__index = function(_, key)
local s = session()
if not s then return nil end
if key == "name" then return s.name end
if key == "rival" then return s.rivalName end
if key == "money" then return s.money end
if key == "map" then return Gen3Compat.gen1MapId(s.map) end
if key == "gen3Map" then return s.map end
if key == "x" or key == "y" or key == "facing" then
local P = package.loaded["src.core.game3.player"]
if P and inField() then
if key == "x" then return P.cellX end
if key == "y" then return P.cellY end
return P.facing
end
return s[key]
end
return nil
end,
__newindex = function(_, key, value)
local s = session()
if not s then return end
if key == "name" then s.name = value
elseif key == "rival" then s.rivalName = value
elseif key == "money" then s.money = value
else
warnOnce("save.player.write." .. tostring(key),
"[%s] save.player.%s is read-only on FireRed; move the player with "
.. "mod.world:warpTo", who("src.core.Game"), tostring(key))
end
end,
})
local SAVE_ABSENT = {
boxes = "FireRed's PC is session.storage (14 boxes of 30 sparse slots); "
.. "require src.pokemon.Boxes, which is served",
box = "FireRed has no Gen 1 save.box",
objectToggles = "an object's visibility IS its hide flag on FireRed",
defeatedTrainers = "trainer defeats are flags 0x500 + trainer id",
hiddenTaken = "hidden items are flags on FireRed",
}
local SAVE_VIEW = setmetatable({}, {
__index = function(_, key)
local s = session()
if not s then return nil end
if key == "flags" then return FLAGS_VIEW end
if key == "vars" then return VARS_VIEW end
if key == "inventory" then return INVENTORY_VIEW end
if key == "player" then return PLAYER_VIEW end
if key == "pokedex" then return POKEDEX_VIEW end
if key == "gen3" then return s end
local why = SAVE_ABSENT[key]
if why then
warnOnce("save.absent." .. key, "[%s] save.%s has no Gen 3 backing: %s",
who("src.core.Game"), key, why)
return nil
end
return s[key]
end,
__newindex = function(_, key, value)
local s = session()
if not s then return end
if key == "flags" or key == "vars" or key == "inventory"
or key == "player" or key == "pokedex" then
warnOnce("save.replace." .. key,
"[%s] save.%s cannot be replaced wholesale on FireRed; write its keys",
who("src.core.Game"), key)
return
end
s[key] = value
end,
})
Gen3Compat.saveView = SAVE_VIEW
-- ------- data views
local recordCache = setmetatable({}, { __mode = "k" })
local function pokemonRecord(ref)
local P = g3("pokemon")
local sp = Gen3Compat.speciesId(ref)
if not (P and sp) then return nil end
local names = P._names
if not names and P.install then pcall(P.install, P._cache) names = P._names end
if not (names and names[sp]) then return nil end
local byModule = recordCache[P]
if not byModule or byModule.names ~= names then
byModule = { names = names, rows = {} }
recordCache[P] = byModule
end
local hit = byModule.rows[sp]
if hit then return hit end
local base = P.stats and P.stats(sp) or {}
local meta = P.speciesMeta and P.speciesMeta(sp) or {}
local root = "data/generated/gba/pokemon/"
hit = {
id = (P.keyName and P.keyName(sp)) or canonicalName(names[sp]),
index = sp,
gen3Species = sp,
name = names[sp],
types = P.types and P.types(sp) or nil,
baseStats = { hp = base.hp, attack = base.atk, defense = base.def,
speed = base.spe, specialAttack = base.spa, specialDefense = base.spd },
catchRate = tonumber(meta.catchRate),
growthRate = P.growthRate and P.growthRate(sp) or nil,
abilities = P.abilities and P.abilities(sp) or nil,
learnset = P.learnset and P.learnset(sp) or nil,
evolutions = P.evolutions and P.evolutions(sp) or nil,
dexEntry = P.dexEntry and P.dexEntry(sp) or nil,
national = P.national and P.national(sp) or nil,
spriteFront = root .. "front/" .. sp .. ".rgba",
spriteBack = root .. "back/" .. sp .. ".rgba",
trueColor = true,
}
byModule.rows[sp] = hit
return hit
end
Gen3Compat.pokemonRecord = pokemonRecord
local function moveRecord(ref)
local M = g3("battle.moves")
local num = Gen3Compat.moveId(ref)
if not (M and num and M.get) then return nil end
local row = M.get(num)
if not row then return nil end
local out = {}
for k, v in pairs(row) do out[k] = v end
out.index = num
out.name = out.name or (M.displayName and M.displayName(num))
return out
end
Gen3Compat.moveRecord = moveRecord
local function itemRecord(ref)
local D = g3("items_data")
local num = Gen3Compat.itemId(ref)
if not (D and num) then return nil end
return D.info(num)
end
Gen3Compat.itemRecord = itemRecord
local function registryOverride(name, id)
local g = live()
local content = g and g.mods and g.mods.content
local reg = content and content[name]
if reg and reg.ops and type(id) == "string" and reg.ops[id] then
return reg:get(id)
end
return nil
end
local function namedView(registry, record)
return setmetatable({}, {
__index = function(_, key)
local over = registryOverride(registry, key)
if over ~= nil then return over end
return record(key)
end,
__newindex = function(_, key)
warnOnce("data." .. registry .. ".write",
"[%s] game.data.%s.%s: FireRed's %s are numeric ROM tables; change "
.. "them through mod.content.%s", who("src.core.Game"), registry,
tostring(key), registry, registry)
end,
})
end
local POKEMON_VIEW = namedView("pokemon", pokemonRecord)
local MOVES_VIEW = namedView("moves", moveRecord)
local ITEMS_VIEW = namedView("items", itemRecord)
local DATA_RENAMES = {
encounters = "gen3Encounters", trainers = "gen3Trainers",
text = "gen3Text", map_scripts = "gen3Scripts", scripts = "gen3Scripts",
}
local DATA_UNBACKED = {
sprites = "FireRed's overworld sprites are baked by the extractor, not a "
.. "data.sprites registry",
field = "FireRed's field rules are src/core/game3/collision.lua and the "
.. "ROM's metatile behaviours, not a data.field record",
constants = "no Gen 1 rule table exists on FireRed",
text_pointers = "the extractor's Gen 1 pointer table has no FireRed "
.. "counterpart",
trainer_headers = "the extractor's Gen 1 header table has no FireRed "
.. "counterpart",
palettes = "FireRed palettes are baked into the extracted RGBA art",
icons = "FireRed icons are src/core/game3/pokemon.lua Pokemon.icon",
battle_anims = "FireRed's battle animations are the pret script VM under "
.. "src/core/game3/battle/anim_*",
type_chart = "FireRed's type chart is src/core/game3/battle/types.lua",
}
local dataProxies = setmetatable({}, { __mode = "kv" })
local function mapsView(data)
return setmetatable({}, {
__index = function(_, key)
local maps = data.maps
if not maps then return nil end
local hit = maps[key]
if hit ~= nil then return hit end
if type(key) == "string" then return maps[MAP_PREFIX .. key] end
return nil
end,
__newindex = function(_, key, value)
if data.maps then data.maps[Gen3Compat.gen3MapId(key) or key] = value end
end,
})
end
local function dataProxy(data)
if not data then return nil end
local hit = dataProxies[data]
if hit then return hit end
local maps = mapsView(data)
hit = setmetatable({}, {
__index = function(_, key)
if key == "pokemon" then return POKEMON_VIEW end
if key == "moves" then return MOVES_VIEW end
if key == "items" then return ITEMS_VIEW end
if key == "maps" then return maps end
local renamed = DATA_RENAMES[key]
if renamed then return data[renamed] end
local why = DATA_UNBACKED[key]
if why then
warnOnce("data." .. key, "[%s] game.data.%s is Gen 1 only: %s",
who("src.core.Game"), key, why)
return nil
end
return data[key]
end,
__newindex = function(_, key, value) data[DATA_RENAMES[key] or key] = value end,
})
dataProxies[data] = hit
return hit
end
Gen3Compat.dataView = dataProxy
-- ------- src.core.Game
local GAME_UNBACKED = {
renderer = "FireRed composites through src/core/game3/display.lua at "
.. "240x160 and never inits the Renderer singleton",
load = "calling it would re-run FireRed's whole boot on top of a running "
.. "game",
bootConfig = "FireRed's new-game inputs are Schema.newGame's opts",
makeTitleState = "src/ui/game3/boot.lua owns the title; nothing returns a "
.. "state to push",
step = "FireRed's logic tick is Game3:fixedUpdate; the input.step hook is "
.. "the replacement",
restoreSave = "FireRed continues through the boot menu "
.. "(Game3:_handleBootAction), not a save table handed in",
linkNet = "FireRed has no link play",
linkSession = "FireRed has no link play",
}
local GAME_STACK_STATICS = {
"worldBgBattleDim", "worldBgBattleInStack", "fillScaleInStack",
"wideBattleInStack", "uiAnchorsHeldInStack", "drawBaseInStack", "dynamicUI",
}
local function buildGame()
local proxy = {}
local statics = nil
local function gen1Static(key)
if statics == nil then
local ok, module = pcall(rawRequire, "src.core.Game")
statics = ok and module or false
end
return statics and statics[key] or nil
end
local translate = {}
function translate.overworld()
if not inField() then return nil end
return Gen3Compat.resolve("src.world.OverworldController")
end
function translate.data(g) return dataProxy(g.data) end
function translate.save()
if not session() then return nil end
return SAVE_VIEW
end
function translate.stack()
warnOnce("game.stack",
"[%s] Game.stack on FireRed is src/ui/game3/stack.lua: push(id, module, "
.. "opts) / pop(id) / top(), not a StateStack of state objects",
who("src.core.Game"))
return ui3("stack")
end
function translate.fixedStep() return rawRequire("src.core.FixedStep") end
function translate.generation() return 3 end
function translate.writeOptions()
return function()
local g = live()
if g and g.writeOptions then return g:writeOptions() end
end
end
function translate.logicSpeed()
return function()
local g = live()
if not g or not g.logicSpeed then return 1 end
return g:logicSpeed()
end
end
function translate.applyOptions()
return function(opts)
local g = live()
if not g or not g.applyOptions then return end
if opts == proxy then opts = nil end
return g:applyOptions(opts or g.options)
end
end
function translate.saveGame()
return function()
local g = live()
if not (g and g.saveGame) then return end
if g.saveOffered and not g:saveOffered() then return false end
return g:saveGame()
end
end
function translate.writeSave() return translate.saveGame() end
function translate.restartWithMods()
return function() return rawRequire("src.core.HostShell").restart() end
end
function translate.recoverInput()
return function()
local g = live()
if not g then return end
local Input = rawRequire("src.core.Input")
Input:reset()
if Input.reconcile then Input:reconcile() end
local okT, Touch = pcall(rawRequire, "src.core.TouchControls")
if okT and Touch and Touch.reset then Touch:reset() end
if g.mods and g.mods.releaseModInput then g.mods:releaseModInput() end
end
end
function translate.zoomStep()
return function(delta)
local g = live()
if g and g.zoomStep then g:zoomStep(delta) end
end
end
local LIVE_FREE = { writeOptions = true, logicSpeed = true, fixedStep = true,
restartWithMods = true, generation = true }
setmetatable(proxy, {
__index = function(_, key)
local why = GAME_UNBACKED[key]
if why then
warnOnce("game." .. key, "[%s] Game.%s has no Gen 3 backing: %s",
who("src.core.Game"), key, why)
return nil
end
local made = translate[key]
if made then
local g = live()
if not g and not LIVE_FREE[key] then return nil end
return made(g)
end
for _, name in ipairs(GAME_STACK_STATICS) do
if name == key then return gen1Static(key) end
end
local g = live()
if not g then return nil end
local value = g[key]
if value == nil then
warnOnce("game.unknown." .. tostring(key),
"[%s] Game.%s is not on the FireRed service owner; it reads nil "
.. "because Game3 has no member of that name",
who("src.core.Game"), tostring(key))
return nil
end
if type(value) == "function" then
return function(first, ...)
if first == proxy then return value(g, ...) end
return value(first, ...)
end
end
return value
end,
__newindex = function(_, key, value)
local g = live()
if not g then return end
if key == "overworld" or key == "save" or key == "data" then
warnOnce("game.write." .. key,
"[%s] Game.%s cannot be replaced on FireRed", who("src.core.Game"), key)
return
end
g[key] = value
end,
})
return proxy
end
COVERAGE["src.core.Game"] = {
kind = "facade", target = "src.core.Game3",
backed = "overworld data data.pokemon data.moves data.items data.maps "
.. "data.encounters data.trainers data.text data.map_scripts data.tilesets "
.. "save save.flags save.vars save.party save.money save.inventory "
.. "save.player save.player.map save.player.x save.player.y "
.. "save.player.facing save.player.rival save.pokedex save.options "
.. "save.modData mods modStatus input touchControls options phase "
.. "fixedStep generation writeOptions logicSpeed applyOptions saveGame "
.. "writeSave restartWithMods recoverInput zoomStep returnToTitle update "
.. "draw keypressed keyreleased gamepadpressed gamepadreleased "
.. "gamepadaxis joystickpressed joystickreleased joystickaxis joystickhat "
.. "joystickremoved focus visible onResume touchpressed touchmoved "
.. "touchreleased mousepressed mousemoved mousereleased wheelmoved "
.. "worldBgBattleDim worldBgBattleInStack fillScaleInStack "
.. "wideBattleInStack uiAnchorsHeldInStack drawBaseInStack dynamicUI",
warned = "stack renderer load bootConfig makeTitleState step restoreSave "
.. "linkNet linkSession data.sprites data.field data.constants "
.. "data.text_pointers data.trainer_headers data.palettes data.icons "
.. "data.battle_anims data.type_chart save.boxes save.box "
.. "save.objectToggles save.defeatedTrainers save.hiddenTaken",
absent = "saveReport save.meta",
notes = {
identity = "the proxy never compares equal to the Game3 instance in the "
.. "game.ready payload; mod.game is the raw Game3",
iteration = "pairs/next/rawget see an EMPTY table on the proxy and on "
.. "every save / data view below it",
save = "a LIVE view over the FireRed session (Runtime.getSession), not "
.. "Game3.save, which is a snapshot rebuilt at each save",
["save.flags"] = "keyed by FireRed flag NAME (FLAG_SYS_POKEMON_GET, or "
.. "the name without FLAG_) or number; a Gen 1 name such as "
.. "EVENT_GOT_STARTER reads nil because FireRed has no such flag",
["save.inventory"] = "item name or FireRed item id -> count; answered "
.. "through src/core/game3/bag.lua, so a write lands in the right pocket",
["save.player.map"] = "the Gen 1 spelling (FR_ prefix stripped); "
.. "save.player.gen3Map is the raw id",
["save.pokedex"] = "seen / caught keyed by species name or FireRed "
.. "species number",
["data.pokemon"] = "a NAME-keyed view assembled from the numeric FireRed "
.. "tables; spriteFront/spriteBack name the cache .rgba, and a "
.. "registry override wins",
["data.maps"] = "keyed by the FR_ id; a Gen 1 spelling without the "
.. "prefix also resolves",
stack = "src/ui/game3/stack.lua, a module-level id stack, warned once",
applyOptions = "Game3:applyOptions(opts) takes the table directly, the "
.. "same contract as Gen 1",
},
}
-- ------- src.world.Map
local MapView = {}
local connViews = setmetatable({}, { __mode = "k" })
local function collision()
return package.loaded["src.core.game3.collision"]
end
function MapView.__index(self, key)
local method = rawget(MapView, key)
if method ~= nil then return method end
local raw = rawget(self, "gen3Id")
local g = live()
local def = g and g.data and g.data.maps and g.data.maps[raw]
if key == "def" then return def end
if key == "tileset" then return def and def.tileset end
if key == "widthCells" then return def and def.width end
if key == "heightCells" then return def and def.height end
if key == "warps" then return def and def.warps end
if key == "connections" then
if not def then return nil end
local hit = connViews[def]
if hit and hit.src == def.connections then return hit.view end
local list = require("src.core.game3.connections").each(def)
local view = setmetatable(list, { __index = function(t, dir)
if type(dir) ~= "string" then return nil end
for i = 1, #t do
if t[i].dir == dir then return t[i] end
end
return nil
end })
connViews[def] = { src = def.connections, view = view }
return view
end
return nil
end
local function isActiveMap(self)
local M = package.loaded["src.core.game3.map"]
return M and M.current == rawget(self, "gen3Id")
end
function MapView:inBounds(cx, cy)
local C = collision()
if not (C and isActiveMap(self)) then return false end
return C.inBounds(cx, cy) and true or false
end
function MapView:isWalkableCell(cx, cy)
local C = collision()
if not (C and isActiveMap(self)) then return false end
return C.isWalkable(cx, cy) and true or false
end
function MapView:isWaterCell(cx, cy)
local C = collision()
if not (C and isActiveMap(self)) then return false end
return C.isWater(cx, cy) and true or false
end
function MapView:isGrassCell(cx, cy)
local C = collision()
if not (C and isActiveMap(self)) then return false end
return C.isGrass(cx, cy) and true or false
end
function MapView:warpAtCell(cx, cy)
local C = collision()
if not (C and isActiveMap(self)) then return nil end
return C.warpAt(cx, cy)
end
function MapView:cellBehavior(cx, cy)
local C = collision()
if not (C and isActiveMap(self)) then return nil end
return C.behavior(cx, cy)
end
function MapView:isOutdoor()
local def = self.def
local pair = def and (def.pair or (def.midLayout and def.midLayout.pair))
return type(pair) == "string" and pair:find("outdoor", 1, true) ~= nil
end
MapView.isOutside = MapView.isOutdoor
function MapView.new()
warnOnce("map.new",
"[%s] src.world.Map.new: FireRed maps are loaded by "
.. "src/core/game3/map.lua Map.load; take the live one from "
.. "OverworldController.map", who("src.world.Map"))
return nil
end
local function mapView(rawId)
if not rawId then return nil end
return setmetatable({ id = Gen3Compat.gen1MapId(rawId), gen3Id = rawId },
MapView)
end
Gen3Compat.mapView = mapView
local function buildMap() return MapView end
COVERAGE["src.world.Map"] = {
kind = "facade", target = "src.core.game3.map",
backed = "id gen3Id def tileset widthCells heightCells warps connections "
.. "inBounds isWalkableCell isWaterCell isGrassCell warpAtCell "
.. "cellBehavior isOutdoor isOutside",
warned = "new",
absent = "cellTile isCounterCell blockAt setBlock tileAt isDoorTileCell "
.. "isWarpTileCell signAtCell connection isPushable defCellTile "
.. "defIsWalkableCell defIsWaterCell defPassable DELTA isFlyTown "
.. "ghostBattles warpPadOrHoleAt walkable doorTiles warpTiles waterTiles "
.. "renderer signAt warpAt",
notes = {
id = "the Gen 1 spelling (FR_OAKS_LAB -> OAKS_LAB); gen3Id is the raw id",
inBounds = "cell queries answer only for the ACTIVE map: FireRed binds one "
.. "collision grid at a time (src/core/game3/collision.lua)",
blockAt = "FireRed has 16px metatiles and no 32px blocks",
},
}
-- ------- src.world.NPC
local function buildNpc()
local NPC = {}
NPC.new = unbacked("src.world.NPC", "new",
"FireRed event objects are plain records built by "
.. "src/core/game3/objects.lua from the map's object templates; "
.. "mod.world:npc hands out a handle onto a live one")
function NPC.facePlayer(npc)
local Objects = package.loaded["src.core.game3.objects"]
if not (Objects and type(npc) == "table" and npc.localId) then return nil end
return Objects.facePlayer(npc.localId, live())
end
function NPC.pose(npc)
if type(npc) ~= "table" then return nil end
return npc.facing, npc.moving
end
return NPC
end
COVERAGE["src.world.NPC"] = {
kind = "facade", target = "src.core.game3.objects",
backed = "facePlayer pose",
warned = "new",
absent = "update walkPhase draw hopStep MOVE __index",
notes = {
instance = "a live FireRed object carries localId cellX cellY px py "
.. "facing moving progress frozen passable sprite def; it has no "
.. "metatable, so getmetatable(npc) == NPC is false",
new = "no constructor: FireRed object templates come from the extracted "
.. "map events",
},
}
-- ------- src.world.Collision
local DELTA = { up = { 0, -1 }, down = { 0, 1 }, left = { -1, 0 }, right = { 1, 0 } }
local function buildCollision()
local Collision = { DELTA = DELTA }
function Collision.target(cx, cy, dir)
local d = DELTA[dir]
if not d then return cx, cy end
return cx + d[1], cy + d[2]
end
function Collision.occupied(entities, cx, cy, ignore)
for _, e in ipairs(entities or {}) do
if e ~= ignore and not e.passable then
if (e.cellX == cx and e.cellY == cy)
or (e.targetX == cx and e.targetY == cy) then
return e
end
end
end
return nil
end
function Collision.load(_data)
warnOnce("collision.load",
"[%s] Collision.load: FireRed has no data.field.tilePairs; passability "
.. "is the ROM's metatile behaviour (src/core/game3/collision.lua)",
who("src.world.Collision"))
return nil
end
local function passthrough(allowed) return allowed end
function Collision.canMove(map, entities, mover, dir)
local tx, ty = Collision.target(mover.cellX, mover.cellY, dir)
local C = collision()
local allowed, why = true, nil
if not C then
allowed, why = false, "bounds"
else
-- pokefirered/src/event_object_movement.c:4830 GetCollisionAtCoords
local ok, reason = C.canEnter(live(), tx, ty,
{ fromX = mover.cellX, fromY = mover.cellY, dir = dir,
surfing = mover.surfing and true or false })
if not ok then
allowed = false
why = (reason == "bounds" or reason == "entity") and reason or "tile"
elseif Collision.occupied(entities, tx, ty, mover) then
allowed, why = false, "entity"
end
end
if Runtime.wantsHook("movement.collision") then
local ctx = { map = map, mover = mover, dir = dir,
fromX = mover.cellX, fromY = mover.cellY,
toX = tx, toY = ty, reason = why }
allowed = Runtime.call("movement.collision", passthrough, allowed, ctx)
why = ctx.reason
end
if allowed then return true end
return false, why
end
return Collision
end
COVERAGE["src.world.Collision"] = {
kind = "facade", target = "src.core.game3.collision",
backed = "DELTA target occupied canMove",
warned = "load",
absent = "",
notes = {
canMove = "the verdict src/core/game3/collision.lua Collision.canEnter "
.. "gives the active map, so `map` is ignored and only the loaded grid "
.. "answers",
target = "returns (cx, cy) unchanged for an unknown dir where Gen 1 errors",
},
}
-- ------- src.world.FieldDefaults
local WORLD_CONSTANTS = {
stepFrames = 16,
turnFrames = 4,
runStepFrames = 8,
bikeStepFrames = 4,
}
local function buildFieldDefaults()
local FieldDefaults = { CONSTANTS = { world = WORLD_CONSTANTS } }
function FieldDefaults.field(_data, key)
warnOnce("fieldDefaults.field." .. tostring(key),
"[%s] src.world.FieldDefaults.field(%s): FireRed has no data.field",
who("src.world.FieldDefaults"), tostring(key))
return nil
end
function FieldDefaults.fieldValue(_data, key, ...)
local path = tostring(key)
for i = 1, select("#", ...) do path = path .. "." .. tostring((select(i, ...))) end
warnOnce("fieldDefaults.fieldValue." .. path,
"[%s] src.world.FieldDefaults: FireRed has no data.field %s",
who("src.world.FieldDefaults"), path)
return nil
end
function FieldDefaults.constant(_data, key)
if key == "world" then return WORLD_CONSTANTS end
warnOnce("fieldDefaults.constant." .. tostring(key),
"[%s] FieldDefaults.constant(%s): the Gen 1 default is Kanto Red's rule "
.. "and FireRed does not carry it", who("src.world.FieldDefaults"),
tostring(key))
return nil
end
function FieldDefaults.world(_data, key)
local value = WORLD_CONSTANTS[key]
if value ~= nil then return value end
warnOnce("fieldDefaults.world." .. tostring(key),
"[%s] FieldDefaults.world(%s): FireRed's step events do not read a "
.. "shared constant table", who("src.world.FieldDefaults"), tostring(key))
return nil
end
FieldDefaults.seed = unbacked("src.world.FieldDefaults", "seed",
"seeding Kanto Red's FIELD into a FireRed dataset would put Gen 1 map ids "
.. "into data.field")
return FieldDefaults
end
COVERAGE["src.world.FieldDefaults"] = {
kind = "facade",
backed = "CONSTANTS CONSTANTS.world constant world",
warned = "field fieldValue seed",
absent = "FIELD CONSTANTS.encounterBuckets CONSTANTS.hmBadges",
notes = {
["CONSTANTS.world"] = "stepFrames 16, turnFrames 4, runStepFrames 8, "
.. "bikeStepFrames 4, the frame counts src/core/game3/player.lua uses",
},
}
-- ------- src.pokemon.Boxes
local function sessionOf(save)
if save == nil or save == SAVE_VIEW then return session() end
if type(save) == "table" and save.storage ~= nil then return save end
return session()
end
local function buildBoxes()
local Storage = g3("storage")
local Boxes = {
COUNT = Storage and Storage.TOTAL_BOXES_COUNT or 14,
CAPACITY = Storage and Storage.IN_BOX_COUNT or 30,
}
function Boxes.ensure(save)
local s = sessionOf(save)
if not (s and Storage) then return {} end
local storage = Storage.ensure(s)
local out = {}
for i = 1, Boxes.COUNT do
out[i] = storage.boxes[i] and storage.boxes[i].mons or {}
end
return out
end
function Boxes.active(save)
local s = sessionOf(save)
if not (s and Storage) then return nil end
local storage = Storage.ensure(s)
local box = storage.boxes[storage.currentBox or 1]
return box and box.mons or nil
end
function Boxes.deposit(save, mon)
local s = sessionOf(save)
if not (s and Storage and mon) then return nil end
local ok, boxId = Storage.depositCaught(s, mon)
if not ok then return nil end
return boxId
end
function Boxes.count(save, index)
local s = sessionOf(save)
if not (s and Storage) then return 0 end
local storage = Storage.ensure(s)
return Storage.countBoxMons(storage, index or storage.currentBox or 1)
end
function Boxes.isFull(save)
local s = sessionOf(save)
if not (s and Storage) then return true end
return Storage.findOpenSlot(Storage.ensure(s)) == nil
end
function Boxes.setCurrent(save, index)
local s = sessionOf(save)
if not (s and Storage) then return nil end
local storage = Storage.ensure(s)
storage.currentBox = math.max(1, math.min(Boxes.COUNT, tonumber(index) or 1))
return storage.currentBox
end
return Boxes
end
COVERAGE["src.pokemon.Boxes"] = {
kind = "facade", target = "src.core.game3.storage",
backed = "COUNT CAPACITY ensure active deposit count isFull setCurrent",
warned = "",
absent = "withdraw release move name rename box",
notes = {
COUNT = "14 boxes on FireRed",
CAPACITY = "30 slots per box on FireRed",
ensure = "each box is session.storage.boxes[i].mons, a SPARSE 30-slot "
.. "table: iterate 1..CAPACITY and skip nil, never trust #box",
deposit = "Storage.depositCaught: first open slot from the current box, "
.. "and depositing heals the mon as the Gen 3 PC does",
},
}
-- ------- src.world.OverworldController
local overworld
local function defaultUpdate() end
local function defaultInteract()
local Field = package.loaded["src.core.game3.field"]
if Field and Field.interact then return Field.interact(live()) end
return false
end
local function defaultTalkTo() return false end
local OW = "src.world.OverworldController"
local function objectsList()
local Objects = package.loaded["src.core.game3.objects"]
local out = {}
if not (Objects and Objects._order and Objects._byId) then return out end
for _, lid in ipairs(Objects._order) do
local eo = Objects._byId[lid]
if eo and eo.visible and not eo.hidden then out[#out + 1] = eo end
end
return out
end
local function playerModule()
return package.loaded["src.core.game3.player"]
end
local function localIdOf(entity)
local P = playerModule()
local Objects = package.loaded["src.core.game3.objects"]
if entity == nil then return nil end
if entity == P then return Objects and Objects.PLAYER_LOCAL_ID or 0xFF end
if type(entity) == "table" and entity.localId then return entity.localId end
return nil
end
function Gen3Compat.worldBusy()
local Field = package.loaded["src.core.game3.field"]
if not (Field and Field.running) then return true, "no overworld" end
if Field.locked then return true, "world is busy" end
local R = package.loaded["src.core.game3.runtime"]
if R and R.uiBusy then
local ok, busy = pcall(R.uiBusy)
if ok and busy then return true, "world is busy" end
end
local S = space()
if S and S.vm and S.vm.isRunning and S.vm:isRunning() then
return true, "a script is running"
end
local B = package.loaded["src.core.game3.battle.init"]
or package.loaded["src.core.game3.battle"]
if B and B.isActive and B.isActive() then return true, "a battle is running" end
local W = package.loaded["src.core.game3.warp"]
if W and W.isBusy and W.isBusy() then return true, "the world is mid-warp" end
local P = playerModule()
if P and P.moving then return true, "world is busy" end
return false
end
local function buildOverworld()
local ow = {
update = defaultUpdate,
interact = defaultInteract,
talkTo = defaultTalkTo,
}
local function ready(member)
if inField() then return true end
warnOnce(OW .. "." .. member .. ".nofield",
"[%s] %s.%s: no FireRed field is up yet; the honest replacement is the "
.. "game.ready / map.entered event", who(OW), OW, member)
return false
end
function ow.npcAtCell(cx, cy)
if not ready("npcAtCell") then return nil end
local Objects = package.loaded["src.core.game3.objects"]
return Objects and Objects.at(cx, cy) or nil
end
function ow.npcByIndex(index)
if not ready("npcByIndex") then return nil end
local Objects = package.loaded["src.core.game3.objects"]
if type(index) ~= "number" or not Objects then return nil end
local eo = Objects._byId and Objects._byId[index]
return eo
end
function ow.setMap(mapId, x, y, facing, _opts)
if not ready("setMap") then return nil end
local raw = Gen3Compat.gen3MapId(mapId)
if not raw then return nil, "unknown map: " .. tostring(mapId) end
local M = g3("map")
return M and M.load(nil, live(), raw, { x = x, y = y,
facing = facing or "down", depth1Connections = true }) or nil
end
function ow.reloadMap(mapId, _reason)
if not ready("reloadMap") then return nil end
local M = g3("map")
local P = playerModule()
if not (M and M.current and P) then return nil end
local raw = mapId and Gen3Compat.gen3MapId(mapId) or M.current
if raw ~= M.current then return true end
M.load(nil, live(), raw, { x = P.cellX, y = P.cellY, facing = P.facing,
depth1Connections = true })
Runtime.emit("map.reloaded", { mapId = raw, reason = "invalidate" })
return true
end
function ow.startWarpTo(mapId, x, y, facing, onDone, opts)
if not ready("startWarpTo") then return nil end
local raw = Gen3Compat.gen3MapId(mapId)
if not raw then return nil, "unknown map: " .. tostring(mapId) end
if onDone then
warnOnce("ow.startWarpTo.onDone",
"[%s] OverworldState.startWarpTo drops onDone on FireRed "
.. "(src/core/game3/warp.lua Warp.request has no completion callback)",
who(OW))
end
local W = g3("warp")
local P = playerModule()
return W and W.request(nil, live(), raw, x, y,
facing or (P and P.facing) or "down",
{ teleport = opts and opts.arrive == "teleport" or nil }) or nil
end
function ow.canCollisionWarp()
local W = package.loaded["src.core.game3.warp"]
return not (W and W.isBusy and W.isBusy())
end
function ow.healPoint()
local s = session()
if not s then return nil end
return { map = Gen3Compat.gen1MapId(s.healMap), gen3Map = s.healMap,
x = s.healX, y = s.healY }
end
function ow.warpToHealPoint(onDone)
if not ready("warpToHealPoint") then return nil end
local Field = g3("field")
if not Field then return nil end
Field.respawnAtHeal({ fieldMove = true })
if onDone then onDone() end
return true
end
function ow.stepForwardOrCrossEdge(dir)
if not ready("stepForwardOrCrossEdge") then return nil end
local P = playerModule()
return P and P.tryMove(dir, live(), false) or nil
end
function ow.checkLedgeHop(dir)
local C = collision()
local P = playerModule()
if not (C and P and inField()) then return false end
return C.ledgeLanding(live(), P.cellX, P.cellY, dir) ~= nil
end
function ow.checkEdgeExit(dir)
local C = collision()
local P = playerModule()
if not (C and P and inField()) then return false end
return C.tryConnection(live(), P.cellX, P.cellY, dir, false) and true or false
end
ow.crossConnection = function(dir) return ow.checkEdgeExit(dir) end
function ow.scriptMove(entity, dir, tiles, onDone, _opts)
if not ready("scriptMove") then return nil, "no overworld" end
local Objects = package.loaded["src.core.game3.objects"]
if not Objects then return nil, "no overworld" end
if not DELTA[dir] then return nil, "unknown direction: " .. tostring(dir) end
local lid = localIdOf(entity)
if not lid then
return nil, "no FireRed localId for that entity: only the player and a "
.. "live map object can be moved"
end
local actions = {}
for _ = 1, math.max(0, tiles or 1) do
actions[#actions + 1] = { kind = "step", dir = dir }
end
Objects.startTrack(lid, actions, onDone)
return true
end
function ow.timeOfDay()
warnOnce("ow.timeOfDay",
"[%s] OverworldState.timeOfDay: FireRed has no clock and no time of "
.. "day", who(OW))
return nil
end
function ow.replaceBlock(bx, by, block)
if not ready("replaceBlock") then return nil end
local Field = g3("field")
if not Field then return nil end
warnOnce("ow.replaceBlock",
"[%s] OverworldState.replaceBlock on FireRed sets one 16px METATILE at "
.. "(x, y) in metatile coordinates; FireRed has no 32px blocks", who(OW))
Field.setMetatile(bx, by, block)
local M = package.loaded["src.core.game3.map"]
Runtime.emit("world.block_replaced",
{ mapId = M and M.current, bx = bx, by = by, block = block })
return true
end
function ow.partyKnows(moveId)
local s = session()
local P = g3("pokemon")
local num = Gen3Compat.moveId(moveId)
if not (s and P and num) then return false end
for _, mon in ipairs(s.party or {}) do
if P.knowsMove(mon, num) then return true end
end
return false
end
function ow.openPC(onDone)
if not ready("openPC") then return nil end
local Hud = ui3("hud")
if not Hud then return nil end
if onDone then
warnOnce("ow.openPC.onDone",
"[%s] OverworldState.openPC drops onDone on FireRed: the PC closes "
.. "itself with no callback", who(OW))
end
Hud.openPc(live(), session())
return true
end
function ow.nurseHeal(onDone)
local s = session()
local Party = g3("party")
if not (s and Party) then return nil end
Party.healAll(s.party)
if onDone then onDone() end
return true
end
function ow.showMapText(key, _npc, onDone)
if not ready("showMapText") then return nil end
local S = space()
local g = live()
local body = (S and S.bundle and S.bundle.text and S.bundle.text[key])
or (g and g.data and g.data.gen3Text and g.data.gen3Text[key])
if body == nil then
warnOnce("ow.showMapText." .. tostring(key),
"[%s] OverworldState.showMapText(%s): FireRed text keys are the "
.. "extracted script labels; a Gen 1 TEXT_* constant resolves to "
.. "nothing", who(OW), tostring(key))
return nil
end
local Message = ui3("message")
if not Message then return nil end
Message.show(body, function()
Message.close()
if onDone then onDone() end
end)
return true
end
local queueApi
function ow.queueScript(rows, extra)
local g = live()
if not g then return nil, "no overworld" end
if not queueApi or queueApi.game ~= g then
queueApi = rawRequire("src.world.game3.WorldAPI").new(g, OW)
end
return queueApi:queueScript(rows, extra)
end
local SEAMS = { update = true, interact = true, talkTo = true }
for key, fn in pairs(ow) do
if type(fn) == "function" and not SEAMS[key] then
ow[key] = function(first, ...)
if first == overworld then return fn(...) end
return fn(first, ...)
end
end
end
local LIVE_FIELD = { map = true, player = true, npcs = true, entities = true }
setmetatable(ow, {
__index = function(_, key)
if not LIVE_FIELD[key] then
if key == "isOverworld" then return true end
return nil
end
if not inField() then return nil end
if key == "map" then
local M = package.loaded["src.core.game3.map"]
return mapView(M and M.current or (session() and session().map))
end
if key == "player" then return playerModule() end
local list = objectsList()
if key == "entities" then
local P = playerModule()
if P then list[#list + 1] = P end
end
return list
end,
__newindex = function(t, key, value)
if LIVE_FIELD[key] then
warnOnce("ow.write." .. key,
"[%s] OverworldState.%s is owned by src/core/game3 and cannot be "
.. "replaced; move the player with warpTo", who(OW), key)
return
end
rawset(t, key, value)
end,
})
overworld = ow
return ow
end
function Gen3Compat.worldTick(dt)
if not overworld or overworld.update == defaultUpdate then return end
overworld.update(overworld, dt)
end
function Gen3Compat.interactWrapper()
if not overworld or overworld.interact == defaultInteract then return nil end
return overworld.interact
end
function Gen3Compat.talkToWrapper()
if not overworld or overworld.talkTo == defaultTalkTo then return nil end
return overworld.talkTo
end
COVERAGE[OW] = {
kind = "facade", target = "src.core.game3.field",
backed = "map player npcs entities isOverworld npcAtCell npcByIndex setMap "
.. "reloadMap startWarpTo canCollisionWarp healPoint warpToHealPoint "
.. "stepForwardOrCrossEdge checkLedgeHop checkEdgeExit crossConnection "
.. "scriptMove partyKnows openPC nurseHeal showMapText queueScript "
.. "update interact talkTo",
warned = "timeOfDay replaceBlock startWarpTo openPC",
absent = "pushableAtCell pooledNPC computeNeighbors rebuildNeighbors "
.. "takeWarp refreshStandingOnWarp dirHeld handleInput connectionLanding "
.. "checkBoulderPush checkForcedMovement updateScriptMoves setDark "
.. "bikeAllowed addRuntimeObject removeRuntimeObject tryHiddenObject "
.. "hasHiddenItemLeft facingIsShoreOrWater facingIsLandDismount trySurf "
.. "tryCut goFishing flyTo trainerDefeated engageTrainer "
.. "checkTrainerSight startTrainerApproach applyFieldPoison neighbors "
.. "ghosts npcPool camera runner tod dark draw",
notes = {
map = "a view with the Gen 1 id (FR_ stripped) plus gen3Id; the cell "
.. "queries answer against the active collision grid",
player = "src/core/game3/player.lua itself: cellX cellY px py facing "
.. "moving surfing biking carry, and writes land on the live avatar",
npcs = "a fresh array per read of the visible FireRed event objects",
update = "a replacement runs each field tick via Gen3Compat.worldTick",
interact = "a replacement owns the A press; the original is Field.interact",
talkTo = "a replacement returning true consumes a scripted NPC talk",
startWarpTo = "Warp.request with the FireRed fade; onDone is dropped",
replaceBlock = "sets one 16px metatile (Field.setMetatile), not a 32px "
.. "Gen 1 block",
npcByIndex = "keyed by the FireRed localId the map's object template "
.. "carries",
timeOfDay = "FireRed has no clock",
flyTo = "the FireRed fly map is src/ui/game3/region_map.lua; no seam",
},
}
-- ------- src.world.PikachuFollower
local function buildFollower()
return require("src.world.game3.Follower")
end
COVERAGE["src.world.PikachuFollower"] = {
kind = "facade",
backed = "current starterInParty talk setShouldSpawn onMapEntered update setVisible at",
warned = "",
absent = "shouldSpawn rebase SPRITE onStep bumpHappiness modifyHappiness "
.. "picLift hopToCounter updateHop",
notes = {
current = "optional mod companion; absent until setShouldSpawn enables it",
talk = "returns false and never calls done",
},
}
-- ------- UI facades
local PATCHED_NIL = {}
local function passThroughProxy(target, overrides, absent, moduleName)
local patched = {}
return setmetatable({}, {
__index = function(_, key)
local mine = patched[key]
if mine == PATCHED_NIL then return nil end
if mine ~= nil then return mine end
local made = overrides[key]
if made ~= nil then return made end
local why = absent[key]
if why then
warnOnce(moduleName .. "." .. key,
"[%s] %s.%s has no Gen 3 backing: %s",
who(moduleName), moduleName, key, why)
return nil
end
return target[key]
end,
__newindex = function(_, key, value)
patched[key] = (value == nil) and PATCHED_NIL or value
target[key] = value
end,
})
end
local UI_ABSENT = "FireRed's screens are module singletons under src/ui/game3 "
.. "drawn at 240x160; there is no Gen 1 state object to reach into"
local function buildPartyMenu()
local PM = ui3("party_menu") or {}
local overrides, absent = {}, {}
local showOrig, closeOrig = PM.show, PM.close
for _, name in ipairs({ "drawIcon", "frameFor", "mirrorsIcon", "iconFrames",
"sgbPalettes", "animateTo", "entryY" }) do
absent[name] = UI_ABSENT
end
function overrides.new(_game, opts)
opts = opts or {}
local s = session()
if not (s and showOrig) then return nil end
local handle = {}
function handle.close() if closeOrig then closeOrig() end end
handle.party = opts.party or s.party
showOrig(handle.party, s.move_overlay, {
session = s,
mode = "list",
onSelect = opts.onSwitch and function(slot)
local mon = slot and handle.party[slot]
if mon then opts.onSwitch(mon, handle) end
end or nil,
onClose = opts.onCancel,
})
return handle
end
function overrides.close() if closeOrig then return closeOrig() end end
return passThroughProxy(PM, overrides, absent, "src.ui.PartyMenu")
end
COVERAGE["src.ui.PartyMenu"] = {
kind = "facade", target = "src.ui.game3.party_menu",
backed = "new close show isOpen update draw handleInput",
warned = "",
absent = "drawIcon frameFor mirrorsIcon iconFrames sgbPalettes animateTo "
.. "entryY bottomMessage index submenu",
notes = {
new = "opens the FireRed party screen over session.party and returns a "
.. "handle { party, close }, not a state object; onSwitch(mon, handle) "
.. "and onCancel are honoured, the rest of opts is not",
},
}
local function buildStartMenu()
local SM = ui3("start_menu") or {}
local overrides = {}
function overrides.new(_game)
local Hud = ui3("hud")
if not (Hud and inField()) then return nil end
if not (SM.isOpen and SM.isOpen()) then Hud.openStartMenu(live(), session()) end
return SM
end
return passThroughProxy(SM, overrides, {}, "src.ui.StartMenu")
end
COVERAGE["src.ui.StartMenu"] = {
kind = "facade", target = "src.ui.game3.start_menu",
backed = "new show close isOpen draw ENTRIES",
warned = "",
absent = "items ITEMS lastIndex",
notes = {
new = "Hud.openStartMenu, which closes any other field menu first; "
.. "returns the FireRed start menu module",
ENTRIES = "the assembled list, rebuilt on every show(); the hook below "
.. "hands it back and a table return replaces it",
["hook ui.start_menu.items"] = "raised from src/ui/game3/start_menu.lua, "
.. "same name and arity as Gen 1 and Gold, but a FireRed row is "
.. "{ id, label } and carries no onSelect to rewire, and a non-table "
.. "return is dropped without the Logger.error the other two emit",
},
}
local function buildOptionsMenu()
local OM = ui3("option_menu") or {}
local overrides = {}
local showOrig = OM.show
function overrides.new(_game, opts)
opts = opts or {}
if not showOrig then return nil end
showOrig({ session = session(), game = live(), onClose = opts.onCancel })
return OM
end
return passThroughProxy(OM, overrides, { sgbPalettes = UI_ABSENT },
"src.ui.OptionsMenu")
end
COVERAGE["src.ui.OptionsMenu"] = {
kind = "facade", target = "src.ui.game3.option_menu",
backed = "new show close isOpen draw handleInput",
warned = "",
absent = "sgbPalettes rows index scroll",
notes = {
new = "OptionMenu.show with opts.onCancel as onClose",
},
}
local function buildBoxMenu()
local PC = ui3("pc_menu") or {}
local overrides = {}
function overrides.new(_game)
local Hud = ui3("hud")
if not (Hud and inField()) then return nil end
Hud.openPc(live(), session())
return PC
end
return passThroughProxy(PC, overrides, {}, "src.ui.BoxMenu")
end
COVERAGE["src.ui.BoxMenu"] = {
kind = "facade", target = "src.ui.game3.pc_menu",
backed = "new show close isOpen draw handleInput",
warned = "",
absent = "items th tx tw scroll clampScroll",
notes = {
new = "Hud.openPc: Bill's / the player's PC top menu, the Gen 1 BoxMenu's "
.. "counterpart",
},
}
-- ------- src.battle.BattleState
local function battleModule()
return package.loaded["src.core.game3.battle.init"]
or package.loaded["src.core.game3.battle"]
end
local function buildBattleState()
local B = {}
local MOVED = "FireRed's battle engine is src/core/game3/battle, over pret's "
.. "battle_main.c; no Gen 1 BattleState object exists"
local absent = {}
for _, name in ipairs({
"newWild", "newTrainer", "makeSafari", "makeGhost", "makeBattler",
"resolveTurn", "executeAction", "performMove", "computeDamage",
"catchAttempt", "runRoll", "enter", "exit", "sgbPalettes", "awardExp",
"throwBall", "storeCaughtMon", "tryRun",
}) do
absent[name] = MOVED
end
function B.isActive()
local M = battleModule()
return M and M.isActive and M.isActive() or false
end
function B.current()
local M = battleModule()
return M and M.isActive and M.isActive() and M.getState and M.getState() or nil
end
function B.say(_self, text)
local Ui = package.loaded["src.core.game3.battle.ui"]
if not (Ui and Ui.push and B.isActive()) then return nil end
Ui.push(tostring(text or ""))
return true
end
B.sayAuto = B.say
local LIVE = { player = true, enemy = true, turnCount = true, phase = true,
result = true, kind = true, wild = true }
return setmetatable(B, {
__index = function(_, key)
if LIVE[key] then
local st = B.current()
if not st then return nil end
if key == "turnCount" then return st.turn end
if key == "phase" then
local M = battleModule()
return M and M._phase
end
return st[key]
end
local why = absent[key]
if why then
warnOnce("battle." .. key, "[%s] BattleState.%s has no Gen 3 backing: %s",
who("src.battle.BattleState"), key, why)
end
return nil
end,
})
end
COVERAGE["src.battle.BattleState"] = {
kind = "facade", target = "src.core.game3.battle",
backed = "isActive current say sayAuto player enemy turnCount phase result "
.. "kind wild",
warned = "",
absent = "newWild newTrainer makeSafari makeGhost makeBattler resolveTurn "
.. "executeAction performMove computeDamage catchAttempt runRoll enter "
.. "exit sgbPalettes awardExp throwBall storeCaughtMon tryRun update draw",
notes = {
player = "the live FireRed battler { mon, side, species, type1, type2, "
.. "ability, stages, status }; mon carries NUMERIC species and moves",
newWild = "ABSENT: start a wild battle with mod.world:startWildBattle, or "
.. "rewrite the species through the encounter.species hook",
say = "queues onto the FireRed battle textbox (battle/ui.lua Ui.push)",
events = "battle.* events and hooks fire from src/core/game3/battle for every "
.. "battler, with battlerId 0-3 in doubles; species, moves and items are "
.. "NAME strings with the FireRed number alongside (speciesId, moveNum, ballId)",
},
}
-- ------- src.script.ScriptRunner
local SR = "src.script.ScriptRunner"
local function buildScriptRunner()
local okGen1, Gen1 = pcall(rawRequire, "src.script.ScriptRunner")
local adapter = {}
adapter.scanLabels = okGen1 and Gen1.scanLabels or nil
function adapter.validate(script, lookup)
if not (okGen1 and Gen1.validate) then return nil end
return Gen1.validate(script, lookup or function(verb)
local g = live()
local commands = g and g.data and g.data.commands
return commands ~= nil and commands[verb] ~= nil
end)
end
local Handle = {}
Handle.__index = function(self, key)
if key == "game" then return live() end
local S = space()
local vm = S and S.vm
if key == "vm" then return vm end
if key == "ctx" then return vm and vm.ctx end
return rawget(Handle, key)
end
function Handle:isRunning()
local S = space()
return S and S.vm and S.vm:isRunning() or false
end
function Handle:run(script, extra)
if extra ~= nil then
warnOnce("runner.run.extra",
"[%s] runner:run's `extra` is unserved on FireRed", who(SR))
end
local S = space()
if not (S and S.vm) then return false end
if type(script) ~= "string" then
warnOnce("runner.run.rows",
"[%s] runner:run on FireRed takes an extracted script KEY (g3:...); a "
.. "Gen 1 row list runs through mod.world:queueScript", who(SR))
return false
end
if S.vm:isRunning() then return false end
return S.startScript(script) and true or false
end
Handle.resume = unbacked(SR, "runner:resume",
"the FireRed VM is ticked by src/core/game3/field.lua; a second drive "
.. "double-steps it")
Handle.update = unbacked(SR, "runner:update",
"the FireRed VM is ticked by src/core/game3/field.lua every frame")
Handle.exec = unbacked(SR, "runner:exec",
"the script.command hook is the supported patch point")
Handle.yield = unbacked(SR, "runner:yield",
"FireRed scripts are pret bytecode, not coroutines")
Handle.makeContext = unbacked(SR, "runner:makeContext",
"Gen3Compat.scriptCtx(vm) builds the Gen 1-shaped ctx events carry")
function adapter.new(_game, _overworld)
return setmetatable({}, Handle)
end
return adapter
end
COVERAGE[SR] = {
kind = "facade", target = "src.core.game3.scripting.vm",
backed = "scanLabels validate new isRunning run game vm ctx",
warned = "resume update exec yield makeContext",
absent = "co waitingFrames parallel waitingCheck overworld __index",
notes = {
run = "takes an extracted FireRed script key and starts it on the one "
.. "Space VM; returns false while another script runs",
new = "a thin handle onto src/core/game3/scripting/space.lua's VM, which "
.. "is replaced on every map change, so the handle resolves it per call",
},
}
-- ------- src.world.WorldAPI
COVERAGE["src.world.WorldAPI"] = {
kind = "alias", target = "src.world.game3.WorldAPI",
backed = "new __index overworld current activeBlockAt canReorderParty "
.. "reorderParty availableFieldActions useFieldAction canFly flyTo "
.. "mapOverview warpTo toggleObject setFlag getFlag effectiveEncounters "
.. "replaceBlock npc queueScript startWildBattle invalidateMap",
warned = "spawnNpc removeNpc",
absent = "",
notes = {
current = "mapId is the raw FR_ id; warpTo accepts either spelling",
setFlag = "a FireRed flag NAME (with or without FLAG_) or number",
replaceBlock = "one 16px metatile, not a 32px block",
queueScript = "the five Gen 2 verbs (start_battle wild, warp, text, "
.. "setflag, clearflag); anything else is refused by name up front",
},
}
-- ------- sprites
local spriteOverrides = { front = {}, back = {} }
local imageCache = {}
local wrappedModules = setmetatable({}, { __mode = "k" })
local reloadRegistered = setmetatable({}, { __mode = "k" })
local function isVanillaPic(path)
return type(path) ~= "string" or path == "" or path:find("%.rgba$") ~= nil
end
local function readImageData(path)
if not (love and love.image and love.image.newImageData) then return nil end
local okA, Assets = pcall(rawRequire, "src.render.Assets")
if okA and Assets and Assets.imageData then
local ok, data = pcall(Assets.imageData, path)
if ok and data then return data end
end
local ok, data = pcall(love.image.newImageData, path)
if ok and data then return data end
return nil
end
local function centredEntry(path)
local hit = imageCache[path]
if hit ~= nil then return hit or nil end
local data = readImageData(path)
if not (data and love.graphics and love.graphics.newImage) then
warnOnce("sprite." .. tostring(path),
"[gen3] sprite override %s could not be loaded", tostring(path))
imageCache[path] = false
return nil
end
local w, h = data:getDimensions()
local out = data
if w ~= 64 or h ~= 64 then
out = love.image.newImageData(64, 64)
local cw, ch = math.min(w, 64), math.min(h, 64)
local sx = math.floor((w - cw) / 2)
local sy = math.floor((h - ch) / 2)
local dx = math.floor((64 - cw) / 2)
local dy = math.floor((64 - ch) / 2)
out:paste(data, dx, dy, sx, sy, cw, ch)
end
local image = love.graphics.newImage(out)
if image.setFilter then image:setFilter("nearest", "nearest") end
hit = { image = image, w = 64, h = 64, path = path }
imageCache[path] = hit
return hit
end
Gen3Compat.centredSprite = centredEntry
local function samePath(path) return path end
local function hookedEntry(side, species, form, vanilla)
if not Runtime.wantsHook("pokemon.sprite") then return vanilla end
local P = g3("pokemon")
local path = spriteOverrides[side][species]
or ("data/generated/gba/pokemon/" .. side .. "/" .. species .. ".rgba")
local g = live()
local ctx = { data = g and dataProxy(g.data), species = Gen3Compat.speciesName(species),
gen3Species = species, form = form, side = side, kind = "battle",
trueColor = true, path = path }
local hooked = Runtime.call("pokemon.sprite", samePath, path, ctx)
if type(hooked) ~= "string" or hooked == path or isVanillaPic(hooked) then
return vanilla
end
return centredEntry(hooked) or vanilla
end
local function wrapPics(P)
if not P or wrappedModules[P] then return end
wrappedModules[P] = true
local frontOrig, backOrig = P.frontPic, P.backPic
if frontOrig then
P.frontPic = function(species, form, shiny, personality)
local sp = tonumber(species)
local path = sp and (tonumber(form) or 0) == 0 and spriteOverrides.front[sp]
local entry = path and centredEntry(path)
if not entry then entry = frontOrig(species, form, shiny, personality) end
if sp then return hookedEntry("front", sp, form, entry) end
return entry
end
P.frontSprite = P.frontPic
end
if backOrig then
P.backPic = function(species, form, shiny)
local sp = tonumber(species)
local path = sp and (tonumber(form) or 0) == 0 and spriteOverrides.back[sp]
local entry = path and centredEntry(path)
if not entry then entry = backOrig(species, form, shiny) end
if sp then return hookedEntry("back", sp, form, entry) end
return entry
end
end
end
local function seed(P)
if not P then return end
P._front = P._front or {}
P._back = P._back or {}
for sp, path in pairs(spriteOverrides.front) do
local entry = centredEntry(path)
if entry then P._front[sp] = entry end
end
for sp, path in pairs(spriteOverrides.back) do
local entry = centredEntry(path)
if entry then P._back[sp] = entry end
end
end
function Gen3Compat.reseedSprites()
local P = package.loaded["src.core.game3.pokemon"]
if P then
wrapPics(P)
seed(P)
end
end
local function collectOverrides(game)
spriteOverrides = { front = {}, back = {} }
local content = game and game.mods and game.mods.content
local reg = content and content.pokemon
if not (reg and reg.ops) then return end
for id in pairs(reg.ops) do
local ok, rec = pcall(reg.get, reg, id)
if ok and type(rec) == "table" then
local sp = Gen3Compat.speciesId(id)
if not sp and type(rec.gen3Species) == "number" then sp = rec.gen3Species end
if sp then
if not isVanillaPic(rec.spriteFront) then
spriteOverrides.front[sp] = rec.spriteFront64 or rec.spriteFront
end
if not isVanillaPic(rec.spriteBack) then
spriteOverrides.back[sp] = rec.spriteBack64 or rec.spriteBack
end
end
end
end
end
function Gen3Compat.spriteOverrides()
local out = { front = {}, back = {} }
for side, map in pairs(spriteOverrides) do
for sp, path in pairs(map) do out[side][sp] = path end
end
return out
end
local function reapply(name, target, what)
local g = live()
local content = g and g.mods and g.mods.content
local reg = content and content[name]
if not (target and reg and reg.ops and next(reg.ops) ~= nil) then return end
local spec = reg.spec
if spec and type(spec.write) == "function" then
local ok, err = pcall(spec.write, target, reg)
if not ok then
warnOnce(name .. ".reapply", "[gen3] mod %s data not re-applied after reload: %s",
what, tostring(err))
end
end
for key in pairs(recordCache) do recordCache[key] = nil end
end
local function reapplyMoves(M)
reapply("moves", M, "move")
end
-- Pokemon.install() replaces every species table with a fresh copy of the
-- ROM pack, and Runtime runs it on entering FireRed -- after the loader has
-- merged the mods onto the old tables. Write the moves and pokemon
-- registries again onto the new ones: move names live in that same pack
-- (Pokemon._moveNames), and so does the battle-move copy the moves writer
-- mirrors its rows into. Moves first, as Loader:_mergeOrder does: the moves
-- writer is what puts a mod's own moves in the move index, and the species
-- writer reads that index to resolve learnsets, egg moves and TM/HM lists.
local function reapplyPokemon(P)
local g = live()
local M = g and g.data and g.data.gen3Moves
if not M then
local okM, Moves = pcall(rawRequire, "src.core.game3.battle.moves")
M = okM and type(Moves) == "table" and Moves or nil
end
reapplyMoves(M)
reapply("pokemon", P, "species")
end
function Gen3Compat.applyMerged(game)
if game then lastGame = game end
imageCache = {}
for key in pairs(recordCache) do recordCache[key] = nil end
local okM, Moves = pcall(rawRequire, "src.core.game3.battle.moves")
if okM and type(Moves) == "table" and type(Moves.onReload) == "function"
and not reloadRegistered[Moves] then
reloadRegistered[Moves] = true
Moves.onReload(reapplyMoves, "gen3compat")
end
collectOverrides(game or live())
local okP, P = pcall(rawRequire, "src.core.game3.pokemon")
if not (okP and P) then return end
wrapPics(P)
seed(P)
if type(P.onReload) == "function" and not reloadRegistered[P] then
reloadRegistered[P] = true
P.onReload(function()
for key in pairs(recordCache) do recordCache[key] = nil end
reapplyPokemon(P)
wrapPics(P)
seed(P)
end, "gen3compat")
end
end
-- ------- script ctx
function Gen3Compat.scriptCtx(vm)
local S = space()
return {
game = live(),
save = SAVE_VIEW,
overworld = inField() and Gen3Compat.resolve(OW) or nil,
runner = vm or (S and S.vm),
vm = vm or (S and S.vm),
generation = 3,
}
end
-- ------- the table
local ADAPTERS = {
["src.core.Game"] = buildGame,
["src.world.NPC"] = buildNpc,
["src.world.Collision"] = buildCollision,
["src.world.FieldDefaults"] = buildFieldDefaults,
["src.pokemon.Boxes"] = buildBoxes,
["src.world.OverworldController"] = buildOverworld,
["src.ui.PartyMenu"] = buildPartyMenu,
["src.ui.StartMenu"] = buildStartMenu,
["src.ui.OptionsMenu"] = buildOptionsMenu,
["src.battle.BattleState"] = buildBattleState,
["src.script.ScriptRunner"] = buildScriptRunner,
["src.world.PikachuFollower"] = buildFollower,
["src.world.Map"] = buildMap,
["src.world.WorldAPI"] = "src.world.game3.WorldAPI",
["src.ui.BoxMenu"] = buildBoxMenu,
}
Gen3Compat.ADAPTERS = ADAPTERS
function Gen3Compat.endSession()
resolveGame, lastGame = nil, nil
built, claimants, warned = {}, {}, {}
end
function Gen3Compat.bind(fn)
resolveGame = fn
end
function Gen3Compat.serves(name)
return ADAPTERS[name] ~= nil
end
function Gen3Compat.modules()
local out = {}
for name in pairs(ADAPTERS) do out[#out + 1] = name end
table.sort(out)
return out
end
function Gen3Compat.coverage(name)
local row = COVERAGE[name]
if not row then return nil end
local members = {}
for _, status in ipairs({ "backed", "absent", "warned" }) do
for _, member in ipairs(words(row[status])) do
members[member] = status
end
end
local notes = {}
for key, value in pairs(row.notes or {}) do notes[key] = value end
return { module = name, kind = row.kind, target = row.target,
members = members, notes = notes }
end
function Gen3Compat.memberStatus(name, member)
local row = Gen3Compat.coverage(name)
return row and row.members[member] or nil
end
function Gen3Compat.resolve(name, modId)
local spec = ADAPTERS[name]
if not spec then return nil end
local module = built[name]
if not module then
module = type(spec) == "string" and rawRequire(spec) or spec()
built[name] = module
end
if modId then
local ids = claimants[name]
if not ids then ids = {} claimants[name] = ids end
local seen = false
for _, id in ipairs(ids) do if id == modId then seen = true break end end
if not seen then ids[#ids + 1] = modId end
end
return module
end
return Gen3Compat