Merge pull request #1546 from 1Jamie/feat/android-exit-game-to-launcher

feat(android): add adaptive icons, dynamic shortcuts, in-process hot-swap, and exit-to-launcher
This commit is contained in:
bryanthaboi
2026-08-19 05:59:34 -04:00
committed by GitHub
45 changed files with 795 additions and 19 deletions
@@ -0,0 +1,80 @@
-- Test returning to launcher from Gen 1 and Gen 2 on Android without closing the process
-- luajit tests/engine/android_exit_to_launcher_test.lua
package.path = "./?.lua;./?/init.lua;" .. package.path
local T = require("tests.harness")
local check, eq = T.check, T.eq
love = love or require("tests.love_stub")
local TitleState = require("src.ui.TitleState")
local Gen2MainMenu = require("src.ui.gen2.MainMenu")
local Runtime = require("src.mods.Runtime")
-- 1. Test Gen 1 TitleState onExit callback support
do
local exitCalled = false
local dummyGame = {
data = { field = {} },
stack = {
states = {},
push = function(self, state) table.insert(self.states, state) end,
top = function(self) return self.states[#self.states] end,
pop = function(self) return table.remove(self.states) end,
},
}
local state = TitleState.new(dummyGame, {
onExit = function()
exitCalled = true
end,
})
state:openMenu()
local menu = dummyGame.stack:top()
check(menu ~= nil, "TitleState:openMenu opens a menu")
local exitItem = nil
for _, item in ipairs(menu.items or {}) do
if tostring(item.label):find("EXIT", 1, true) then
exitItem = item
break
end
end
check(exitItem ~= nil, "Gen 1 TitleState menu contains an EXIT GAME item")
if exitItem and exitItem.onSelect then
exitItem.onSelect()
end
check(exitCalled, "Selecting EXIT GAME in Gen 1 TitleState invokes onExit callback")
end
-- 2. Test Gen 2 MainMenu onExit callback support
do
local exitCalled = false
local dummyGame2 = {
data = {},
stack = {
states = {},
push = function(self, state) table.insert(self.states, state) end,
top = function(self) return self.states[#self.states] end,
pop = function(self) return table.remove(self.states) end,
},
}
local menu = Gen2MainMenu.new(dummyGame2, {
hasSave = false,
onExit = function()
exitCalled = true
end,
})
menu:choose("exit")
check(exitCalled, "Selecting EXIT GAME in Gen 2 MainMenu invokes onExit callback")
end
-- 3. Test Runtime.reset restores NullEvents and NullHooks
do
Runtime.install({ emit = function() end }, { call = function() end }, { "err" })
check(Runtime.errors ~= nil, "Runtime has errors list after install")
Runtime.reset()
check(Runtime.errors == nil, "Runtime.reset clears errors")
check(Runtime.currentMod == nil, "Runtime.reset clears currentMod")
check(Runtime.modRequire == nil, "Runtime.reset clears modRequire")
end
T.finish("android_exit_to_launcher_test")
@@ -0,0 +1,81 @@
-- tests/engine/android_shortcuts_payload_test.lua
-- Tests Android dynamic shortcuts synchronization, launch options intent resolution,
-- and love.handlers.intent_game in-process game hot-swapping.
-- luajit tests/engine/android_shortcuts_payload_test.lua
package.path = "./?.lua;./?/init.lua;" .. package.path
local T = require("tests.harness")
local check, eq = T.check, T.eq
love = love or require("tests.love_stub")
require("main")
-- 1. Verify LaunchOptions handles getLaunchGame on Android
local LaunchOptions = require("src.core.LaunchOptions")
local savedOS = love.system and love.system.getOS
local savedGetLaunchGame = love.system and love.system.getLaunchGame
love.system = love.system or {}
love.system.getOS = function() return "Android" end
love.system.getLaunchGame = function() return "gold" end
local game, slot = LaunchOptions.resolve({})
check(game == "gold", "LaunchOptions resolves intent game from love.system.getLaunchGame on Android")
local gameCli, slotCli = LaunchOptions.resolve({ "--game=red" })
check(gameCli == "red", "CLI --game flag overrides intent game")
-- 2. Verify RomImporter.syncAndroidShortcuts ranking and 4-item cap
local RomImporter = require("src.import.RomImporter")
local originalIsReady = RomImporter.isReady
local capturedShortcuts = nil
love.system.updateShortcuts = function(versions)
capturedShortcuts = versions
return true
end
-- Mock isReady
RomImporter.isReady = function(v)
return v == "red" or v == "gold" or v == "blue" or v == "yellow"
end
local ok = RomImporter.syncAndroidShortcuts("gold")
check(ok == true, "syncAndroidShortcuts returns true on Android")
check(#capturedShortcuts == 4, "syncAndroidShortcuts caps at 4 items")
check(capturedShortcuts[1] == "gold", "activeVersion 'gold' is placed first")
-- Test with subset of ready games (e.g. only Red and Gold)
RomImporter.isReady = function(v)
return v == "red" or v == "gold"
end
capturedShortcuts = nil
RomImporter.syncAndroidShortcuts("red")
check(#capturedShortcuts == 2, "syncAndroidShortcuts only includes ready ROMs")
check(capturedShortcuts[1] == "red" and capturedShortcuts[2] == "gold", "ready ROMs correctly passed")
-- Test on non-Android platform (safe no-op)
love.system.getOS = function() return "Linux" end
capturedShortcuts = nil
local nonAndroidOk = RomImporter.syncAndroidShortcuts("red")
check(nonAndroidOk == false, "syncAndroidShortcuts safely no-ops on non-Android")
check(capturedShortcuts == nil, "no shortcuts updated on non-Android")
-- 3. Verify love.handlers.intent_game definition
check(type(love.handlers.intent_game) == "function", "main.lua defines love.handlers.intent_game")
-- Restore
RomImporter.isReady = originalIsReady
if savedOS then
love.system.getOS = savedOS
else
love.system.getOS = nil
end
love.system.getLaunchGame = savedGetLaunchGame
love.system.updateShortcuts = nil
print("8/8 checks passed (android_shortcuts_payload_test)")