From 28440eb936531d8542a52413a1b289949e60bb1c Mon Sep 17 00:00:00 2001 From: Adrian Castro <22133246+castdrian@users.noreply.github.com> Date: Mon, 10 Aug 2026 20:49:47 +0200 Subject: [PATCH 1/3] fix(ios): expose runtime data in Documents --- mobile/ios/README.md | 15 +++-- mobile/ios/native/GRBootstrap.m | 3 + mobile/ios/native/GRPickerBridge.swift | 93 +++++++++++++++++++++++--- mobile/ios/patch_love_src.py | 66 +++++++++++++++++- scripts/build_ios.sh | 25 +++++++ 5 files changed, 183 insertions(+), 19 deletions(-) diff --git a/mobile/ios/README.md b/mobile/ios/README.md index e1a9f48a..e1924b20 100644 --- a/mobile/ios/README.md +++ b/mobile/ios/README.md @@ -17,8 +17,10 @@ > for picker results (iOS pickers are in-process modals, so Android's > refocus rescan never fires). > -> The note below about a missing "UIDocumentPicker handoff" is -> resolved by this bridge. +> The LÖVE save directory is patched to the public `Documents` root, so the +> Files app exposes installed mods, save slots, ROM caches, options, logs, and +> other runtime data. Existing data from the old private Application Support +> location is migrated on the next launch. macOS + Xcode only. Fetches the **LÖVE 12.0** source tree and matching Apple dependencies from the official [LÖVE source](https://github.com/love2d/love) @@ -34,9 +36,9 @@ Pin file: [`LOVE_VERSION`](./LOVE_VERSION) → `12.0`. scripts/build_ios.sh --fetch ``` -The embedded `game.love` contains no ROM or generated game data. The current -first-boot importer has desktop file pickers only, so a production iOS release -still needs a UIDocumentPicker handoff that passes the selected ROM to LÖVE. +The embedded `game.love` contains no ROM or generated game data. The native +document picker delivers user-selected ROMs, mods, and saves into the public +save directory. Default output: an unsigned Simulator `.app` under `mobile/ios/build/` (no Apple Developer account required). A convenience copy also lands under @@ -78,7 +80,7 @@ Manual out-of-band steps: | Path | Role | |------|------| | `LOVE_VERSION` | Engine pin (`12.0`) | -| `overlays/love-ios.plist` | Portrait-only Info.plist + display name **Pokemon Red** (copied over the upstream plist every build) | +| `overlays/love-ios.plist` | Info.plist with public Documents sharing and display name **Pokemon Red** (copied over the upstream plist every build) | | `love-src/` | Downloaded LÖVE 12.0 source tree (**gitignored**, do not commit) | | `cache/` | Temporary source and dependency checkout data (**gitignored**) | | `build/` | `xcodebuild` derived data (**gitignored**) | @@ -103,6 +105,7 @@ Re-run it if either dependency directory is absent. | Display name | Pokemon Red | | `PRODUCT_NAME` | PokemonRed | | Bundle ID | `com.theboisclub.pokemonred` | +| Save directory | `Documents` | | Orientations | Portrait only (`UIInterfaceOrientationPortrait`) | Overrides are applied by the build script (`xcodebuild` settings + plist overlay) diff --git a/mobile/ios/native/GRBootstrap.m b/mobile/ios/native/GRBootstrap.m index 4b56de9c..de7a59e8 100644 --- a/mobile/ios/native/GRBootstrap.m +++ b/mobile/ios/native/GRBootstrap.m @@ -16,6 +16,9 @@ static void GRBootstrapInstall(void) queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *note) { Class bridge = NSClassFromString(@"GRPickerBridge"); + if ([bridge respondsToSelector:@selector(preparePublicDocuments)]) { + [bridge performSelector:@selector(preparePublicDocuments)]; + } if ([bridge respondsToSelector:@selector(sweepInbox)]) { [bridge performSelector:@selector(sweepInbox)]; } diff --git a/mobile/ios/native/GRPickerBridge.swift b/mobile/ios/native/GRPickerBridge.swift index 1793f7c4..c571461b 100644 --- a/mobile/ios/native/GRPickerBridge.swift +++ b/mobile/ios/native/GRPickerBridge.swift @@ -26,8 +26,6 @@ public final class GRPickerBridge: NSObject { // silently doing nothing. private static var liveDelegates: [PickerDelegate] = [] - // conf.lua t.identity — where LÖVE puts the fused save directory on iOS - // (/Library/Application Support/). private static let loveIdentity = "pokemon-love2d" @objc(httpDownloadWithUrl:destination:userAgent:accept:) @@ -177,10 +175,10 @@ public final class GRPickerBridge: NSObject { // UIApplicationDidBecomeActive (see GRBootstrap.m). @objc public static func sweepInbox() { let fm = FileManager.default - guard let docs = fm.urls(for: .documentDirectory, in: .userDomainMask).first, - let appSupport = fm.urls(for: .applicationSupportDirectory, - in: .userDomainMask).first else { return } - let saveDir = appSupport.appendingPathComponent(loveIdentity, isDirectory: true) + migrateLegacySaveDirectory() + guard let docs = documentsDirectory(), + let saveDir = publicSaveDirectory() else { return } + guard docs.standardizedFileURL != saveDir.standardizedFileURL else { return } let wanted: Set = ["gb", "gbc", "zip", "sav"] guard let items = try? fm.contentsOfDirectory(at: docs, includingPropertiesForKeys: nil) else { return } @@ -197,15 +195,21 @@ public final class GRPickerBridge: NSObject { } } + @objc public static func preparePublicDocuments() { + migrateLegacySaveDirectory() + if let saveDir = publicSaveDirectory() { + ensureDirectory(saveDir) + } + } + // MARK: - Helpers private static func resolvedSaveDir(_ cstr: UnsafePointer?) -> URL? { var dir = cstr.map { String(cString: $0) } ?? "" if dir.isEmpty { - guard let appSupport = FileManager.default - .urls(for: .applicationSupportDirectory, in: .userDomainMask).first - else { return nil } - dir = appSupport.appendingPathComponent(loveIdentity).path + migrateLegacySaveDirectory() + guard let saveDir = publicSaveDirectory() else { return nil } + dir = saveDir.path } let url = URL(fileURLWithPath: dir, isDirectory: true) ensureDirectory(url) @@ -217,6 +221,75 @@ public final class GRPickerBridge: NSObject { withIntermediateDirectories: true) } + private static func documentsDirectory() -> URL? { + FileManager.default.urls(for: .documentDirectory, + in: .userDomainMask).first + } + + private static func publicSaveDirectory() -> URL? { + documentsDirectory() + } + + private static func legacySaveDirectory() -> URL? { + FileManager.default.urls(for: .applicationSupportDirectory, + in: .userDomainMask).first? + .appendingPathComponent(loveIdentity, isDirectory: true) + } + + private static func migrateLegacySaveDirectory() { + let fm = FileManager.default + guard let destination = publicSaveDirectory(), + let legacy = legacySaveDirectory(), + fm.fileExists(atPath: legacy.path) else { + return + } + ensureDirectory(destination) + mergeDirectory(from: legacy, to: destination) + try? fm.removeItem(at: legacy) + } + + private static func mergeDirectory(from source: URL, to destination: URL) { + let fm = FileManager.default + ensureDirectory(destination) + guard let items = try? fm.contentsOfDirectory(at: source, + includingPropertiesForKeys: nil) + else { return } + for item in items { + let target = destination.appendingPathComponent(item.lastPathComponent) + var sourceIsDirectory = ObjCBool(false) + fm.fileExists(atPath: item.path, isDirectory: &sourceIsDirectory) + var targetIsDirectory = ObjCBool(false) + let targetExists = fm.fileExists(atPath: target.path, + isDirectory: &targetIsDirectory) + if sourceIsDirectory.boolValue && targetExists && targetIsDirectory.boolValue { + mergeDirectory(from: item, to: target) + continue + } + if targetExists { + if !sourceIsDirectory.boolValue && !targetIsDirectory.boolValue && + fm.contentsEqual(atPath: item.path, andPath: target.path) { + try? fm.removeItem(at: item) + } else { + moveToLegacyName(item, in: destination) + } + continue + } + try? fm.moveItem(at: item, to: target) + } + } + + private static func moveToLegacyName(_ item: URL, in destination: URL) { + let fm = FileManager.default + let base = item.lastPathComponent + ".legacy" + var target = destination.appendingPathComponent(base) + var suffix = 2 + while fm.fileExists(atPath: target.path) { + target = destination.appendingPathComponent("\(base).\(suffix)") + suffix += 1 + } + try? fm.moveItem(at: item, to: target) + } + private static func copyItem(at src: URL, into dir: URL, named name: String) { let scoped = src.startAccessingSecurityScopedResource() defer { if scoped { src.stopAccessingSecurityScopedResource() } } diff --git a/mobile/ios/patch_love_src.py b/mobile/ios/patch_love_src.py index d640bee7..b6d6eb40 100644 --- a/mobile/ios/patch_love_src.py +++ b/mobile/ios/patch_love_src.py @@ -27,6 +27,8 @@ NATIVE_SRC = IOS_DIR / "native" NATIVE_DST = LOVE_SRC / "platform" / "xcode" / "ios" / "native" WRAP_SYSTEM = LOVE_SRC / "src" / "modules" / "system" / "wrap_System.cpp" PBXPROJ = LOVE_SRC / "platform" / "xcode" / "love.xcodeproj" / "project.pbxproj" +APPLE_MM = LOVE_SRC / "src" / "common" / "apple.mm" +FILESYSTEM_CPP = LOVE_SRC / "src" / "modules" / "filesystem" / "physfs" / "Filesystem.cpp" ENTITLEMENTS_SRC = IOS_DIR / "overlays" / "love-ios.entitlements" NATIVE_FILES = ("GRPickerBridge.swift", "GRHealthBridge.swift", "GRBootstrap.m") @@ -245,19 +247,20 @@ def fail(msg): sys.exit(1) -def pristine(path: Path) -> str: +def pristine(path: Path, patched_markers=None) -> str: """Text of `path` before any of our patching: backed by a `.orig` stash. The stash is only trusted if it is itself unpatched; that protects against a stash accidentally taken after an earlier patch run. """ + patched_markers = tuple(patched_markers or (MARKER, ID_FILE_PICKER)) orig = path.with_suffix(path.suffix + ".orig") if orig.is_file(): text = orig.read_text() - if MARKER not in text and ID_FILE_PICKER not in text: + if not any(marker in text for marker in patched_markers): return text text = path.read_text() - if MARKER in text or ID_FILE_PICKER in text: + if any(marker in text for marker in patched_markers): fail(f"{path} is already patched and no pristine .orig stash exists;\n" f" delete {LOVE_SRC} and re-run scripts/build_ios.sh --fetch") orig.write_text(text) @@ -299,6 +302,62 @@ def patch_wrap_system(): "(pickFile/createFile/syncHealthSteps/httpDownload)") +def patch_public_documents(): + text = pristine( + APPLE_MM, + ("#ifdef LOVE_IOS\n" + "\t\t\tnsdir = NSDocumentDirectory;\n" + "#else\n",), + ) + original = ( + "\t\tcase USER_DIRECTORY_APPSUPPORT:\n" + "\t\t\tnsdir = NSApplicationSupportDirectory;\n" + "\t\t\tbreak;" + ) + replacement = ( + "\t\tcase USER_DIRECTORY_APPSUPPORT:\n" + "#ifdef LOVE_IOS\n" + "\t\t\tnsdir = NSDocumentDirectory;\n" + "#else\n" + "\t\t\tnsdir = NSApplicationSupportDirectory;\n" + "#endif\n" + "\t\t\tbreak;" + ) + if original not in text: + fail(f"iOS app-support path anchor not found in {APPLE_MM}") + APPLE_MM.write_text(text.replace(original, replacement, 1)) + + filesystem_text = pristine( + FILESYSTEM_CPP, + ("#ifdef LOVE_IOS\n" + "\t\t\tsuffix.clear();\n" + "#else\n",), + ) + filesystem_original = ( + "\t\tstd::string suffix;\n" + "\t\tif (isFused())\n" + "\t\t\tsuffix = std::string(LOVE_PATH_SEPARATOR) + saveIdentity;\n" + "\t\telse\n" + "\t\t\tsuffix = std::string(LOVE_PATH_SEPARATOR LOVE_APPDATA_FOLDER LOVE_PATH_SEPARATOR) + saveIdentity;" + ) + filesystem_replacement = ( + "\t\tstd::string suffix;\n" + "#ifdef LOVE_IOS\n" + "\t\t\tsuffix.clear();\n" + "#else\n" + "\t\tif (isFused())\n" + "\t\t\tsuffix = std::string(LOVE_PATH_SEPARATOR) + saveIdentity;\n" + "\t\telse\n" + "\t\t\tsuffix = std::string(LOVE_PATH_SEPARATOR LOVE_APPDATA_FOLDER LOVE_PATH_SEPARATOR) + saveIdentity;\n" + "#endif" + ) + if filesystem_original not in filesystem_text: + fail(f"iOS save directory suffix anchor not found in {FILESYSTEM_CPP}") + FILESYSTEM_CPP.write_text(filesystem_text.replace(filesystem_original, + filesystem_replacement, 1)) + print("patch_love_src: iOS save directory routed to Documents root") + + def patch_pbxproj(): text = pristine(PBXPROJ) @@ -362,6 +421,7 @@ def main(): if not LOVE_SRC.is_dir(): fail("love-src/ missing; run scripts/build_ios.sh --fetch first") copy_native_files() + patch_public_documents() patch_wrap_system() patch_pbxproj() diff --git a/scripts/build_ios.sh b/scripts/build_ios.sh index c138d3f6..4c843429 100755 --- a/scripts/build_ios.sh +++ b/scripts/build_ios.sh @@ -217,6 +217,14 @@ apply_ios_branding() { cp "$OVERLAY_PLIST" "$dest" } +verify_documents_overlay() { + local sharing in_place + sharing="$(/usr/libexec/PlistBuddy -c 'Print :UIFileSharingEnabled' "$OVERLAY_PLIST" 2>/dev/null || true)" + in_place="$(/usr/libexec/PlistBuddy -c 'Print :LSSupportsOpeningDocumentsInPlace' "$OVERLAY_PLIST" 2>/dev/null || true)" + [ "$sharing" = "true" ] && [ "$in_place" = "true" ] \ + || fail "iOS plist overlay must enable UIFileSharingEnabled and LSSupportsOpeningDocumentsInPlace" +} + apply_ios_icon() { local source="$ROOT/assets/logo/gen1recomp_cover.png" local target="$XCODE_DIR/Images.xcassets/iOS AppIcon.appiconset" @@ -578,6 +586,18 @@ verify_native_bridge() { say "native bridge present (pickFile, createFile)" } +verify_documents_configuration() { + local app="$1" + local plist="$app/Info.plist" + local sharing in_place + [ -f "$plist" ] || fail "built iOS app is missing Info.plist: $plist" + sharing="$(/usr/libexec/PlistBuddy -c 'Print :UIFileSharingEnabled' "$plist" 2>/dev/null || true)" + in_place="$(/usr/libexec/PlistBuddy -c 'Print :LSSupportsOpeningDocumentsInPlace' "$plist" 2>/dev/null || true)" + [ "$sharing" = "true" ] && [ "$in_place" = "true" ] \ + || fail "built iOS app does not expose its Documents folder in $(basename "$app")" + say "public Documents exposure present (file sharing + in-place access)" +} + run_xcodebuild() { local config sdk destination if $RELEASE; then @@ -618,6 +638,8 @@ run_xcodebuild() { PRODUCT_BUNDLE_IDENTIFIER="$BUNDLE_ID" MARKETING_VERSION="$marketing_version" CURRENT_PROJECT_VERSION="$project_version" + INFOPLIST_KEY_UIFileSharingEnabled=YES + INFOPLIST_KEY_LSSupportsOpeningDocumentsInPlace=YES ONLY_ACTIVE_ARCH=NO DISABLE_MANUAL_TARGET_ORDER_BUILD_WARNING=YES ) @@ -693,6 +715,8 @@ run_xcodebuild() { fi fi + verify_documents_configuration "$app" + # Fuse even if the pbxproj wire-up failed, LÖVE runs any bundled *.love. # Byte-compare, never just existence: xcodebuild's incremental Copy Bundle # Resources can leave a previous build's game.love in a surviving .app, and @@ -774,6 +798,7 @@ install_to_device() { # --------------------------------------------------------------- main apply_ios_branding +verify_documents_overlay 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" From 5482ac590dcfb8f01ec71e638eb94756aa9ca9af Mon Sep 17 00:00:00 2001 From: Adrian Castro <22133246+castdrian@users.noreply.github.com> Date: Mon, 10 Aug 2026 22:52:33 +0200 Subject: [PATCH 2/3] fix(ios): guard missing game payload --- scripts/build_ios.sh | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/scripts/build_ios.sh b/scripts/build_ios.sh index 4c843429..3a87dce9 100755 --- a/scripts/build_ios.sh +++ b/scripts/build_ios.sh @@ -598,6 +598,13 @@ verify_documents_configuration() { say "public Documents exposure present (file sharing + in-place access)" } +verify_game_payload() { + local app="$1" + [ -s "$app/game.love" ] \ + || fail "built iOS app is missing game.love: $app" + say "game.love present ($(du -h "$app/game.love" | cut -f1))" +} + run_xcodebuild() { local config sdk destination if $RELEASE; then @@ -727,6 +734,7 @@ run_xcodebuild() { cp "$LOVE_FILE" "$app/game.love" fi + verify_game_payload "$app" verify_native_bridge "$app" local dist_dir="$DIST/${config}-${sdk}" From 41e6507665af4925d9584e37daa3eb799a89784b Mon Sep 17 00:00:00 2001 From: Adrian Castro <22133246+castdrian@users.noreply.github.com> Date: Mon, 10 Aug 2026 22:56:09 +0200 Subject: [PATCH 3/3] docs(ios): document public Documents storage --- mobile/ios/README.md | 194 +++++++++++++++++++------------------------ 1 file changed, 87 insertions(+), 107 deletions(-) diff --git a/mobile/ios/README.md b/mobile/ios/README.md index e1924b20..5ae5439c 100644 --- a/mobile/ios/README.md +++ b/mobile/ios/README.md @@ -1,133 +1,113 @@ -# iOS build (LÖVE 12.0) +# iOS build -> **Native ROM/mod/save import.** The iOS build ships a Swift -> document-picker bridge (`native/GRPickerBridge.swift` + `GRBootstrap.m`) -> that `patch_love_src.py` wires into the LÖVE tree on every build: -> -> - `love.system.pickFile("rom"|"mod"|"sav")` and `love.system.createFile` -> are exposed to Lua on iOS (same contract as love-android's SAF picker: -> picks land in the save dir as `picked_rom.gb` / `picked_mod.zip` / -> `picked_save.sav`; exports signal via `export_done.flag`). -> - The Info.plist overlay enables `UIFileSharingEnabled` + -> `LSSupportsOpeningDocumentsInPlace`, and `GRBootstrap.m` sweeps -> `.gb/.gbc/.zip/.sav` files dropped in Documents (Files app / Finder) -> into the LÖVE save dir on every activation — drop a ROM, open the app, -> and it imports with no taps. -> - `src/import/RomImporter.lua` treats iOS as a mobile platform and polls -> for picker results (iOS pickers are in-process modals, so Android's -> refocus rescan never fires). -> -> The LÖVE save directory is patched to the public `Documents` root, so the -> Files app exposes installed mods, save slots, ROM caches, options, logs, and -> other runtime data. Existing data from the old private Application Support -> location is migrated on the next launch. +This directory contains the macOS/Xcode build used to package Gen1 Recomp as +an iOS app with LÖVE 12.0. -macOS + Xcode only. Fetches the **LÖVE 12.0** source tree and matching Apple -dependencies from the official [LÖVE source](https://github.com/love2d/love) -and [Apple dependencies](https://github.com/love2d/love-apple-dependencies) -repositories. `conf.lua` declares LÖVE 12.0 on iOS and 11.5 elsewhere. +## User data location -Pin file: [`LOVE_VERSION`](./LOVE_VERSION) → `12.0`. +The app uses the public iOS Documents directory as its LÖVE save directory. +There is no `pokemon-love2d` subdirectory and the app does not create a +README file there. When browsing `On My iPhone > gen1recomp` in Files, the +directory contains the app's runtime data directly, including: -## Quick start (simulator) +- installed mods and downloaded ROMs +- save files and save-state data +- options, caches, logs, and other files created by the game + +The build enables `UIFileSharingEnabled` and +`LSSupportsOpeningDocumentsInPlace`, so the same directory is available in +Files and Finder. Files copied into the app's Documents directory are used by +the game on its next activation. + +Existing installations are migrated automatically. Files from the old +private `Application Support/pokemon-love2d` directory are merged into +Documents on launch; conflicts are retained with a `.legacy` suffix. + +## Build + +Run these commands from the repository root: ```bash -# Fetch LÖVE 12.0 iOS sources and dependencies (once) + build for Simulator scripts/build_ios.sh --fetch +scripts/build_ios.sh ``` -The embedded `game.love` contains no ROM or generated game data. The native -document picker delivers user-selected ROMs, mods, and saves into the public -save directory. +`--fetch` downloads the pinned LÖVE source and matching Apple dependencies +into the gitignored `love-src/` directory. It is only needed when that tree is +missing. The default build targets the iOS Simulator in Debug configuration. -Default output: an unsigned Simulator `.app` under `mobile/ios/build/` -(no Apple Developer account required). A convenience copy also lands under -`dist/ios/-/`. - -Install on a booted simulator (example): +For a physical device or a release build: ```bash -xcrun simctl install booted mobile/ios/build/Build/Products/Debug-iphonesimulator/PokemonRed.app -xcrun simctl launch booted com.theboisclub.pokemonred +scripts/build_ios.sh --device --install +scripts/build_ios.sh --device --release --install ``` -Or open `mobile/ios/love-src/platform/xcode/love.xcodeproj` in Xcode, -select the `love-ios` target, and Run on a Simulator after -`scripts/build_ios.sh --package-only` (or a full build) has placed `game.love`. +Device builds require a paired, unlocked device and a valid Apple signing +identity. Set `DEVELOPMENT_TEAM` or `CODE_SIGN_IDENTITY` when automatic +signing cannot select the intended account. Add `--ipa` to create +`dist/ios/gen1recomp.ipa`. -## Device / Release +The script verifies the final app before packaging it: -```bash -scripts/build_ios.sh --device # Debug, physical device SDK -scripts/build_ios.sh --device --release # Release configuration +- the public Documents plist settings are present +- the native picker bridge is present +- `game.love` exists and is non-empty + +If the payload is missing, the build fails instead of producing a blank app. + +## Useful options + +| Option | Purpose | +| --- | --- | +| `--fetch` | Fetch LÖVE 12.0 and Apple dependencies when `love-src/` is missing | +| `--device` | Build for `iphoneos` instead of the Simulator | +| `--release` | Use the Release configuration | +| `--install` | Install a device build on the first connected device | +| `--ipa` | Create an IPA after a device build | +| `--version X.Y.Z` | Stamp the engine and app version | +| `--package-only` | Package `game.love` and apply the iOS plist overlay without Xcode | + +`scripts/build.sh ios` delegates to this script and forwards the iOS release +option. + +## Output + +Simulator and device app bundles are copied to: + +```text +dist/ios/Debug-iphonesimulator/gen1recomp.app +dist/ios/Release-iphonesimulator/gen1recomp.app +dist/ios/Debug-iphoneos/gen1recomp.app +dist/ios/Release-iphoneos/gen1recomp.app ``` -Device builds need a signing identity and provisioning profile configured in -Xcode (or via `DEVELOPMENT_TEAM` / `CODE_SIGN_IDENTITY` env vars). This repo -does **not** store certificates, profiles, or App Store Connect secrets. +The intermediate Xcode products are under `mobile/ios/build/`. Both locations +are gitignored. -Manual out-of-band steps: - -1. Apple Developer account + App ID for `com.theboisclub.pokemonred` -2. Development or Distribution certificate + provisioning profile -3. In Xcode: open `love.xcodeproj` → target `love-ios` → Signing & Capabilities - → select your Team (or set `DEVELOPMENT_TEAM=XXXXXXXXXX` when invoking - `scripts/build_ios.sh --device`) -4. Archive / export an `.ipa` from Xcode Organizer for TestFlight / Ad Hoc - -## Layout - -| Path | Role | -|------|------| -| `LOVE_VERSION` | Engine pin (`12.0`) | -| `overlays/love-ios.plist` | Info.plist with public Documents sharing and display name **Pokemon Red** (copied over the upstream plist every build) | -| `love-src/` | Downloaded LÖVE 12.0 source tree (**gitignored**, do not commit) | -| `cache/` | Temporary source and dependency checkout data (**gitignored**) | -| `build/` | `xcodebuild` derived data (**gitignored**) | - -Game payload lands at: - -`love-src/platform/xcode/ios/resources/game.love` - -and is fused into the built `.app` (LÖVE auto-runs any bundled `*.love`). - -## Apple libraries dependency - -`scripts/build_ios.sh --fetch` retrieves the matching iOS libraries and the -SDL3 framework from -[love-apple-dependencies](https://github.com/love2d/love-apple-dependencies). -Re-run it if either dependency directory is absent. +The bundled game payload is staged at +`love-src/platform/xcode/ios/resources/game.love` and copied into the final +app bundle. The payload contains the game, not user-generated ROMs, mods, or +saves; those are created at runtime in Documents. ## App identity -| Field | Value | -|-------|--------| -| Display name | Pokemon Red | -| `PRODUCT_NAME` | PokemonRed | -| Bundle ID | `com.theboisclub.pokemonred` | -| Save directory | `Documents` | -| Orientations | Portrait only (`UIInterfaceOrientationPortrait`) | +| Field | Default | +| --- | --- | +| Display name | `gen1recomp` | +| Product name | `gen1recomp` | +| Bundle identifier | `com.theboisclub.gen1recomp` | +| Save directory | Public `Documents` root | +| Orientation | Portrait | -Overrides are applied by the build script (`xcodebuild` settings + plist overlay) -so refreshing `love-src/` does not lose branding. +Set `GEN1_BUNDLE_ID` to use a different bundle identifier for local device +builds. -## Flags (`scripts/build_ios.sh`) +## Prerequisites -| Flag | Meaning | -|------|---------| -| *(default)* | Simulator, Debug, no signing | -| `--fetch` | Fetch the LÖVE 12.0 source tree and Apple dependencies if `love-src/` is missing | -| `--device` | Build against `iphoneos` instead of `iphonesimulator` | -| `--release` | `Release` configuration instead of `Debug` | -| `--package-only` | Zip `game.love` + apply plist overlay; skip `xcodebuild` | +- macOS with Xcode and `xcodebuild` +- the iOS and iOS Simulator platforms installed in Xcode +- a fetched `love-src/` tree, or the `--fetch` option +- the matching iOS libraries and SDL3 framework under `love-src/` -Also: `scripts/build.sh ios` delegates here (`--release` is forwarded). - -## Preconditions - -- macOS (Darwin) with Xcode + `xcodebuild` on `PATH` -- iOS platform installed in Xcode (Settings → Platforms). `xcodebuild -showsdks` - should list `iphonesimulator` / `iphoneos`. A partial install can fail IB/xib - compiles with `iOS … Platform Not Installed` even when the SDK name appears. -- `love-src/` present (`--fetch`) -- iOS libraries under `love-src/platform/xcode/ios/libraries/` and SDL3 under `love-src/platform/xcode/shared/Frameworks/` +Use `xcodebuild -showsdks` to confirm that the required SDKs are installed.