mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-20 04:31:09 +02:00
Merge pull request #1544 from castdrian/safe-mode-report-issue
feat(launcher): add safe mode and issue reporting
This commit is contained in:
@@ -0,0 +1,219 @@
|
|||||||
|
local SaveData = require("src.core.SaveData")
|
||||||
|
local Version = require("src.core.Version")
|
||||||
|
|
||||||
|
local IssueReport = {}
|
||||||
|
|
||||||
|
local FORM_URL = "https://github.com/bryanthaboi/gen1recomp/issues/new"
|
||||||
|
local TEMPLATE = "bug_report.yml"
|
||||||
|
|
||||||
|
local function clean(value)
|
||||||
|
if value == nil then return nil end
|
||||||
|
local text = tostring(value):gsub("^%s+", ""):gsub("%s+$", "")
|
||||||
|
if text == "" or text == "unknown" or text == "Unknown" then return nil end
|
||||||
|
return text
|
||||||
|
end
|
||||||
|
|
||||||
|
local function call(fn, ...)
|
||||||
|
if type(fn) ~= "function" then return nil end
|
||||||
|
local ok, a, b, c, d, e = pcall(fn, ...)
|
||||||
|
if not ok then return nil end
|
||||||
|
return a, b, c, d, e
|
||||||
|
end
|
||||||
|
|
||||||
|
local function invoke(fn, ...)
|
||||||
|
if type(fn) ~= "function" then return false end
|
||||||
|
local ok, result = pcall(fn, ...)
|
||||||
|
return ok, result
|
||||||
|
end
|
||||||
|
|
||||||
|
local function commandValue(command)
|
||||||
|
if not io or type(io.popen) ~= "function" then return nil end
|
||||||
|
local ok, pipe = pcall(io.popen, command, "r")
|
||||||
|
if not ok or not pipe then return nil end
|
||||||
|
local readOK, value = pcall(pipe.read, pipe, "*l")
|
||||||
|
pcall(pipe.close, pipe)
|
||||||
|
if not readOK then return nil end
|
||||||
|
return clean(value)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function percentEncode(value)
|
||||||
|
local text = tostring(value or "")
|
||||||
|
return (text:gsub("([^%w%-_%.~])", function(char)
|
||||||
|
return ("%%%02X"):format(char:byte())
|
||||||
|
end))
|
||||||
|
end
|
||||||
|
|
||||||
|
local function formOS(raw)
|
||||||
|
local values = {
|
||||||
|
["OS X"] = "macOS",
|
||||||
|
macOS = "macOS",
|
||||||
|
Windows = "Windows",
|
||||||
|
Linux = "Linux",
|
||||||
|
Android = "Android",
|
||||||
|
iOS = "iOS",
|
||||||
|
NX = "Nintendo Switch",
|
||||||
|
UWP = "Xbox",
|
||||||
|
Xbox = "Xbox",
|
||||||
|
}
|
||||||
|
return values[raw] or ""
|
||||||
|
end
|
||||||
|
|
||||||
|
local function loveVersion()
|
||||||
|
local major, minor, revision, codename = call(love and love.getVersion)
|
||||||
|
if not major then return "" end
|
||||||
|
local result = tostring(major) .. "." .. tostring(minor) .. "." .. tostring(revision)
|
||||||
|
if codename and codename ~= "" then result = result .. " (" .. tostring(codename) .. ")" end
|
||||||
|
return result
|
||||||
|
end
|
||||||
|
|
||||||
|
local function appVersion()
|
||||||
|
local version = clean(Version.engine)
|
||||||
|
if not version or version == "0.0.0" or version == "0.0.0-dev" then return "" end
|
||||||
|
return version
|
||||||
|
end
|
||||||
|
|
||||||
|
local function deviceModel(rawOS, system)
|
||||||
|
local model = clean(call(system.getModel))
|
||||||
|
if model then return model end
|
||||||
|
if rawOS == "OS X" or rawOS == "macOS" then
|
||||||
|
return commandValue("sysctl -n hw.model 2>/dev/null")
|
||||||
|
end
|
||||||
|
if rawOS == "Windows" then
|
||||||
|
return commandValue("powershell.exe -NoProfile -NonInteractive -Command \"(Get-CimInstance Win32_ComputerSystem).Model\" 2>NUL")
|
||||||
|
end
|
||||||
|
if rawOS == "Linux" then
|
||||||
|
return commandValue("cat /sys/devices/virtual/dmi/id/product_name 2>/dev/null")
|
||||||
|
or commandValue("cat /sys/devices/virtual/dmi/id/model 2>/dev/null")
|
||||||
|
end
|
||||||
|
if rawOS == "Android" then
|
||||||
|
return commandValue("getprop ro.product.model 2>/dev/null")
|
||||||
|
end
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
|
local function modRows(context)
|
||||||
|
if context and type(context.mods) == "table" then return context.mods end
|
||||||
|
local ok, LauncherMods = pcall(require, "src.mods.LauncherMods")
|
||||||
|
if ok and LauncherMods and LauncherMods.list then
|
||||||
|
local listed = call(LauncherMods.list)
|
||||||
|
if type(listed) == "table" then return listed end
|
||||||
|
end
|
||||||
|
return {}
|
||||||
|
end
|
||||||
|
|
||||||
|
local function modNames(rows, safeMode)
|
||||||
|
local enabled = {}
|
||||||
|
for _, mod in ipairs(rows or {}) do
|
||||||
|
if type(mod) == "table" then
|
||||||
|
local name = clean(mod.name or mod.id)
|
||||||
|
if name and not safeMode and mod.enabled == true then
|
||||||
|
enabled[#enabled + 1] = name
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
table.sort(enabled)
|
||||||
|
return enabled
|
||||||
|
end
|
||||||
|
|
||||||
|
local function metadata(options, context)
|
||||||
|
local system = love and love.system or {}
|
||||||
|
local graphics = love and love.graphics or {}
|
||||||
|
local window = love and love.window or {}
|
||||||
|
local rawOS = clean(call(system.getOS))
|
||||||
|
local model = deviceModel(rawOS, system)
|
||||||
|
local renderer, rendererVersion, _, rendererDevice = call(graphics.getRendererInfo)
|
||||||
|
local width, height = call(graphics.getDimensions)
|
||||||
|
local pixelWidth, pixelHeight = call(graphics.getPixelDimensions)
|
||||||
|
local modeWidth, modeHeight, flags = call(window.getMode)
|
||||||
|
local safeMode = SaveData.isSafeMode(options)
|
||||||
|
local rows = modRows(context or {})
|
||||||
|
local enabledMods = modNames(rows, safeMode)
|
||||||
|
local lines = { "Diagnostics:" }
|
||||||
|
local function add(label, value)
|
||||||
|
value = clean(value)
|
||||||
|
if value then lines[#lines + 1] = "- " .. label .. ": " .. value end
|
||||||
|
end
|
||||||
|
add("Platform", formOS(rawOS))
|
||||||
|
local hardware = model
|
||||||
|
if rendererDevice and rendererDevice ~= model then
|
||||||
|
hardware = hardware and (hardware .. " (" .. rendererDevice .. ")") or rendererDevice
|
||||||
|
end
|
||||||
|
add("Device", hardware)
|
||||||
|
local rendererDetails = clean(renderer)
|
||||||
|
if rendererDetails and clean(rendererVersion) then
|
||||||
|
rendererDetails = rendererDetails .. " " .. clean(rendererVersion)
|
||||||
|
end
|
||||||
|
add("Renderer", rendererDetails)
|
||||||
|
local displayWidth, displayHeight = width or modeWidth or pixelWidth, height or modeHeight or pixelHeight
|
||||||
|
if displayWidth and displayHeight then
|
||||||
|
add("Display", tostring(displayWidth) .. "x" .. tostring(displayHeight))
|
||||||
|
end
|
||||||
|
if pixelWidth and pixelHeight
|
||||||
|
and (pixelWidth ~= displayWidth or pixelHeight ~= displayHeight) then
|
||||||
|
add("Pixel display", tostring(pixelWidth) .. "x" .. tostring(pixelHeight))
|
||||||
|
end
|
||||||
|
if flags and flags.fullscreen == true then add("Fullscreen", "yes") end
|
||||||
|
local version = appVersion()
|
||||||
|
add("App", version ~= "" and Version.title() or "gen1recomp")
|
||||||
|
add("LÖVE", loveVersion())
|
||||||
|
if safeMode then add("Safe mode", "on") end
|
||||||
|
return {
|
||||||
|
rawOS = rawOS,
|
||||||
|
os = formOS(rawOS),
|
||||||
|
device = model,
|
||||||
|
version = version,
|
||||||
|
safeMode = safeMode,
|
||||||
|
enabledMods = enabledMods,
|
||||||
|
metadata = table.concat(lines, "\n"),
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
function IssueReport.build(options, context)
|
||||||
|
options = options or SaveData.loadOptions()
|
||||||
|
context = context or {}
|
||||||
|
local info = metadata(options, context)
|
||||||
|
local fields = {
|
||||||
|
summary = "",
|
||||||
|
mods_which = #info.enabledMods > 0 and table.concat(info.enabledMods, ", ") or "",
|
||||||
|
version = info.version or "",
|
||||||
|
location = "",
|
||||||
|
screenshot = "",
|
||||||
|
steps = "",
|
||||||
|
expected = "",
|
||||||
|
extra = info.metadata,
|
||||||
|
}
|
||||||
|
local params = {
|
||||||
|
"template=" .. percentEncode(TEMPLATE),
|
||||||
|
"title=" .. percentEncode("bug: replace this with a meaningful title"),
|
||||||
|
}
|
||||||
|
local order = { "summary", "mods_which",
|
||||||
|
"version", "location", "screenshot", "steps", "expected", "extra" }
|
||||||
|
for _, key in ipairs(order) do
|
||||||
|
params[#params + 1] = key .. "=" .. percentEncode(fields[key])
|
||||||
|
end
|
||||||
|
return FORM_URL .. "?" .. table.concat(params, "&"), fields, info
|
||||||
|
end
|
||||||
|
|
||||||
|
function IssueReport.open(options, context)
|
||||||
|
local url = IssueReport.build(options, context)
|
||||||
|
local system = love and love.system or {}
|
||||||
|
local opened, openResult = invoke(system.openURL, url)
|
||||||
|
if opened and openResult ~= false then
|
||||||
|
return true, url
|
||||||
|
end
|
||||||
|
local copied, copyResult = invoke(system.setClipboardText, url)
|
||||||
|
if copied and copyResult ~= false then
|
||||||
|
return true, url, "Issue URL copied to the clipboard."
|
||||||
|
end
|
||||||
|
local filesystem = love and love.filesystem or {}
|
||||||
|
local written, writeResult = invoke(filesystem.write, "issue-report-url.txt", url)
|
||||||
|
if written and writeResult ~= false then
|
||||||
|
return true, url, "Issue URL saved to issue-report-url.txt."
|
||||||
|
end
|
||||||
|
return false, url, "No browser, clipboard, or writable save directory is available for the issue report."
|
||||||
|
end
|
||||||
|
|
||||||
|
IssueReport.percentEncode = percentEncode
|
||||||
|
IssueReport.metadata = metadata
|
||||||
|
|
||||||
|
return IssueReport
|
||||||
@@ -300,6 +300,7 @@ function SaveData.defaultOptions()
|
|||||||
-- Native mod enablement is an installation option, not save-slot data.
|
-- Native mod enablement is an installation option, not save-slot data.
|
||||||
-- Missing entries mean enabled so newly installed mods work by default.
|
-- Missing entries mean enabled so newly installed mods work by default.
|
||||||
mods = {},
|
mods = {},
|
||||||
|
safeMode = false,
|
||||||
-- Mods the player forced past the target gate (Loader:_gateGeneration).
|
-- Mods the player forced past the target gate (Loader:_gateGeneration).
|
||||||
-- modsGen2[id][version] = true, one answer per game; a bare `true` is the
|
-- 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).
|
-- pre-per-game shape and means the Gen 2 games only (see modForced).
|
||||||
@@ -386,6 +387,16 @@ function SaveData.mergeOptions(loaded)
|
|||||||
return opts
|
return opts
|
||||||
end
|
end
|
||||||
|
|
||||||
|
function SaveData.isSafeMode(options)
|
||||||
|
return type(options) == "table" and options.safeMode == true
|
||||||
|
end
|
||||||
|
|
||||||
|
function SaveData.setSafeMode(options, enabled)
|
||||||
|
if type(options) ~= "table" then return false end
|
||||||
|
options.safeMode = enabled == true
|
||||||
|
return options.safeMode
|
||||||
|
end
|
||||||
|
|
||||||
function SaveData.encode(data)
|
function SaveData.encode(data)
|
||||||
return SaveSerializer.encode(data)
|
return SaveSerializer.encode(data)
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -422,7 +422,7 @@ local function discoverModSchemas(opts)
|
|||||||
-- except experimental mods, which stay off until opted in.
|
-- except experimental mods, which stay off until opted in.
|
||||||
local flag = require("src.core.SaveData").modEnabled(opts, m.id)
|
local flag = require("src.core.SaveData").modEnabled(opts, m.id)
|
||||||
local enabled = flag == true or (flag == nil and not m.experimental)
|
local enabled = flag == true or (flag == nil and not m.experimental)
|
||||||
if enabled then
|
if enabled and not SaveData.isSafeMode(opts) then
|
||||||
local chunk = fs.load(path .. "/" .. m.options_schema)
|
local chunk = fs.load(path .. "/" .. m.options_schema)
|
||||||
if chunk then
|
if chunk then
|
||||||
local okR, schema = pcall(chunk)
|
local okR, schema = pcall(chunk)
|
||||||
@@ -521,9 +521,49 @@ local function modRows(opts, mod)
|
|||||||
return true
|
return true
|
||||||
end }
|
end }
|
||||||
end
|
end
|
||||||
|
for _, row in ipairs(rows) do
|
||||||
|
row.safeModeBlocked = true
|
||||||
|
if row.step then
|
||||||
|
local step = row.step
|
||||||
|
row.step = function(dir)
|
||||||
|
if SaveData.isSafeMode(opts) then return false end
|
||||||
|
return step(dir)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if row.setText then
|
||||||
|
local setText = row.setText
|
||||||
|
row.setText = function(text)
|
||||||
|
if SaveData.isSafeMode(opts) then return false end
|
||||||
|
return setText(text)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
return rows
|
return rows
|
||||||
end
|
end
|
||||||
|
|
||||||
|
local function troubleshootingRows(opts, hooks)
|
||||||
|
return {
|
||||||
|
{
|
||||||
|
label = Strings("SAFE MODE"),
|
||||||
|
actionLabel = function()
|
||||||
|
return SaveData.isSafeMode(opts) and Strings("Turn off") or Strings("Turn on")
|
||||||
|
end,
|
||||||
|
action = function()
|
||||||
|
SaveData.setSafeMode(opts, not SaveData.isSafeMode(opts))
|
||||||
|
return true
|
||||||
|
end,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label = Strings("REPORT ISSUE"),
|
||||||
|
actionLabel = Strings("Report bug"),
|
||||||
|
action = function()
|
||||||
|
if hooks and hooks.reportIssue then hooks.reportIssue(opts) end
|
||||||
|
return false
|
||||||
|
end,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
-- ------- Gen 2 (Gold)
|
-- ------- Gen 2 (Gold)
|
||||||
--
|
--
|
||||||
-- Gold reads NONE of the rows above. Its OPTION screen writes a different
|
-- Gold reads NONE of the rows above. Its OPTION screen writes a different
|
||||||
@@ -704,6 +744,10 @@ function LauncherSettings.open(hooks, version)
|
|||||||
sections[#sections + 1] = { title = mod.name, rows = rows }
|
sections[#sections + 1] = { title = mod.name, rows = rows }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
sections[#sections + 1] = {
|
||||||
|
title = Strings("TROUBLESHOOTING"),
|
||||||
|
rows = troubleshootingRows(opts, hooks),
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
opts = opts,
|
opts = opts,
|
||||||
version = version,
|
version = version,
|
||||||
|
|||||||
+31
-11
@@ -682,6 +682,7 @@ end
|
|||||||
|
|
||||||
local function modStatusColor(status)
|
local function modStatusColor(status)
|
||||||
if status == "ok" then return Strings("Ready"), PAL.green end
|
if status == "ok" then return Strings("Ready"), PAL.green end
|
||||||
|
if status == "safe_mode" then return Strings("Safe mode"), PAL.yellow end
|
||||||
if status == "needs_import" then return Strings("Import required"), PAL.yellow end
|
if status == "needs_import" then return Strings("Import required"), PAL.yellow end
|
||||||
if status == "conflict" then return Strings("Conflict"), PAL.red end
|
if status == "conflict" then return Strings("Conflict"), PAL.red end
|
||||||
-- not a fault: the mod is intact, this is simply not a game it is for
|
-- not a fault: the mod is intact, this is simply not a game it is for
|
||||||
@@ -1814,10 +1815,11 @@ end
|
|||||||
|
|
||||||
-- One compact coloured checkbox for each game. The cartridge colour carries
|
-- One compact coloured checkbox for each game. The cartridge colour carries
|
||||||
-- the game identity even when the row is narrow.
|
-- the game identity even when the row is narrow.
|
||||||
local function modGameCheckbox(x, y, size, checked, game, id)
|
local function modGameCheckbox(x, y, size, checked, game, id, enabled)
|
||||||
local color = cartColor(game)
|
enabled = enabled ~= false
|
||||||
local focused = Kit.focusable(id, x, y, size, size)
|
local color = enabled and cartColor(game) or PAL.steel
|
||||||
local hot = focused or Kit.hover(x, y, size, size)
|
local focused = enabled and Kit.focusable(id, x, y, size, size)
|
||||||
|
local hot = enabled and (focused or Kit.hover(x, y, size, size))
|
||||||
if love.graphics then
|
if love.graphics then
|
||||||
Theme.fillRounded(x, y, size, size, PAL.bg, 1)
|
Theme.fillRounded(x, y, size, size, PAL.bg, 1)
|
||||||
if checked then
|
if checked then
|
||||||
@@ -1829,12 +1831,13 @@ local function modGameCheckbox(x, y, size, checked, game, id)
|
|||||||
hot and Theme.A.focus or Theme.A.hairline, 1)
|
hot and Theme.A.focus or Theme.A.hairline, 1)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
return Kit.press(x, y, size, size) or Kit._activateId == id
|
return enabled and (Kit.press(x, y, size, size) or Kit._activateId == id)
|
||||||
end
|
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")
|
||||||
|
local safeMode = imp.safeMode == true
|
||||||
local mods = imp.mods or {}
|
local mods = imp.mods or {}
|
||||||
local gap = m.gap
|
local gap = m.gap
|
||||||
local cy = y
|
local cy = y
|
||||||
@@ -1866,9 +1869,11 @@ local function buildModsPanel(imp, x, y, w, availH, m)
|
|||||||
action = function() imp:chooseMod() end })
|
action = function() imp:chooseMod() end })
|
||||||
btn(imp, place(disableW), cy, disableW, bh, "mods-disable-all", Strings("Disable all"), {
|
btn(imp, place(disableW), cy, disableW, bh, "mods-disable-all", Strings("Disable all"), {
|
||||||
kind = "warn", font = "small",
|
kind = "warn", font = "small",
|
||||||
|
enabled = not safeMode,
|
||||||
action = function() imp:_setAllMods(false) end })
|
action = function() imp:_setAllMods(false) end })
|
||||||
btn(imp, place(enableW), cy, enableW, bh, "mods-enable-all", Strings("Enable all"), {
|
btn(imp, place(enableW), cy, enableW, bh, "mods-enable-all", Strings("Enable all"), {
|
||||||
kind = "good", font = "small",
|
kind = "good", font = "small",
|
||||||
|
enabled = not safeMode,
|
||||||
action = function() imp:_setAllMods(true) end })
|
action = function() imp:_setAllMods(true) end })
|
||||||
btn(imp, place(checkFullW), cy, checkFullW, bh, "mods-check-updates", Strings("Check for updates"), {
|
btn(imp, place(checkFullW), cy, checkFullW, bh, "mods-check-updates", Strings("Check for updates"), {
|
||||||
font = "small",
|
font = "small",
|
||||||
@@ -1917,7 +1922,9 @@ local function buildModsPanel(imp, x, y, w, availH, m)
|
|||||||
|
|
||||||
-- notice line
|
-- notice line
|
||||||
local noticeText, noticeCol
|
local noticeText, noticeCol
|
||||||
if imp.modNotice then
|
if safeMode then
|
||||||
|
noticeText, noticeCol = "Safe mode is on. All mods are disabled. Turn it off in Settings to change mod toggles.", PAL.yellow
|
||||||
|
elseif imp.modNotice then
|
||||||
noticeText = imp.modNotice.text
|
noticeText = imp.modNotice.text
|
||||||
noticeCol = imp.modNotice.ok and PAL.green or PAL.red
|
noticeCol = imp.modNotice.ok and PAL.green or PAL.red
|
||||||
else
|
else
|
||||||
@@ -2043,7 +2050,7 @@ local function buildModsPanel(imp, x, y, w, availH, m)
|
|||||||
local togKey = "mod-toggle-" .. mod.id .. "-" .. game
|
local togKey = "mod-toggle-" .. mod.id .. "-" .. game
|
||||||
if modGameCheckbox(tx, gamesY, togH,
|
if modGameCheckbox(tx, gamesY, togH,
|
||||||
mod.enabledByVersion and mod.enabledByVersion[game] == true,
|
mod.enabledByVersion and mod.enabledByVersion[game] == true,
|
||||||
game, togKey) then
|
game, togKey, not safeMode) then
|
||||||
local version = game
|
local version = game
|
||||||
queueAction(imp, togKey, function() imp:_toggleMod(mod.id, nil, version) end)
|
queueAction(imp, togKey, function() imp:_toggleMod(mod.id, nil, version) end)
|
||||||
flipped = true
|
flipped = true
|
||||||
@@ -3079,6 +3086,7 @@ local function buildProfilesModal(imp, m)
|
|||||||
btn(imp, place(swBtnW), ry + math.floor(4 * m.s), swBtnW, rowH - math.floor(8 * m.s), "prof-sw-" .. i,
|
btn(imp, place(swBtnW), ry + math.floor(4 * m.s), swBtnW, rowH - math.floor(8 * m.s), "prof-sw-" .. i,
|
||||||
Strings("Switch"), {
|
Strings("Switch"), {
|
||||||
kind = "good", font = "micro",
|
kind = "good", font = "micro",
|
||||||
|
enabled = not imp.safeMode,
|
||||||
action = function()
|
action = function()
|
||||||
LauncherMods.applyProfile(p.name, options)
|
LauncherMods.applyProfile(p.name, options)
|
||||||
if imp._refreshMods then imp:_refreshMods() end
|
if imp._refreshMods then imp:_refreshMods() end
|
||||||
@@ -3105,8 +3113,10 @@ local function buildModHeaderActionsModal(imp, m)
|
|||||||
local btns = {
|
local btns = {
|
||||||
{ label = Strings("Mod profiles..."), action = function() imp._profilesPopup = true end },
|
{ label = Strings("Mod profiles..."), action = function() imp._profilesPopup = true end },
|
||||||
{ label = Strings("Check for updates"), action = function() imp:_syncModUpdateInfo(true) end },
|
{ label = Strings("Check for updates"), action = function() imp:_syncModUpdateInfo(true) end },
|
||||||
{ label = Strings("Enable all mods"), kind = "good", action = function() imp:_setAllMods(true) end },
|
{ label = Strings("Enable all mods"), kind = "good", enabled = not imp.safeMode,
|
||||||
{ label = Strings("Disable all mods"), kind = "warn", action = function() imp:_setAllMods(false) end },
|
action = function() imp:_setAllMods(true) end },
|
||||||
|
{ label = Strings("Disable all mods"), kind = "warn", enabled = not imp.safeMode,
|
||||||
|
action = function() imp:_setAllMods(false) end },
|
||||||
{ label = Strings("Sort mods..."), action = function() imp._sortPopup = "mods" end },
|
{ label = Strings("Sort mods..."), action = function() imp._sortPopup = "mods" end },
|
||||||
}
|
}
|
||||||
local h = pad + Kit.textHeight("button") + math.floor(12 * m.s)
|
local h = pad + Kit.textHeight("button") + math.floor(12 * m.s)
|
||||||
@@ -3119,6 +3129,7 @@ local function buildModHeaderActionsModal(imp, m)
|
|||||||
for i, b in ipairs(btns) do
|
for i, b in ipairs(btns) do
|
||||||
btn(imp, px + pad, cy, pw - 2 * pad, m.btnH, "modheadact-" .. i, b.label, {
|
btn(imp, px + pad, cy, pw - 2 * pad, m.btnH, "modheadact-" .. i, b.label, {
|
||||||
kind = b.kind or "ghost", font = "small",
|
kind = b.kind or "ghost", font = "small",
|
||||||
|
enabled = b.enabled,
|
||||||
action = function()
|
action = function()
|
||||||
imp._modHeaderActionsPopup = nil
|
imp._modHeaderActionsPopup = nil
|
||||||
b.action()
|
b.action()
|
||||||
@@ -3737,6 +3748,7 @@ end
|
|||||||
|
|
||||||
local function buildSettingsModal(imp, m)
|
local function buildSettingsModal(imp, m)
|
||||||
local model = imp._settings
|
local model = imp._settings
|
||||||
|
local SaveData = require("src.core.SaveData")
|
||||||
local pad = math.floor(18 * m.s)
|
local pad = math.floor(18 * m.s)
|
||||||
local w = math.floor(640 * m.s)
|
local w = math.floor(640 * m.s)
|
||||||
local h = math.floor(math.min(m.H - 2 * m.pad, m.H * 0.9))
|
local h = math.floor(math.min(m.H - 2 * m.pad, m.H * 0.9))
|
||||||
@@ -3826,6 +3838,8 @@ local function buildSettingsModal(imp, m)
|
|||||||
item.header)
|
item.header)
|
||||||
else
|
else
|
||||||
local row = item.row
|
local row = item.row
|
||||||
|
local rowEnabled = not row.safeModeBlocked
|
||||||
|
or not SaveData.isSafeMode(model.opts)
|
||||||
local key = "set-" .. i
|
local key = "set-" .. i
|
||||||
Kit.card(px + pad, ry, pw - 2 * pad, rowH, "hairline")
|
Kit.card(px + pad, ry, pw - 2 * pad, rowH, "hairline")
|
||||||
local ix = px + pad + math.floor(12 * m.s)
|
local ix = px + pad + math.floor(12 * m.s)
|
||||||
@@ -3855,6 +3869,7 @@ local function buildSettingsModal(imp, m)
|
|||||||
ctlY + (m.btnH - Kit.textHeight("small")) / 2, PAL.detail)
|
ctlY + (m.btnH - Kit.textHeight("small")) / 2, PAL.detail)
|
||||||
btn(imp, rx - ew, ctlY, ew, m.btnH,
|
btn(imp, rx - ew, ctlY, ew, m.btnH,
|
||||||
key .. "-edit", Strings("Edit"), { kind = "accent", font = "small",
|
key .. "-edit", Strings("Edit"), { kind = "accent", font = "small",
|
||||||
|
enabled = rowEnabled,
|
||||||
action = function()
|
action = function()
|
||||||
imp._settingsText = { row = row, text = tostring(row.value() or ""),
|
imp._settingsText = { row = row, text = tostring(row.value() or ""),
|
||||||
maxLen = row.editText.maxLen }
|
maxLen = row.editText.maxLen }
|
||||||
@@ -3863,12 +3878,14 @@ local function buildSettingsModal(imp, m)
|
|||||||
elseif row.action then
|
elseif row.action then
|
||||||
-- A plain action row (Reset rebinds, Touch controls): the whole right
|
-- A plain action row (Reset rebinds, Touch controls): the whole right
|
||||||
-- side is one button rather than a value ladder.
|
-- side is one button rather than a value ladder.
|
||||||
local aw = Kit.textWidth("small", row.actionLabel or Strings("Run"))
|
local actionLabel = type(row.actionLabel) == "function"
|
||||||
|
and row.actionLabel() or row.actionLabel or Strings("Run")
|
||||||
|
local aw = Kit.textWidth("small", actionLabel)
|
||||||
+ math.floor(24 * m.s)
|
+ math.floor(24 * m.s)
|
||||||
Kit.text("small", Kit.ellipsize("small", row.label,
|
Kit.text("small", Kit.ellipsize("small", row.label,
|
||||||
labelW or (inner - aw - math.floor(12 * m.s))), ix, labelY, PAL.text)
|
labelW or (inner - aw - math.floor(12 * m.s))), ix, labelY, PAL.text)
|
||||||
btn(imp, rx - aw, ctlY, aw, m.btnH,
|
btn(imp, rx - aw, ctlY, aw, m.btnH,
|
||||||
key .. "-act", row.actionLabel or Strings("Run"), {
|
key .. "-act", actionLabel, {
|
||||||
kind = row.danger and "danger" or "ghost", font = "small",
|
kind = row.danger and "danger" or "ghost", font = "small",
|
||||||
action = function()
|
action = function()
|
||||||
if row.action() ~= false then model.save() end
|
if row.action() ~= false then model.save() end
|
||||||
@@ -3884,12 +3901,14 @@ local function buildSettingsModal(imp, m)
|
|||||||
or valW
|
or valW
|
||||||
btn(imp, rx - stepW, ctlY, stepW, m.btnH,
|
btn(imp, rx - stepW, ctlY, stepW, m.btnH,
|
||||||
key .. "-next", ">", { font = "small",
|
key .. "-next", ">", { font = "small",
|
||||||
|
enabled = rowEnabled,
|
||||||
action = function() if row.step and row.step(1) then model.save() end end })
|
action = function() if row.step and row.step(1) then model.save() end end })
|
||||||
Kit.textCenter("small", Kit.ellipsize("small", tostring(row.value()), vw),
|
Kit.textCenter("small", Kit.ellipsize("small", tostring(row.value()), vw),
|
||||||
rx - stepW - vw, ctlY + (m.btnH - Kit.textHeight("small")) / 2, vw,
|
rx - stepW - vw, ctlY + (m.btnH - Kit.textHeight("small")) / 2, vw,
|
||||||
PAL.heading)
|
PAL.heading)
|
||||||
btn(imp, rx - stepW - vw - stepW, ctlY, stepW,
|
btn(imp, rx - stepW - vw - stepW, ctlY, stepW,
|
||||||
m.btnH, key .. "-prev", "<", { font = "small",
|
m.btnH, key .. "-prev", "<", { font = "small",
|
||||||
|
enabled = rowEnabled,
|
||||||
action = function() if row.step and row.step(-1) then model.save() end end })
|
action = function() if row.step and row.step(-1) then model.save() end end })
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -4046,6 +4065,7 @@ local function buildDepResolverModal(imp, m)
|
|||||||
local bw = Kit.textWidth("small", btnLabel) + math.floor(20 * m.s)
|
local bw = Kit.textWidth("small", btnLabel) + math.floor(20 * m.s)
|
||||||
btn(imp, place(bw), ly, bw, chipH, "dep-dis-" .. i, btnLabel, {
|
btn(imp, place(bw), ly, bw, chipH, "dep-dis-" .. i, btnLabel, {
|
||||||
kind = "warn", font = "small",
|
kind = "warn", font = "small",
|
||||||
|
enabled = not imp.safeMode,
|
||||||
action = function()
|
action = function()
|
||||||
local LauncherMods = require("src.mods.LauncherMods")
|
local LauncherMods = require("src.mods.LauncherMods")
|
||||||
LauncherMods.setEnabled(dep.id, false, imp.modScope)
|
LauncherMods.setEnabled(dep.id, false, imp.modScope)
|
||||||
|
|||||||
@@ -3625,6 +3625,10 @@ function RomImporter:_openSettings()
|
|||||||
-- The tab rides along: the editor persists the layout into that game's own
|
-- The tab rides along: the editor persists the layout into that game's own
|
||||||
-- option block, and Gold's is not the flat Gen 1 one (#1100).
|
-- option block, and Gold's is not the flat Gen 1 one (#1100).
|
||||||
local hooks = {}
|
local hooks = {}
|
||||||
|
local version = self.tab
|
||||||
|
hooks.reportIssue = function(opts)
|
||||||
|
return self:_reportIssue(opts, version)
|
||||||
|
end
|
||||||
if self.onEditTouchControls then
|
if self.onEditTouchControls then
|
||||||
local version = self.tab
|
local version = self.tab
|
||||||
hooks.editTouchControls = function()
|
hooks.editTouchControls = function()
|
||||||
@@ -3642,11 +3646,13 @@ function RomImporter:_openSettings()
|
|||||||
-- The tab the gear was opened on decides the row set: Gold reads a
|
-- The tab the gear was opened on decides the row set: Gold reads a
|
||||||
-- different option block entirely, and offering it Gen 1's rows meant a
|
-- different option block entirely, and offering it Gen 1's rows meant a
|
||||||
-- dozen controls that changed nothing (see LauncherSettings.gen2Rows).
|
-- dozen controls that changed nothing (see LauncherSettings.gen2Rows).
|
||||||
local version = self.tab
|
|
||||||
local ok, model = pcall(function()
|
local ok, model = pcall(function()
|
||||||
return require("src.import.LauncherSettings").open(hooks, version)
|
return require("src.import.LauncherSettings").open(hooks, version)
|
||||||
end)
|
end)
|
||||||
if ok and model then self._settings = model end
|
if ok and model then
|
||||||
|
self._settings = model
|
||||||
|
self._settingsSafeModeAtOpen = require("src.core.SaveData").isSafeMode(model.opts)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Quit from the launcher's own X. It goes through love.event.quit so main.lua's
|
-- Quit from the launcher's own X. It goes through love.event.quit so main.lua's
|
||||||
@@ -3657,8 +3663,38 @@ function RomImporter:_quitApp()
|
|||||||
end
|
end
|
||||||
|
|
||||||
function RomImporter:_closeSettings()
|
function RomImporter:_closeSettings()
|
||||||
if self._settings then self._settings.save() end
|
local model = self._settings
|
||||||
|
if model then
|
||||||
|
model.save()
|
||||||
|
local safeMode = require("src.core.SaveData").isSafeMode(model.opts)
|
||||||
|
if safeMode ~= self._settingsSafeModeAtOpen then
|
||||||
|
self.mods = nil
|
||||||
|
self.safeMode = safeMode
|
||||||
|
self._modSortCache = nil
|
||||||
|
self._modInfoFetch = nil
|
||||||
|
end
|
||||||
|
end
|
||||||
self._settings = nil
|
self._settings = nil
|
||||||
|
self._settingsSafeModeAtOpen = nil
|
||||||
|
end
|
||||||
|
|
||||||
|
function RomImporter:_reportIssue(options, version)
|
||||||
|
local ok, IssueReport = pcall(require, "src.core.IssueReport")
|
||||||
|
if not ok then
|
||||||
|
self.modNotice = { ok = false, text = "Could not prepare the issue report." }
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
local opened, url, reason = IssueReport.open(options, {
|
||||||
|
version = version,
|
||||||
|
mods = self.mods,
|
||||||
|
})
|
||||||
|
if not opened then
|
||||||
|
self.modNotice = { ok = false, text = reason or "Could not open the issue report." }
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
self._lastIssueReportURL = url
|
||||||
|
if reason then self.modNotice = { ok = true, text = reason } end
|
||||||
|
return true
|
||||||
end
|
end
|
||||||
|
|
||||||
function RomImporter:_commitSettingsText()
|
function RomImporter:_commitSettingsText()
|
||||||
@@ -3998,6 +4034,8 @@ end
|
|||||||
-- so a still list costs nothing after the first paint.
|
-- so a still list costs nothing after the first paint.
|
||||||
function RomImporter:_refreshMods()
|
function RomImporter:_refreshMods()
|
||||||
local LauncherMods = require("src.mods.LauncherMods")
|
local LauncherMods = require("src.mods.LauncherMods")
|
||||||
|
local SaveData = require("src.core.SaveData")
|
||||||
|
self.safeMode = SaveData.isSafeMode(SaveData.loadOptions())
|
||||||
-- Once per session, ahead of the first listing: pull in any mod the player
|
-- Once per session, ahead of the first listing: pull in any mod the player
|
||||||
-- unzipped beside the executable, which an ordinary (non-portable) install
|
-- unzipped beside the executable, which an ordinary (non-portable) install
|
||||||
-- has no way to read. It happens here rather than behind a button because
|
-- has no way to read. It happens here rather than behind a button because
|
||||||
@@ -4137,6 +4175,10 @@ end
|
|||||||
-- so that game's checkbox and status chips reflect the new resolution.
|
-- so that game's checkbox and status chips reflect the new resolution.
|
||||||
-- Enabling an experimental mod arms a confirmation for that same game.
|
-- Enabling an experimental mod arms a confirmation for that same game.
|
||||||
function RomImporter:_toggleMod(id, confirmed, version)
|
function RomImporter:_toggleMod(id, confirmed, version)
|
||||||
|
if self.safeMode then
|
||||||
|
self.modNotice = { ok = false, text = "Safe mode is active. Turn it off in Settings to change mods." }
|
||||||
|
return
|
||||||
|
end
|
||||||
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
|
||||||
@@ -4177,6 +4219,10 @@ end
|
|||||||
-- must not be the way around it. Disabling needs no confirm -- it is the
|
-- must not be the way around it. Disabling needs no confirm -- it is the
|
||||||
-- recovery action, and Delete is the only destructive one on this panel.
|
-- recovery action, and Delete is the only destructive one on this panel.
|
||||||
function RomImporter:_setAllMods(want, confirmed)
|
function RomImporter:_setAllMods(want, confirmed)
|
||||||
|
if self.safeMode then
|
||||||
|
self.modNotice = { ok = false, text = "Safe mode is active. Turn it off in Settings to change mods." }
|
||||||
|
return
|
||||||
|
end
|
||||||
local LauncherMods = require("src.mods.LauncherMods")
|
local LauncherMods = require("src.mods.LauncherMods")
|
||||||
local ids, experimental = {}, false
|
local ids, experimental = {}, false
|
||||||
for _, m in ipairs(self.mods or {}) do
|
for _, m in ipairs(self.mods or {}) do
|
||||||
|
|||||||
@@ -291,6 +291,7 @@ function LauncherMods.deriveList(manifests, options, version)
|
|||||||
local ordered = {}
|
local ordered = {}
|
||||||
for _, m in ipairs(manifests) do ordered[#ordered + 1] = m end
|
for _, m in ipairs(manifests) do ordered[#ordered + 1] = m end
|
||||||
table.sort(ordered, function(a, b) return a.id < b.id end)
|
table.sort(ordered, function(a, b) return a.id < b.id end)
|
||||||
|
local safeMode = SaveData.isSafeMode(options)
|
||||||
|
|
||||||
-- the override is one answer per game (SaveData.modForced), the same scope
|
-- the override is one answer per game (SaveData.modForced), the same scope
|
||||||
-- the loader resolves it under
|
-- the loader resolves it under
|
||||||
@@ -304,17 +305,24 @@ function LauncherMods.deriveList(manifests, options, version)
|
|||||||
-- 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 for the selected game.
|
-- setEnabled writes and the loader loads for the selected game.
|
||||||
local decided = SaveData.modEnabled(options, m.id, SaveData.modScope(version))
|
if not safeMode then
|
||||||
if decided == nil then decided = not m.experimental end
|
local decided = SaveData.modEnabled(options, m.id, SaveData.modScope(version))
|
||||||
if decided then enabledSet[m.id] = true end
|
if decided == nil then decided = not m.experimental end
|
||||||
|
if decided then enabledSet[m.id] = true end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
local out = {}
|
local out = {}
|
||||||
for _, m in ipairs(ordered) do
|
for _, m in ipairs(ordered) do
|
||||||
local enabled = enabledSet[m.id] == true
|
local enabled = not safeMode and enabledSet[m.id] == true
|
||||||
local forced = forcedFor(m.id)
|
local forced = forcedFor(m.id)
|
||||||
local status, detail =
|
local status, detail
|
||||||
statusFor(byId, m.id, enabledSet, enabled, version, forcedFor)
|
if safeMode then
|
||||||
|
status, detail = "safe_mode", "Disabled by safe mode"
|
||||||
|
else
|
||||||
|
status, detail =
|
||||||
|
statusFor(byId, m.id, enabledSet, enabled, version, forcedFor)
|
||||||
|
end
|
||||||
-- nil, not false, when the panel is showing every game at once
|
-- nil, not false, when the panel is showing every game at once
|
||||||
local here = nil
|
local here = nil
|
||||||
if version then here = ModTargets.runsHere(m, version, nil, forced) end
|
if version then here = ModTargets.runsHere(m, version, nil, forced) end
|
||||||
@@ -332,7 +340,8 @@ function LauncherMods.deriveList(manifests, options, version)
|
|||||||
local answers = {}
|
local answers = {}
|
||||||
for _, game in ipairs(GameVersion.ORDER) do
|
for _, game in ipairs(GameVersion.ORDER) do
|
||||||
local answer = SaveData.modEnabled(options, m.id, game)
|
local answer = SaveData.modEnabled(options, m.id, game)
|
||||||
answers[game] = answer == true or (answer == nil and not m.experimental)
|
answers[game] = not safeMode
|
||||||
|
and (answer == true or (answer == nil and not m.experimental))
|
||||||
end
|
end
|
||||||
return answers
|
return answers
|
||||||
end)(),
|
end)(),
|
||||||
@@ -346,6 +355,7 @@ function LauncherMods.deriveList(manifests, options, version)
|
|||||||
-- panel is showing (src/mods/ModTargets.lua)
|
-- panel is showing (src/mods/ModTargets.lua)
|
||||||
targets = ModTargets.chip(m),
|
targets = ModTargets.chip(m),
|
||||||
targetsHere = here,
|
targetsHere = here,
|
||||||
|
safeMode = safeMode,
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
return out
|
return out
|
||||||
@@ -586,6 +596,7 @@ end
|
|||||||
-- answer. The loader and the in-game manager use the same scope on next boot.
|
-- answer. The loader and the in-game manager use the same scope on next boot.
|
||||||
function LauncherMods.setEnabled(id, enabled, version)
|
function LauncherMods.setEnabled(id, enabled, version)
|
||||||
local options = SaveData.loadOptions()
|
local options = SaveData.loadOptions()
|
||||||
|
if SaveData.isSafeMode(options) then return false end
|
||||||
SaveData.setModEnabled(options, id, enabled, SaveData.modScope(version))
|
SaveData.setModEnabled(options, id, enabled, SaveData.modScope(version))
|
||||||
SaveData.saveOptions(options)
|
SaveData.saveOptions(options)
|
||||||
LauncherMods.syncActiveProfile(options)
|
LauncherMods.syncActiveProfile(options)
|
||||||
@@ -599,6 +610,7 @@ end
|
|||||||
-- and leaves a half-applied state behind if one of them fails.
|
-- and leaves a half-applied state behind if one of them fails.
|
||||||
function LauncherMods.setAllEnabled(ids, enabled, version)
|
function LauncherMods.setAllEnabled(ids, enabled, version)
|
||||||
local options = SaveData.loadOptions()
|
local options = SaveData.loadOptions()
|
||||||
|
if SaveData.isSafeMode(options) then return false end
|
||||||
local scope = SaveData.modScope(version)
|
local scope = SaveData.modScope(version)
|
||||||
for _, id in ipairs(ids or {}) do
|
for _, id in ipairs(ids or {}) do
|
||||||
if scope then
|
if scope then
|
||||||
@@ -1184,6 +1196,7 @@ end
|
|||||||
|
|
||||||
function LauncherMods.applyProfile(profileName, options)
|
function LauncherMods.applyProfile(profileName, options)
|
||||||
options = options or SaveData.loadOptions()
|
options = options or SaveData.loadOptions()
|
||||||
|
if SaveData.isSafeMode(options) then return false end
|
||||||
local profiles = options.modProfiles or {}
|
local profiles = options.modProfiles or {}
|
||||||
local targetProfile
|
local targetProfile
|
||||||
for _, p in ipairs(profiles) do
|
for _, p in ipairs(profiles) do
|
||||||
|
|||||||
+10
-1
@@ -259,6 +259,7 @@ function Loader.new(opts)
|
|||||||
modInput = {}, modEnv = {}, stepsQueues = {},
|
modInput = {}, modEnv = {}, stepsQueues = {},
|
||||||
fs = (opts and opts.fs) or (love and love.filesystem),
|
fs = (opts and opts.fs) or (love and love.filesystem),
|
||||||
dev = dev,
|
dev = dev,
|
||||||
|
safeMode = false,
|
||||||
-- Which generation this boot is (1 or 2). Fixed at construction: the
|
-- Which generation this boot is (1 or 2). Fixed at construction: the
|
||||||
-- active version is set once in main.lua's bootGame before anything
|
-- active version is set once in main.lua's bootGame before anything
|
||||||
-- builds a loader, and a run never changes generation underneath one.
|
-- builds a loader, and a run never changes generation underneath one.
|
||||||
@@ -300,6 +301,8 @@ end
|
|||||||
function Loader:_loadState()
|
function Loader:_loadState()
|
||||||
self.disabled = {}
|
self.disabled = {}
|
||||||
local options = SaveData.loadOptions(self.fs)
|
local options = SaveData.loadOptions(self.fs)
|
||||||
|
self.safeMode = SaveData.isSafeMode(options)
|
||||||
|
Runtime.safeMode = self.safeMode
|
||||||
local scope = self:_enableScope()
|
local scope = self:_enableScope()
|
||||||
local ids = {}
|
local ids = {}
|
||||||
for id in pairs(options.mods or {}) do ids[id] = true end
|
for id in pairs(options.mods or {}) do ids[id] = true end
|
||||||
@@ -415,6 +418,7 @@ function Loader:_writeOptionSchemas()
|
|||||||
end
|
end
|
||||||
|
|
||||||
function Loader:setEnabled(id, enabled)
|
function Loader:setEnabled(id, enabled)
|
||||||
|
if self.safeMode then return false end
|
||||||
if not self.mods[id] then return false end
|
if not self.mods[id] then return false end
|
||||||
self.disabled[id] = not enabled
|
self.disabled[id] = not enabled
|
||||||
self.mods[id].enabled = enabled
|
self.mods[id].enabled = enabled
|
||||||
@@ -427,6 +431,7 @@ end
|
|||||||
-- choice could not be persisted for a game, so the caller does not promise a
|
-- choice could not be persisted for a game, so the caller does not promise a
|
||||||
-- restart will honour it.
|
-- restart will honour it.
|
||||||
function Loader:setGen2Forced(id, forced)
|
function Loader:setGen2Forced(id, forced)
|
||||||
|
if self.safeMode then return false, false end
|
||||||
if not self.mods[id] then return false, false end
|
if not self.mods[id] then return false, false end
|
||||||
self.gen2Forced[id] = forced or nil
|
self.gen2Forced[id] = forced or nil
|
||||||
self:_saveState()
|
self:_saveState()
|
||||||
@@ -1539,6 +1544,9 @@ 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()
|
||||||
|
if self.safeMode then
|
||||||
|
for id in pairs(self.mods) do self.disabled[id] = true end
|
||||||
|
end
|
||||||
-- Existing installs stored one shared answer. Once their manifests are
|
-- Existing installs stored one shared answer. Once their manifests are
|
||||||
-- known, split that answer across every game before the next launcher/game
|
-- known, split that answer across every game before the next launcher/game
|
||||||
-- toggle can change one independently. _loadState already used the same
|
-- toggle can change one independently. _loadState already used the same
|
||||||
@@ -1574,7 +1582,7 @@ function Loader:load(data)
|
|||||||
-- the one build where its env var is set.
|
-- the one build where its env var is set.
|
||||||
for id, mod in pairs(self.mods) do
|
for id, mod in pairs(self.mods) do
|
||||||
local envName = mod.manifest.force_enable_env
|
local envName = mod.manifest.force_enable_env
|
||||||
if envName and os.getenv(envName) == "1" then
|
if not self.safeMode and envName and os.getenv(envName) == "1" then
|
||||||
self.disabled[id] = nil
|
self.disabled[id] = nil
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -1740,6 +1748,7 @@ function Loader:status()
|
|||||||
local manifest = {}
|
local manifest = {}
|
||||||
for key, value in pairs(mod.manifest) do manifest[key] = value end
|
for key, value in pairs(mod.manifest) do manifest[key] = value end
|
||||||
manifest.enabled = mod.enabled ~= false
|
manifest.enabled = mod.enabled ~= false
|
||||||
|
manifest.safeMode = self.safeMode == true
|
||||||
manifest.state = mod.state or (manifest.enabled and "loaded" or "disabled")
|
manifest.state = mod.state or (manifest.enabled and "loaded" or "disabled")
|
||||||
manifest.error = mod.failure
|
manifest.error = mod.failure
|
||||||
-- set instead of `error` when the mod was left out for a reason that is
|
-- set instead of `error` when the mod was left out for a reason that is
|
||||||
|
|||||||
@@ -365,9 +365,13 @@ end
|
|||||||
|
|
||||||
function ManagerState:detailRows(m)
|
function ManagerState:detailRows(m)
|
||||||
local rows = {}
|
local rows = {}
|
||||||
rows[#rows + 1] = { label = m.enabled and "DISABLE" or "ENABLE",
|
if Runtime.safeMode then
|
||||||
action = function() self:beginToggle(m) end }
|
rows[#rows + 1] = { label = "SAFE MODE ACTIVE", inert = true }
|
||||||
if self:schemaFor(m) then
|
else
|
||||||
|
rows[#rows + 1] = { label = m.enabled and "DISABLE" or "ENABLE",
|
||||||
|
action = function() self:beginToggle(m) end }
|
||||||
|
end
|
||||||
|
if not Runtime.safeMode and self:schemaFor(m) then
|
||||||
rows[#rows + 1] = { label = Strings("OPTIONS.."),
|
rows[#rows + 1] = { label = Strings("OPTIONS.."),
|
||||||
action = function() self:openOptions(m) end }
|
action = function() self:openOptions(m) end }
|
||||||
end
|
end
|
||||||
@@ -383,7 +387,8 @@ function ManagerState:detailRows(m)
|
|||||||
-- what this mod does.
|
-- what this mod does.
|
||||||
local loader = self.game.mods
|
local loader = self.game.mods
|
||||||
local version, gen = self:targetGame()
|
local version, gen = self:targetGame()
|
||||||
if loader and loader.setGen2Forced and not ModTargets.supports(m, version, gen) then
|
if loader and loader.setGen2Forced and not Runtime.safeMode
|
||||||
|
and not ModTargets.supports(m, version, gen) then
|
||||||
rows[#rows + 1] = {
|
rows[#rows + 1] = {
|
||||||
label = m.gen2Forced and Strings("DON'T TRY HERE") or Strings("TRY HERE ANYWAY"),
|
label = m.gen2Forced and Strings("DON'T TRY HERE") or Strings("TRY HERE ANYWAY"),
|
||||||
action = function() self:toggleGen2Force(m) end }
|
action = function() self:toggleGen2Force(m) end }
|
||||||
@@ -674,6 +679,10 @@ end
|
|||||||
-- ------- the enable/disable flow
|
-- ------- the enable/disable flow
|
||||||
|
|
||||||
function ManagerState:beginToggle(m)
|
function ManagerState:beginToggle(m)
|
||||||
|
if Runtime.safeMode then
|
||||||
|
self:notify("SAFE MODE ACTIVE")
|
||||||
|
return
|
||||||
|
end
|
||||||
if not m then return end
|
if not m then return end
|
||||||
local want = not m.enabled
|
local want = not m.enabled
|
||||||
local loader = self.game.mods
|
local loader = self.game.mods
|
||||||
@@ -711,6 +720,10 @@ end
|
|||||||
-- override is scoped to THIS game, and a boot that cannot name one keeps it in
|
-- override is scoped to THIS game, and a boot that cannot name one keeps it in
|
||||||
-- memory only, which the notice says rather than promising a restart.
|
-- memory only, which the notice says rather than promising a restart.
|
||||||
function ManagerState:toggleGen2Force(m)
|
function ManagerState:toggleGen2Force(m)
|
||||||
|
if Runtime.safeMode then
|
||||||
|
self:notify("SAFE MODE ACTIVE")
|
||||||
|
return
|
||||||
|
end
|
||||||
local loader = self.game.mods
|
local loader = self.game.mods
|
||||||
if not (loader and loader.setGen2Forced) then return end
|
if not (loader and loader.setGen2Forced) then return end
|
||||||
local want = not m.gen2Forced
|
local want = not m.gen2Forced
|
||||||
@@ -743,6 +756,10 @@ function ManagerState:enableScope()
|
|||||||
end
|
end
|
||||||
|
|
||||||
function ManagerState:commitToggle(apply)
|
function ManagerState:commitToggle(apply)
|
||||||
|
if Runtime.safeMode then
|
||||||
|
self:notify("SAFE MODE ACTIVE")
|
||||||
|
return
|
||||||
|
end
|
||||||
local loader = self.game.mods
|
local loader = self.game.mods
|
||||||
local opts = self:optionsTable()
|
local opts = self:optionsTable()
|
||||||
local scope = self:enableScope()
|
local scope = self:enableScope()
|
||||||
@@ -757,6 +774,10 @@ function ManagerState:commitToggle(apply)
|
|||||||
end
|
end
|
||||||
|
|
||||||
function ManagerState:discardChanges()
|
function ManagerState:discardChanges()
|
||||||
|
if Runtime.safeMode then
|
||||||
|
self:notify("SAFE MODE ACTIVE")
|
||||||
|
return
|
||||||
|
end
|
||||||
local loader = self.game.mods
|
local loader = self.game.mods
|
||||||
local opts = self:optionsTable()
|
local opts = self:optionsTable()
|
||||||
local scope = self:enableScope()
|
local scope = self:enableScope()
|
||||||
@@ -802,6 +823,10 @@ function ManagerState:persistOptions()
|
|||||||
end
|
end
|
||||||
|
|
||||||
function ManagerState:applyProfile(p)
|
function ManagerState:applyProfile(p)
|
||||||
|
if Runtime.safeMode then
|
||||||
|
self:notify("SAFE MODE ACTIVE")
|
||||||
|
return
|
||||||
|
end
|
||||||
local mods = self:manifestMap()
|
local mods = self:manifestMap()
|
||||||
local set = self:enabledSet()
|
local set = self:enabledSet()
|
||||||
local combined = {}
|
local combined = {}
|
||||||
@@ -963,6 +988,10 @@ function ManagerState:optionValue(modId, row)
|
|||||||
end
|
end
|
||||||
|
|
||||||
function ManagerState:setOption(modId, key, value)
|
function ManagerState:setOption(modId, key, value)
|
||||||
|
if Runtime.safeMode then
|
||||||
|
self:notify("SAFE MODE ACTIVE")
|
||||||
|
return false
|
||||||
|
end
|
||||||
local save = self.game.save
|
local save = self.game.save
|
||||||
if save and save.options then
|
if save and save.options then
|
||||||
save.options.modOptions = save.options.modOptions or {}
|
save.options.modOptions = save.options.modOptions or {}
|
||||||
@@ -1123,6 +1152,10 @@ function ManagerState:buildOptionRows(m, schema)
|
|||||||
end
|
end
|
||||||
|
|
||||||
function ManagerState:openOptions(m)
|
function ManagerState:openOptions(m)
|
||||||
|
if Runtime.safeMode then
|
||||||
|
self:notify("SAFE MODE ACTIVE")
|
||||||
|
return
|
||||||
|
end
|
||||||
local schema = self:schemaFor(m)
|
local schema = self:schemaFor(m)
|
||||||
if not schema then
|
if not schema then
|
||||||
self:notify("NO OPTIONS")
|
self:notify("NO OPTIONS")
|
||||||
|
|||||||
@@ -33,6 +33,8 @@ Runtime.currentMod = nil
|
|||||||
-- currentMod went back to nil (src/mods/Sandbox.lua)
|
-- currentMod went back to nil (src/mods/Sandbox.lua)
|
||||||
Runtime.modRequire = nil
|
Runtime.modRequire = nil
|
||||||
|
|
||||||
|
Runtime.safeMode = false
|
||||||
|
|
||||||
function Runtime.install(events, hooks, errors)
|
function Runtime.install(events, hooks, errors)
|
||||||
Runtime.events, Runtime.hooks = events, hooks
|
Runtime.events, Runtime.hooks = events, hooks
|
||||||
Runtime.errors = errors
|
Runtime.errors = errors
|
||||||
|
|||||||
@@ -0,0 +1,137 @@
|
|||||||
|
package.path = "./?.lua;./?/init.lua;" .. package.path
|
||||||
|
|
||||||
|
local S = require("tests.harness").suite("safe mode and issue report")
|
||||||
|
local check = S.check
|
||||||
|
|
||||||
|
local SaveData = require("src.core.SaveData")
|
||||||
|
local LauncherMods = require("src.mods.LauncherMods")
|
||||||
|
local IssueReport = require("src.core.IssueReport")
|
||||||
|
local Version = require("src.core.Version")
|
||||||
|
|
||||||
|
local options = SaveData.defaultOptions()
|
||||||
|
check(not SaveData.isSafeMode(options), "safe mode defaults off")
|
||||||
|
SaveData.setSafeMode(options, true)
|
||||||
|
check(SaveData.isSafeMode(options), "safe mode can be enabled")
|
||||||
|
|
||||||
|
local manifests = {
|
||||||
|
{ id = "alpha", name = "Alpha", version = "1.0.0", experimental = false,
|
||||||
|
raw = {}, dependencySpecs = {}, conflictSpecs = {} },
|
||||||
|
{ id = "beta", name = "Beta", version = "1.0.0", experimental = false,
|
||||||
|
raw = {}, dependencySpecs = {}, conflictSpecs = {} },
|
||||||
|
}
|
||||||
|
options.mods.alpha = false
|
||||||
|
options.mods.beta = true
|
||||||
|
local rows = LauncherMods.deriveList(manifests, options, "red")
|
||||||
|
check(#rows == 2, "safe mode keeps installed mods visible")
|
||||||
|
check(not rows[1].enabled and not rows[2].enabled,
|
||||||
|
"safe mode disables every launcher mod row")
|
||||||
|
check(rows[1].status == "safe_mode" and rows[2].status == "safe_mode",
|
||||||
|
"safe mode explains every disabled launcher row")
|
||||||
|
|
||||||
|
SaveData.setSafeMode(options, false)
|
||||||
|
rows = LauncherMods.deriveList(manifests, options, "red")
|
||||||
|
local byId = {}
|
||||||
|
for _, row in ipairs(rows) do byId[row.id] = row end
|
||||||
|
check(not byId.alpha.enabled and byId.beta.enabled,
|
||||||
|
"turning safe mode off restores saved mod choices")
|
||||||
|
|
||||||
|
local previousLove = _G.love
|
||||||
|
local openedURL
|
||||||
|
_G.love = {
|
||||||
|
getVersion = function() return 12, 0, 0, "Mysterious Mysteries" end,
|
||||||
|
system = {
|
||||||
|
getOS = function() return "iOS" end,
|
||||||
|
getModel = function() return "iPad Test" end,
|
||||||
|
openURL = function(url) openedURL = url end,
|
||||||
|
},
|
||||||
|
graphics = {
|
||||||
|
getRendererInfo = function()
|
||||||
|
return "Metal", "3.0", "Apple", "Simulator GPU"
|
||||||
|
end,
|
||||||
|
getDimensions = function() return 1024, 768 end,
|
||||||
|
getPixelDimensions = function() return 2048, 1536 end,
|
||||||
|
},
|
||||||
|
window = {
|
||||||
|
getMode = function() return 1024, 768, { fullscreen = false } end,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
local url, fields, info = IssueReport.build({
|
||||||
|
safeMode = true,
|
||||||
|
lastVersion = "gold",
|
||||||
|
}, {
|
||||||
|
version = "gold",
|
||||||
|
mods = { { id = "alpha", name = "Alpha", enabled = true } },
|
||||||
|
})
|
||||||
|
check(url:find("template=bug_report.yml", 1, true) ~= nil,
|
||||||
|
"report URL selects the bug form")
|
||||||
|
check(url:find("title=bug%3A%20replace%20this%20with%20a%20meaningful%20title", 1, true) ~= nil,
|
||||||
|
"report URL uses the requested bug title")
|
||||||
|
check(info.os == "iOS",
|
||||||
|
"report metadata maps the platform")
|
||||||
|
check(fields.mods_which == "",
|
||||||
|
"safe mode leaves the optional mod list blank")
|
||||||
|
check(not url:find("game=", 1, true)
|
||||||
|
and not url:find("os=", 1, true)
|
||||||
|
and not url:find("mods_enabled=", 1, true),
|
||||||
|
"report URL omits unsupported dropdown and checkbox prefills")
|
||||||
|
check(fields.summary == "" and fields.location == "" and fields.screenshot == ""
|
||||||
|
and fields.steps == "" and fields.expected == "",
|
||||||
|
"report leaves user-entered fields blank")
|
||||||
|
check(info.metadata:find("Device: iPad Test", 1, true) ~= nil
|
||||||
|
and info.metadata:find("LÖVE: 12.0.0", 1, true) ~= nil
|
||||||
|
and info.metadata:find("Safe mode: on", 1, true) ~= nil,
|
||||||
|
"report metadata includes device and app details")
|
||||||
|
check(not info.metadata:find("unknown", 1, true),
|
||||||
|
"report metadata omits unknown values")
|
||||||
|
check(not info.metadata:find("Game id", 1, true)
|
||||||
|
and not info.metadata:find("Game:", 1, true)
|
||||||
|
and not info.metadata:find("Mods:", 1, true)
|
||||||
|
and not info.metadata:find("Processors", 1, true)
|
||||||
|
and not info.metadata:find("Power", 1, true),
|
||||||
|
"report metadata omits redundant system fields")
|
||||||
|
|
||||||
|
local previousEngine = Version.engine
|
||||||
|
Version.engine = "0.1.50"
|
||||||
|
local _, versionFields, versionInfo = IssueReport.build({}, { mods = {} })
|
||||||
|
check(versionFields.version == "0.1.50"
|
||||||
|
and versionInfo.metadata:find("App: gen1recomp v0.1.50", 1, true) ~= nil,
|
||||||
|
"report uses the stamped app version")
|
||||||
|
Version.engine = previousEngine
|
||||||
|
local _, _, developmentInfo = IssueReport.build({}, { mods = {} })
|
||||||
|
check(not developmentInfo.metadata:find("0.0.0-dev", 1, true),
|
||||||
|
"report omits an unstamped development version")
|
||||||
|
|
||||||
|
local previousOS = love.system.getOS
|
||||||
|
local previousModel = love.system.getModel
|
||||||
|
local previousIO = _G.io
|
||||||
|
love.system.getOS = function() return "OS X" end
|
||||||
|
love.system.getModel = nil
|
||||||
|
_G.io = {
|
||||||
|
popen = function()
|
||||||
|
return {
|
||||||
|
read = function() return "MacBookPro18,3" end,
|
||||||
|
close = function() end,
|
||||||
|
}
|
||||||
|
end,
|
||||||
|
}
|
||||||
|
local desktopInfo = IssueReport.metadata({}, { mods = {} })
|
||||||
|
check(desktopInfo.device == "MacBookPro18,3",
|
||||||
|
"report finds desktop device model when LOVE has no model")
|
||||||
|
love.system.getOS = function() return "UWP" end
|
||||||
|
local xboxInfo = IssueReport.metadata({}, { mods = {} })
|
||||||
|
check(xboxInfo.os == "Xbox", "report maps the Xbox runtime platform")
|
||||||
|
love.system.getOS = previousOS
|
||||||
|
love.system.getModel = previousModel
|
||||||
|
_G.io = previousIO
|
||||||
|
|
||||||
|
local opened = IssueReport.open({ safeMode = false }, {
|
||||||
|
version = "red",
|
||||||
|
mods = {},
|
||||||
|
})
|
||||||
|
check(opened and openedURL and openedURL:find("title=bug%3A%20replace%20this%20with%20a%20meaningful%20title", 1, true) ~= nil,
|
||||||
|
"report action opens the generated URL")
|
||||||
|
|
||||||
|
_G.love = previousLove
|
||||||
|
|
||||||
|
S.finish()
|
||||||
Reference in New Issue
Block a user