fix(ios): consolidate mobile integration changes

This commit is contained in:
Adrian Castro
2026-08-03 11:09:30 +02:00
parent 21276e20d1
commit 01f68d4038
7 changed files with 119 additions and 43 deletions
+1 -1
View File
@@ -44,7 +44,7 @@ jobs:
echo "changed=true" >> "$GITHUB_OUTPUT"
exit 0
fi
if git diff --name-only "$BASE_SHA" "$HEAD_SHA" | grep -Eq '^(mobile/ios/|scripts/build_ios\.sh$|\.github/workflows/(ci|release)\.yml$)'; then
if git diff --name-only "$BASE_SHA" "$HEAD_SHA" | grep -Eq '^(mobile/ios/|scripts/build_ios\.sh$)'; then
echo "changed=true" >> "$GITHUB_OUTPUT"
else
echo "changed=false" >> "$GITHUB_OUTPUT"
+17 -12
View File
@@ -422,15 +422,9 @@ function love.touchpressed(id, x, y, dx, dy, pressure)
return TouchEditor.touchpressed(id, x, y)
end
if Importer then
-- iOS: LÖVE already synthesizes a mousepressed for the primary touch,
-- and love.mousepressed below forwards that to the Importer, so
-- forwarding here too fires every launcher button twice per tap. The
-- resulting double-present was fatal for the document picker: the
-- second sheet stole the first one's weakly-held delegate, so picking
-- a file silently did nothing. Android keeps the forward for upstream
-- parity (its SAF picker is a separate activity and tolerates the
-- re-launch).
if love.system.getOS() == "iOS" then return end
if love.system.getOS() == "iOS" then
return Importer:touchpressed(id, x, y)
end
return Importer:mousepressed(x, y, 1)
end
Game:touchpressed(id, x, y)
@@ -442,7 +436,12 @@ function love.touchmoved(id, x, y, dx, dy, pressure)
if love.system.getOS() == "iOS" then return end
return TouchEditor.touchmoved(id, x, y)
end
if Importer then return end
if Importer then
if love.system.getOS() == "iOS" then
return Importer:touchmoved(id, x, y)
end
return
end
Game:touchmoved(id, x, y)
end
@@ -452,7 +451,12 @@ function love.touchreleased(id, x, y, dx, dy, pressure)
if love.system.getOS() == "iOS" then return end
return TouchEditor.touchreleased(id, x, y)
end
if Importer then return end
if Importer then
if love.system.getOS() == "iOS" then
return Importer:touchreleased(id, x, y)
end
return
end
Game:touchreleased(id, x, y)
end
@@ -488,7 +492,8 @@ function love.mousepressed(x, y, button, istouch)
-- returns early on iOS and never forwards, so there the synthesized mouse
-- press is the ONLY event the launcher gets. Filtering istouch on both
-- killed every tap on iOS outright.
if istouch and love.system.getOS() == "Android" then return end
if istouch and (love.system.getOS() == "Android"
or love.system.getOS() == "iOS") then return end
return Importer:mousepressed(x, y, button)
end
if editorMode and EditorApp.mousepressed then
+3 -10
View File
@@ -32,7 +32,7 @@
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>${PRODUCT_NAME}</string>
<string>gen1recomp</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
@@ -44,9 +44,9 @@
<key>LSRequiresIPhoneOS</key>
<true/>
<key>LSSupportsOpeningDocumentsInPlace</key>
<false/>
<true/>
<key>UIFileSharingEnabled</key>
<false/>
<true/>
<key>UILaunchStoryboardName</key>
<string>Launch Screen</string>
<key>UIStatusBarHidden</key>
@@ -96,13 +96,6 @@
</dict>
</dict>
</array>
<!-- gen1recomp iOS: expose the app's Documents folder in the Files app /
Finder so ROMs, mod .zips, and .sav files can be dropped in without
the picker; GRBootstrap sweeps them into the LÖVE save dir. -->
<key>UIFileSharingEnabled</key>
<true/>
<key>LSSupportsOpeningDocumentsInPlace</key>
<true/>
<!-- gen1recomp iOS: Pokéwalker mod reads step counts (opt-in, in the
in-game mod manager) and converts them to Pokémon EXP. -->
<key>NSHealthShareUsageDescription</key>
+45 -2
View File
@@ -106,6 +106,47 @@ WRAP_REGISTRATION = """#ifdef LOVE_IOS
#endif
"""
WRAP_SYNC_FUNCS = """
#ifdef LOVE_IOS
static const char *gr_saveDirectory()
{
static std::string saveDirectory;
auto fs = Module::getInstance<love::filesystem::Filesystem>(Module::M_FILESYSTEM);
if (fs == nullptr)
return "";
saveDirectory = fs->getSaveDirectory();
return saveDirectory.c_str();
}
static int gr_callBridge(lua_State *L, const char *className,
const char *selector, const char *arg)
{
Class cls = objc_getClass(className);
if (cls == nullptr)
{
lua_pushboolean(L, 0);
return 1;
}
typedef signed char (*GRMsg)(Class, SEL, const char *, const char *);
signed char ok = ((GRMsg)objc_msgSend)(cls, sel_registerName(selector),
arg, gr_saveDirectory());
lua_pushboolean(L, ok != 0);
return 1;
}
int w_syncHealthSteps(lua_State *L)
{
return gr_callBridge(L, "GRHealthBridge", "syncStepsWithCommand:saveDir:", "sync");
}
#endif
"""
WRAP_SYNC_REGISTRATION = """#ifdef LOVE_IOS
{ "syncHealthSteps", w_syncHealthSteps },
#endif
"""
# Deterministic 24-hex-digit object IDs, chosen not to collide with the
# upstream project (grep-verified against love-11.5's pbxproj).
ID_FILE_PICKER = "6E1AC0DE0001000000000001"
@@ -174,11 +215,13 @@ def patch_wrap_system():
anchor = "static const luaL_Reg functions[] ="
if anchor not in text:
fail(f"anchor not found in {WRAP_SYSTEM}")
text = text.replace(anchor, WRAP_FUNCS + anchor, 1)
has_native_picker = re.search(r"\bint w_pickFile\s*\(", text) is not None
text = text.replace(anchor, (WRAP_SYNC_FUNCS if has_native_picker else WRAP_FUNCS) + anchor, 1)
reg_anchor = '\t{ "vibrate", w_vibrate },\n'
if reg_anchor not in text:
fail(f"registration anchor not found in {WRAP_SYSTEM}")
text = text.replace(reg_anchor, reg_anchor + WRAP_REGISTRATION, 1)
registration = WRAP_SYNC_REGISTRATION if has_native_picker else WRAP_REGISTRATION
text = text.replace(reg_anchor, reg_anchor + registration, 1)
WRAP_SYSTEM.write_text(text)
print("patch_love_src: wrap_System.cpp patched "
"(pickFile/createFile/syncHealthSteps)")
+4 -7
View File
@@ -218,7 +218,7 @@ apply_ios_branding() {
}
apply_ios_icon() {
local source="$ROOT/assets/logo/logo.png"
local source="$ROOT/assets/logo/gen1recomp_cover.png"
local target="$XCODE_DIR/Images.xcassets/iOS AppIcon.appiconset"
[ -f "$source" ] || fail "missing iOS icon source: $source"
[ -d "$target" ] || fail "missing iOS app icon set: $target"
@@ -614,7 +614,6 @@ run_xcodebuild() {
ONLY_ACTIVE_ARCH=NO
DISABLE_MANUAL_TARGET_ORDER_BUILD_WARNING=YES
)
if ! $DEVICE; then
# Simulator: ad-hoc signing (no certificate needed). A plain unsigned
# build would drop the entitlements file, and HealthKit refuses to run
@@ -624,9 +623,6 @@ run_xcodebuild() {
else
warn "device build: configure signing in Xcode or set DEVELOPMENT_TEAM / CODE_SIGN_IDENTITY"
if [ -n "${DEVELOPMENT_TEAM:-}" ]; then
# Automatic signing + provisioning updates lets xcodebuild register the
# bundle ID / create a development profile from the CLI, so a device
# build works without ever opening the project in Xcode.
args+=(DEVELOPMENT_TEAM="$DEVELOPMENT_TEAM"
CODE_SIGN_STYLE=Automatic
-allowProvisioningUpdates)
@@ -680,8 +676,9 @@ run_xcodebuild() {
if [ ! -d "$app" ]; then
# PRODUCT_NAME override can still leave love.app on older projects
if [ -d "$products/love.app" ]; then
app="$products/love.app"
warn "built app is love.app (PRODUCT_NAME override not applied); fusing game.love anyway"
app="$products/$APP_NAME.app"
mv "$products/love.app" "$app"
warn "renamed love.app to $APP_NAME.app"
else
warn "xcodebuild finished but no .app under $products"
find "$BUILD_DIR/Build/Products" -name '*.app' 2>/dev/null | head -20 || true
+37 -3
View File
@@ -768,6 +768,13 @@ end
-- _pollPickedFiles must stay armed so it consumes the file when it lands
-- moments later (it clears pickPending itself once something is found).
function RomImporter:focus(f)
if not f then
self._activeTouch = nil
self._pagePress = nil
self._slotPress = nil
self._modPress = nil
return
end
if not (f and self.android and self.workState ~= "working") then return end
-- SAF create-document finished: GameActivity wrote export_done.flag.
if love.filesystem.getInfo("export_done.flag", "file") then
@@ -2987,6 +2994,29 @@ function RomImporter:mousepressed(x, y, button)
end
end
function RomImporter:touchpressed(id, x, y)
if self._activeTouch ~= nil and self._activeTouch ~= id then
self._pagePress = nil
self._slotPress = nil
self._modPress = nil
self._activeTouch = nil
end
if self._activeTouch ~= nil then return end
self._activeTouch = id
self:mousepressed(x, y, 1, id)
end
function RomImporter:touchmoved(id, _, y)
if self._activeTouch ~= id then return end
self:_updateDrag(true, y)
end
function RomImporter:touchreleased(id, _, y)
if self._activeTouch == nil then return end
self:_updateDrag(false, y)
self._activeTouch = nil
end
function RomImporter:keypressed(key)
if self._rename then
if key == "backspace" then
@@ -3688,9 +3718,7 @@ function RomImporter:_pointerHold()
return true, ty
end
function RomImporter:_updateSlotDrag()
if self.android and not self.touchPollable then return end
local down, py = self:_pointerHold()
function RomImporter:_updateDrag(down, py)
py = py or self._my
local maxPage = self._pageMax or 0
@@ -3748,6 +3776,12 @@ function RomImporter:_updateSlotDrag()
end
end
function RomImporter:_updateSlotDrag()
if self.ios or (self.android and not self.touchPollable) then return end
local down, py = self:_pointerHold()
self:_updateDrag(down, py)
end
-- Mouse wheel over a game tab scrolls its save-slot list (installed onto the
-- global love.wheelmoved in new(); see the chain there). Clamped to the last
-- content extent draw computed for that version.
+12 -8
View File
@@ -135,21 +135,25 @@ contract:choose("red")
check(picks == 2,
"choose() still reopens the picker per call (#420/#442 contract, got " .. picks .. ")")
-- 7. iOS taps must survive the Android double-fire guard. love.touchpressed in
-- main.lua returns early on iOS and never forwards, so the synthesized
-- love.mousepressed is the ONLY event the launcher gets there. Filtering
-- istouch on both platforms killed every tap on iOS. The guard is Android
-- only, and this pins the asymmetry the guard depends on.
local touchForwardsToImporter = {
Android = true, -- love.touchpressed -> Importer:mousepressed
iOS = false, -- returns early; mousepressed(istouch=true) is the only path
Android = true,
iOS = true,
}
for os, forwards in pairs(touchForwardsToImporter) do
local dropSynthesized = (os == "Android")
local dropSynthesized = true
check(dropSynthesized == forwards,
os .. ": the synthesized mouse press is dropped only where touch already forwarded")
end
local touch = importer("iOS")
touch:touchpressed(101, 20, 20)
check(touch._activeTouch == 101, "iOS touch press captures the active touch")
touch:touchreleased(202, 20, 20)
check(touch._activeTouch == nil, "iOS release clears the active touch even if its id changes")
touch:touchpressed(303, 20, 20)
check(touch._activeTouch == 303, "iOS accepts the next touch after release")
touch:touchreleased(303, 20, 20)
love.system.getOS = saved.getOS
love.system.pickFile = saved.pickFile
love.filesystem.getDirectoryItems = saved.getDirectoryItems