mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-15 07:41:21 +02:00
check bonxes
This commit is contained in:
+8
-6
@@ -130,18 +130,20 @@ before `Game:load`, so **it never loads a mod's entry chunk**; only
|
||||
duplicate) and returns one row per mod:
|
||||
`{id, name, version, badge, description, enabled, status, statusDetail}`.
|
||||
`badge` is the manifest's `category`, falling back to `profile`, then
|
||||
`"MOD"`, uppercased. `enabled` reads `options.mods[id]` (missing means
|
||||
enabled, matching the loader's own default).
|
||||
`"MOD"`, uppercased. `enabledByVersion` contains an answer for each game;
|
||||
missing entries default to enabled (except experimental mods), matching the
|
||||
loader. On the first run with per-game controls, legacy shared choices are
|
||||
copied to every installed game's answer.
|
||||
- `status` is `"ok"`, `"warn"`, or `"conflict"`, computed by the pure
|
||||
`LauncherMods.deriveList`/`statusFor` against `ManagerState.resolveToggle`
|
||||
and the validated manifests: `conflict` when enabling this mod collides
|
||||
with another enabled one; `warn` for an out-of-range `game_version` or an
|
||||
absent/disabled/wrong-version hard dependency; `ok` otherwise. Having no
|
||||
`love.*` calls, this half is table-driven by the test suite on its own.
|
||||
- `LauncherMods.setEnabled(id, bool)` persists `options.mods[id]` as a plain
|
||||
boolean, the exact shape `Loader:_saveState` writes, so the running game
|
||||
and the in-game `ManagerState` see the change on next boot. The mods panel
|
||||
calls this on every toggle and re-derives the list right away
|
||||
- `LauncherMods.setEnabled(id, bool, version)` persists the selected game's
|
||||
answer, so the running game and the in-game `ManagerState` see the change on
|
||||
next boot. The MODS panel renders a coloured checkbox for Red, Blue, Yellow,
|
||||
and Gold on every row and re-derives the list right away
|
||||
(`RomImporter:_refreshMods`) so a status change (e.g. a new conflict)
|
||||
shows without waiting for a reload.
|
||||
- `LauncherMods.installZip(path)` mounts the archive with
|
||||
|
||||
@@ -96,14 +96,14 @@ The launcher asks the same question of a mod's dependencies: one whose hard
|
||||
dependency does not run on the selected game reads `Needs <id> (not for Gold)`,
|
||||
matching the loader's contagious skip.
|
||||
|
||||
A separate overlay, `options.modsByVersion[version][id]`, is where a per-game
|
||||
enable flag will live. It is a preview: `SaveData.PER_VERSION_MODS` is `false`,
|
||||
so `SaveData.modScope` answers nil for every caller and the launcher panel, the
|
||||
in-game manager and the loader all read *and* write the one shared
|
||||
`options.mods` flag. Nothing consults the overlay for enablement until that
|
||||
flips, which is deliberate: the overlay is plantable from an imported
|
||||
`.g1rmodlist`, and a reader scoped differently from the writers would show a
|
||||
mod set no boot would honour.
|
||||
A separate overlay, `options.modsByVersion[version][id]`, holds each game's
|
||||
enable flag. The launcher shows a coloured Red / Blue / Yellow / Gold checkbox
|
||||
for every installed mod, and the loader and in-game manager read the same
|
||||
game-specific answer on the next boot. On the first launch after this feature,
|
||||
the existing shared state is copied to every game, so a mod that was enabled
|
||||
remains enabled everywhere; after that, changing one checkbox affects only
|
||||
that game. New mods still default to enabled on every game (experimental mods
|
||||
retain their explicit opt-in default).
|
||||
|
||||
That is deliberate. Gold reimplements the battle engine, the overworld, the
|
||||
script VM and the save format, so a Gen 1 mod dropped into a Gold boot would
|
||||
|
||||
@@ -260,18 +260,13 @@ selected game reads `Needs <id> (not for Gold)` rather than `Ready`.
|
||||
|
||||
### One limit worth knowing
|
||||
|
||||
**Per-game enable flags are still a preview.** The overlay
|
||||
`options.modsByVersion[version][id]` exists and every surface goes through
|
||||
`SaveData.modEnabled` / `SaveData.setModEnabled`, but
|
||||
`SaveData.PER_VERSION_MODS` is `false`
|
||||
(`src/core/SaveData.lua:489`). While it is false, `SaveData.modScope` answers
|
||||
nil for every caller, so the launcher panel, the in-game manager *and* the
|
||||
loader all read and write the one shared `options.mods` flag and the overlay is
|
||||
not consulted for enablement anywhere. That matters because the overlay is
|
||||
plantable from an imported `.g1rmodlist`: keeping every reader on the same
|
||||
scope as every writer is what stops a stored per-game flag from showing a mod
|
||||
set no boot would honour. Nothing about this affects a mod author; it affects
|
||||
what a player can currently express.
|
||||
**Enablement is per game.** The overlay
|
||||
`options.modsByVersion[version][id]` is read and written through
|
||||
`SaveData.modEnabled` / `SaveData.setModEnabled` by the launcher, in-game
|
||||
manager, and loader. Existing shared settings are copied to every game the
|
||||
first time this version sees the installed mods; from then on, each coloured
|
||||
game checkbox changes only that game's next boot. Nothing about this affects a
|
||||
mod author; it affects what a player can express.
|
||||
|
||||
Targeting is a different question from enablement and *is* enforced per game,
|
||||
as above. The two do not share a switch.
|
||||
|
||||
+60
-7
@@ -304,6 +304,10 @@ function SaveData.defaultOptions()
|
||||
-- options.lua written before this key keeps its exact meaning. Read and
|
||||
-- written through SaveData.modEnabled / SaveData.setModEnabled.
|
||||
modsByVersion = {},
|
||||
-- Set after the first per-game enablement migration. Older options files
|
||||
-- have only `mods`, so the migration copies each installed mod's current
|
||||
-- answer to every game before game-specific toggles begin changing it.
|
||||
modsByVersionMigrated = false,
|
||||
-- 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} }.
|
||||
@@ -563,19 +567,68 @@ end
|
||||
-- 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
|
||||
-- Per-game answers are live. Every reader and writer goes through modScope,
|
||||
-- so a choice made in the launcher is the choice the next boot loads.
|
||||
SaveData.PER_VERSION_MODS = true
|
||||
|
||||
-- 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.
|
||||
-- The version a write is scoped to: per-game controls name one game; a nil
|
||||
-- caller still addresses the legacy shared fallback.
|
||||
function SaveData.modScope(version)
|
||||
if SaveData.PER_VERSION_MODS then return version end
|
||||
return nil
|
||||
end
|
||||
|
||||
-- Promote an installation that predates per-game flags. `mods` may contain
|
||||
-- manifest rows ({ id, experimental }) or bare ids. A mod with no old entry
|
||||
-- had the loader default: on, except for experimental mods. Copy that answer
|
||||
-- to every game once, preserving any per-game overlay somebody imported before
|
||||
-- this feature shipped. New installs need no rows here: an absent answer
|
||||
-- still defaults to enabled for every game.
|
||||
function SaveData.migrateModEnablement(options, mods)
|
||||
if type(options) ~= "table" or options.modsByVersionMigrated then return false end
|
||||
options.mods = type(options.mods) == "table" and options.mods or {}
|
||||
options.modsByVersion = type(options.modsByVersion) == "table"
|
||||
and options.modsByVersion or {}
|
||||
|
||||
local known = {}
|
||||
for id in pairs(options.mods) do
|
||||
if type(id) == "string" and id ~= "" then known[id] = { id = id } end
|
||||
end
|
||||
for version, bucket in pairs(options.modsByVersion) do
|
||||
if GameVersion.VERSIONS[version] and type(bucket) == "table" then
|
||||
for id in pairs(bucket) do
|
||||
if type(id) == "string" and id ~= "" then known[id] = known[id] or { id = id } end
|
||||
end
|
||||
end
|
||||
end
|
||||
for _, mod in ipairs(mods or {}) do
|
||||
local id = type(mod) == "table" and mod.id or mod
|
||||
if type(id) == "string" and id ~= "" then
|
||||
known[id] = type(mod) == "table" and mod or (known[id] or { id = id })
|
||||
end
|
||||
end
|
||||
|
||||
-- Do not create options.lua just to record an empty migration on a fresh
|
||||
-- no-mod boot. Keep it pending until there is a real installed or saved
|
||||
-- mod answer to preserve.
|
||||
if next(known) == nil then return false end
|
||||
|
||||
for id, mod in pairs(known) do
|
||||
local shared = options.mods[id]
|
||||
if type(shared) ~= "boolean" then shared = not (mod.experimental == true) end
|
||||
for _, version in ipairs(GameVersion.ORDER) do
|
||||
local bucket = options.modsByVersion[version]
|
||||
if type(bucket) ~= "table" then
|
||||
bucket = {}
|
||||
options.modsByVersion[version] = bucket
|
||||
end
|
||||
if type(bucket[id]) ~= "boolean" then bucket[id] = shared end
|
||||
end
|
||||
end
|
||||
options.modsByVersionMigrated = true
|
||||
return true
|
||||
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).
|
||||
|
||||
+51
-22
@@ -1139,6 +1139,28 @@ local function drawCheck(x, y, size, color)
|
||||
love.graphics.pop()
|
||||
end
|
||||
|
||||
-- One compact coloured checkbox for each game. The cartridge colour carries
|
||||
-- the game identity even when the row is narrow; the letter keeps an unchecked
|
||||
-- box legible without relying on colour alone.
|
||||
local function modGameCheckbox(x, y, size, checked, game, id)
|
||||
local color = cartColor(game)
|
||||
local focused = Kit.focusable(id, x, y, size, size)
|
||||
local hot = focused or Kit.hover(x, y, size, size)
|
||||
if love.graphics then
|
||||
if checked then
|
||||
Theme.fillRounded(x, y, size, size, color, 1)
|
||||
drawCheck(x, y, size, PAL.inverse)
|
||||
else
|
||||
Theme.fillRounded(x, y, size, size, PAL.bg, 1)
|
||||
Kit.textCenterBold("micro", game:sub(1, 1):upper(), x,
|
||||
y + (size - Kit.textHeight("micro")) / 2, size, color)
|
||||
end
|
||||
Theme.strokeRounded(x, y, size, size, color,
|
||||
hot and Theme.A.focus or Theme.A.hover, 1)
|
||||
end
|
||||
return Kit.press(x, y, size, size) or Kit._activateId == id
|
||||
end
|
||||
|
||||
local function buildModsPanel(imp, x, y, w, availH, m)
|
||||
imp:_ensureMods()
|
||||
local ModUpdate = require("src.mods.ModUpdate")
|
||||
@@ -1225,15 +1247,14 @@ local function buildModsPanel(imp, x, y, w, availH, m)
|
||||
mods = sorted
|
||||
end
|
||||
|
||||
-- A mod row is a fixed height: name line, version + status line, one line
|
||||
-- of description, and an action row. Fixed because a page of uniform rows
|
||||
-- is what lets perPage come from the viewport.
|
||||
local chipH = math.max(Kit.tapMin(), math.floor(30 * m.s))
|
||||
-- Text block on the left, chips right-aligned beside it: one row, not a
|
||||
-- text block with a button strip stacked under it.
|
||||
-- A mod row is a fixed height: its details first, then a dedicated second
|
||||
-- line of per-game checkboxes. Fixed because a page of uniform rows is
|
||||
-- what lets perPage come from the viewport.
|
||||
local togH = math.floor(26 * m.s)
|
||||
local gamesLabel = Strings("Enable for:")
|
||||
local textH = Kit.textHeight("button") + math.floor(4 * m.s)
|
||||
+ Kit.textHeight("small") + math.floor(2 * m.s) + Kit.textHeight("small")
|
||||
local rowH = math.floor(8 * m.s) + math.max(textH, chipH)
|
||||
local rowH = math.floor(8 * m.s) + textH + math.floor(8 * m.s) + togH
|
||||
+ math.floor(8 * m.s)
|
||||
local pagerH = math.max(Kit.tapMin(), math.floor(30 * m.s))
|
||||
local listH = availH - (cy - y) - pagerH - gap
|
||||
@@ -1258,28 +1279,36 @@ local function buildModsPanel(imp, x, y, w, availH, m)
|
||||
local px, inner = x + pad, w - 2 * pad
|
||||
local ly = ry + math.floor(10 * m.s)
|
||||
|
||||
local togW = math.floor(56 * m.s)
|
||||
local togH = math.floor(26 * m.s)
|
||||
local togGap = math.floor(4 * m.s)
|
||||
local info = mod.github and mod.github ~= "" and imp:_modUpdateInfo(mod.id)
|
||||
|
||||
local togKey = "mod-toggle-" .. mod.id
|
||||
-- The toggle reports its own new value, but the importer owns the state:
|
||||
-- queue the flip and let _toggleMod (which may raise an experimental-mod
|
||||
-- confirm) decide what actually happens.
|
||||
local _, flipped = Kit.toggle(px + inner - togW,
|
||||
ry + (rowH - togH) / 2, togW, togH, mod.enabled, togKey)
|
||||
if flipped then
|
||||
queueAction(imp, togKey, function() imp:_toggleMod(mod.id) end)
|
||||
-- These answer separate games, not a single shared install flag. The
|
||||
-- importer receives the game id so an experimental confirmation also
|
||||
-- applies only to the checkbox the player pressed.
|
||||
local flipped = false
|
||||
local gamesY = ry + math.floor(8 * m.s) + textH + math.floor(8 * m.s)
|
||||
Kit.text("micro", gamesLabel, px,
|
||||
gamesY + (togH - Kit.textHeight("micro")) / 2, PAL.muted)
|
||||
local tx = px + Kit.textWidth("micro", gamesLabel) + math.floor(10 * m.s)
|
||||
for _, game in ipairs(GameVersion.ORDER) do
|
||||
local togKey = "mod-toggle-" .. mod.id .. "-" .. game
|
||||
if modGameCheckbox(tx, gamesY, togH,
|
||||
mod.enabledByVersion and mod.enabledByVersion[game] == true,
|
||||
game, togKey) then
|
||||
local version = game
|
||||
queueAction(imp, togKey, function() imp:_toggleMod(mod.id, nil, version) end)
|
||||
flipped = true
|
||||
end
|
||||
tx = tx + togH + togGap
|
||||
end
|
||||
-- The toggle sits inside the row's rect, so its press also passes the
|
||||
-- row's hit test; `flipped` gates the row action to everywhere else.
|
||||
-- The checkboxes sit inside the row's rect, so their press also passes the
|
||||
-- row hit test; `flipped` gates the row action to everywhere else.
|
||||
if not flipped
|
||||
and (Kit.press(x, ry, w, rowH) or Kit._activateId == rowKey) then
|
||||
local id = mod.id
|
||||
queueAction(imp, rowKey, function() imp._modActions = id end)
|
||||
end
|
||||
local chipsW = togW + math.floor(6 * m.s)
|
||||
local textW = inner - chipsW - math.floor(12 * m.s)
|
||||
local textW = inner
|
||||
|
||||
local badgeW = Kit.textWidth("micro", mod.badge) + math.floor(12 * m.s)
|
||||
-- the games the mod is for, beside its category: the same chip the
|
||||
@@ -1741,7 +1770,7 @@ local function buildConfirmModal(imp, m)
|
||||
elseif c.kind == "importOversize" then
|
||||
imp:_importSave(c.version, c.source, true)
|
||||
else
|
||||
imp:_toggleMod(c.id, true)
|
||||
imp:_toggleMod(c.id, true, c.version)
|
||||
end
|
||||
end,
|
||||
})
|
||||
|
||||
@@ -3064,15 +3064,19 @@ function RomImporter:_modUpdateInfo(id)
|
||||
return self.modUpdateInfo and self.modUpdateInfo[id] or nil
|
||||
end
|
||||
|
||||
-- Flip a mod's enabled flag (persisted via LauncherMods.setEnabled) and relist
|
||||
-- so the toggle, count, and every status chip reflect the new resolution.
|
||||
-- Enabling an experimental mod arms a confirm first.
|
||||
function RomImporter:_toggleMod(id, confirmed)
|
||||
-- Flip one game's mod flag (persisted via LauncherMods.setEnabled) and relist
|
||||
-- so that game's checkbox and status chips reflect the new resolution.
|
||||
-- Enabling an experimental mod arms a confirmation for that same game.
|
||||
function RomImporter:_toggleMod(id, confirmed, version)
|
||||
local LauncherMods = require("src.mods.LauncherMods")
|
||||
local cur, experimental = false, false
|
||||
for _, m in ipairs(self.mods or {}) do
|
||||
if m.id == id then
|
||||
cur = m.enabled
|
||||
if version and m.enabledByVersion then
|
||||
cur = m.enabledByVersion[version] == true
|
||||
else
|
||||
cur = m.enabled
|
||||
end
|
||||
experimental = m.experimental == true
|
||||
break
|
||||
end
|
||||
@@ -3080,7 +3084,7 @@ function RomImporter:_toggleMod(id, confirmed)
|
||||
local want = not cur
|
||||
if want and experimental and not confirmed then
|
||||
self._modConfirm = {
|
||||
kind = "experimental", id = id,
|
||||
kind = "experimental", id = id, version = version,
|
||||
title = "Experimental mod",
|
||||
yesLabel = "Enable",
|
||||
lines = {
|
||||
@@ -3092,7 +3096,7 @@ function RomImporter:_toggleMod(id, confirmed)
|
||||
return
|
||||
end
|
||||
self._modConfirm = nil
|
||||
LauncherMods.setEnabled(id, want, self.modScope)
|
||||
LauncherMods.setEnabled(id, want, version or self.modScope)
|
||||
self:_refreshMods()
|
||||
end
|
||||
|
||||
|
||||
+44
-11
@@ -36,6 +36,7 @@ local ModTargets = require("src.mods.ModTargets")
|
||||
local Semver = require("src.mods.Semver")
|
||||
local Version = require("src.core.Version")
|
||||
local SaveData = require("src.core.SaveData")
|
||||
local GameVersion = require("src.core.GameVersion")
|
||||
local CacheFs = require("src.import.CacheFs")
|
||||
|
||||
local LauncherMods = {}
|
||||
@@ -107,8 +108,9 @@ end
|
||||
-- deriveList(manifests, options [, version]) -> the panel row list, pure.
|
||||
-- manifests is an array of validated manifests (Manifest.validate output);
|
||||
-- options is the options table (options.mods, options.modsByVersion and
|
||||
-- options.modsGen2 are read). `version` is the game the panel is showing:
|
||||
-- nil keeps the pre-per-game view, where the shared flag is the whole answer.
|
||||
-- options.modsGen2 are read). `version` is the game the panel is showing;
|
||||
-- each row also carries its answer for every game so the launcher can render
|
||||
-- the coloured game checkboxes together.
|
||||
-- Rows come back sorted by id so the panel order is stable.
|
||||
function LauncherMods.deriveList(manifests, options, version)
|
||||
local ordered = {}
|
||||
@@ -126,8 +128,7 @@ function LauncherMods.deriveList(manifests, options, version)
|
||||
-- this game's choice, then the shared flag, then the default: enabled,
|
||||
-- matching the loader -- except experimental mods, which stay off until
|
||||
-- the player opts in. Scoped through modScope, so this reads exactly what
|
||||
-- setEnabled writes and the loader loads: while per-game flags are a
|
||||
-- preview the shared flag is the whole answer on every surface.
|
||||
-- setEnabled writes and the loader loads for the selected game.
|
||||
local decided = SaveData.modEnabled(options, m.id, SaveData.modScope(version))
|
||||
if decided == nil then decided = not m.experimental end
|
||||
if decided then enabledSet[m.id] = true end
|
||||
@@ -152,6 +153,14 @@ function LauncherMods.deriveList(manifests, options, version)
|
||||
badge = badge,
|
||||
description = m.description or "",
|
||||
enabled = enabled,
|
||||
enabledByVersion = (function()
|
||||
local answers = {}
|
||||
for _, game in ipairs(GameVersion.ORDER) do
|
||||
local answer = SaveData.modEnabled(options, m.id, game)
|
||||
answers[game] = answer == true or (answer == nil and not m.experimental)
|
||||
end
|
||||
return answers
|
||||
end)(),
|
||||
status = status,
|
||||
statusDetail = detail,
|
||||
github = m.github,
|
||||
@@ -281,7 +290,14 @@ end
|
||||
function LauncherMods.list(version)
|
||||
local ok, result = pcall(function()
|
||||
local options = SaveData.loadOptions()
|
||||
return LauncherMods.deriveList(discover(), options, version)
|
||||
local manifests = discover()
|
||||
-- The first build containing game-specific switches turns the old shared
|
||||
-- state into one explicit answer per installed mod and game. Saving here
|
||||
-- means users who only visit the launcher still receive the migration.
|
||||
if SaveData.migrateModEnablement(options, manifests) then
|
||||
SaveData.saveOptions(options)
|
||||
end
|
||||
return LauncherMods.deriveList(manifests, options, version)
|
||||
end)
|
||||
if not ok then
|
||||
-- a single bad options/mod file must not blank the launcher
|
||||
@@ -364,10 +380,8 @@ function LauncherMods.translationStrings()
|
||||
return merged
|
||||
end
|
||||
|
||||
-- setEnabled(id, enabled [, version]): persist options.mods[id] in the exact
|
||||
-- shape Loader:_saveState writes (a plain boolean), so the running game and
|
||||
-- the in-game ManagerState pick it up unchanged. With `version` the choice
|
||||
-- lands in that game's overlay instead and no other game moves.
|
||||
-- setEnabled(id, enabled [, version]): with a game, persist just that game's
|
||||
-- answer. The loader and the in-game manager use the same scope on next boot.
|
||||
function LauncherMods.setEnabled(id, enabled, version)
|
||||
local options = SaveData.loadOptions()
|
||||
SaveData.setModEnabled(options, id, enabled, SaveData.modScope(version))
|
||||
@@ -384,7 +398,15 @@ function LauncherMods.setAllEnabled(ids, enabled, version)
|
||||
local options = SaveData.loadOptions()
|
||||
local scope = SaveData.modScope(version)
|
||||
for _, id in ipairs(ids or {}) do
|
||||
SaveData.setModEnabled(options, id, enabled, scope)
|
||||
if scope then
|
||||
SaveData.setModEnabled(options, id, enabled, scope)
|
||||
elseif SaveData.PER_VERSION_MODS then
|
||||
for _, game in ipairs(GameVersion.ORDER) do
|
||||
SaveData.setModEnabled(options, id, enabled, game)
|
||||
end
|
||||
else
|
||||
SaveData.setModEnabled(options, id, enabled)
|
||||
end
|
||||
end
|
||||
SaveData.saveOptions(options)
|
||||
return true
|
||||
@@ -843,7 +865,7 @@ end
|
||||
|
||||
-- uninstall(id) -> true | nil, errString
|
||||
-- Removes mods/<id>/ from wherever it was installed (the portable game folder
|
||||
-- or the save directory, CacheFs decides -- #330) and clears options.mods[id]
|
||||
-- or the save directory, CacheFs decides -- #330) and clears every enable flag
|
||||
-- so the loader and in-game manager no longer see it. Rejects missing ids.
|
||||
-- Does not touch other mods' enable state.
|
||||
function LauncherMods.uninstall(id)
|
||||
@@ -872,8 +894,19 @@ function LauncherMods.uninstall(id)
|
||||
-- Drop the enable flag so a reinstall of the same id starts from the
|
||||
-- loader's default (enabled) rather than a stale false.
|
||||
local options = SaveData.loadOptions()
|
||||
local changed = false
|
||||
if options.mods and options.mods[id] ~= nil then
|
||||
options.mods[id] = nil
|
||||
changed = true
|
||||
end
|
||||
for _, version in ipairs(GameVersion.ORDER) do
|
||||
local bucket = options.modsByVersion and options.modsByVersion[version]
|
||||
if type(bucket) == "table" and bucket[id] ~= nil then
|
||||
bucket[id] = nil
|
||||
changed = true
|
||||
end
|
||||
end
|
||||
if changed then
|
||||
SaveData.saveOptions(options)
|
||||
end
|
||||
return true
|
||||
|
||||
+20
-4
@@ -270,8 +270,7 @@ function Loader:_targetVersion()
|
||||
return version
|
||||
end
|
||||
|
||||
-- The version an enable flag is read and written under: this game once
|
||||
-- per-game flags are live, nil (the shared flag) while they are a preview.
|
||||
-- The version an enable flag is read and written under: this running game.
|
||||
-- Reads and writes go through the same answer so the two can never drift.
|
||||
function Loader:_enableScope()
|
||||
return SaveData.modScope(self:_targetVersion())
|
||||
@@ -1306,13 +1305,30 @@ function Loader:load(data)
|
||||
require("src.mods.Builtins").install(self.content, data, self.generation)
|
||||
self:_loadState()
|
||||
self:_discover()
|
||||
-- Existing installs stored one shared answer. Once their manifests are
|
||||
-- known, split that answer across every game before the next launcher/game
|
||||
-- toggle can change one independently. _loadState already used the same
|
||||
-- fallback, so this write cannot change the current boot's result.
|
||||
do
|
||||
local options = SaveData.loadOptions(self.fs)
|
||||
local installed = {}
|
||||
for id, mod in pairs(self.mods) do
|
||||
installed[#installed + 1] = {
|
||||
id = id,
|
||||
experimental = mod.manifest and mod.manifest.experimental == true,
|
||||
}
|
||||
end
|
||||
if SaveData.migrateModEnablement(options, installed) and self.fs.write then
|
||||
SaveData.saveOptions(options, self.fs)
|
||||
end
|
||||
end
|
||||
-- Experimental mods stay off until the player opts in: a missing
|
||||
-- options.mods entry normally means enabled, but experimental flips that.
|
||||
do
|
||||
local options = SaveData.loadOptions(self.fs)
|
||||
local modsOpt = options.mods or {}
|
||||
local scope = self:_enableScope()
|
||||
for id, mod in pairs(self.mods) do
|
||||
if not self.disabled[id] and modsOpt[id] == nil
|
||||
if not self.disabled[id] and SaveData.modEnabled(options, id, scope) == nil
|
||||
and mod.manifest.experimental then
|
||||
self.disabled[id] = true
|
||||
end
|
||||
|
||||
@@ -735,8 +735,7 @@ function ManagerState:toggleGen2Force(m)
|
||||
}, apply)
|
||||
end
|
||||
|
||||
-- Where the loader persisted an enable flag: this game's slot once it keeps
|
||||
-- them per game, the shared flag until then (SaveData.modScope).
|
||||
-- Where the loader persists an enable flag: this running game's slot.
|
||||
function ManagerState:enableScope()
|
||||
return SaveData.modScope((self:targetGame()))
|
||||
end
|
||||
|
||||
@@ -56,10 +56,24 @@ do
|
||||
local m = byId(LauncherMods.deriveList(manifests, { mods = { bbb = false } }))
|
||||
check(m.aaa.enabled, "a mod with no options entry defaults to enabled")
|
||||
check(not m.bbb.enabled, "an explicit false disables the mod")
|
||||
check(m.aaa.enabledByVersion.red and m.aaa.enabledByVersion.gold,
|
||||
"every row exposes its enabled answer for each game")
|
||||
eq(m.aaa.status, "ok", "a healthy enabled mod is ok")
|
||||
eq(m.aaa.statusDetail, "Ready", "ok detail reads Ready")
|
||||
end
|
||||
|
||||
do
|
||||
local manifests = {
|
||||
mf({ id = "one", name = "One", version = "1.0.0", entry = "m.lua" }),
|
||||
}
|
||||
local row = byId(LauncherMods.deriveList(manifests, {
|
||||
mods = { one = true }, modsByVersion = { gold = { one = false } },
|
||||
}, "gold")).one
|
||||
check(row.enabledByVersion.red, "the Red checkbox keeps the shared answer")
|
||||
check(not row.enabledByVersion.gold, "the Gold checkbox reads Gold's answer")
|
||||
check(not row.enabled, "the selected game's row state matches its checkbox")
|
||||
end
|
||||
|
||||
-- ------- experimental defaults to disabled; github surfaces on the row
|
||||
|
||||
do
|
||||
@@ -291,10 +305,9 @@ end
|
||||
|
||||
-- ------- enable flags: the panel reads exactly what the switch writes
|
||||
--
|
||||
-- One scope for both halves (SaveData.modScope). While per-game flags are a
|
||||
-- preview the shared flag is the whole answer, so a modsByVersion overlay --
|
||||
-- which an imported .g1rmodlist can plant, ModProfile.restoreVersions -- can
|
||||
-- never leave the switch showing an answer no writer can reach.
|
||||
-- One scope for both halves (SaveData.modScope). Per-game flags are live, so
|
||||
-- a modsByVersion answer -- including one restored from a .g1rmodlist -- is
|
||||
-- both what the launcher shows and what the next boot reads.
|
||||
|
||||
local SaveData = require("src.core.SaveData")
|
||||
|
||||
@@ -309,9 +322,8 @@ do
|
||||
}
|
||||
local planted = { mods = { one = true },
|
||||
modsByVersion = { gold = { one = false } } }
|
||||
local expected = SaveData.PER_VERSION_MODS and false or true
|
||||
eq(byId(LauncherMods.deriveList(manifests, planted, "gold")).one.enabled,
|
||||
expected, "the overlay is read exactly when a write can reach it")
|
||||
false, "the overlay is read exactly when a write can reach it")
|
||||
|
||||
-- the round trip, the thing the dead switch failed: flip it, re-derive
|
||||
local options = { mods = {} }
|
||||
|
||||
@@ -162,6 +162,32 @@ do
|
||||
"modScope follows the PER_VERSION_MODS switch")
|
||||
end
|
||||
|
||||
do
|
||||
-- First launch after per-game controls shipped: preserve every old answer
|
||||
-- and make the old implicit defaults explicit for the installed set.
|
||||
local opts = {
|
||||
mods = { old_on = true, old_off = false },
|
||||
modsByVersion = { gold = { old_on = false } },
|
||||
}
|
||||
check(SaveData.migrateModEnablement(opts, {
|
||||
{ id = "old_on" }, { id = "old_off" }, { id = "implicit" },
|
||||
{ id = "lab", experimental = true },
|
||||
}), "legacy mod state is migrated once")
|
||||
check(opts.modsByVersionMigrated, "the migration is marked complete")
|
||||
eq(SaveData.modEnabled(opts, "old_on", "red"), true,
|
||||
"an old enabled mod is enabled for Red")
|
||||
eq(SaveData.modEnabled(opts, "old_on", "gold"), false,
|
||||
"an already-stored preview answer is preserved")
|
||||
eq(SaveData.modEnabled(opts, "old_off", "blue"), false,
|
||||
"an old disabled mod stays disabled for every game")
|
||||
eq(SaveData.modEnabled(opts, "implicit", "yellow"), true,
|
||||
"an old implicit default is enabled for every game")
|
||||
eq(SaveData.modEnabled(opts, "lab", "red"), false,
|
||||
"an experimental mod keeps its existing opt-in default")
|
||||
check(not SaveData.migrateModEnablement(opts, { { id = "newer" } }),
|
||||
"a later mod install does not rerun the legacy migration")
|
||||
end
|
||||
|
||||
-- ------- a profile carries the per-game half of a setup
|
||||
|
||||
do
|
||||
|
||||
@@ -1017,8 +1017,10 @@ press(ms, "select")
|
||||
check(avail[1].enabled == false, "SELECT quick-toggles the focused mod")
|
||||
check(ms:isStaged(avail[1]), "a flip against boot state is staged")
|
||||
check(ms:glyphFor(avail[1]) == ".", "staged mods show the staged glyph")
|
||||
check(mgame.save.options.mods.badmod == false,
|
||||
"the live options table mirrors the flip")
|
||||
local managerScope = ms:enableScope()
|
||||
check(managerScope and mgame.save.options.modsByVersion
|
||||
and mgame.save.options.modsByVersion[managerScope].badmod == false,
|
||||
"the live options table mirrors the flip for this game")
|
||||
check(ms.restartPending, "staged changes arm the apply screen")
|
||||
ms:discardChanges()
|
||||
check(avail[1].enabled == true and not ms.restartPending,
|
||||
|
||||
Reference in New Issue
Block a user