#!/usr/bin/env python3 """Applies gen1recomp's iOS native-bridge patches to the fetched LÖVE 12.0 source tree (mobile/ios/love-src/). Idempotent AND re-appliable: the first run stashes a pristine `.orig` copy of every file it rewrites, and later runs always start over from that copy — so editing the patch content here just works on the next build, no manual restore needed. What it does: 1. Copies mobile/ios/native/ (GRPickerBridge.swift, GRHealthBridge.swift, GRBootstrap.m) and the HealthKit entitlements into the LÖVE tree. 2. Patches liblove's wrap_System.cpp to expose love.system.pickFile, love.system.createFile, and love.system.syncHealthSteps on iOS (each calls a GR*Bridge Swift class through the Objective-C runtime, so liblove never links against Swift directly). 3. Patches love.xcodeproj so the love-ios app target compiles the native files (Swift 5, iOS 14 deployment for UTType/forExporting APIs). """ import re import shutil import sys from pathlib import Path IOS_DIR = Path(__file__).resolve().parent LOVE_SRC = IOS_DIR / "love-src" 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") MARKER = "gen1recomp iOS picker bridge" # Headers must land outside `namespace love { namespace system {`. WRAP_INCLUDES = """ // %s: headers for the native-bridge functions below. #ifdef LOVE_IOS #include #include #include #include "filesystem/Filesystem.h" #endif """ % MARKER WRAP_FUNCS = """ // --- %s ------------------------------------------------- // love.system.pickFile / createFile / syncHealthSteps for iOS. pickFile and // createFile mirror the love-android extension this project's importer // already targets; syncHealthSteps feeds the Pokéwalker mod. Implemented in // Swift (GR*Bridge classes, love-ios app target); reached via the ObjC // runtime so liblove itself needs no Swift interop. #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_pickFile(lua_State *L) { const char *kind = luaL_optstring(L, 1, "rom"); return gr_callBridge(L, "GRPickerBridge", "presentPickerWithKind:saveDir:", kind); } // love.system.pickFileKinds() -> "rom,mod,sav,stadium", or nil off iOS. // // So a caller can ask what this build's picker understands BEFORE opening it. // An unknown kind is refused (GRPickerBridge), and a refusal looks exactly // like a picker that would not open -- so a caller with a fallback worth // showing needs to know which it is facing. A mod that guesses instead has // no way back: before the refusal landed, an unrecognised kind wrote // picked_rom.gb and the ROM importer deleted it. // // nil where there is no bridge at all, which reads the same as "no kinds". int w_pickFileKinds(lua_State *L) { Class cls = objc_getClass("GRPickerBridge"); if (cls == nullptr) { lua_pushnil(L); return 1; } // Fetched through the runtime: wrap_System.cpp is compiled as C++ rather // than Objective-C++, so no Foundation type may be NAMED here -- writing // `NSString` alone breaks the whole translation unit. objc_msgSend is a // plain C entry point and `id` comes from objc/runtime.h, so the string // is asked for its UTF8 bytes without ever being typed. typedef id (*GRObj)(Class, SEL); id kinds = ((GRObj)objc_msgSend)(cls, sel_registerName("supportedPickerKinds")); if (kinds == nullptr) { lua_pushnil(L); return 1; } typedef const char *(*GRUTF8)(id, SEL); const char *bytes = ((GRUTF8)objc_msgSend)(kinds, sel_registerName("UTF8String")); if (bytes == nullptr || bytes[0] == '\0') { lua_pushnil(L); return 1; } lua_pushstring(L, bytes); return 1; } int w_createFile(lua_State *L) { const char *name = luaL_optstring(L, 1, "export.sav"); return gr_callBridge(L, "GRPickerBridge", "presentExportWithName:saveDir:", name); } int w_syncHealthSteps(lua_State *L) { return gr_callBridge(L, "GRHealthBridge", "syncStepsWithCommand:saveDir:", "sync"); } #endif // LOVE_IOS // --------------------------------------------------------------------------- """ % MARKER WRAP_REGISTRATION = """#ifdef LOVE_IOS { "pickFile", w_pickFile }, { "pickFileKinds", w_pickFileKinds }, { "createFile", w_createFile }, { "syncHealthSteps", w_syncHealthSteps }, { "httpDownload", w_httpDownload }, #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 }, { "httpDownload", w_httpDownload }, #endif """ BRIDGE_EXTRA_FUNCS = """ #ifdef LOVE_IOS int w_httpDownload(lua_State *L) { const char *url = luaL_checkstring(L, 1); const char *destination = luaL_checkstring(L, 2); const char *userAgent = luaL_optstring(L, 3, "gen1recomp"); const char *accept = luaL_optstring(L, 4, ""); Class cls = objc_getClass("GRPickerBridge"); if (cls == nullptr) { lua_pushboolean(L, 0); return 1; } typedef signed char (*GRDownload)(Class, SEL, const char *, const char *, const char *, const char *); signed char ok = ((GRDownload)objc_msgSend)( cls, sel_registerName("httpDownloadWithUrl:destination:userAgent:accept:"), url, destination, userAgent, accept); lua_pushboolean(L, ok != 0); return 1; } #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" ID_FILE_OBJC = "6E1AC0DE0001000000000002" ID_FILE_HEALTH = "6E1AC0DE0001000000000003" ID_BUILD_PICKER = "6E1AC0DE0002000000000001" ID_BUILD_OBJC = "6E1AC0DE0002000000000002" ID_BUILD_HEALTH = "6E1AC0DE0002000000000003" SOURCES_PHASE_ID = "FA0B7F021A95AAF3000E1D17" # love-ios Sources phase IOS_APP_CONFIG_IDS = ( "FA0B7F261A95AAF4000E1D17", # Debug "FA0B7F271A95AAF4000E1D17", # Release "FA0B7F281A95AAF4000E1D17", # Distribution ) PBX_SOURCES = ( ("GRPickerBridge.swift", ID_FILE_PICKER, ID_BUILD_PICKER, "sourcecode.swift"), ("GRHealthBridge.swift", ID_FILE_HEALTH, ID_BUILD_HEALTH, "sourcecode.swift"), ("GRBootstrap.m", ID_FILE_OBJC, ID_BUILD_OBJC, "sourcecode.c.objc"), ) def fail(msg): print(f"patch_love_src: error: {msg}", file=sys.stderr) sys.exit(1) 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 not any(marker in text for marker in patched_markers): return text text = path.read_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) return text def copy_native_files(): NATIVE_DST.mkdir(parents=True, exist_ok=True) for name in NATIVE_FILES: src = NATIVE_SRC / name if not src.is_file(): fail(f"missing {src}") shutil.copy2(src, NATIVE_DST / name) if not ENTITLEMENTS_SRC.is_file(): fail(f"missing {ENTITLEMENTS_SRC}") shutil.copy2(ENTITLEMENTS_SRC, NATIVE_DST / "love-ios.entitlements") print(f"patch_love_src: native files -> {NATIVE_DST}") def patch_wrap_system(): text = pristine(WRAP_SYSTEM) include_anchor = '#include "sdl/System.h"\n' if include_anchor not in text: fail(f"include anchor not found in {WRAP_SYSTEM}") text = text.replace(include_anchor, include_anchor + WRAP_INCLUDES, 1) anchor = "static const luaL_Reg functions[] =" if anchor not in text: fail(f"anchor not found in {WRAP_SYSTEM}") has_native_picker = re.search(r"\bint w_pickFile\s*\(", text) is not None bridge_funcs = WRAP_SYNC_FUNCS if has_native_picker else WRAP_FUNCS text = text.replace(anchor, bridge_funcs + BRIDGE_EXTRA_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}") 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/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) build_files = "".join( f"\t\t{build_id} /* {name} in Sources */ = " f"{{isa = PBXBuildFile; fileRef = {file_id} /* {name} */; }};\n" for name, file_id, build_id, _ in PBX_SOURCES ) anchor = "/* Begin PBXBuildFile section */\n" if anchor not in text: fail("PBXBuildFile section not found") text = text.replace(anchor, anchor + build_files, 1) file_refs = "".join( f"\t\t{file_id} /* {name} */ = " f"{{isa = PBXFileReference; lastKnownFileType = {ftype}; " f"name = {name}; path = ios/native/{name}; " f"sourceTree = SOURCE_ROOT; }};\n" for name, file_id, _, ftype in PBX_SOURCES ) anchor = "/* Begin PBXFileReference section */\n" if anchor not in text: fail("PBXFileReference section not found") text = text.replace(anchor, anchor + file_refs, 1) # Add the files to the love-ios Sources phase. phase_re = re.compile( re.escape(SOURCES_PHASE_ID) + r" /\* Sources \*/ = \{.*?files = \(\n", re.S) m = phase_re.search(text) if not m: fail("love-ios Sources phase not found") insertion = "".join( f"\t\t\t\t{build_id} /* {name} in Sources */,\n" for name, _, build_id, _ in PBX_SOURCES ) text = text[: m.end()] + insertion + text[m.end():] # Swift + modern deployment target + HealthKit entitlements on the # love-ios app target only (UTType and forExporting need iOS 14; # liblove stays as upstream). for config_id in IOS_APP_CONFIG_IDS: cfg_re = re.compile( re.escape(config_id) + r" /\* \w+ \*/ = \{.*?buildSettings = \{\n", re.S) m = cfg_re.search(text) if not m: fail(f"build configuration {config_id} not found") settings = ( "\t\t\t\tSWIFT_VERSION = 5.0;\n" "\t\t\t\tIPHONEOS_DEPLOYMENT_TARGET = 14.0;\n" '\t\t\t\tCODE_SIGN_ENTITLEMENTS = "ios/native/love-ios.entitlements";\n' ) text = text[: m.end()] + settings + text[m.end():] PBXPROJ.write_text(text) print("patch_love_src: love.xcodeproj patched (native sources + Swift + entitlements)") 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() if __name__ == "__main__": main()