From 9ab80adaca88e0e6625d221e13783a771d0e4613 Mon Sep 17 00:00:00 2001 From: Adrian Castro <22133246+castdrian@users.noreply.github.com> Date: Wed, 19 Aug 2026 14:37:27 +0200 Subject: [PATCH 1/5] feat(launcher): add bug tab and native device reporting --- mobile/ios/native/GRBootstrap.m | 21 ++++ mobile/ios/patch_love_src.py | 29 +++++ src/core/IssueReport.lua | 99 ++++++++++++++--- src/import/LauncherSettings.lua | 27 ----- src/import/LauncherView.lua | 101 +++++++++++++++++- src/import/RomImporter.lua | 52 +++++---- .../ios_required_import_picker_test.lua | 8 ++ tests/engine/launcher_skins_tab.lua | 21 ++++ tests/engine/safe_mode_issue_report.lua | 20 +++- 9 files changed, 311 insertions(+), 67 deletions(-) diff --git a/mobile/ios/native/GRBootstrap.m b/mobile/ios/native/GRBootstrap.m index de7a59e8..f41439a4 100644 --- a/mobile/ios/native/GRBootstrap.m +++ b/mobile/ios/native/GRBootstrap.m @@ -6,6 +6,27 @@ // Registered from a constructor so no LÖVE/SDL source needs to know about it. #import +#import + +@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)) static void GRBootstrapInstall(void) diff --git a/mobile/ios/patch_love_src.py b/mobile/ios/patch_love_src.py index 4f715c7c..837210d8 100644 --- a/mobile/ios/patch_love_src.py +++ b/mobile/ios/patch_love_src.py @@ -156,6 +156,7 @@ int w_syncHealthSteps(lua_State *L) """ % MARKER WRAP_REGISTRATION = """#ifdef LOVE_IOS + { "getDeviceModel", w_getDeviceModel }, { "pickFile", w_pickFile }, { "pickFileKinds", w_pickFileKinds }, { "createFile", w_createFile }, @@ -202,6 +203,7 @@ int w_syncHealthSteps(lua_State *L) """ WRAP_SYNC_REGISTRATION = """#ifdef LOVE_IOS + { "getDeviceModel", w_getDeviceModel }, { "syncHealthSteps", w_syncHealthSteps }, { "httpDownload", w_httpDownload }, { "httpRequest", w_httpRequest }, @@ -210,6 +212,33 @@ WRAP_SYNC_REGISTRATION = """#ifdef LOVE_IOS BRIDGE_EXTRA_FUNCS = """ #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) { const char *url = luaL_checkstring(L, 1); diff --git a/src/core/IssueReport.lua b/src/core/IssueReport.lua index 910c1f74..4f63ffe2 100644 --- a/src/core/IssueReport.lua +++ b/src/core/IssueReport.lua @@ -6,6 +6,49 @@ local IssueReport = {} local FORM_URL = "https://github.com/bryanthaboi/gen1recomp/issues/new" 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) if value == nil then return nil end local text = tostring(value):gsub("^%s+", ""):gsub("%s+$", "") @@ -36,6 +79,16 @@ local function commandValue(command) return clean(value) 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 text = tostring(value or "") return (text:gsub("([^%w%-_%.~])", function(char) @@ -66,6 +119,25 @@ local function loveVersion() return result 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 version = clean(Version.engine) 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 local function deviceModel(rawOS, system) - local model = clean(call(system.getModel)) - if model then return model end + local nativeModel = clean(call(system.getDeviceModel)) + if nativeModel then return friendlyModel(nativeModel) end 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 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 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") + 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")) end if rawOS == "Android" then - return commandValue("getprop ro.product.model 2>/dev/null") + return friendlyModel(commandValue("getprop ro.product.model 2>/dev/null")) end return nil end @@ -121,7 +198,7 @@ local function metadata(options, context) 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 renderer, rendererVersion = call(graphics.getRendererInfo) local width, height = call(graphics.getDimensions) local pixelWidth, pixelHeight = call(graphics.getPixelDimensions) 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 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) + add("Device", model) local rendererDetails = clean(renderer) if rendererDetails and clean(rendererVersion) then rendererDetails = rendererDetails .. " " .. clean(rendererVersion) diff --git a/src/import/LauncherSettings.lua b/src/import/LauncherSettings.lua index a73d7eed..dc14a5c1 100644 --- a/src/import/LauncherSettings.lua +++ b/src/import/LauncherSettings.lua @@ -541,29 +541,6 @@ local function modRows(opts, mod) return rows 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) -- -- 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 } end end - sections[#sections + 1] = { - title = Strings("TROUBLESHOOTING"), - rows = troubleshootingRows(opts, hooks), - } return { opts = opts, version = version, diff --git a/src/import/LauncherView.lua b/src/import/LauncherView.lua index 32593ab8..1a76fe69 100644 --- a/src/import/LauncherView.lua +++ b/src/import/LauncherView.lua @@ -875,6 +875,35 @@ local function drawSyncGlyph(x, y, w, h, hot) byy + head) love.graphics.pop() 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 local function drawCross(x, y, size, color) @@ -926,10 +955,13 @@ local HEADER_TABS = { { id = "mods", key = "tab-mods" }, { id = "find", key = "tab-find" }, { id = "skins", key = "tab-skins", glyph = true }, + { id = "bug", key = "tab-bug", glyph = true }, } for _, t in ipairs(HEADER_TABS) do 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 -- 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 local noticeText, noticeCol 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 noticeText = imp.modNotice.text 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 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) imp:_ensureFind() imp:_ensureMods() @@ -4822,6 +4917,8 @@ function LauncherView.draw(imp) contentH = buildFindPanel(imp, x, py, panelW, budgetH, m) elseif imp.tab == "skins" then contentH = buildSkinsPanel(imp, x, py, panelW, budgetH, m) + elseif imp.tab == "bug" then + contentH = buildBugPanel(imp, x, py, panelW, budgetH, m) else contentH = buildGamePanel(imp, x, py, panelW, availH, m, imp.tab, budgetH) end diff --git a/src/import/RomImporter.lua b/src/import/RomImporter.lua index 8b1e3bea..a06aa5f5 100644 --- a/src/import/RomImporter.lua +++ b/src/import/RomImporter.lua @@ -1266,6 +1266,7 @@ end function RomImporter:_applyLastVersionTab() local okLO, LO = pcall(require, "src.core.LaunchOptions") if okLO and LO.pendingTab then return end + if os.getenv("POKEPORT_LAUNCHER_TAB") then return end local okOpt, opts = pcall(function() return require("src.core.SaveData").loadOptions() end) @@ -1337,7 +1338,7 @@ function RomImporter.new(onComplete, opts) -- player at least arrives on the tab they asked for (src/core/LaunchOptions). tab = (function() 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)(), logo = love.graphics.newImage("assets/logo/logo.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 }. -- requiredImportNotice stays inside the imported-files modal so validation -- 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 = -- every game). Rows resolve their enable-state and their "runs here" -- verdict against it (src/mods/ModTargets.lua). @@ -2755,7 +2757,7 @@ function RomImporter:resumeAfterOverlay() end 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 for i, id in ipairs(order) do 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). local hooks = {} local version = self.tab - hooks.reportIssue = function(opts) - return self:_reportIssue(opts, version) - end if self.onEditTouchControls then local version = self.tab hooks.editTouchControls = function() @@ -3655,7 +3654,6 @@ function RomImporter:_openSettings() end) if ok and model then self._settings = model - self._settingsSafeModeAtOpen = require("src.core.SaveData").isSafeMode(model.opts) end end @@ -3670,22 +3668,36 @@ function RomImporter:_closeSettings() 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._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 function RomImporter:_reportIssue(options, version) + self.issueNotice = nil local ok, IssueReport = pcall(require, "src.core.IssueReport") 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 end local opened, url, reason = IssueReport.open(options, { @@ -3693,11 +3705,11 @@ function RomImporter:_reportIssue(options, version) mods = self.mods, }) 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 end 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 end @@ -4180,7 +4192,7 @@ end -- Enabling an experimental mod arms a confirmation for that same game. 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." } + self.modNotice = { ok = false, text = "Safe mode is active. Turn it off in the Bug tab to change mods." } return end local LauncherMods = require("src.mods.LauncherMods") @@ -4224,7 +4236,7 @@ end -- recovery action, and Delete is the only destructive one on this panel. 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." } + self.modNotice = { ok = false, text = "Safe mode is active. Turn it off in the Bug tab to change mods." } return end local LauncherMods = require("src.mods.LauncherMods") diff --git a/tests/engine/ios_required_import_picker_test.lua b/tests/engine/ios_required_import_picker_test.lua index 56a13b1c..db66b9c1 100644 --- a/tests/engine/ios_required_import_picker_test.lua +++ b/tests/engine/ios_required_import_picker_test.lua @@ -29,4 +29,12 @@ check(patch:find("int w_pickFileKinds", 1, true) check(patch:find('("GRPickerBridge.swift", ID_FILE_PICKER', 1, true), "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") diff --git a/tests/engine/launcher_skins_tab.lua b/tests/engine/launcher_skins_tab.lua index 3c69039e..154b4af5 100644 --- a/tests/engine/launcher_skins_tab.lua +++ b/tests/engine/launcher_skins_tab.lua @@ -50,20 +50,34 @@ local imp = read("src/import/RomImporter.lua") check(view:find('id = "skins"', 1, true) ~= nil, "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, "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 local order = view:match("local HEADER_TABS = %{(.-)%}\n") check(order ~= nil, "HEADER_TABS found") if order then local findAt = order:find('id = "find"', 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, "the skins tab sits immediately after Find") + check(skinsAt and bugAt and bugAt > skinsAt, + "the bug tab sits after Skins") end check(view:find('imp.tab == "skins"', 1, true) ~= nil, "the panel dispatch routes the skins tab") 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 check(view:find("if imp.onOpenSkinStudio then", 1, true) ~= nil, "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 = %{(.-)%}") check(cycle and cycle:find('"skins"', 1, true) ~= nil, "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") check(switch and switch:find("_ensureSkins(true)", 1, true) ~= nil, "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") diff --git a/tests/engine/safe_mode_issue_report.lua b/tests/engine/safe_mode_issue_report.lua index 428bbcd8..64154df5 100644 --- a/tests/engine/safe_mode_issue_report.lua +++ b/tests/engine/safe_mode_issue_report.lua @@ -41,7 +41,8 @@ _G.love = { getVersion = function() return 12, 0, 0, "Mysterious Mysteries" end, system = { 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, }, graphics = { @@ -78,10 +79,13 @@ check(not url:find("game=", 1, true) 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 +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("Safe mode: on", 1, true) ~= nil, "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), "report metadata omits unknown values") 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 previousModel = love.system.getModel +local previousDeviceModel = love.system.getDeviceModel local previousIO = _G.io love.system.getOS = function() return "OS X" end love.system.getModel = nil +love.system.getDeviceModel = nil _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 { - read = function() return "MacBookPro18,3" end, + read = function() return output end, close = function() end, } end, } 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") 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 +love.system.getDeviceModel = previousDeviceModel _G.io = previousIO local opened = IssueReport.open({ safeMode = false }, { From a7c19be88ff9b7f63099f7b107fa9185b8376a46 Mon Sep 17 00:00:00 2001 From: Adrian Castro <22133246+castdrian@users.noreply.github.com> Date: Wed, 19 Aug 2026 15:18:54 +0200 Subject: [PATCH 2/5] fix(launcher): use standard bug report icon --- assets/launcher/bug.png | Bin 0 -> 2963 bytes src/import/LauncherView.lua | 42 ++++++---------------------- tests/engine/launcher_skins_tab.lua | 4 +-- 3 files changed, 10 insertions(+), 36 deletions(-) create mode 100644 assets/launcher/bug.png diff --git a/assets/launcher/bug.png b/assets/launcher/bug.png new file mode 100644 index 0000000000000000000000000000000000000000..b435b7fd1533084beb2048acfa41a5b0b431f59c GIT binary patch literal 2963 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 Date: Wed, 19 Aug 2026 15:22:50 +0200 Subject: [PATCH 3/5] fix(launcher): use rounded bug report icon --- assets/launcher/bug.png | Bin 2963 -> 3151 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/assets/launcher/bug.png b/assets/launcher/bug.png index b435b7fd1533084beb2048acfa41a5b0b431f59c..6c7bc24d28a0dd4e4cb92dc72e097b3876f57216 100644 GIT binary patch delta 2979 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 Z@PFUe%F>J+MY{k1002ovPDHLkV1oUoq_6-0 delta 2790 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 From b27e5ab017b0ee82ef683bb6b4ccc0b8672d154b Mon Sep 17 00:00:00 2001 From: Adrian Castro <22133246+castdrian@users.noreply.github.com> Date: Wed, 19 Aug 2026 15:24:31 +0200 Subject: [PATCH 4/5] fix(launcher): simplify bug report card title --- src/import/LauncherView.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/import/LauncherView.lua b/src/import/LauncherView.lua index 3900680c..38c75357 100644 --- a/src/import/LauncherView.lua +++ b/src/import/LauncherView.lua @@ -2377,7 +2377,7 @@ local function buildBugPanel(imp, x, y, w, availH, m) 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("GitHub bug form"), textX, cy + pad, PAL.heading) + Kit.text("small", Strings("Something not working?"), textX, cy + pad, PAL.heading) Kit.textWrapped("small", reportDetail, textX, cy + pad + Kit.textHeight("small") + math.floor(4 * m.s), reportTextW, PAL.muted, 3) From 7d9e99ea1863818fb48d7c1499d7ea008a68129d Mon Sep 17 00:00:00 2001 From: Adrian Castro <22133246+castdrian@users.noreply.github.com> Date: Wed, 19 Aug 2026 15:59:42 +0200 Subject: [PATCH 5/5] chore(repo): add code owners --- .github/CODEOWNERS | 1 + 1 file changed, 1 insertion(+) create mode 100644 .github/CODEOWNERS diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 00000000..7f9a1ae7 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1 @@ +* @bryanthaboi