diff --git a/conf.lua b/conf.lua index e96a18a7..08bd3583 100644 --- a/conf.lua +++ b/conf.lua @@ -49,7 +49,7 @@ function love.conf(t) t.window.minwidth = 480 t.window.minheight = 360 end - t.version = "11.5" + t.version = love._os == "iOS" and "12.0" or "11.5" t.window.vsync = 1 t.modules.joystick = true t.modules.physics = false diff --git a/mobile/ios/overlays/love-ios.plist b/mobile/ios/overlays/love-ios.plist index d4c9c52d..0d8e0f02 100644 --- a/mobile/ios/overlays/love-ios.plist +++ b/mobile/ios/overlays/love-ios.plist @@ -5,7 +5,7 @@ CFBundleDevelopmentRegion en CFBundleDisplayName - Pokemon Red + gen1recomp CFBundleDocumentTypes @@ -49,10 +49,6 @@ UILaunchStoryboardName Launch Screen - UIRequiredDeviceCapabilities - - opengles-2 - UIStatusBarHidden UIViewControllerBasedStatusBarAppearance diff --git a/scripts/build.sh b/scripts/build.sh index eb719f8b..409eebe6 100755 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -35,6 +35,7 @@ TARGET="all" NOTARY_PROFILE="notary-profile" NOTARIZE=true IOS_RELEASE=false +IOS_IPA=false say() { printf '\033[1;32m==>\033[0m %s\n' "$*"; } warn() { printf '\033[1;33mwarn:\033[0m %s\n' "$*" >&2; } @@ -48,6 +49,7 @@ while [ $# -gt 0 ]; do --notary-profile) NOTARY_PROFILE="$2"; shift ;; --no-notarize) NOTARIZE=false ;; --release) IOS_RELEASE=true ;; + --ipa) IOS_IPA=true ;; *) fail "unknown argument: $1" ;; esac shift @@ -298,6 +300,9 @@ build_android() { if [ "$VERSION_EXPLICIT" = true ]; then args+=(--version "$VERSION") fi + if [ "$IOS_IPA" = true ]; then + args+=(--ipa) + fi "$ROOT/scripts/build_android.sh" ${args[@]+"${args[@]}"} } diff --git a/scripts/build_ios.sh b/scripts/build_ios.sh index 869287d1..70311d34 100755 --- a/scripts/build_ios.sh +++ b/scripts/build_ios.sh @@ -50,7 +50,7 @@ DISPLAY_NAME="gen1recomp" # capabilities like HealthKit are involved), so a per-team default # lets anyone build without colliding with someone else's app # 4. simulator: the project default (no App ID registration involved) -BUNDLE_ID="${GEN1_BUNDLE_ID:-}" +BUNDLE_ID="${GEN1_BUNDLE_ID:-com.theboisclub.gen1recomp}" if [ -z "$BUNDLE_ID" ] && [ -f "$IOS_DIR/bundle_id.local" ]; then BUNDLE_ID="$(tr -d '[:space:]' < "$IOS_DIR/bundle_id.local")" fi @@ -65,6 +65,7 @@ DEVICE=false RELEASE=false PACKAGE_ONLY=false INSTALL=false +CREATE_IPA=false # Last resort for an incomplete source export, mirroring build_android.sh. MANIFEST_BASE_URL="${MANIFEST_BASE_URL:-https://raw.githubusercontent.com/bryanthaboi/gen1recomp/main}" MANIFESTS="" @@ -82,6 +83,7 @@ while [ $# -gt 0 ]; do --release) RELEASE=true ;; --package-only) PACKAGE_ONLY=true ;; --install) INSTALL=true ;; + --ipa) CREATE_IPA=true ;; --version) VERSION="$2"; shift ;; -h|--help) sed -n '2,24p' "$0" @@ -92,6 +94,10 @@ while [ $# -gt 0 ]; do shift done +if $CREATE_IPA; then + DEVICE=true +fi + VERSION_CODE="" if [ -n "$VERSION" ]; then if ! printf '%s' "$VERSION" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+$'; then @@ -232,6 +238,30 @@ apply_ios_branding() { cp "$OVERLAY_PLIST" "$dest" } +apply_ios_icon() { + local source="$ROOT/assets/logo/logo.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" + local entry name size + while IFS=: read -r name size; do + sips -z "$size" "$size" "$source" --out "$target/$name" >/dev/null + done <<'EOF' +icon-1024pt@1x.png:1024 +icon-29pt@1x.png:29 +icon-29pt@2x.png:58 +icon-29pt@3x.png:87 +icon-40pt@1x.png:40 +icon-40pt@2x.png:80 +icon-40pt@3x.png:120 +icon-60pt@2x.png:120 +icon-60pt@3x.png:180 +icon-76pt@1x.png:76 +icon-76pt@2x.png:152 +icon-83.5pt@2x.png:167 +EOF +} + # --------------------------------------------------------------- game.love # Every version's import manifest has to ship or that game's ROM import fails in # the built app: decodeManifest (src/import/RomImporter.lua) errors outright when @@ -326,8 +356,9 @@ pack_game_love() { # in the built app while dev, which reads the source tree, stayed green. archive_entries="$(unzip -Z1 "$LOVE_FILE")" # shellcheck disable=SC2086 # MANIFESTS is a deliberate word list - for required in tools/save-editor/App.lua tools/save-editor/Kit.lua \ - tools/save-editor/panels/Party.lua $MANIFESTS; do + for required in src/update/Boot.lua tools/save-editor/App.lua \ + tools/save-editor/Kit.lua tools/save-editor/panels/Party.lua \ + $MANIFESTS; do printf '%s\n' "$archive_entries" | grep -qx "$required" \ || fail "game.love is missing $required" done @@ -640,6 +671,7 @@ install_to_device() { # --------------------------------------------------------------- main apply_ios_branding +apply_ios_icon say "applying iOS native bridge patches (picker/Files support)" python3 "$IOS_DIR/patch_love_src.py" || fail "patch_love_src.py failed" ensure_manifests diff --git a/src/import/RomImporter.lua b/src/import/RomImporter.lua index 18f9099d..90f2aa4a 100644 --- a/src/import/RomImporter.lua +++ b/src/import/RomImporter.lua @@ -648,6 +648,8 @@ function RomImporter.new(onComplete, opts) -- Android SAF create-document: which game's SAVE FILES card should show -- "Save exported." when export_done.flag appears on focus. androidPendingExportVersion = nil, + iosPendingKind = nil, + iosPendingVersion = nil, -- Virtual pointer for handhelds / gamepads (Anbernic stock OS has no -- mouse). D-pad + left stick move it; A clicks; shoulders cycle tabs; -- right stick scrolls the save-slot / mods lists. @@ -829,7 +831,11 @@ local function resetPointerCursor(self) if not (love.mouse.isCursorSupported and love.mouse.isCursorSupported()) then return end - self.arrowCursor = self.arrowCursor or love.mouse.getSystemCursor("arrow") + if not self.arrowCursor then + local ok, cursor = pcall(love.mouse.getSystemCursor, "arrow") + if not ok then return end + self.arrowCursor = cursor + end love.mouse.setCursor(self.arrowCursor) end @@ -1015,6 +1021,14 @@ end -- which focus/Choose consumes on return. function RomImporter:chooseMod() if self.workState == "working" then return end + if self.ios and love.system.getPickedFile then + self.iosPendingKind = "mod" + if not pickFile("mod") then + self.iosPendingKind = nil + self.modNotice = { ok = false, text = "Could not open the file picker." } + end + return + end if self.android then local name = findPendingMod(true, self.pickSkip) if name then @@ -1076,6 +1090,16 @@ end -- Android mirrors ROM / mod import via love.system.pickFile("sav"). function RomImporter:chooseSaveImport(version) if self.workState == "working" then return end + if self.ios and love.system.getPickedFile then + self.iosPendingKind = "sav" + self.iosPendingVersion = version + if not pickFile("sav") then + self.iosPendingKind = nil + self.iosPendingVersion = nil + self.saveNotice[version] = { ok = false, text = "Could not open the file picker." } + end + return + end if self.android then local name = findPendingSav(true, self.pickSkip) if name then @@ -1128,7 +1152,7 @@ function RomImporter:exportSave(version) return end self.androidPendingExportVersion = version - if love.system.createFile and love.system.createFile(suggested) then + if love.system.createFile and love.system.createFile(suggested, love.filesystem.getSaveDirectory()) then self.pickPending = true self.pickTimer = 0 self.saveNotice[version] = { ok = true, @@ -1165,6 +1189,14 @@ end function RomImporter:choose(version) if self.workState == "working" then return end self.chooseVersion = version or "red" + if self.ios and love.system.getPickedFile then + self.iosPendingKind = "rom" + if not pickFile("rom") then + self.iosPendingKind = nil + self:setError("Could not open the file picker.") + end + return + end if self.android then -- Prefer a not-yet-imported .gb/.gbc already in the save dir (USB copy, or -- a fresh SAF pick). Never reuse an already-imported cart's file -- that @@ -1279,6 +1311,37 @@ end function RomImporter:update(dt) self.pulse = self.pulse + dt self:_updatePadCursor(dt) + if self.ios and love.system.getPickedFile and self.workState ~= "working" then + local path = love.system.getPickedFile() + if path then + local kind = self.iosPendingKind or "rom" + local version = self.iosPendingVersion + self.iosPendingKind = nil + self.iosPendingVersion = nil + if kind == "mod" then + self:_installMod(path) + elseif kind == "sav" then + self:_importSave(version or self:_savedropTarget(), path) + else + self:startPath(path) + end + elseif love.system.getPickError then + local errorText = love.system.getPickError() + if errorText then + local kind = self.iosPendingKind or "rom" + local version = self.iosPendingVersion or self:_savedropTarget() + self.iosPendingKind = nil + self.iosPendingVersion = nil + if kind == "mod" then + self.modNotice = { ok = false, text = errorText } + elseif kind == "sav" then + self.saveNotice[version] = { ok = false, text = errorText } + else + self:setError(errorText) + end + end + end + end self:_pollPickedFiles(dt) if self.workState ~= "working" or not self.worker then return end local started = love.timer.getTime() @@ -2523,8 +2586,11 @@ function RomImporter:draw() if self._hoverEnabled and not self._padCursorActive and love.mouse.isCursorSupported and love.mouse.isCursorSupported() then if self._anyHover then - self.handCursor = self.handCursor or love.mouse.getSystemCursor("hand") - love.mouse.setCursor(self.handCursor) + if not self.handCursor then + local ok, cursor = pcall(love.mouse.getSystemCursor, "hand") + if ok then self.handCursor = cursor end + end + if self.handCursor then love.mouse.setCursor(self.handCursor) end else resetPointerCursor(self) end diff --git a/tests/rom_importer_cursor_test.lua b/tests/rom_importer_cursor_test.lua index 21e8ad13..2f8a41f3 100644 --- a/tests/rom_importer_cursor_test.lua +++ b/tests/rom_importer_cursor_test.lua @@ -36,4 +36,13 @@ ri:play("red") eq(booted, "red", "android play still boots") eq(currentCursor, "hand", "android play leaves the cursor alone") +booted = nil +currentCursor = "hand" +ri.android = false +ri.arrowCursor = nil +love.mouse.getSystemCursor = function() error("CreateSystemCursor is not currently supported") end +ri:play("red") +eq(booted, "red", "unsupported system cursors still allow boot") +eq(currentCursor, "hand", "unsupported system cursors leave the existing cursor alone") + S.finish() diff --git a/tests/rom_importer_ios_save_export_test.lua b/tests/rom_importer_ios_save_export_test.lua new file mode 100644 index 00000000..692945b9 --- /dev/null +++ b/tests/rom_importer_ios_save_export_test.lua @@ -0,0 +1,64 @@ +package.path = "./?.lua;./?/init.lua;" .. package.path +if not _G.love then _G.love = require("tests.love_stub") end + +local S = require("tests.harness").suite("rom importer iOS save export") +local eq = S.eq +local check = S.check + +love.system = love.system or {} +local saved = { + createFile = love.system.createFile, + read = love.filesystem.read, + write = love.filesystem.write, + getSaveDirectory = love.filesystem.getSaveDirectory, +} + +local writes = {} +love.filesystem.read = function(name) + eq(name, "exports/red.sav", "export reads the generated save") + return "save-data" +end +love.filesystem.write = function(name, data) + writes[#writes + 1] = { name = name, data = data } + return true +end +love.filesystem.getSaveDirectory = function() return "/tmp/gen1recomp-save" end + +local createCalls = {} +love.system.createFile = function(name, saveDirectory) + createCalls[#createCalls + 1] = { name = name, saveDirectory = saveDirectory } + return true +end + +package.loaded["src.import.SaveFileIO"] = { + exportActiveSlot = function(version) + eq(version, "red", "exports the selected game save") + return true, "exports/red.sav" + end, +} + +local RomImporter = require("src.import.RomImporter") +local ri = setmetatable({ + android = true, + ios = true, + workState = nil, + saveNotice = {}, +}, RomImporter) + +ri:exportSave("red") + +eq(#writes, 1, "stages the save for iOS export") +eq(writes[1].name, "pending_export.sav", "uses the native bridge staging name") +eq(writes[1].data, "save-data", "preserves the exported save data") +eq(#createCalls, 1, "opens the system destination picker") +eq(createCalls[1].name, "red.sav", "suggests the original save name") +eq(createCalls[1].saveDirectory, "/tmp/gen1recomp-save", "passes the exact staging directory") +check(ri.saveNotice.red.ok, "reports that the destination picker opened") + +love.system.createFile = saved.createFile +love.filesystem.read = saved.read +love.filesystem.write = saved.write +love.filesystem.getSaveDirectory = saved.getSaveDirectory +package.loaded["src.import.SaveFileIO"] = nil + +S.finish()