mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-21 13:09:54 +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:
|
duplicate) and returns one row per mod:
|
||||||
`{id, name, version, badge, description, enabled, status, statusDetail}`.
|
`{id, name, version, badge, description, enabled, status, statusDetail}`.
|
||||||
`badge` is the manifest's `category`, falling back to `profile`, then
|
`badge` is the manifest's `category`, falling back to `profile`, then
|
||||||
`"MOD"`, uppercased. `enabled` reads `options.mods[id]` (missing means
|
`"MOD"`, uppercased. `enabledByVersion` contains an answer for each game;
|
||||||
enabled, matching the loader's own default).
|
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
|
- `status` is `"ok"`, `"warn"`, or `"conflict"`, computed by the pure
|
||||||
`LauncherMods.deriveList`/`statusFor` against `ManagerState.resolveToggle`
|
`LauncherMods.deriveList`/`statusFor` against `ManagerState.resolveToggle`
|
||||||
and the validated manifests: `conflict` when enabling this mod collides
|
and the validated manifests: `conflict` when enabling this mod collides
|
||||||
with another enabled one; `warn` for an out-of-range `game_version` or an
|
with another enabled one; `warn` for an out-of-range `game_version` or an
|
||||||
absent/disabled/wrong-version hard dependency; `ok` otherwise. Having no
|
absent/disabled/wrong-version hard dependency; `ok` otherwise. Having no
|
||||||
`love.*` calls, this half is table-driven by the test suite on its own.
|
`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
|
- `LauncherMods.setEnabled(id, bool, version)` persists the selected game's
|
||||||
boolean, the exact shape `Loader:_saveState` writes, so the running game
|
answer, so the running game and the in-game `ManagerState` see the change on
|
||||||
and the in-game `ManagerState` see the change on next boot. The mods panel
|
next boot. The MODS panel renders a coloured checkbox for Red, Blue, Yellow,
|
||||||
calls this on every toggle and re-derives the list right away
|
and Gold on every row and re-derives the list right away
|
||||||
(`RomImporter:_refreshMods`) so a status change (e.g. a new conflict)
|
(`RomImporter:_refreshMods`) so a status change (e.g. a new conflict)
|
||||||
shows without waiting for a reload.
|
shows without waiting for a reload.
|
||||||
- `LauncherMods.installZip(path)` mounts the archive with
|
- `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)`,
|
dependency does not run on the selected game reads `Needs <id> (not for Gold)`,
|
||||||
matching the loader's contagious skip.
|
matching the loader's contagious skip.
|
||||||
|
|
||||||
A separate overlay, `options.modsByVersion[version][id]`, is where a per-game
|
A separate overlay, `options.modsByVersion[version][id]`, holds each game's
|
||||||
enable flag will live. It is a preview: `SaveData.PER_VERSION_MODS` is `false`,
|
enable flag. The launcher shows a coloured Red / Blue / Yellow / Gold checkbox
|
||||||
so `SaveData.modScope` answers nil for every caller and the launcher panel, the
|
for every installed mod, and the loader and in-game manager read the same
|
||||||
in-game manager and the loader all read *and* write the one shared
|
game-specific answer on the next boot. On the first launch after this feature,
|
||||||
`options.mods` flag. Nothing consults the overlay for enablement until that
|
the existing shared state is copied to every game, so a mod that was enabled
|
||||||
flips, which is deliberate: the overlay is plantable from an imported
|
remains enabled everywhere; after that, changing one checkbox affects only
|
||||||
`.g1rmodlist`, and a reader scoped differently from the writers would show a
|
that game. New mods still default to enabled on every game (experimental mods
|
||||||
mod set no boot would honour.
|
retain their explicit opt-in default).
|
||||||
|
|
||||||
That is deliberate. Gold reimplements the battle engine, the overworld, the
|
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
|
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
|
### One limit worth knowing
|
||||||
|
|
||||||
**Per-game enable flags are still a preview.** The overlay
|
**Enablement is per game.** The overlay
|
||||||
`options.modsByVersion[version][id]` exists and every surface goes through
|
`options.modsByVersion[version][id]` is read and written through
|
||||||
`SaveData.modEnabled` / `SaveData.setModEnabled`, but
|
`SaveData.modEnabled` / `SaveData.setModEnabled` by the launcher, in-game
|
||||||
`SaveData.PER_VERSION_MODS` is `false`
|
manager, and loader. Existing shared settings are copied to every game the
|
||||||
(`src/core/SaveData.lua:489`). While it is false, `SaveData.modScope` answers
|
first time this version sees the installed mods; from then on, each coloured
|
||||||
nil for every caller, so the launcher panel, the in-game manager *and* the
|
game checkbox changes only that game's next boot. Nothing about this affects a
|
||||||
loader all read and write the one shared `options.mods` flag and the overlay is
|
mod author; it affects what a player can express.
|
||||||
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.
|
|
||||||
|
|
||||||
Targeting is a different question from enablement and *is* enforced per game,
|
Targeting is a different question from enablement and *is* enforced per game,
|
||||||
as above. The two do not share a switch.
|
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
|
-- options.lua written before this key keeps its exact meaning. Read and
|
||||||
-- written through SaveData.modEnabled / SaveData.setModEnabled.
|
-- written through SaveData.modEnabled / SaveData.setModEnabled.
|
||||||
modsByVersion = {},
|
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
|
-- Named setups the player can switch between (#593; src/mods/ModProfile.lua
|
||||||
-- owns the shape, src/mods/ManagerState.lua the UI): each row is
|
-- owns the shape, src/mods/ManagerState.lua the UI): each row is
|
||||||
-- { name, enabled = {id=bool}, options = {id={k=v}}, slots = {version=slotId} }.
|
-- { 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
|
-- 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.
|
-- 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
|
-- Per-game answers are live. Every reader and writer goes through modScope,
|
||||||
-- flags once, before any entry chunk (src/mods/Loader.lua _loadState), so this
|
-- so a choice made in the launcher is the choice the next boot loads.
|
||||||
-- flips on with that read and not before: until then every writer keeps to the
|
SaveData.PER_VERSION_MODS = true
|
||||||
-- 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
|
-- The version a write is scoped to: per-game controls name one game; a nil
|
||||||
-- flags are live, nil (the shared flag) while they are only a preview.
|
-- caller still addresses the legacy shared fallback.
|
||||||
function SaveData.modScope(version)
|
function SaveData.modScope(version)
|
||||||
if SaveData.PER_VERSION_MODS then return version end
|
if SaveData.PER_VERSION_MODS then return version end
|
||||||
return nil
|
return nil
|
||||||
end
|
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
|
-- true/false as chosen for `version`, else the shared flag, else nil -- the
|
||||||
-- caller owns the default (the loader enables, the launcher keeps
|
-- caller owns the default (the loader enables, the launcher keeps
|
||||||
-- experimental mods off until asked).
|
-- experimental mods off until asked).
|
||||||
|
|||||||
+51
-22
@@ -1139,6 +1139,28 @@ local function drawCheck(x, y, size, color)
|
|||||||
love.graphics.pop()
|
love.graphics.pop()
|
||||||
end
|
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)
|
local function buildModsPanel(imp, x, y, w, availH, m)
|
||||||
imp:_ensureMods()
|
imp:_ensureMods()
|
||||||
local ModUpdate = require("src.mods.ModUpdate")
|
local ModUpdate = require("src.mods.ModUpdate")
|
||||||
@@ -1225,15 +1247,14 @@ local function buildModsPanel(imp, x, y, w, availH, m)
|
|||||||
mods = sorted
|
mods = sorted
|
||||||
end
|
end
|
||||||
|
|
||||||
-- A mod row is a fixed height: name line, version + status line, one line
|
-- A mod row is a fixed height: its details first, then a dedicated second
|
||||||
-- of description, and an action row. Fixed because a page of uniform rows
|
-- line of per-game checkboxes. Fixed because a page of uniform rows is
|
||||||
-- is what lets perPage come from the viewport.
|
-- what lets perPage come from the viewport.
|
||||||
local chipH = math.max(Kit.tapMin(), math.floor(30 * m.s))
|
local togH = math.floor(26 * m.s)
|
||||||
-- Text block on the left, chips right-aligned beside it: one row, not a
|
local gamesLabel = Strings("Enable for:")
|
||||||
-- text block with a button strip stacked under it.
|
|
||||||
local textH = Kit.textHeight("button") + math.floor(4 * m.s)
|
local textH = Kit.textHeight("button") + math.floor(4 * m.s)
|
||||||
+ Kit.textHeight("small") + math.floor(2 * m.s) + Kit.textHeight("small")
|
+ 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)
|
+ math.floor(8 * m.s)
|
||||||
local pagerH = math.max(Kit.tapMin(), math.floor(30 * m.s))
|
local pagerH = math.max(Kit.tapMin(), math.floor(30 * m.s))
|
||||||
local listH = availH - (cy - y) - pagerH - gap
|
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 px, inner = x + pad, w - 2 * pad
|
||||||
local ly = ry + math.floor(10 * m.s)
|
local ly = ry + math.floor(10 * m.s)
|
||||||
|
|
||||||
local togW = math.floor(56 * m.s)
|
local togGap = math.floor(4 * m.s)
|
||||||
local togH = math.floor(26 * m.s)
|
|
||||||
local info = mod.github and mod.github ~= "" and imp:_modUpdateInfo(mod.id)
|
local info = mod.github and mod.github ~= "" and imp:_modUpdateInfo(mod.id)
|
||||||
|
|
||||||
local togKey = "mod-toggle-" .. mod.id
|
-- These answer separate games, not a single shared install flag. The
|
||||||
-- The toggle reports its own new value, but the importer owns the state:
|
-- importer receives the game id so an experimental confirmation also
|
||||||
-- queue the flip and let _toggleMod (which may raise an experimental-mod
|
-- applies only to the checkbox the player pressed.
|
||||||
-- confirm) decide what actually happens.
|
local flipped = false
|
||||||
local _, flipped = Kit.toggle(px + inner - togW,
|
local gamesY = ry + math.floor(8 * m.s) + textH + math.floor(8 * m.s)
|
||||||
ry + (rowH - togH) / 2, togW, togH, mod.enabled, togKey)
|
Kit.text("micro", gamesLabel, px,
|
||||||
if flipped then
|
gamesY + (togH - Kit.textHeight("micro")) / 2, PAL.muted)
|
||||||
queueAction(imp, togKey, function() imp:_toggleMod(mod.id) end)
|
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
|
end
|
||||||
-- The toggle sits inside the row's rect, so its press also passes the
|
-- The checkboxes sit inside the row's rect, so their press also passes the
|
||||||
-- row's hit test; `flipped` gates the row action to everywhere else.
|
-- row hit test; `flipped` gates the row action to everywhere else.
|
||||||
if not flipped
|
if not flipped
|
||||||
and (Kit.press(x, ry, w, rowH) or Kit._activateId == rowKey) then
|
and (Kit.press(x, ry, w, rowH) or Kit._activateId == rowKey) then
|
||||||
local id = mod.id
|
local id = mod.id
|
||||||
queueAction(imp, rowKey, function() imp._modActions = id end)
|
queueAction(imp, rowKey, function() imp._modActions = id end)
|
||||||
end
|
end
|
||||||
local chipsW = togW + math.floor(6 * m.s)
|
local textW = inner
|
||||||
local textW = inner - chipsW - math.floor(12 * m.s)
|
|
||||||
|
|
||||||
local badgeW = Kit.textWidth("micro", mod.badge) + math.floor(12 * m.s)
|
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
|
-- 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
|
elseif c.kind == "importOversize" then
|
||||||
imp:_importSave(c.version, c.source, true)
|
imp:_importSave(c.version, c.source, true)
|
||||||
else
|
else
|
||||||
imp:_toggleMod(c.id, true)
|
imp:_toggleMod(c.id, true, c.version)
|
||||||
end
|
end
|
||||||
end,
|
end,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -3064,15 +3064,19 @@ function RomImporter:_modUpdateInfo(id)
|
|||||||
return self.modUpdateInfo and self.modUpdateInfo[id] or nil
|
return self.modUpdateInfo and self.modUpdateInfo[id] or nil
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Flip a mod's enabled flag (persisted via LauncherMods.setEnabled) and relist
|
-- Flip one game's mod flag (persisted via LauncherMods.setEnabled) and relist
|
||||||
-- so the toggle, count, and every status chip reflect the new resolution.
|
-- so that game's checkbox and status chips reflect the new resolution.
|
||||||
-- Enabling an experimental mod arms a confirm first.
|
-- Enabling an experimental mod arms a confirmation for that same game.
|
||||||
function RomImporter:_toggleMod(id, confirmed)
|
function RomImporter:_toggleMod(id, confirmed, version)
|
||||||
local LauncherMods = require("src.mods.LauncherMods")
|
local LauncherMods = require("src.mods.LauncherMods")
|
||||||
local cur, experimental = false, false
|
local cur, experimental = false, false
|
||||||
for _, m in ipairs(self.mods or {}) do
|
for _, m in ipairs(self.mods or {}) do
|
||||||
if m.id == id then
|
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
|
experimental = m.experimental == true
|
||||||
break
|
break
|
||||||
end
|
end
|
||||||
@@ -3080,7 +3084,7 @@ function RomImporter:_toggleMod(id, confirmed)
|
|||||||
local want = not cur
|
local want = not cur
|
||||||
if want and experimental and not confirmed then
|
if want and experimental and not confirmed then
|
||||||
self._modConfirm = {
|
self._modConfirm = {
|
||||||
kind = "experimental", id = id,
|
kind = "experimental", id = id, version = version,
|
||||||
title = "Experimental mod",
|
title = "Experimental mod",
|
||||||
yesLabel = "Enable",
|
yesLabel = "Enable",
|
||||||
lines = {
|
lines = {
|
||||||
@@ -3092,7 +3096,7 @@ function RomImporter:_toggleMod(id, confirmed)
|
|||||||
return
|
return
|
||||||
end
|
end
|
||||||
self._modConfirm = nil
|
self._modConfirm = nil
|
||||||
LauncherMods.setEnabled(id, want, self.modScope)
|
LauncherMods.setEnabled(id, want, version or self.modScope)
|
||||||
self:_refreshMods()
|
self:_refreshMods()
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
+44
-11
@@ -36,6 +36,7 @@ local ModTargets = require("src.mods.ModTargets")
|
|||||||
local Semver = require("src.mods.Semver")
|
local Semver = require("src.mods.Semver")
|
||||||
local Version = require("src.core.Version")
|
local Version = require("src.core.Version")
|
||||||
local SaveData = require("src.core.SaveData")
|
local SaveData = require("src.core.SaveData")
|
||||||
|
local GameVersion = require("src.core.GameVersion")
|
||||||
local CacheFs = require("src.import.CacheFs")
|
local CacheFs = require("src.import.CacheFs")
|
||||||
|
|
||||||
local LauncherMods = {}
|
local LauncherMods = {}
|
||||||
@@ -107,8 +108,9 @@ end
|
|||||||
-- deriveList(manifests, options [, version]) -> the panel row list, pure.
|
-- deriveList(manifests, options [, version]) -> the panel row list, pure.
|
||||||
-- manifests is an array of validated manifests (Manifest.validate output);
|
-- manifests is an array of validated manifests (Manifest.validate output);
|
||||||
-- options is the options table (options.mods, options.modsByVersion and
|
-- options is the options table (options.mods, options.modsByVersion and
|
||||||
-- options.modsGen2 are read). `version` is the game the panel is showing:
|
-- 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.
|
-- 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.
|
-- Rows come back sorted by id so the panel order is stable.
|
||||||
function LauncherMods.deriveList(manifests, options, version)
|
function LauncherMods.deriveList(manifests, options, version)
|
||||||
local ordered = {}
|
local ordered = {}
|
||||||
@@ -126,8 +128,7 @@ function LauncherMods.deriveList(manifests, options, version)
|
|||||||
-- this game's choice, then the shared flag, then the default: enabled,
|
-- this game's choice, then the shared flag, then the default: enabled,
|
||||||
-- matching the loader -- except experimental mods, which stay off until
|
-- matching the loader -- except experimental mods, which stay off until
|
||||||
-- the player opts in. Scoped through modScope, so this reads exactly what
|
-- the player opts in. Scoped through modScope, so this reads exactly what
|
||||||
-- setEnabled writes and the loader loads: while per-game flags are a
|
-- setEnabled writes and the loader loads for the selected game.
|
||||||
-- preview the shared flag is the whole answer on every surface.
|
|
||||||
local decided = SaveData.modEnabled(options, m.id, SaveData.modScope(version))
|
local decided = SaveData.modEnabled(options, m.id, SaveData.modScope(version))
|
||||||
if decided == nil then decided = not m.experimental end
|
if decided == nil then decided = not m.experimental end
|
||||||
if decided then enabledSet[m.id] = true end
|
if decided then enabledSet[m.id] = true end
|
||||||
@@ -152,6 +153,14 @@ function LauncherMods.deriveList(manifests, options, version)
|
|||||||
badge = badge,
|
badge = badge,
|
||||||
description = m.description or "",
|
description = m.description or "",
|
||||||
enabled = enabled,
|
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,
|
status = status,
|
||||||
statusDetail = detail,
|
statusDetail = detail,
|
||||||
github = m.github,
|
github = m.github,
|
||||||
@@ -281,7 +290,14 @@ end
|
|||||||
function LauncherMods.list(version)
|
function LauncherMods.list(version)
|
||||||
local ok, result = pcall(function()
|
local ok, result = pcall(function()
|
||||||
local options = SaveData.loadOptions()
|
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)
|
end)
|
||||||
if not ok then
|
if not ok then
|
||||||
-- a single bad options/mod file must not blank the launcher
|
-- a single bad options/mod file must not blank the launcher
|
||||||
@@ -364,10 +380,8 @@ function LauncherMods.translationStrings()
|
|||||||
return merged
|
return merged
|
||||||
end
|
end
|
||||||
|
|
||||||
-- setEnabled(id, enabled [, version]): persist options.mods[id] in the exact
|
-- setEnabled(id, enabled [, version]): with a game, persist just that game's
|
||||||
-- shape Loader:_saveState writes (a plain boolean), so the running game and
|
-- answer. The loader and the in-game manager use the same scope on next boot.
|
||||||
-- the in-game ManagerState pick it up unchanged. With `version` the choice
|
|
||||||
-- lands in that game's overlay instead and no other game moves.
|
|
||||||
function LauncherMods.setEnabled(id, enabled, version)
|
function LauncherMods.setEnabled(id, enabled, version)
|
||||||
local options = SaveData.loadOptions()
|
local options = SaveData.loadOptions()
|
||||||
SaveData.setModEnabled(options, id, enabled, SaveData.modScope(version))
|
SaveData.setModEnabled(options, id, enabled, SaveData.modScope(version))
|
||||||
@@ -384,7 +398,15 @@ function LauncherMods.setAllEnabled(ids, enabled, version)
|
|||||||
local options = SaveData.loadOptions()
|
local options = SaveData.loadOptions()
|
||||||
local scope = SaveData.modScope(version)
|
local scope = SaveData.modScope(version)
|
||||||
for _, id in ipairs(ids or {}) do
|
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
|
end
|
||||||
SaveData.saveOptions(options)
|
SaveData.saveOptions(options)
|
||||||
return true
|
return true
|
||||||
@@ -843,7 +865,7 @@ end
|
|||||||
|
|
||||||
-- uninstall(id) -> true | nil, errString
|
-- uninstall(id) -> true | nil, errString
|
||||||
-- Removes mods/<id>/ from wherever it was installed (the portable game folder
|
-- 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.
|
-- so the loader and in-game manager no longer see it. Rejects missing ids.
|
||||||
-- Does not touch other mods' enable state.
|
-- Does not touch other mods' enable state.
|
||||||
function LauncherMods.uninstall(id)
|
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
|
-- Drop the enable flag so a reinstall of the same id starts from the
|
||||||
-- loader's default (enabled) rather than a stale false.
|
-- loader's default (enabled) rather than a stale false.
|
||||||
local options = SaveData.loadOptions()
|
local options = SaveData.loadOptions()
|
||||||
|
local changed = false
|
||||||
if options.mods and options.mods[id] ~= nil then
|
if options.mods and options.mods[id] ~= nil then
|
||||||
options.mods[id] = nil
|
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)
|
SaveData.saveOptions(options)
|
||||||
end
|
end
|
||||||
return true
|
return true
|
||||||
|
|||||||
+20
-4
@@ -270,8 +270,7 @@ function Loader:_targetVersion()
|
|||||||
return version
|
return version
|
||||||
end
|
end
|
||||||
|
|
||||||
-- The version an enable flag is read and written under: this game once
|
-- The version an enable flag is read and written under: this running game.
|
||||||
-- per-game flags are live, nil (the shared flag) while they are a preview.
|
|
||||||
-- Reads and writes go through the same answer so the two can never drift.
|
-- Reads and writes go through the same answer so the two can never drift.
|
||||||
function Loader:_enableScope()
|
function Loader:_enableScope()
|
||||||
return SaveData.modScope(self:_targetVersion())
|
return SaveData.modScope(self:_targetVersion())
|
||||||
@@ -1306,13 +1305,30 @@ function Loader:load(data)
|
|||||||
require("src.mods.Builtins").install(self.content, data, self.generation)
|
require("src.mods.Builtins").install(self.content, data, self.generation)
|
||||||
self:_loadState()
|
self:_loadState()
|
||||||
self:_discover()
|
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
|
-- Experimental mods stay off until the player opts in: a missing
|
||||||
-- options.mods entry normally means enabled, but experimental flips that.
|
-- options.mods entry normally means enabled, but experimental flips that.
|
||||||
do
|
do
|
||||||
local options = SaveData.loadOptions(self.fs)
|
local options = SaveData.loadOptions(self.fs)
|
||||||
local modsOpt = options.mods or {}
|
local scope = self:_enableScope()
|
||||||
for id, mod in pairs(self.mods) do
|
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
|
and mod.manifest.experimental then
|
||||||
self.disabled[id] = true
|
self.disabled[id] = true
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -735,8 +735,7 @@ function ManagerState:toggleGen2Force(m)
|
|||||||
}, apply)
|
}, apply)
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Where the loader persisted an enable flag: this game's slot once it keeps
|
-- Where the loader persists an enable flag: this running game's slot.
|
||||||
-- them per game, the shared flag until then (SaveData.modScope).
|
|
||||||
function ManagerState:enableScope()
|
function ManagerState:enableScope()
|
||||||
return SaveData.modScope((self:targetGame()))
|
return SaveData.modScope((self:targetGame()))
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -56,10 +56,24 @@ do
|
|||||||
local m = byId(LauncherMods.deriveList(manifests, { mods = { bbb = false } }))
|
local m = byId(LauncherMods.deriveList(manifests, { mods = { bbb = false } }))
|
||||||
check(m.aaa.enabled, "a mod with no options entry defaults to enabled")
|
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(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.status, "ok", "a healthy enabled mod is ok")
|
||||||
eq(m.aaa.statusDetail, "Ready", "ok detail reads Ready")
|
eq(m.aaa.statusDetail, "Ready", "ok detail reads Ready")
|
||||||
end
|
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
|
-- ------- experimental defaults to disabled; github surfaces on the row
|
||||||
|
|
||||||
do
|
do
|
||||||
@@ -291,10 +305,9 @@ end
|
|||||||
|
|
||||||
-- ------- enable flags: the panel reads exactly what the switch writes
|
-- ------- enable flags: the panel reads exactly what the switch writes
|
||||||
--
|
--
|
||||||
-- One scope for both halves (SaveData.modScope). While per-game flags are a
|
-- One scope for both halves (SaveData.modScope). Per-game flags are live, so
|
||||||
-- preview the shared flag is the whole answer, so a modsByVersion overlay --
|
-- a modsByVersion answer -- including one restored from a .g1rmodlist -- is
|
||||||
-- which an imported .g1rmodlist can plant, ModProfile.restoreVersions -- can
|
-- both what the launcher shows and what the next boot reads.
|
||||||
-- never leave the switch showing an answer no writer can reach.
|
|
||||||
|
|
||||||
local SaveData = require("src.core.SaveData")
|
local SaveData = require("src.core.SaveData")
|
||||||
|
|
||||||
@@ -309,9 +322,8 @@ do
|
|||||||
}
|
}
|
||||||
local planted = { mods = { one = true },
|
local planted = { mods = { one = true },
|
||||||
modsByVersion = { gold = { one = false } } }
|
modsByVersion = { gold = { one = false } } }
|
||||||
local expected = SaveData.PER_VERSION_MODS and false or true
|
|
||||||
eq(byId(LauncherMods.deriveList(manifests, planted, "gold")).one.enabled,
|
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
|
-- the round trip, the thing the dead switch failed: flip it, re-derive
|
||||||
local options = { mods = {} }
|
local options = { mods = {} }
|
||||||
|
|||||||
@@ -162,6 +162,32 @@ do
|
|||||||
"modScope follows the PER_VERSION_MODS switch")
|
"modScope follows the PER_VERSION_MODS switch")
|
||||||
end
|
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
|
-- ------- a profile carries the per-game half of a setup
|
||||||
|
|
||||||
do
|
do
|
||||||
|
|||||||
@@ -1017,8 +1017,10 @@ press(ms, "select")
|
|||||||
check(avail[1].enabled == false, "SELECT quick-toggles the focused mod")
|
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:isStaged(avail[1]), "a flip against boot state is staged")
|
||||||
check(ms:glyphFor(avail[1]) == ".", "staged mods show the staged glyph")
|
check(ms:glyphFor(avail[1]) == ".", "staged mods show the staged glyph")
|
||||||
check(mgame.save.options.mods.badmod == false,
|
local managerScope = ms:enableScope()
|
||||||
"the live options table mirrors the flip")
|
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")
|
check(ms.restartPending, "staged changes arm the apply screen")
|
||||||
ms:discardChanges()
|
ms:discardChanges()
|
||||||
check(avail[1].enabled == true and not ms.restartPending,
|
check(avail[1].enabled == true and not ms.restartPending,
|
||||||
|
|||||||
Reference in New Issue
Block a user