mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-12 08:21:02 +02:00
902f0d73d4
Makes the iOS build a first-class citizen: ROM/mod/save import through
the system document picker (the README's missing "UIDocumentPicker
handoff"), Files-app drop-in support, and an opt-in Apple Health
step-sync seam consumed by a new gallery mod (Pokewalker).
Native layer (mobile/ios/native/, wired by mobile/ios/patch_love_src.py
on every build, so the fetched love-src tree stays pristine + re-patchable):
- GRPickerBridge.swift: love.system.pickFile("rom"|"mod"|"sav") and
love.system.createFile on iOS with the same contract as love-android's
SAF picker (picked_rom.gb / picked_mod.zip / picked_save.sav /
export_done.flag in the save dir). Reached from wrap_System.cpp via the
ObjC runtime, so liblove needs no Swift interop.
- GRBootstrap.m: sweeps .gb/.gbc/.zip/.sav dropped in Documents (Files
app / Finder sharing) into the save dir on every activation;
UIFileSharingEnabled + LSSupportsOpeningDocumentsInPlace in the plist
overlay. Drop a ROM, open the app, it imports with zero taps.
- GRHealthBridge.swift: love.system.syncHealthSteps() -> read-only
HealthKit step query anchored to the last sync, delivered as
steps_pending.json (merge-not-overwrite). HealthKit entitlement +
usage description included.
Lua:
- RomImporter: iOS rides the Android mobile flows; a 0.5s poll consumes
picker deliveries (iOS pickers are in-process modals, so the Android
refocus rescan never fires); failed pick copies surface as an
on-screen notice via pick_error.txt.
- main.lua: on iOS, stop forwarding touchpressed to the Importer - LOVE
already synthesizes a mousepressed for the primary touch, and the
resulting same-frame double-present made the document picker
auto-dismiss with zero documents (silent import failure).
- mods/pokewalker: opt-in Pokewalker mod (manifest v2, MECHANIC,
permissions declared, mod.card, CHANGELOG, headless test suite 9/9,
modkit validate --base imported + lint clean). Fused into iOS
game.love only; loads dormant anywhere without the bridge.
Build (scripts/build_ios.sh):
- Fix Xcode 26: the global PRODUCT_NAME override also renamed liblove.a
and broke the app link; the app bundle is renamed after the build
instead.
- Fix nondeterministic pack failures: grep -q + pipefail races SIGPIPE
on the game.love content checks.
- Simulator builds sign ad-hoc so entitlements embed (HealthKit works in
the simulator).
- Device builds: signing team auto-detected from the keychain,
CODE_SIGN_STYLE=Automatic + -allowProvisioningUpdates for CLI-only
provisioning, per-team derived bundle ID (explicit App IDs are
globally unique, so third parties can't sign the project default),
gitignored mobile/ios/bundle_id.local pin, and --install to push to a
connected iPhone.
- docs/ios-install.md: a zero-knowledge walkthrough from bare Mac to
playing on an iPhone.
Backward compatibility: no behavior change on desktop or Android. The
new love.system functions exist only under LOVE_IOS; RomImporter's
mobile flag simply includes iOS alongside Android; the main.lua change
is iOS-gated; the Pokewalker mod is packed only by the iOS build script
and its option defaults off.
Verified on an iPhone 17 Pro simulator and an iPhone 16 Pro device:
scripted ROM import to title screen, Files-drop zero-tap import,
picker-driven mod install and save import/export, HealthKit permission
sheet + step credit (4000 steps -> +200 EXP at the default rate through
the engine growth curve).
55 lines
2.1 KiB
Lua
55 lines
2.1 KiB
Lua
-- Standalone: luajit mods/pokewalker/tests/pokewalker_test.lua
|
|
-- Exercises the stated effect: opt-in gating, the native-bridge seam, and
|
|
-- steps converting to EXP through the engine's own growth math.
|
|
package.path = "./?.lua;./?/init.lua;" .. package.path
|
|
|
|
local T = require("tests.modkit")
|
|
local Data = require("src.core.Data")
|
|
Data:load()
|
|
|
|
-- The native Health bridge only exists inside the iOS app; stand it in so
|
|
-- the mod sees the same surface it does on device.
|
|
local syncCalls = 0
|
|
love.system = love.system or {}
|
|
love.system.syncHealthSteps = function()
|
|
syncCalls = syncCalls + 1
|
|
return true
|
|
end
|
|
|
|
local run = T.sdk.loadMod("mods/pokewalker", { data = Data })
|
|
T.eq(#run.errors, 0, "loads clean (" .. tostring(run.errors[1]) .. ")")
|
|
|
|
local events = run.loader.events
|
|
local Pokemon = require("src.pokemon.Pokemon")
|
|
local mon = Pokemon.new(Data, "PIDGEY", 5)
|
|
local game = { data = Data, save = { party = { mon } } }
|
|
|
|
-- Dormant until opted in: seeded steps survive every event untouched and
|
|
-- the native bridge is never poked (no permission prompt without consent).
|
|
love.filesystem.write("steps_pending.json", '{"steps": 4000}')
|
|
events:emit("game.ready", { game = game })
|
|
events:emit("map.entered", {})
|
|
T.check(love.filesystem.read("steps_pending.json") ~= nil,
|
|
"opt-out leaves pending steps untouched")
|
|
T.eq(syncCalls, 0, "opt-out never calls the native bridge")
|
|
|
|
-- Opted in: 4000 steps at the default 20 steps/EXP credit the lead mon.
|
|
run.loader.modOptions.pokewalker = { enabled = true }
|
|
local expBefore = mon.exp
|
|
events:emit("save.loaded", {})
|
|
T.eq(mon.exp, expBefore + 200, "4000 steps at 20 steps/EXP = +200 EXP")
|
|
T.eq(mon.level, 8, "level-ups ride the engine growth curve (5 -> 8)")
|
|
T.check(mon.stats.hp > 0 and mon.hp <= mon.stats.hp,
|
|
"stat recalc keeps HP within the new maximum")
|
|
T.check(love.filesystem.read("steps_pending.json") == nil,
|
|
"pending file is consumed exactly once")
|
|
T.check(syncCalls > 0, "opt-in requests a native sync")
|
|
|
|
-- A consumed file plus more events must not double-credit.
|
|
local expAfter = mon.exp
|
|
events:emit("map.entered", {})
|
|
T.eq(mon.exp, expAfter, "no pending file, no phantom EXP")
|
|
|
|
run.release()
|
|
T.finish("pokewalker")
|