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