mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-20 12:40:21 +02:00
feat(launcher): add bug tab and native device reporting
This commit is contained in:
@@ -6,6 +6,27 @@
|
|||||||
// Registered from a constructor so no LÖVE/SDL source needs to know about it.
|
// Registered from a constructor so no LÖVE/SDL source needs to know about it.
|
||||||
|
|
||||||
#import <UIKit/UIKit.h>
|
#import <UIKit/UIKit.h>
|
||||||
|
#import <sys/utsname.h>
|
||||||
|
|
||||||
|
@interface GRDeviceBridge : NSObject
|
||||||
|
+ (NSString *)deviceModel;
|
||||||
|
@end
|
||||||
|
|
||||||
|
@implementation GRDeviceBridge
|
||||||
|
+ (NSString *)deviceModel
|
||||||
|
{
|
||||||
|
#if TARGET_OS_SIMULATOR
|
||||||
|
NSString *simulatorModel = NSProcessInfo.processInfo.environment[@"SIMULATOR_MODEL_IDENTIFIER"];
|
||||||
|
if (simulatorModel.length > 0) return simulatorModel;
|
||||||
|
#endif
|
||||||
|
struct utsname systemInfo;
|
||||||
|
if (uname(&systemInfo) == 0) {
|
||||||
|
NSString *model = [NSString stringWithUTF8String:systemInfo.machine];
|
||||||
|
if (model.length > 0) return model;
|
||||||
|
}
|
||||||
|
return @"";
|
||||||
|
}
|
||||||
|
@end
|
||||||
|
|
||||||
__attribute__((constructor))
|
__attribute__((constructor))
|
||||||
static void GRBootstrapInstall(void)
|
static void GRBootstrapInstall(void)
|
||||||
|
|||||||
@@ -156,6 +156,7 @@ int w_syncHealthSteps(lua_State *L)
|
|||||||
""" % MARKER
|
""" % MARKER
|
||||||
|
|
||||||
WRAP_REGISTRATION = """#ifdef LOVE_IOS
|
WRAP_REGISTRATION = """#ifdef LOVE_IOS
|
||||||
|
{ "getDeviceModel", w_getDeviceModel },
|
||||||
{ "pickFile", w_pickFile },
|
{ "pickFile", w_pickFile },
|
||||||
{ "pickFileKinds", w_pickFileKinds },
|
{ "pickFileKinds", w_pickFileKinds },
|
||||||
{ "createFile", w_createFile },
|
{ "createFile", w_createFile },
|
||||||
@@ -202,6 +203,7 @@ int w_syncHealthSteps(lua_State *L)
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
WRAP_SYNC_REGISTRATION = """#ifdef LOVE_IOS
|
WRAP_SYNC_REGISTRATION = """#ifdef LOVE_IOS
|
||||||
|
{ "getDeviceModel", w_getDeviceModel },
|
||||||
{ "syncHealthSteps", w_syncHealthSteps },
|
{ "syncHealthSteps", w_syncHealthSteps },
|
||||||
{ "httpDownload", w_httpDownload },
|
{ "httpDownload", w_httpDownload },
|
||||||
{ "httpRequest", w_httpRequest },
|
{ "httpRequest", w_httpRequest },
|
||||||
@@ -210,6 +212,33 @@ WRAP_SYNC_REGISTRATION = """#ifdef LOVE_IOS
|
|||||||
|
|
||||||
BRIDGE_EXTRA_FUNCS = """
|
BRIDGE_EXTRA_FUNCS = """
|
||||||
#ifdef LOVE_IOS
|
#ifdef LOVE_IOS
|
||||||
|
int w_getDeviceModel(lua_State *L)
|
||||||
|
{
|
||||||
|
Class cls = objc_getClass("GRDeviceBridge");
|
||||||
|
if (cls == nullptr)
|
||||||
|
{
|
||||||
|
lua_pushnil(L);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
typedef id (*GRObj)(Class, SEL);
|
||||||
|
id value = ((GRObj)objc_msgSend)(cls, sel_registerName("deviceModel"));
|
||||||
|
if (value == nullptr)
|
||||||
|
{
|
||||||
|
lua_pushnil(L);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
typedef const char *(*GRUTF8)(id, SEL);
|
||||||
|
const char *bytes = ((GRUTF8)objc_msgSend)(value,
|
||||||
|
sel_registerName("UTF8String"));
|
||||||
|
if (bytes == nullptr || bytes[0] == '\\0')
|
||||||
|
{
|
||||||
|
lua_pushnil(L);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
lua_pushstring(L, bytes);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
int w_httpDownload(lua_State *L)
|
int w_httpDownload(lua_State *L)
|
||||||
{
|
{
|
||||||
const char *url = luaL_checkstring(L, 1);
|
const char *url = luaL_checkstring(L, 1);
|
||||||
|
|||||||
+86
-13
@@ -6,6 +6,49 @@ local IssueReport = {}
|
|||||||
local FORM_URL = "https://github.com/bryanthaboi/gen1recomp/issues/new"
|
local FORM_URL = "https://github.com/bryanthaboi/gen1recomp/issues/new"
|
||||||
local TEMPLATE = "bug_report.yml"
|
local TEMPLATE = "bug_report.yml"
|
||||||
|
|
||||||
|
local APPLE_MODELS = {
|
||||||
|
["iPhone14,2"] = "iPhone 13 Pro",
|
||||||
|
["iPhone14,3"] = "iPhone 13 Pro Max",
|
||||||
|
["iPhone14,4"] = "iPhone 13 mini",
|
||||||
|
["iPhone14,5"] = "iPhone 13",
|
||||||
|
["iPhone14,7"] = "iPhone 14",
|
||||||
|
["iPhone14,8"] = "iPhone 14 Plus",
|
||||||
|
["iPhone15,2"] = "iPhone 14 Pro",
|
||||||
|
["iPhone15,3"] = "iPhone 14 Pro Max",
|
||||||
|
["iPhone15,4"] = "iPhone 15",
|
||||||
|
["iPhone15,5"] = "iPhone 15 Plus",
|
||||||
|
["iPhone16,1"] = "iPhone 15 Pro",
|
||||||
|
["iPhone16,2"] = "iPhone 15 Pro Max",
|
||||||
|
["iPhone17,1"] = "iPhone 16 Pro",
|
||||||
|
["iPhone17,2"] = "iPhone 16 Pro Max",
|
||||||
|
["iPhone17,3"] = "iPhone 16",
|
||||||
|
["iPhone17,4"] = "iPhone 16 Plus",
|
||||||
|
["iPhone17,5"] = "iPhone 16e",
|
||||||
|
["Mac14,2"] = "MacBook Air (13-inch, M2)",
|
||||||
|
["Mac14,3"] = "Mac mini (M2)",
|
||||||
|
["Mac14,5"] = "MacBook Pro (14-inch, M2 Max)",
|
||||||
|
["Mac14,6"] = "MacBook Pro (16-inch, M2 Max)",
|
||||||
|
["Mac14,7"] = "MacBook Pro (13-inch, M2)",
|
||||||
|
["Mac14,9"] = "MacBook Pro (14-inch, M2 Pro)",
|
||||||
|
["Mac14,10"] = "MacBook Pro (16-inch, M2 Pro)",
|
||||||
|
["Mac14,12"] = "Mac mini (M2 Pro)",
|
||||||
|
["Mac14,13"] = "Mac Studio (M2 Max)",
|
||||||
|
["Mac14,14"] = "Mac Studio (M2 Ultra)",
|
||||||
|
["Mac14,15"] = "MacBook Air (15-inch, M2)",
|
||||||
|
["Mac15,3"] = "MacBook Pro (14-inch, M3)",
|
||||||
|
["Mac15,6"] = "MacBook Pro (14-inch, M3 Pro)",
|
||||||
|
["Mac15,7"] = "MacBook Pro (16-inch, M3 Pro)",
|
||||||
|
["Mac15,12"] = "MacBook Air (13-inch, M3)",
|
||||||
|
["Mac15,13"] = "MacBook Air (15-inch, M3)",
|
||||||
|
["Mac16,1"] = "MacBook Pro (14-inch, M4)",
|
||||||
|
["Mac16,5"] = "MacBook Pro (16-inch, M4 Max)",
|
||||||
|
["Mac16,6"] = "MacBook Pro (14-inch, M4 Max)",
|
||||||
|
["Mac16,7"] = "MacBook Pro (16-inch, M4 Pro)",
|
||||||
|
["Mac16,8"] = "MacBook Pro (14-inch, M4 Pro)",
|
||||||
|
["Mac16,10"] = "Mac mini (M4)",
|
||||||
|
["Mac16,11"] = "Mac mini (M4 Pro)",
|
||||||
|
}
|
||||||
|
|
||||||
local function clean(value)
|
local function clean(value)
|
||||||
if value == nil then return nil end
|
if value == nil then return nil end
|
||||||
local text = tostring(value):gsub("^%s+", ""):gsub("%s+$", "")
|
local text = tostring(value):gsub("^%s+", ""):gsub("%s+$", "")
|
||||||
@@ -36,6 +79,16 @@ local function commandValue(command)
|
|||||||
return clean(value)
|
return clean(value)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
local function commandText(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, "*a")
|
||||||
|
pcall(pipe.close, pipe)
|
||||||
|
if not readOK then return nil end
|
||||||
|
return clean(value)
|
||||||
|
end
|
||||||
|
|
||||||
local function percentEncode(value)
|
local function percentEncode(value)
|
||||||
local text = tostring(value or "")
|
local text = tostring(value or "")
|
||||||
return (text:gsub("([^%w%-_%.~])", function(char)
|
return (text:gsub("([^%w%-_%.~])", function(char)
|
||||||
@@ -66,6 +119,25 @@ local function loveVersion()
|
|||||||
return result
|
return result
|
||||||
end
|
end
|
||||||
|
|
||||||
|
local function friendlyModel(identifier)
|
||||||
|
identifier = clean(identifier)
|
||||||
|
if not identifier then return nil end
|
||||||
|
return APPLE_MODELS[identifier] or identifier
|
||||||
|
end
|
||||||
|
|
||||||
|
local function macModel()
|
||||||
|
local details = commandText("system_profiler SPHardwareDataType 2>/dev/null")
|
||||||
|
if details then
|
||||||
|
local name = clean(details:match("Model Name:%s*([^\r\n]+)"))
|
||||||
|
local chip = clean(details:match("Chip:%s*([^\r\n]+)"))
|
||||||
|
if name and chip and not name:find(chip, 1, true) then
|
||||||
|
return name .. " (" .. chip .. ")"
|
||||||
|
end
|
||||||
|
if name then return name end
|
||||||
|
end
|
||||||
|
return friendlyModel(commandValue("sysctl -n hw.model 2>/dev/null"))
|
||||||
|
end
|
||||||
|
|
||||||
local function appVersion()
|
local function appVersion()
|
||||||
local version = clean(Version.engine)
|
local version = clean(Version.engine)
|
||||||
if not version or version == "0.0.0" or version == "0.0.0-dev" then return "" end
|
if not version or version == "0.0.0" or version == "0.0.0-dev" then return "" end
|
||||||
@@ -73,20 +145,25 @@ local function appVersion()
|
|||||||
end
|
end
|
||||||
|
|
||||||
local function deviceModel(rawOS, system)
|
local function deviceModel(rawOS, system)
|
||||||
local model = clean(call(system.getModel))
|
local nativeModel = clean(call(system.getDeviceModel))
|
||||||
if model then return model end
|
if nativeModel then return friendlyModel(nativeModel) end
|
||||||
if rawOS == "OS X" or rawOS == "macOS" then
|
if rawOS == "OS X" or rawOS == "macOS" then
|
||||||
return commandValue("sysctl -n hw.model 2>/dev/null")
|
return macModel()
|
||||||
|
end
|
||||||
|
local model = clean(call(system.getModel))
|
||||||
|
if model and not model:lower():find("gpu", 1, true)
|
||||||
|
and not model:lower():find("renderer", 1, true) then
|
||||||
|
return friendlyModel(model)
|
||||||
end
|
end
|
||||||
if rawOS == "Windows" then
|
if rawOS == "Windows" then
|
||||||
return commandValue("powershell.exe -NoProfile -NonInteractive -Command \"(Get-CimInstance Win32_ComputerSystem).Model\" 2>NUL")
|
return friendlyModel(commandValue("powershell.exe -NoProfile -NonInteractive -Command \"(Get-CimInstance Win32_ComputerSystem).Model\" 2>NUL"))
|
||||||
end
|
end
|
||||||
if rawOS == "Linux" then
|
if rawOS == "Linux" then
|
||||||
return commandValue("cat /sys/devices/virtual/dmi/id/product_name 2>/dev/null")
|
return friendlyModel(commandValue("cat /sys/devices/virtual/dmi/id/product_name 2>/dev/null")
|
||||||
or commandValue("cat /sys/devices/virtual/dmi/id/model 2>/dev/null")
|
or commandValue("cat /sys/devices/virtual/dmi/id/model 2>/dev/null"))
|
||||||
end
|
end
|
||||||
if rawOS == "Android" then
|
if rawOS == "Android" then
|
||||||
return commandValue("getprop ro.product.model 2>/dev/null")
|
return friendlyModel(commandValue("getprop ro.product.model 2>/dev/null"))
|
||||||
end
|
end
|
||||||
return nil
|
return nil
|
||||||
end
|
end
|
||||||
@@ -121,7 +198,7 @@ local function metadata(options, context)
|
|||||||
local window = love and love.window or {}
|
local window = love and love.window or {}
|
||||||
local rawOS = clean(call(system.getOS))
|
local rawOS = clean(call(system.getOS))
|
||||||
local model = deviceModel(rawOS, system)
|
local model = deviceModel(rawOS, system)
|
||||||
local renderer, rendererVersion, _, rendererDevice = call(graphics.getRendererInfo)
|
local renderer, rendererVersion = call(graphics.getRendererInfo)
|
||||||
local width, height = call(graphics.getDimensions)
|
local width, height = call(graphics.getDimensions)
|
||||||
local pixelWidth, pixelHeight = call(graphics.getPixelDimensions)
|
local pixelWidth, pixelHeight = call(graphics.getPixelDimensions)
|
||||||
local modeWidth, modeHeight, flags = call(window.getMode)
|
local modeWidth, modeHeight, flags = call(window.getMode)
|
||||||
@@ -134,11 +211,7 @@ local function metadata(options, context)
|
|||||||
if value then lines[#lines + 1] = "- " .. label .. ": " .. value end
|
if value then lines[#lines + 1] = "- " .. label .. ": " .. value end
|
||||||
end
|
end
|
||||||
add("Platform", formOS(rawOS))
|
add("Platform", formOS(rawOS))
|
||||||
local hardware = model
|
add("Device", model)
|
||||||
if rendererDevice and rendererDevice ~= model then
|
|
||||||
hardware = hardware and (hardware .. " (" .. rendererDevice .. ")") or rendererDevice
|
|
||||||
end
|
|
||||||
add("Device", hardware)
|
|
||||||
local rendererDetails = clean(renderer)
|
local rendererDetails = clean(renderer)
|
||||||
if rendererDetails and clean(rendererVersion) then
|
if rendererDetails and clean(rendererVersion) then
|
||||||
rendererDetails = rendererDetails .. " " .. clean(rendererVersion)
|
rendererDetails = rendererDetails .. " " .. clean(rendererVersion)
|
||||||
|
|||||||
@@ -541,29 +541,6 @@ local function modRows(opts, mod)
|
|||||||
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
|
||||||
@@ -744,10 +721,6 @@ 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,
|
||||||
|
|||||||
@@ -875,6 +875,35 @@ local function drawSyncGlyph(x, y, w, h, hot)
|
|||||||
byy + head)
|
byy + head)
|
||||||
love.graphics.pop()
|
love.graphics.pop()
|
||||||
end
|
end
|
||||||
|
|
||||||
|
local function drawBugGlyph(x, y, w, h, hot)
|
||||||
|
local box = math.min(w, h)
|
||||||
|
local bx = x + (w - box) / 2
|
||||||
|
local by = y + (h - box) / 2
|
||||||
|
local ink = hot and PAL.inverse or PAL.ink
|
||||||
|
local bodyW = box * 0.28
|
||||||
|
local bodyH = box * 0.46
|
||||||
|
local bodyX = bx + (box - bodyW) / 2
|
||||||
|
local bodyY = by + box * 0.30
|
||||||
|
local radius = math.max(1, box * 0.12)
|
||||||
|
Theme.fillRounded(bodyX, bodyY, bodyW, bodyH, ink, 1, radius)
|
||||||
|
Theme.fillRounded(bx + box * 0.38, by + box * 0.17,
|
||||||
|
box * 0.24, box * 0.24, ink, 1, box * 0.12)
|
||||||
|
love.graphics.push("all")
|
||||||
|
love.graphics.setColor(ink)
|
||||||
|
love.graphics.setLineWidth(math.max(1, box * 0.07))
|
||||||
|
love.graphics.setLineJoin("bevel")
|
||||||
|
for _, offset in ipairs({ 0.35, 0.50, 0.65 }) do
|
||||||
|
love.graphics.line(bodyX, by + box * offset,
|
||||||
|
bx + box * 0.12, by + box * (offset - 0.07))
|
||||||
|
love.graphics.line(bodyX + bodyW, by + box * offset,
|
||||||
|
bx + box * 0.88, by + box * (offset - 0.07))
|
||||||
|
end
|
||||||
|
love.graphics.line(bx + box * 0.44, by + box * 0.18,
|
||||||
|
bx + box * 0.30, by + box * 0.08)
|
||||||
|
love.graphics.line(bx + box * 0.56, by + box * 0.18,
|
||||||
|
bx + box * 0.70, by + box * 0.08)
|
||||||
|
love.graphics.pop()
|
||||||
end
|
end
|
||||||
|
|
||||||
local function drawCross(x, y, size, color)
|
local function drawCross(x, y, size, color)
|
||||||
@@ -926,10 +955,13 @@ local HEADER_TABS = {
|
|||||||
{ id = "mods", key = "tab-mods" },
|
{ id = "mods", key = "tab-mods" },
|
||||||
{ id = "find", key = "tab-find" },
|
{ id = "find", key = "tab-find" },
|
||||||
{ id = "skins", key = "tab-skins", glyph = true },
|
{ id = "skins", key = "tab-skins", glyph = true },
|
||||||
|
{ id = "bug", key = "tab-bug", glyph = true },
|
||||||
}
|
}
|
||||||
for _, t in ipairs(HEADER_TABS) do
|
for _, t in ipairs(HEADER_TABS) do
|
||||||
t.opts = { face = "tab", font = "tab", color = t.color, letter = t.letter }
|
t.opts = { face = "tab", font = "tab", color = t.color, letter = t.letter }
|
||||||
if t.glyph then t.opts.drawFn = drawSkinGlyph end
|
if t.glyph then
|
||||||
|
t.opts.drawFn = t.id == "bug" and drawBugGlyph or drawSkinGlyph
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Which cartridge the dropdown is showing: the open game tab, else the last
|
-- Which cartridge the dropdown is showing: the open game tab, else the last
|
||||||
@@ -1923,7 +1955,7 @@ local function buildModsPanel(imp, x, y, w, availH, m)
|
|||||||
-- notice line
|
-- notice line
|
||||||
local noticeText, noticeCol
|
local noticeText, noticeCol
|
||||||
if safeMode 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
|
noticeText, noticeCol = "Safe mode is on. All mods are disabled. Turn it off in the Bug tab to change mod toggles.", PAL.yellow
|
||||||
elseif imp.modNotice then
|
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
|
||||||
@@ -2322,6 +2354,69 @@ local function buildSkinsPanel(imp, x, y, w, availH, m)
|
|||||||
return cy + hintH - y
|
return cy + hintH - y
|
||||||
end
|
end
|
||||||
|
|
||||||
|
local function buildBugPanel(imp, x, y, w, availH, m)
|
||||||
|
local SaveData = require("src.core.SaveData")
|
||||||
|
local gap = m.gap
|
||||||
|
local pad = math.floor(16 * m.s)
|
||||||
|
local cy = y
|
||||||
|
local safeMode = imp:_safeModeEnabled()
|
||||||
|
|
||||||
|
Kit.text("button", Strings("Bug reports"), x, cy, PAL.heading)
|
||||||
|
cy = cy + Kit.textHeight("button") + gap
|
||||||
|
|
||||||
|
if imp.issueNotice then
|
||||||
|
cy = cy + Kit.textWrapped("small", imp.issueNotice.text, x, cy, w,
|
||||||
|
imp.issueNotice.ok and PAL.green or PAL.red, 2) + gap
|
||||||
|
end
|
||||||
|
|
||||||
|
local switchW = math.floor(92 * m.s)
|
||||||
|
local switchH = math.max(m.btnH, Kit.tapMin())
|
||||||
|
local detail = safeMode
|
||||||
|
and Strings("All mods are disabled and their toggles are locked until safe mode is turned off.")
|
||||||
|
or Strings("Temporarily disable every mod while you reproduce a bug.")
|
||||||
|
local textW = math.max(0, w - 2 * pad - switchW - gap)
|
||||||
|
local detailH = Kit.wrapHeight("small", detail, textW, 3)
|
||||||
|
local safeH = math.max(switchH, Kit.textHeight("small") + math.floor(4 * m.s) + detailH)
|
||||||
|
+ 2 * pad
|
||||||
|
Kit.card(x, cy, w, safeH)
|
||||||
|
local textX = x + pad
|
||||||
|
local textY = cy + pad
|
||||||
|
Kit.text("small", Strings("Safe mode"), textX, textY, PAL.heading)
|
||||||
|
Kit.textWrapped("small", detail, textX,
|
||||||
|
textY + Kit.textHeight("small") + math.floor(4 * m.s), textW,
|
||||||
|
PAL.muted, 3)
|
||||||
|
local toggleX = x + w - pad - switchW
|
||||||
|
local toggleY = cy + math.floor((safeH - switchH) / 2)
|
||||||
|
local _, changed = Kit.toggle(toggleX, toggleY, switchW, switchH, safeMode,
|
||||||
|
"bug-safe-mode")
|
||||||
|
if changed then
|
||||||
|
queueAction(imp, "bug-safe-mode", function() imp:_toggleSafeMode() end)
|
||||||
|
end
|
||||||
|
cy = cy + safeH + gap
|
||||||
|
|
||||||
|
local reportLabel = Strings("Report a bug")
|
||||||
|
local reportW = math.min(w - 2 * pad,
|
||||||
|
Kit.textWidth("small", reportLabel) + math.floor(32 * m.s))
|
||||||
|
local reportDetail = Strings("Open GitHub with the bug form and the available system information filled in.")
|
||||||
|
local reportTextW = math.max(0, w - 2 * pad - reportW - gap)
|
||||||
|
local reportDetailH = Kit.wrapHeight("small", reportDetail, reportTextW, 3)
|
||||||
|
local reportH = math.max(m.btnH, Kit.textHeight("small") + math.floor(4 * m.s) + reportDetailH)
|
||||||
|
+ 2 * pad
|
||||||
|
Kit.card(x, cy, w, reportH)
|
||||||
|
Kit.text("small", Strings("Report an issue"), textX, cy + pad, PAL.heading)
|
||||||
|
Kit.textWrapped("small", reportDetail, textX,
|
||||||
|
cy + pad + Kit.textHeight("small") + math.floor(4 * m.s), reportTextW,
|
||||||
|
PAL.muted, 3)
|
||||||
|
btn(imp, x + w - pad - reportW,
|
||||||
|
cy + math.floor((reportH - m.btnH) / 2), reportW, m.btnH,
|
||||||
|
"bug-report", reportLabel, {
|
||||||
|
kind = "accent", font = "small",
|
||||||
|
action = function()
|
||||||
|
imp:_ensureMods()
|
||||||
|
imp:_reportIssue(SaveData.loadOptions(), nil)
|
||||||
|
end })
|
||||||
|
end
|
||||||
|
|
||||||
local function buildFindPanel(imp, x, y, w, availH, m)
|
local function buildFindPanel(imp, x, y, w, availH, m)
|
||||||
imp:_ensureFind()
|
imp:_ensureFind()
|
||||||
imp:_ensureMods()
|
imp:_ensureMods()
|
||||||
@@ -4822,6 +4917,8 @@ function LauncherView.draw(imp)
|
|||||||
contentH = buildFindPanel(imp, x, py, panelW, budgetH, m)
|
contentH = buildFindPanel(imp, x, py, panelW, budgetH, m)
|
||||||
elseif imp.tab == "skins" then
|
elseif imp.tab == "skins" then
|
||||||
contentH = buildSkinsPanel(imp, x, py, panelW, budgetH, m)
|
contentH = buildSkinsPanel(imp, x, py, panelW, budgetH, m)
|
||||||
|
elseif imp.tab == "bug" then
|
||||||
|
contentH = buildBugPanel(imp, x, py, panelW, budgetH, m)
|
||||||
else
|
else
|
||||||
contentH = buildGamePanel(imp, x, py, panelW, availH, m, imp.tab, budgetH)
|
contentH = buildGamePanel(imp, x, py, panelW, availH, m, imp.tab, budgetH)
|
||||||
end
|
end
|
||||||
|
|||||||
+32
-20
@@ -1266,6 +1266,7 @@ end
|
|||||||
function RomImporter:_applyLastVersionTab()
|
function RomImporter:_applyLastVersionTab()
|
||||||
local okLO, LO = pcall(require, "src.core.LaunchOptions")
|
local okLO, LO = pcall(require, "src.core.LaunchOptions")
|
||||||
if okLO and LO.pendingTab then return end
|
if okLO and LO.pendingTab then return end
|
||||||
|
if os.getenv("POKEPORT_LAUNCHER_TAB") then return end
|
||||||
local okOpt, opts = pcall(function()
|
local okOpt, opts = pcall(function()
|
||||||
return require("src.core.SaveData").loadOptions()
|
return require("src.core.SaveData").loadOptions()
|
||||||
end)
|
end)
|
||||||
@@ -1337,7 +1338,7 @@ function RomImporter.new(onComplete, opts)
|
|||||||
-- player at least arrives on the tab they asked for (src/core/LaunchOptions).
|
-- player at least arrives on the tab they asked for (src/core/LaunchOptions).
|
||||||
tab = (function()
|
tab = (function()
|
||||||
local okLO, LO = pcall(require, "src.core.LaunchOptions")
|
local okLO, LO = pcall(require, "src.core.LaunchOptions")
|
||||||
return (okLO and LO.pendingTab) or "red"
|
return (okLO and LO.pendingTab) or os.getenv("POKEPORT_LAUNCHER_TAB") or "red"
|
||||||
end)(),
|
end)(),
|
||||||
logo = love.graphics.newImage("assets/logo/logo.png"),
|
logo = love.graphics.newImage("assets/logo/logo.png"),
|
||||||
bcg = love.graphics.newImage("assets/logo/bcg.png"),
|
bcg = love.graphics.newImage("assets/logo/bcg.png"),
|
||||||
@@ -1364,7 +1365,8 @@ function RomImporter.new(onComplete, opts)
|
|||||||
-- in draw); modNotice is the last install/delete result { ok, text }.
|
-- in draw); modNotice is the last install/delete result { ok, text }.
|
||||||
-- requiredImportNotice stays inside the imported-files modal so validation
|
-- requiredImportNotice stays inside the imported-files modal so validation
|
||||||
-- failures are visible beside the file picker that caused them.
|
-- failures are visible beside the file picker that caused them.
|
||||||
mods = nil, modScroll = 0, modNotice = nil, requiredImportNotice = nil,
|
mods = nil, modScroll = 0, modNotice = nil, issueNotice = nil,
|
||||||
|
requiredImportNotice = nil,
|
||||||
-- Which game the MODS panel is answering for (a GameVersion id, nil =
|
-- Which game the MODS panel is answering for (a GameVersion id, nil =
|
||||||
-- every game). Rows resolve their enable-state and their "runs here"
|
-- every game). Rows resolve their enable-state and their "runs here"
|
||||||
-- verdict against it (src/mods/ModTargets.lua).
|
-- verdict against it (src/mods/ModTargets.lua).
|
||||||
@@ -2755,7 +2757,7 @@ function RomImporter:resumeAfterOverlay()
|
|||||||
end
|
end
|
||||||
|
|
||||||
function RomImporter:_cycleTab(delta)
|
function RomImporter:_cycleTab(delta)
|
||||||
local order = { "red", "blue", "yellow", "gold", "mods", "find", "skins" }
|
local order = { "red", "blue", "yellow", "gold", "mods", "find", "skins", "bug" }
|
||||||
local idx = 1
|
local idx = 1
|
||||||
for i, id in ipairs(order) do
|
for i, id in ipairs(order) do
|
||||||
if id == self.tab then idx = i; break end
|
if id == self.tab then idx = i; break end
|
||||||
@@ -3630,9 +3632,6 @@ function RomImporter:_openSettings()
|
|||||||
-- 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
|
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()
|
||||||
@@ -3655,7 +3654,6 @@ function RomImporter:_openSettings()
|
|||||||
end)
|
end)
|
||||||
if ok and model then
|
if ok and model then
|
||||||
self._settings = model
|
self._settings = model
|
||||||
self._settingsSafeModeAtOpen = require("src.core.SaveData").isSafeMode(model.opts)
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -3670,22 +3668,36 @@ function RomImporter:_closeSettings()
|
|||||||
local model = self._settings
|
local model = self._settings
|
||||||
if model then
|
if model then
|
||||||
model.save()
|
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
|
end
|
||||||
self._settings = nil
|
self._settings = nil
|
||||||
self._settingsSafeModeAtOpen = nil
|
end
|
||||||
|
|
||||||
|
function RomImporter:_safeModeEnabled()
|
||||||
|
if self.safeMode == nil then
|
||||||
|
local SaveData = require("src.core.SaveData")
|
||||||
|
self.safeMode = SaveData.isSafeMode(SaveData.loadOptions())
|
||||||
|
end
|
||||||
|
return self.safeMode == true
|
||||||
|
end
|
||||||
|
|
||||||
|
function RomImporter:_toggleSafeMode()
|
||||||
|
local SaveData = require("src.core.SaveData")
|
||||||
|
local options = SaveData.loadOptions()
|
||||||
|
local enabled = not SaveData.isSafeMode(options)
|
||||||
|
SaveData.setSafeMode(options, enabled)
|
||||||
|
SaveData.saveOptions(options)
|
||||||
|
self.safeMode = enabled
|
||||||
|
self.mods = nil
|
||||||
|
self._modSortCache = nil
|
||||||
|
self._modInfoFetch = nil
|
||||||
|
self.modNotice = nil
|
||||||
end
|
end
|
||||||
|
|
||||||
function RomImporter:_reportIssue(options, version)
|
function RomImporter:_reportIssue(options, version)
|
||||||
|
self.issueNotice = nil
|
||||||
local ok, IssueReport = pcall(require, "src.core.IssueReport")
|
local ok, IssueReport = pcall(require, "src.core.IssueReport")
|
||||||
if not ok then
|
if not ok then
|
||||||
self.modNotice = { ok = false, text = "Could not prepare the issue report." }
|
self.issueNotice = { ok = false, text = "Could not prepare the issue report." }
|
||||||
return false
|
return false
|
||||||
end
|
end
|
||||||
local opened, url, reason = IssueReport.open(options, {
|
local opened, url, reason = IssueReport.open(options, {
|
||||||
@@ -3693,11 +3705,11 @@ function RomImporter:_reportIssue(options, version)
|
|||||||
mods = self.mods,
|
mods = self.mods,
|
||||||
})
|
})
|
||||||
if not opened then
|
if not opened then
|
||||||
self.modNotice = { ok = false, text = reason or "Could not open the issue report." }
|
self.issueNotice = { ok = false, text = reason or "Could not open the issue report." }
|
||||||
return false
|
return false
|
||||||
end
|
end
|
||||||
self._lastIssueReportURL = url
|
self._lastIssueReportURL = url
|
||||||
if reason then self.modNotice = { ok = true, text = reason } end
|
if reason then self.issueNotice = { ok = true, text = reason } end
|
||||||
return true
|
return true
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -4180,7 +4192,7 @@ end
|
|||||||
-- 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
|
if self.safeMode then
|
||||||
self.modNotice = { ok = false, text = "Safe mode is active. Turn it off in Settings to change mods." }
|
self.modNotice = { ok = false, text = "Safe mode is active. Turn it off in the Bug tab to change mods." }
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
local LauncherMods = require("src.mods.LauncherMods")
|
local LauncherMods = require("src.mods.LauncherMods")
|
||||||
@@ -4224,7 +4236,7 @@ end
|
|||||||
-- 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
|
if self.safeMode then
|
||||||
self.modNotice = { ok = false, text = "Safe mode is active. Turn it off in Settings to change mods." }
|
self.modNotice = { ok = false, text = "Safe mode is active. Turn it off in the Bug tab to change mods." }
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
local LauncherMods = require("src.mods.LauncherMods")
|
local LauncherMods = require("src.mods.LauncherMods")
|
||||||
|
|||||||
@@ -29,4 +29,12 @@ check(patch:find("int w_pickFileKinds", 1, true)
|
|||||||
check(patch:find('("GRPickerBridge.swift", ID_FILE_PICKER', 1, true),
|
check(patch:find('("GRPickerBridge.swift", ID_FILE_PICKER', 1, true),
|
||||||
"iOS build patch compiles the required-import picker bridge")
|
"iOS build patch compiles the required-import picker bridge")
|
||||||
|
|
||||||
|
local bootstrap = read("mobile/ios/native/GRBootstrap.m")
|
||||||
|
check(bootstrap:find("struct utsname", 1, true)
|
||||||
|
and bootstrap:find("SIMULATOR_MODEL_IDENTIFIER", 1, true),
|
||||||
|
"iOS native bridge reads the hardware model on device and simulator")
|
||||||
|
check(patch:find("int w_getDeviceModel", 1, true)
|
||||||
|
and patch:find('{ "getDeviceModel", w_getDeviceModel }', 1, true),
|
||||||
|
"iOS liblove patch exposes the hardware model to Lua")
|
||||||
|
|
||||||
print("ios_required_import_picker_test: ok")
|
print("ios_required_import_picker_test: ok")
|
||||||
|
|||||||
@@ -50,20 +50,34 @@ local imp = read("src/import/RomImporter.lua")
|
|||||||
|
|
||||||
check(view:find('id = "skins"', 1, true) ~= nil,
|
check(view:find('id = "skins"', 1, true) ~= nil,
|
||||||
"LauncherView registers a skins tab")
|
"LauncherView registers a skins tab")
|
||||||
|
check(view:find('id = "bug"', 1, true) ~= nil,
|
||||||
|
"LauncherView registers a bug tab")
|
||||||
check(view:find("drawSkinGlyph", 1, true) ~= nil,
|
check(view:find("drawSkinGlyph", 1, true) ~= nil,
|
||||||
"the skins tab draws its own glyph rather than shipping an asset")
|
"the skins tab draws its own glyph rather than shipping an asset")
|
||||||
|
check(view:find("drawBugGlyph", 1, true) ~= nil,
|
||||||
|
"the bug tab draws its own glyph rather than shipping an asset")
|
||||||
-- the tab has to be next to Find, which is what the request was
|
-- the tab has to be next to Find, which is what the request was
|
||||||
local order = view:match("local HEADER_TABS = %{(.-)%}\n")
|
local order = view:match("local HEADER_TABS = %{(.-)%}\n")
|
||||||
check(order ~= nil, "HEADER_TABS found")
|
check(order ~= nil, "HEADER_TABS found")
|
||||||
if order then
|
if order then
|
||||||
local findAt = order:find('id = "find"', 1, true)
|
local findAt = order:find('id = "find"', 1, true)
|
||||||
local skinsAt = order:find('id = "skins"', 1, true)
|
local skinsAt = order:find('id = "skins"', 1, true)
|
||||||
|
local bugAt = order:find('id = "bug"', 1, true)
|
||||||
check(findAt and skinsAt and skinsAt > findAt,
|
check(findAt and skinsAt and skinsAt > findAt,
|
||||||
"the skins tab sits immediately after Find")
|
"the skins tab sits immediately after Find")
|
||||||
|
check(skinsAt and bugAt and bugAt > skinsAt,
|
||||||
|
"the bug tab sits after Skins")
|
||||||
end
|
end
|
||||||
check(view:find('imp.tab == "skins"', 1, true) ~= nil,
|
check(view:find('imp.tab == "skins"', 1, true) ~= nil,
|
||||||
"the panel dispatch routes the skins tab")
|
"the panel dispatch routes the skins tab")
|
||||||
check(view:find("buildSkinsPanel", 1, true) ~= nil, "and a panel builds it")
|
check(view:find("buildSkinsPanel", 1, true) ~= nil, "and a panel builds it")
|
||||||
|
check(view:find('imp.tab == "bug"', 1, true) ~= nil,
|
||||||
|
"the panel dispatch routes the bug tab")
|
||||||
|
check(view:find("buildBugPanel", 1, true) ~= nil, "and the bug panel builds it")
|
||||||
|
check(view:find('Kit.toggle', 1, true) ~= nil,
|
||||||
|
"the bug panel uses a switch for safe mode")
|
||||||
|
check(view:find('bug-report', 1, true) ~= nil,
|
||||||
|
"the bug panel has a report action")
|
||||||
-- the panel must not offer the studio when the host did not supply it
|
-- the panel must not offer the studio when the host did not supply it
|
||||||
check(view:find("if imp.onOpenSkinStudio then", 1, true) ~= nil,
|
check(view:find("if imp.onOpenSkinStudio then", 1, true) ~= nil,
|
||||||
"the Studio button is hidden without a host hook (mobile)")
|
"the Studio button is hidden without a host hook (mobile)")
|
||||||
@@ -79,8 +93,15 @@ check(imp:find("_installMod", 1, true) ~= nil,
|
|||||||
local cycle = imp:match("local order = %{(.-)%}")
|
local cycle = imp:match("local order = %{(.-)%}")
|
||||||
check(cycle and cycle:find('"skins"', 1, true) ~= nil,
|
check(cycle and cycle:find('"skins"', 1, true) ~= nil,
|
||||||
"shoulder-button tab cycling reaches the skins tab")
|
"shoulder-button tab cycling reaches the skins tab")
|
||||||
|
check(cycle and cycle:find('"bug"', 1, true) ~= nil,
|
||||||
|
"shoulder-button tab cycling reaches the bug tab")
|
||||||
local switch = imp:match("function RomImporter:_switchTab%(id%)(.-)\nend")
|
local switch = imp:match("function RomImporter:_switchTab%(id%)(.-)\nend")
|
||||||
check(switch and switch:find("_ensureSkins(true)", 1, true) ~= nil,
|
check(switch and switch:find("_ensureSkins(true)", 1, true) ~= nil,
|
||||||
"switching to the tab re-reads the skin list")
|
"switching to the tab re-reads the skin list")
|
||||||
|
check(imp:find('function RomImporter:_safeModeEnabled', 1, true) ~= nil
|
||||||
|
and imp:find('function RomImporter:_toggleSafeMode', 1, true) ~= nil,
|
||||||
|
"the importer owns the safe mode state")
|
||||||
|
check(imp:find('function RomImporter:_reportIssue', 1, true) ~= nil,
|
||||||
|
"the importer owns issue report opening")
|
||||||
|
|
||||||
T.finish("launcher_skins_tab")
|
T.finish("launcher_skins_tab")
|
||||||
|
|||||||
@@ -41,7 +41,8 @@ _G.love = {
|
|||||||
getVersion = function() return 12, 0, 0, "Mysterious Mysteries" end,
|
getVersion = function() return 12, 0, 0, "Mysterious Mysteries" end,
|
||||||
system = {
|
system = {
|
||||||
getOS = function() return "iOS" end,
|
getOS = function() return "iOS" end,
|
||||||
getModel = function() return "iPad Test" end,
|
getDeviceModel = function() return "iPhone16,2" end,
|
||||||
|
getModel = function() return "Apple A17 Pro GPU" end,
|
||||||
openURL = function(url) openedURL = url end,
|
openURL = function(url) openedURL = url end,
|
||||||
},
|
},
|
||||||
graphics = {
|
graphics = {
|
||||||
@@ -78,10 +79,13 @@ check(not url:find("game=", 1, true)
|
|||||||
check(fields.summary == "" and fields.location == "" and fields.screenshot == ""
|
check(fields.summary == "" and fields.location == "" and fields.screenshot == ""
|
||||||
and fields.steps == "" and fields.expected == "",
|
and fields.steps == "" and fields.expected == "",
|
||||||
"report leaves user-entered fields blank")
|
"report leaves user-entered fields blank")
|
||||||
check(info.metadata:find("Device: iPad Test", 1, true) ~= nil
|
check(info.device == "iPhone 15 Pro Max"
|
||||||
|
and info.metadata:find("Device: iPhone 15 Pro Max", 1, true) ~= nil
|
||||||
and info.metadata:find("LÖVE: 12.0.0", 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,
|
and info.metadata:find("Safe mode: on", 1, true) ~= nil,
|
||||||
"report metadata includes device and app details")
|
"report metadata includes device and app details")
|
||||||
|
check(not info.metadata:find("Simulator GPU", 1, true),
|
||||||
|
"report metadata does not mistake the renderer device for the device")
|
||||||
check(not info.metadata:find("unknown", 1, true),
|
check(not info.metadata:find("unknown", 1, true),
|
||||||
"report metadata omits unknown values")
|
"report metadata omits unknown values")
|
||||||
check(not info.metadata:find("Game id", 1, true)
|
check(not info.metadata:find("Game id", 1, true)
|
||||||
@@ -104,25 +108,31 @@ check(not developmentInfo.metadata:find("0.0.0-dev", 1, true),
|
|||||||
|
|
||||||
local previousOS = love.system.getOS
|
local previousOS = love.system.getOS
|
||||||
local previousModel = love.system.getModel
|
local previousModel = love.system.getModel
|
||||||
|
local previousDeviceModel = love.system.getDeviceModel
|
||||||
local previousIO = _G.io
|
local previousIO = _G.io
|
||||||
love.system.getOS = function() return "OS X" end
|
love.system.getOS = function() return "OS X" end
|
||||||
love.system.getModel = nil
|
love.system.getModel = nil
|
||||||
|
love.system.getDeviceModel = nil
|
||||||
_G.io = {
|
_G.io = {
|
||||||
popen = function()
|
popen = function(command)
|
||||||
|
local output = command:find("system_profiler", 1, true)
|
||||||
|
and "Hardware Overview:\n Model Name: MacBook Air\n Model Identifier: Mac14,15\n Chip: Apple M2\n"
|
||||||
|
or "Mac14,15\n"
|
||||||
return {
|
return {
|
||||||
read = function() return "MacBookPro18,3" end,
|
read = function() return output end,
|
||||||
close = function() end,
|
close = function() end,
|
||||||
}
|
}
|
||||||
end,
|
end,
|
||||||
}
|
}
|
||||||
local desktopInfo = IssueReport.metadata({}, { mods = {} })
|
local desktopInfo = IssueReport.metadata({}, { mods = {} })
|
||||||
check(desktopInfo.device == "MacBookPro18,3",
|
check(desktopInfo.device == "MacBook Air (Apple M2)",
|
||||||
"report finds desktop device model when LOVE has no model")
|
"report finds desktop device model when LOVE has no model")
|
||||||
love.system.getOS = function() return "UWP" end
|
love.system.getOS = function() return "UWP" end
|
||||||
local xboxInfo = IssueReport.metadata({}, { mods = {} })
|
local xboxInfo = IssueReport.metadata({}, { mods = {} })
|
||||||
check(xboxInfo.os == "Xbox", "report maps the Xbox runtime platform")
|
check(xboxInfo.os == "Xbox", "report maps the Xbox runtime platform")
|
||||||
love.system.getOS = previousOS
|
love.system.getOS = previousOS
|
||||||
love.system.getModel = previousModel
|
love.system.getModel = previousModel
|
||||||
|
love.system.getDeviceModel = previousDeviceModel
|
||||||
_G.io = previousIO
|
_G.io = previousIO
|
||||||
|
|
||||||
local opened = IssueReport.open({ safeMode = false }, {
|
local opened = IssueReport.open({ safeMode = false }, {
|
||||||
|
|||||||
Reference in New Issue
Block a user