G2 support

This commit is contained in:
bryanthaboi
2026-08-11 11:52:56 -04:00
parent 79ed37699e
commit ae6cac89e1
489 changed files with 226677 additions and 1798 deletions
+144 -1
View File
@@ -295,6 +295,15 @@ function SaveData.defaultOptions()
-- Native mod enablement is an installation option, not save-slot data.
-- Missing entries mean enabled so newly installed mods work by default.
mods = {},
-- Mods the player forced past the target gate (Loader:_gateGeneration).
-- modsGen2[id][version] = true, one answer per game; a bare `true` is the
-- pre-per-game shape and means the Gen 2 games only (see modForced).
modsGen2 = {},
-- Per-game enablement: modsByVersion[version][id] answers for that game
-- only and falls through to the shared mods[id] above when absent, so an
-- options.lua written before this key keeps its exact meaning. Read and
-- written through SaveData.modEnabled / SaveData.setModEnabled.
modsByVersion = {},
-- Named setups the player can switch between (#593; src/mods/ModProfile.lua
-- owns the shape, src/mods/ManagerState.lua the UI): each row is
-- { name, enabled = {id=bool}, options = {id={k=v}}, slots = {version=slotId} }.
@@ -541,6 +550,127 @@ function SaveData.loadOptions(fs)
return SaveData.mergeOptions(data)
end
-- ------- per-game mod enablement
--
-- One installed mod, one id, one enable flag per game that wants to differ.
-- options.mods is the shared answer every version used to get; the overlay
-- only holds the games the player actually chose for, so a mod set can differ
-- between Red and Gold without either one owning the other's flags.
-- Whether a per-game answer is honoured at boot. The loader reads the enable
-- flags once, before any entry chunk (src/mods/Loader.lua _loadState), so this
-- flips on with that read and not before: until then every writer keeps to the
-- shared flag and no surface promises what the boot does not do.
SaveData.PER_VERSION_MODS = false
-- The version a write should be scoped to: the game asked for once per-game
-- flags are live, nil (the shared flag) while they are only a preview.
function SaveData.modScope(version)
if SaveData.PER_VERSION_MODS then return version end
return nil
end
-- true/false as chosen for `version`, else the shared flag, else nil -- the
-- caller owns the default (the loader enables, the launcher keeps
-- experimental mods off until asked).
function SaveData.modEnabled(options, id, version)
local byVersion = options and options.modsByVersion
local bucket = version and type(byVersion) == "table" and byVersion[version]
if type(bucket) == "table" and type(bucket[id]) == "boolean" then
return bucket[id]
end
local shared = options and options.mods
if type(shared) == "table" and type(shared[id]) == "boolean" then
return shared[id]
end
return nil
end
-- Write the choice for one game, or the shared flag when version is nil. A
-- per-game entry that agrees with the shared flag is dropped rather than
-- stored, so the overlay stays the list of deliberate differences.
function SaveData.setModEnabled(options, id, enabled, version)
if type(options) ~= "table" or type(id) ~= "string" or id == "" then
return options
end
enabled = enabled and true or false
if not version then
options.mods = options.mods or {}
options.mods[id] = enabled
return options
end
options.modsByVersion = options.modsByVersion or {}
local bucket = options.modsByVersion[version] or {}
options.modsByVersion[version] = bucket
-- no shared flag reads as enabled, the same default the loader applies to a
-- missing entry, so a fresh install never fills the overlay with agreement
local shared = options.mods and options.mods[id]
if type(shared) ~= "boolean" then shared = true end
if shared == enabled then
bucket[id] = nil
else
bucket[id] = enabled
end
return options
end
-- ------- the player's target override
--
-- The manifest's `games` is the AUTHOR's claim and the loader enforces it
-- (Loader:_gateGeneration); this is the player's per-game override of that
-- claim. Scoped by version, because "run it on Gold anyway" is not an answer
-- about Red: a version-blind flag forced a mod past a gate on a game its
-- owner was never asked about.
-- A pre-per-game `true` could only ever take effect on a Gen 2 boot (the gate
-- returned early on Gen 1), so that is exactly what it is read as here.
local function forcedGenerations(entry)
return entry == true and 2 or nil
end
function SaveData.modForced(options, id, version, generation)
local entry = type(options) == "table" and type(options.modsGen2) == "table"
and options.modsGen2[id]
if entry == nil or entry == false then return false end
local gen = generation or (version and GameVersion.generation(version))
local legacy = forcedGenerations(entry)
if legacy then return legacy == gen end
if type(entry) ~= "table" then return false end
if version then return entry[version] == true end
-- no version, only a generation: a harness seam, so ask whether ANY game of
-- that generation was forced rather than inventing a game
for id2, on in pairs(entry) do
if on == true and GameVersion.generation(id2) == gen then return true end
end
return false
end
-- Write the override for one game. Without a version there is no game to
-- answer for, so this writes nothing rather than guessing (the caller keeps
-- the choice in memory for this boot and says so).
function SaveData.setModForced(options, id, forced, version)
if type(options) ~= "table" or type(id) ~= "string" or id == "" then
return false
end
if not (version and GameVersion.VERSIONS[version]) then return false end
options.modsGen2 = options.modsGen2 or {}
local entry = options.modsGen2[id]
if type(entry) ~= "table" then
-- migrate the legacy flag in place, keeping the games it already covered
local expanded = {}
if forcedGenerations(entry) then
for _, other in ipairs(GameVersion.ORDER) do
if GameVersion.generation(other) == 2 then expanded[other] = true end
end
end
entry = expanded
options.modsGen2[id] = entry
end
entry[version] = forced and true or nil
if next(entry) == nil then options.modsGen2[id] = nil end
return true
end
-- ------- save slots
-- A version's playthroughs live in numbered slots under saves/<version>/;
@@ -677,7 +807,20 @@ function SaveData.slotSummary(save)
for _ in pairs((save.pokedex and save.pokedex.owned) or {}) do
dexCount = dexCount + 1
end
local t = math.floor(save.playTime or 0)
-- playTime is a plain seconds count in a Gen 1 save but a
-- { hours, minutes, seconds, frames } table in a Gen 2 (Gold) save, matching
-- the cart's wGameTime* bytes. The launcher calls slotSummary on EVERY
-- version's slot, so this has to read both shapes or the whole launcher
-- crashes the moment a Gold save exists (math.floor on the table).
local pt = save.playTime
local t
if type(pt) == "table" then
t = (tonumber(pt.hours) or 0) * 3600
+ (tonumber(pt.minutes) or 0) * 60
+ (tonumber(pt.seconds) or 0)
else
t = math.floor(tonumber(pt) or 0)
end
local timeText = ("%d:%02d"):format(math.floor(t / 3600),
math.floor(t / 60) % 60)
return name, {