Files
gen1recomp/src/core/Data.lua
T
2026-07-21 05:50:40 -04:00

208 lines
7.9 KiB
Lua

-- Loads generated data from either the private first-boot cache or the
-- optional source-tree developer build.
local Logger = require("src.core.Logger")
local Data = {}
local MODULES = {
"constants", "maps", "tilesets", "text", "text_pointers",
"trainer_headers", "font", "sprites", "pokemon", "moves", "items",
"type_chart", "trainers", "encounters", "field", "battle_anims",
}
-- Optional for compatibility with developer and stale caches.
local OPTIONAL = { "audio", "palettes", "icons" }
-- The rules the engine still carries as literals. The constants registry
-- deep-merges over these, so a value has to exist before a mod can patch
-- it; each one is the number the engine hard-codes today, so seeding them
-- changes nothing on a mod-free boot.
local CONSTANT_DEFAULTS = {
bagSize = 20, -- BAG_ITEM_CAPACITY (src/inventory/Bag.lua)
partyMax = 6, -- PARTY_LENGTH (src/pokemon/Party.lua)
boxCount = 12, boxSize = 20, -- Bill's PC (src/pokemon/Boxes.lua)
moveMax = 4,
levelCap = 100,
coinCap = 9999, -- MAX_COINS (src/ui/SlotMachine.lua)
-- move-slot repair when a scrub empties a mon (src/core/SaveData.lua);
-- a total conversion without TACKLE patches this to its own floor
fallbackMove = "TACKLE",
hmMoves = { "CUT", "FLY", "SURF", "STRENGTH", "FLASH" }, -- IsMoveHM
-- gym order (data/scripts/victories.lua); list position is the badge
-- number the trainer card draws
badges = {
{ id = "BOULDERBADGE" }, { id = "CASCADEBADGE" }, { id = "THUNDERBADGE" },
{ id = "RAINBOWBADGE" }, { id = "SOULBADGE" }, { id = "MARSHBADGE" },
{ id = "VOLCANOBADGE" }, { id = "EARTHBADGE" },
},
}
-- field.boot is the total-conversion override point for the new game; the
-- values match what SaveData.newGame and the Oak speech used to inline.
local BOOT_DEFAULTS = {
-- special_warps.asm NewGameWarp: REDS_HOUSE_2F, 3, 6 -- the bedroom, not
-- the tile outside the house. lastHeal is deliberately absent: SaveData
-- derives the vanilla blackout point, and seeding it here would leak into
-- total conversions that patch the spawn without naming a heal point.
startMap = "REDS_HOUSE_2F", startX = 3, startY = 6, startFacing = "down",
playerName = "RED", rivalName = "BLUE",
startMoney = 3000,
screens = { splash = "IntroMovie", title = "TitleState", newGame = "OakSpeech" },
}
local function copy(value)
if type(value) ~= "table" then return value end
local out = {}
for k, v in pairs(value) do out[k] = copy(v) end
return out
end
-- Fills only what the cache is missing, so an importer that learns to
-- stamp one of these keys silently takes over from the engine.
function Data:seedDefaults()
local constants = self.constants
for key, value in pairs(CONSTANT_DEFAULTS) do
if constants[key] == nil then constants[key] = copy(value) end
end
-- derived, not literal: a dataset with a different roster gets the right
-- upper bound without 151 being written down anywhere
if constants.dexSize == nil then
local highest = 0
for _, def in pairs(self.pokemon) do
if def.dex and def.dex > highest then highest = def.dex end
end
constants.dexSize = highest
end
if constants.dexDigits == nil then
constants.dexDigits = math.max(3, #tostring(constants.dexSize))
end
local boot = self.field.boot
if boot == nil then
boot = {}
self.field.boot = boot
end
for key, value in pairs(BOOT_DEFAULTS) do
if boot[key] == nil then boot[key] = copy(value) end
end
-- the naming screen presets the importer already extracts but nothing
-- ever read (field.presetNames)
if boot.namePresets == nil then
local presets = self.field.presetNames or {}
boot.namePresets = {
player = copy(presets.player) or { "RED", "ASH", "JACK" },
rival = copy(presets.rival) or { "BLUE", "GARY", "JOHN" },
}
end
-- the overworld's Kanto literals, same fill-if-absent contract; required
-- here rather than at the top so core keeps out of src/world at load time
require("src.world.FieldDefaults").seed(self)
end
-- POKEPORT_DATA_DIR points a test runner at another dataset root (the
-- ROM-free fixture set, tests/fixture_data); unset -- every shipped build
-- -- the generated modules load exactly as before. loadfile skips the
-- require cache, so each overridden load hands back fresh tables.
local function loadModule(dir, name)
if dir then
local chunk, err = loadfile(dir .. "/" .. name .. ".lua")
if not chunk then return false, err end
return pcall(chunk)
end
return pcall(require, "data.generated." .. name)
end
function Data:load()
local dir = os.getenv("POKEPORT_DATA_DIR")
for _, name in ipairs(MODULES) do
local ok, mod = loadModule(dir, name)
if not ok then
if dir then
error(("missing data module '%s/%s.lua' (POKEPORT_DATA_DIR).\n(%s)")
:format(dir, name, mod))
end
error(("missing generated data module 'data/generated/%s.lua'.\n" ..
"Import the ROM again or rebuild developer data.\n(%s)")
:format(name, mod))
end
self[name] = mod
end
for _, name in ipairs(OPTIONAL) do
local ok, mod = loadModule(dir, name)
self[name] = ok and mod or nil
if not ok then
Logger.warn("optional data module '%s' missing (feature disabled)", name)
end
end
-- before the mod loader runs: the deep registries fold over these
self:seedDefaults()
-- the top-level keys a pristine load leaves behind, so reloadGenerated can
-- strip whatever a mod merge added since; kept on self (assigned before the
-- scan so it counts itself) because tests load other tables through this
-- method, and a shared upvalue would let them clobber the singleton's set
local pristine = {}
self._pristineKeys = pristine
for key in pairs(self) do pristine[key] = true end
Logger.info("generated data loaded (%d maps, %d species, %d moves)",
(function() local n = 0 for _ in pairs(self.maps) do n = n + 1 end return n end)(),
(function() local n = 0 for _ in pairs(self.pokemon) do n = n + 1 end return n end)(),
(function() local n = 0 for _ in pairs(self.moves) do n = n + 1 end return n end)())
end
-- dev-mode hot reload only (src/dev/HotReload.lua): drop every namespace the
-- mod merge created, then re-require the generated modules so base records
-- return to their on-disk values even where a mod edited them in place
function Data:reloadGenerated()
local pristine = self._pristineKeys
if pristine then
for key in pairs(self) do
if not pristine[key] then self[key] = nil end
end
end
for _, name in ipairs(MODULES) do
package.loaded["data.generated." .. name] = nil
end
for _, name in ipairs(OPTIONAL) do
package.loaded["data.generated." .. name] = nil
end
self:load()
end
-- Resolve a dotted target path, creating empty tables on the way. Only the
-- mod merge calls this; a vanilla boot never does, so an unmodded Data
-- table is byte-identical to a pre-registry-v2 one.
function Data.ensure(data, path)
local node = data
for key in path:gmatch("[^%.]+") do
if node[key] == nil then node[key] = {} end
node = node[key]
end
return node
end
-- Resolve a TEXT_* constant on a map to a plain string (or nil if the text
-- needs a hand-ported script; see data/scripts/).
function Data:resolveText(mapLabel, textConst)
local entry = self:textEntry(mapLabel, textConst)
if not entry then return nil end
if entry.text then
local s = self.text[entry.text]
if s then return s, entry.asm end
end
return nil, entry.asm
end
-- The raw text-pointer entry (carries mart/nurse/pc markers and the label).
function Data:textEntry(mapLabel, textConst)
local perMap = self.text_pointers[mapLabel]
return perMap and perMap[textConst] or nil
end
-- Trainer sight/dialogue header for a map object (or nil).
function Data:trainerHeader(mapLabel, objIndex)
local perMap = self.trainer_headers[mapLabel]
return perMap and perMap[objIndex] or nil
end
return Data