Files
gen1recomp/tools/save-editor/Gen.lua
T
bryanthaboi d615ad39ab bazinga 2
2026-09-13 03:34:39 -04:00

754 lines
24 KiB
Lua

-- Generation adapter for the save editor. Panels stay generation-blind;
-- Ops and App read Gold vs RBY vs FRLG through this module so a write never
-- lands in wrong-generation fields.
local GameVersion = require("src.core.GameVersion")
local Gen = {}
local function versionGeneration(version)
if type(version) ~= "string" then return nil end
local info = GameVersion.info(version)
if info then return info.generation or 1 end
return nil
end
function Gen.of(save, version)
if type(save) == "table" then
if save.generation == 3 or save.engine == "game3" or save.version == "firered" then return 3 end
if save.healMap ~= nil or (save.bag and save.bag.pockets ~= nil) or (save.dex and save.dex.national ~= nil) then return 3 end
if save.generation == 2 then return 2 end
local fromVersion = versionGeneration(save.version)
if fromVersion then return fromVersion end
end
local fromArg = versionGeneration(version)
if fromArg then return fromArg end
return GameVersion.generation()
end
local function versionOf(save, version)
if type(save) == "table" and GameVersion.VERSIONS[save.version] then
return save.version
end
if type(version) == "string" and GameVersion.VERSIONS[version] then
return version
end
return GameVersion.get()
end
Gen.versionOf = versionOf
function Gen.engineOf(save, version)
return GameVersion.engine(versionOf(save, version))
end
function Gen.editionLabel(save, version)
local info = GameVersion.info(versionOf(save, version))
if Gen.of(save, version) == 3 then
return tostring((info and info.label) or "FIRE RED"):upper()
end
return tostring((info and info.label) or "GEN 2"):upper()
end
function Gen.hasCaughtData(save, version)
if Gen.of(save, version) ~= 2 then return false end
return require("src.battle.gen2.Mon").hasCaughtData(versionOf(save, version))
end
-- engine/menus/init_gender.asm:23-38 (Crystal), FRLG Oak intro (gender 0/1)
function Gen.hasPlayerGender(save, version)
local g = Gen.of(save, version)
if g == 3 then return true end
if g == 2 then return Gen.engineOf(save, version) == "crystal" end
return false
end
function Gen.ofState(S)
if not S then return GameVersion.generation() end
return Gen.of(S.save, S.version)
end
function Gen.is2(save, version)
return Gen.of(save, version) == 2
end
function Gen.is3(save, version)
return Gen.of(save, version) == 3
end
local function overlayRecords(base, overlay)
if not overlay then return base or {} end
if not base or base == overlay then return overlay end
local out = {}
for id, def in pairs(base) do out[id] = def end
for id, def in pairs(overlay) do
local prior = out[id]
if type(def) == "table" and type(prior) == "table" then
local merged = {}
for k, v in pairs(prior) do merged[k] = v end
for k, v in pairs(def) do merged[k] = v end
out[id] = merged
else
out[id] = def
end
end
return out
end
function Gen.maps(data)
if type(data) ~= "table" then return {} end
local m = overlayRecords(data.maps, data.gen2Maps)
return overlayRecords(m, data.game3Maps)
end
function Gen.tilesets(data)
if type(data) ~= "table" then return {} end
local t = overlayRecords(data.tilesets, data.gen2Tilesets)
return overlayRecords(t, data.game3Tilesets)
end
-- Point the Gen 2 Data keys at the tables Data:load already filled
function Gen.bindGoldData(data)
if type(data) ~= "table" then return data end
if data.maps and data.gen2Maps == nil then data.gen2Maps = data.maps end
if data.tilesets and data.gen2Tilesets == nil then
data.gen2Tilesets = data.tilesets
end
if data.palettes and data.gen2Palettes == nil then
data.gen2Palettes = data.palettes
end
local loadGen = function(rel)
local CacheFs = require("src.import.CacheFs")
local bytes = CacheFs.readActive("data/generated/" .. rel .. ".lua")
if type(bytes) == "string" then
local chunk = loadstring(bytes, "@gold/data/generated/" .. rel .. ".lua")
if chunk then
local ok, res = pcall(chunk)
if ok and type(res) == "table" then return res end
end
end
local ok, res = pcall(require, "data.generated." .. rel)
if ok and type(res) == "table" then return res end
return nil
end
data.gen2Palettes = data.gen2Palettes or loadGen("palettes")
data.gen2Constants = data.gen2Constants or loadGen("constants")
data.gen2Icons = data.gen2Icons or loadGen("icons")
data.gen2Pokedex = data.gen2Pokedex or loadGen("pokedex")
data.gen2Landmarks = data.gen2Landmarks or loadGen("landmarks")
data.gen2Roofs = data.gen2Roofs or loadGen("roofs") or data.roofs
data.gen2Sprites = data.gen2Sprites or loadGen("sprites")
return data
end
-- Bind Game 3 (FireRed) data into Data table for Save Editor
function Gen.bindGame3Data(data)
if type(data) ~= "table" then return data end
local okD, Dataset = pcall(require, "src.core.game3.dataset")
if okD and Dataset then
if Dataset.mountExtractRoots then pcall(Dataset.mountExtractRoots) end
local g3Maps = Dataset.buildMaps()
if g3Maps then
local cache = Dataset.cache and Dataset.cache()
if cache and Dataset.attachMidLayouts then
pcall(Dataset.attachMidLayouts, g3Maps, cache)
end
local okN, NativeTileset = pcall(require, "src.core.game3.tileset_native")
if okN and NativeTileset and NativeTileset.install and cache then
pcall(NativeTileset.install, cache, nil)
end
data.game3Maps = g3Maps
data.maps = overlayRecords(data.maps, g3Maps)
end
end
local okP, Pokemon = pcall(require, "src.core.game3.pokemon")
if okP and Pokemon then
pcall(Pokemon.install, nil)
data.pokemon = data.pokemon or {}
for id = 1, 412 do
local name = Pokemon.name(id)
if name and name ~= "??????????" and name ~= "" then
local def = {
id = name,
name = name,
species = id,
speciesId = id,
dex = id,
growthRate = (Pokemon.speciesMeta and Pokemon.speciesMeta(id) and Pokemon.speciesMeta(id).growthRate) or 0,
}
data.pokemon[name] = def
data.pokemon[id] = def
end
end
data.moves = data.moves or {}
for id = 1, 354 do
local mName = Pokemon.moveName(id)
local bMove = Pokemon.battleMove(id)
if mName and mName ~= "-------" and not mName:find("^MOVE %d+") then
local mDef = {
id = mName,
moveId = id,
name = mName,
pp = (bMove and tonumber(bMove.pp)) or 10,
power = (bMove and tonumber(bMove.power)) or 40,
type = (bMove and bMove.type) or "NORMAL",
}
data.moves[mName] = mDef
data.moves[id] = mDef
end
end
end
local okI, ItemsData = pcall(require, "src.core.game3.items_data")
if okI and ItemsData then
data.items = data.items or {}
for k, v in pairs(ItemsData.BY_HOST or {}) do
local iDef = { id = k, name = v.name or k, pocket = v.pocket, itemId = v.frlg }
data.items[k] = iDef
end
for num = 1, 375 do
local info = ItemsData.info(num)
if info and info.name and info.name ~= "none" and info.name ~= "" then
local normName = info.name:upper():gsub("[^A-Z0-9_]", "_"):gsub("_+", "_")
local iDef = { id = normName, name = info.name, pocket = info.pocket, itemId = num }
data.items[normName] = iDef
data.items[num] = iDef
end
end
end
return data
end
function Gen.newGame(version)
local id = type(version) == "string" and GameVersion.VERSIONS[version] and version
or nil
local g = versionGeneration(id) or GameVersion.generation()
if g == 3 then
return require("src.core.game3.save_schema_firered").newGame({ version = id })
elseif g == 2 then
local Save2 = require("src.core.gen2.Save")
local save = Save2.newGame({
playerName = id and Save2.defaultPlayerName(id) or nil,
})
if id then save.version = id end
return save
end
return require("src.core.SaveData").newGame({ version = id })
end
function Gen.validate(save, data)
local g = Gen.of(save)
if g == 3 then
return { lostMons = {}, lostItems = {}, remappedMaps = {} }
elseif g == 2 then
return require("src.core.gen2.Save").validate(save)
end
return require("src.core.SaveData").validate(save, data)
end
function Gen.emptyReport(save, report)
local g = Gen.of(save)
if g == 3 then
if not report then return true end
return #(report.lostMons or {}) == 0 and #(report.lostItems or {}) == 0 and #(report.remappedMaps or {}) == 0
elseif g == 2 then
return require("src.core.gen2.Save").emptyReport(report)
end
return require("src.core.SaveData").emptyReport(report)
end
function Gen.hydrateMon(data, mon)
if type(mon) ~= "table" then return mon end
local isG3 = mon.speciesId ~= nil or mon.personality ~= nil or mon.ivs ~= nil or (mon.evs ~= nil and mon.evs.spa ~= nil)
if isG3 then
local okP, Pokemon = pcall(require, "src.core.game3.pokemon")
if okP and Pokemon then
local spId = tonumber(mon.speciesId or mon.species)
or (Pokemon.speciesFromName and Pokemon.speciesFromName(tostring(mon.species)))
if spId then
mon.speciesId = spId
local name = Pokemon.name(spId)
if name and name ~= "" and name ~= "??????????" then
mon.species = name
end
end
mon.ivs = mon.ivs or { hp = 0, atk = 0, def = 0, spe = 0, spa = 0, spd = 0 }
mon.evs = mon.evs or { hp = 0, atk = 0, def = 0, spe = 0, spa = 0, spd = 0 }
mon.dvs = mon.dvs or {
attack = math.floor((mon.ivs.atk or 0) / 2),
defense = math.floor((mon.ivs.def or 0) / 2),
speed = math.floor((mon.ivs.spe or 0) / 2),
special = math.floor(((mon.ivs.spa or 0) + (mon.ivs.spd or 0)) / 4),
hp = math.floor((mon.ivs.hp or 0) / 2),
}
Pokemon.applyStats(mon)
mon.stats = {
hp = mon.maxHp or mon.hp or 10,
attack = mon.attack or 10,
defense = mon.defense or 10,
speed = mon.speed or 10,
spAtk = mon.spAtk or mon.spa or 10,
spDef = mon.spDef or mon.spd or 10,
specialAttack = mon.spAtk or mon.spa or 10,
specialDefense = mon.spDef or mon.spd or 10,
}
end
return mon
end
local def = data and data.pokemon and data.pokemon[mon.species]
local gen2 = (mon.stats and mon.stats.specialAttack)
or (def and def.baseStats and def.baseStats.specialAttack)
or mon.experience ~= nil
if gen2 then
require("src.battle.gen2.Mon").refreshStats(mon, data)
else
require("src.pokemon.Stats").ensure(def, mon)
end
return mon
end
function Gen.hydrateSave(data, save)
if type(save) ~= "table" then return save end
local g = Gen.of(save)
if g == 3 then
save.inventory = save.inventory or {}
save.bagOrder = save.bagOrder or {}
save.pcItems = save.pcItems or {}
save.bag = save.bag or require("src.core.game3.bag").new()
if save.inventory then
local arraySlots = {}
for k, v in pairs(save.inventory) do
if type(k) == "number" and type(v) == "table" then
arraySlots[#arraySlots + 1] = k
local sId = v.id or v.itemId or v.name or v[1]
local sQty = tonumber(v.qty or v.quantity or v.count or v[2]) or 1
if sId and sQty > 0 then
local okD, ItemsData = pcall(require, "src.core.game3.items_data")
local okI, Items = pcall(require, "src.core.game3.items")
local num = okD and ItemsData and ItemsData.toNumericId(sId)
local name = (num and okI and Items and Items.FRLG_TO_HOST[num]) or (okD and ItemsData and ItemsData.bagKey(sId)) or tostring(sId)
save.inventory[name] = sQty
end
end
end
for _, k in ipairs(arraySlots) do
save.inventory[k] = nil
end
end
if save.bag and save.bag.pockets then
local okD, ItemsData = pcall(require, "src.core.game3.items_data")
local okI, Items = pcall(require, "src.core.game3.items")
if okD and ItemsData and okI and Items then
for _, pocketList in pairs(save.bag.pockets) do
for _, slot in ipairs(pocketList or {}) do
if slot.id and (tonumber(slot.qty) or 0) > 0 then
local num = ItemsData.toNumericId(slot.id)
local name = (num and Items.FRLG_TO_HOST[num]) or ItemsData.bagKey(slot.id) or tostring(slot.id)
save.inventory[name] = tonumber(slot.qty) or 1
end
end
end
end
end
if save.pcItems then
local arraySlots = {}
for k, v in pairs(save.pcItems) do
if type(k) == "number" and type(v) == "table" then
arraySlots[#arraySlots + 1] = k
local sId = v.id or v.itemId or v.name or v[1]
local sQty = tonumber(v.qty or v.quantity or v.count or v[2]) or 1
if sId and sQty > 0 then
local okD, ItemsData = pcall(require, "src.core.game3.items_data")
local okI, Items = pcall(require, "src.core.game3.items")
local num = okD and ItemsData and ItemsData.toNumericId(sId)
local name = (num and okI and Items and Items.FRLG_TO_HOST[num]) or (okD and ItemsData and ItemsData.bagKey(sId)) or tostring(sId)
save.pcItems[name] = sQty
end
end
end
for _, k in ipairs(arraySlots) do
save.pcItems[k] = nil
end
end
local pcLists = {}
if type(save.storage) == "table" and type(save.storage.items) == "table" then
pcLists[1] = save.storage.items
elseif type(save.pc) == "table" and type(save.pc.items) == "table" then
pcLists[1] = save.pc.items
end
for _, pcList in ipairs(pcLists) do
local okD, ItemsData = pcall(require, "src.core.game3.items_data")
local okI, Items = pcall(require, "src.core.game3.items")
if okD and ItemsData and okI and Items then
for _, slot in ipairs(pcList) do
local id = slot.id or slot.itemId
local qty = slot.qty or slot.quantity or 1
if id and (tonumber(qty) or 0) > 0 then
local num = ItemsData.toNumericId(id)
local name = (num and Items.FRLG_TO_HOST[num]) or ItemsData.bagKey(id) or tostring(id)
save.pcItems[name] = tonumber(qty) or 1
end
end
end
end
for _, mon in ipairs(save.party or {}) do Gen.hydrateMon(data, mon) end
for _, box in ipairs(save.boxes or {}) do
if type(box) == "table" then
for _, mon in ipairs(box) do Gen.hydrateMon(data, mon) end
end
end
return save
elseif g == 2 then
local Mon = require("src.battle.gen2.Mon")
Mon.eachSaveMon(save, function(mon) Mon.refreshStats(mon, data) end)
return save
end
for _, mon in ipairs(save.party or {}) do Gen.hydrateMon(data, mon) end
for _, box in ipairs(save.boxes or {}) do
if type(box) == "table" then
for _, mon in ipairs(box) do Gen.hydrateMon(data, mon) end
end
end
if save.daycare and save.daycare.mon then
Gen.hydrateMon(data, save.daycare.mon)
end
return save
end
function Gen.ensureBoxes(save)
local g = Gen.of(save)
if g == 3 then
save.boxes = save.boxes or {}
for i = 1, 14 do
save.boxes[i] = save.boxes[i] or {}
end
save.currentBox = math.max(1, math.min(14, save.currentBox or 1))
return save.boxes
elseif g == 2 then
local Boxes2 = require("src.core.gen2.Boxes")
save.boxes = save.boxes or {}
for i = 1, Boxes2.NUM_BOXES do
save.boxes[i] = save.boxes[i] or {}
end
save.currentBox = math.max(1, math.min(Boxes2.NUM_BOXES, save.currentBox or 1))
return save.boxes
end
return require("src.pokemon.Boxes").ensure(save)
end
function Gen.boxCount(save)
local g = Gen.of(save)
if g == 3 then return 14 end
if g == 2 then return require("src.core.gen2.Boxes").NUM_BOXES end
return require("src.pokemon.Boxes").COUNT
end
function Gen.boxCapacity(save)
local g = Gen.of(save)
if g == 3 then return 30 end
if g == 2 then return require("src.core.gen2.Boxes").MONS_PER_BOX end
return require("src.pokemon.Boxes").CAPACITY
end
function Gen.money(save)
if type(save) ~= "table" then return 0 end
if Gen.of(save) == 2 then
return (save.player and save.player.money) or 0
end
return save.money or 0
end
function Gen.setMoney(save, amount)
if type(save) ~= "table" then return end
if Gen.of(save) == 2 then
save.player = save.player or {}
save.player.money = amount
else
save.money = amount
end
end
function Gen.coins(save)
if type(save) ~= "table" then return 0 end
if Gen.of(save) == 2 then
return (save.player and save.player.coins) or 0
end
return save.coins or 0
end
function Gen.setCoins(save, amount)
if type(save) ~= "table" then return end
if Gen.of(save) == 2 then
save.player = save.player or {}
save.player.coins = amount
else
save.coins = amount
end
end
function Gen.playerGender(save)
if type(save) ~= "table" then return "male" end
if save.gender == 1 or (save.player and (save.player.gender == 1 or save.player.gender == "female")) then
return "female"
end
return "male"
end
function Gen.setPlayerGender(save, gender)
if type(save) ~= "table" then return "male" end
local isFemale = (gender == "female" or gender == 1)
save.gender = isFemale and 1 or 0
if save.player then
save.player.gender = isFemale and "female" or "male"
end
return isFemale and "female" or "male"
end
function Gen.landmarkName(data, index)
index = math.floor(tonumber(index) or 0)
local Mon = require("src.battle.gen2.Mon")
if index == Mon.LANDMARK_EVENT then return "EVENT" end
if index == Mon.LANDMARK_GIFT then return "GIFT" end
if index == 0 then return "UNKNOWN" end
local rows = data and data.gen2Landmarks and data.gen2Landmarks.landmarks
for _, rec in pairs(rows or {}) do
if type(rec) == "table" and rec.index == index then
return (tostring(rec.name or rec.id):gsub("\n", " "))
end
end
return ("#%d"):format(index)
end
function Gen.dexOwnedKey(save)
if Gen.of(save) == 2 then return "caught" end
return "owned"
end
function Gen.playerMap(save)
if type(save) ~= "table" then return "FR_PALLET_TOWN", 0, 0 end
local g = Gen.of(save)
if g == 3 then
local map = save.map or (save.position and save.position.map) or (save.player and save.player.map) or "FR_PALLET_TOWN"
local x = save.x or (save.position and save.position.x) or (save.player and save.player.x) or 0
local y = save.y or (save.position and save.position.y) or (save.player and save.player.y) or 0
local facing = save.facing or (save.position and save.position.facing) or "down"
return map, x, y, facing
elseif g == 2 then
local p = save.position
if p and p.map then return p.map, p.x or 0, p.y or 0, p.facing end
if type(save.spawn) == "table" then
return save.spawn.map or "PLAYERS_HOUSE_2F", save.spawn.x or 0, save.spawn.y or 0, save.spawn.facing
elseif type(save.spawn) == "string" then
return save.spawn, 0, 0
end
return "PLAYERS_HOUSE_2F", 3, 3
end
local p = save.player or {}
return p.map or "REDS_HOUSE_2F", p.x or 0, p.y or 0
end
function Gen.setPlayerHere(save, mapId, x, y, facing)
if type(save) ~= "table" then return end
local g = Gen.of(save)
if g == 3 then
save.map = mapId
save.x = x
save.y = y
save.facing = facing or save.facing or "down"
save.position = {
map = mapId,
x = x,
y = y,
facing = save.facing,
}
if save.player then
save.player.map = mapId
save.player.x = x
save.player.y = y
end
return
elseif g == 2 then
local prev = save.position or {}
save.position = {
map = mapId,
x = x,
y = y,
facing = facing or prev.facing or "down",
}
return
end
save.player = save.player or {}
save.player.map = mapId
save.player.x = x
save.player.y = y
end
local JOHTO = {
"ZEPHYR", "HIVE", "PLAIN", "FOG", "MINERAL", "STORM", "GLACIER", "RISING",
}
local KANTO = {
"BOULDER", "CASCADE", "THUNDER", "RAINBOW",
"SOUL", "MARSH", "VOLCANO", "EARTH",
}
function Gen.badgeIds(save, cat)
local g = Gen.of(save)
if g == 3 then
local ids = {}
for _, name in ipairs(KANTO) do ids[#ids + 1] = name .. "BADGE" end
return ids
elseif g == 2 then
local ids = {}
for _, name in ipairs(JOHTO) do ids[#ids + 1] = name end
for _, name in ipairs(KANTO) do ids[#ids + 1] = name end
return ids
end
local ids = {}
for _, id in ipairs((cat and cat.items) or {}) do
if tostring(id):find("BADGE", 1, true) then ids[#ids + 1] = id end
end
return ids
end
local KANTO_SET = {}
for _, name in ipairs(KANTO) do KANTO_SET[name] = true end
function Gen.hasBadge(save, id)
if type(save) ~= "table" then return false end
local g = Gen.of(save)
if g == 3 then
local okF, Flags = pcall(require, "src.core.game3.scripting.flags")
if okF and Flags then
return Flags.hasBadge(save, id)
end
return save.flags and (save.flags[id] or save.flags[id:gsub("BADGE$", "")]) == true
elseif g == 2 then
local p = save.player or {}
local store = KANTO_SET[id] and (p.kantoBadges or {}) or (p.badges or {})
if store[id] then return true end
local list = KANTO_SET[id] and KANTO or JOHTO
for index, name in ipairs(list) do
if name == id then return store[index] == true end
end
return false
end
return save.inventory and save.inventory[id] and true or false
end
function Gen.toggleBadge(save, id)
if type(save) ~= "table" then return false end
local g = Gen.of(save)
if g == 3 then
local okF, Flags = pcall(require, "src.core.game3.scripting.flags")
if okF and Flags then
local on = Flags.hasBadge(save, id)
Flags.setBadge(save, id, not on)
return not on
end
local on = Gen.hasBadge(save, id)
save.flags = save.flags or {}
save.flags[id] = (not on) and true or nil
return not on
elseif g == 2 then
save.player = save.player or {}
local storeName = KANTO_SET[id] and "kantoBadges" or "badges"
save.player[storeName] = save.player[storeName] or {}
local store = save.player[storeName]
local on = Gen.hasBadge(save, id)
store[id] = (not on) and true or nil
for index, name in ipairs(KANTO_SET[id] and KANTO or JOHTO) do
if name == id then store[index] = nil end
end
return not on
end
save.inventory = save.inventory or {}
local on = save.inventory[id] and true or false
save.inventory[id] = (not on) and 1 or nil
return not on
end
local function gen2FlagId(save, name)
return require("Gen2Flags").byName(Gen.engineOf(save))[name]
end
function Gen.getFlag(save, name)
if type(save) ~= "table" then return false end
local g = Gen.of(save)
if g == 3 then
local okF, Flags = pcall(require, "src.core.game3.scripting.flags")
if okF and Flags then
return Flags.getFlag(save, nil, name)
end
return save.flags and (save.flags[name] == true or save.flags[tostring(name)] == true)
elseif g == 2 then
local id = gen2FlagId(save, name)
if id then
local Events2 = require("src.world.gen2.Events")
local ev = Events2.new()
ev:restore(save.events)
return ev:get(id)
end
return save.flags and save.flags[name] == true
end
return save.flags and save.flags[name] == true
end
function Gen.setFlag(save, name, on)
if type(save) ~= "table" then return end
local g = Gen.of(save)
if g == 3 then
local okF, Flags = pcall(require, "src.core.game3.scripting.flags")
if okF and Flags then
Flags.setFlag(save, nil, name, on)
return
end
save.flags = save.flags or {}
save.flags[name] = on and true or nil
return
elseif g == 2 then
local id = gen2FlagId(save, name)
if id then
local Events2 = require("src.world.gen2.Events")
local ev = Events2.new()
ev:restore(save.events)
ev:set(id, on and true or false)
save.events = ev:serialize()
return
end
save.flags = save.flags or {}
save.flags[name] = on and true or nil
return
end
save.flags = save.flags or {}
save.flags[name] = on and true or nil
end
function Gen.flagCount(save)
if type(save) ~= "table" then return 0 end
local g = Gen.of(save)
if g == 2 then
local n = 0
for _ in pairs(save.events or {}) do n = n + 1 end
for _ in pairs(save.flags or {}) do n = n + 1 end
return n
end
local n = 0
for _ in pairs(save.flags or {}) do n = n + 1 end
return n
end
function Gen.exp(mon)
return mon.exp or mon.experience or 0
end
return Gen