diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index afc99c23..2b85e32e 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -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"
diff --git a/main.lua b/main.lua
index 4212d3b1..20488a26 100644
--- a/main.lua
+++ b/main.lua
@@ -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
diff --git a/mobile/ios/overlays/love-ios.plist b/mobile/ios/overlays/love-ios.plist
index 0d8e0f02..6d7e2b30 100644
--- a/mobile/ios/overlays/love-ios.plist
+++ b/mobile/ios/overlays/love-ios.plist
@@ -32,7 +32,7 @@
CFBundleInfoDictionaryVersion
6.0
CFBundleName
- ${PRODUCT_NAME}
+ gen1recomp
CFBundlePackageType
APPL
CFBundleShortVersionString
@@ -44,9 +44,9 @@
LSRequiresIPhoneOS
LSSupportsOpeningDocumentsInPlace
-
+
UIFileSharingEnabled
-
+
UILaunchStoryboardName
Launch Screen
UIStatusBarHidden
@@ -96,13 +96,6 @@
-
- UIFileSharingEnabled
-
- LSSupportsOpeningDocumentsInPlace
-
NSHealthShareUsageDescription
diff --git a/mobile/ios/patch_love_src.py b/mobile/ios/patch_love_src.py
index 9c0fa0ac..cf09295e 100644
--- a/mobile/ios/patch_love_src.py
+++ b/mobile/ios/patch_love_src.py
@@ -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(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)")
diff --git a/scripts/build_ios.sh b/scripts/build_ios.sh
index 7d5f1020..d3fc2d77 100755
--- a/scripts/build_ios.sh
+++ b/scripts/build_ios.sh
@@ -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
diff --git a/src/import/RomImporter.lua b/src/import/RomImporter.lua
index 49e57149..387b0c56 100644
--- a/src/import/RomImporter.lua
+++ b/src/import/RomImporter.lua
@@ -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.
diff --git a/tests/rom_importer_double_pick_test.lua b/tests/rom_importer_double_pick_test.lua
index 97b2fca0..769406f9 100644
--- a/tests/rom_importer_double_pick_test.lua
+++ b/tests/rom_importer_double_pick_test.lua
@@ -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