iOS support: JvmDowngrader build pipeline (MobiVM) + platform fixes#11190
iOS support: JvmDowngrader build pipeline (MobiVM) + platform fixes#11190shoeless wants to merge 36 commits into
Conversation
Fixes required to run on iOS/RoboVM, all guarded to leave desktop and Android behavior unchanged: - ForgeProfileProperties: honor forge.ios.userDir/cacheDir system properties so data/cache land in the writable Documents sandbox instead of the read-only app bundle (image cache, prefs, downloads). - ImageCache: bypass AssetManager for downloaded images in Documents/cache (libGDX iOS can't load those FileHandles) - decode bytes to Pixmap, retain Pixmaps (iOS textures need them alive), LRU-capped texture cache sized by device RAM, RGB565 downscale to 512px; texture-to-path keying for border records (Texture.toString isn't unique); defer preloadCache to the GL thread (background-thread GL uploads render blank); invalidate cached card art when a texture loads from disk (previously only downloads triggered refresh). - Assets: HybridFileHandleResolver (absolute paths -> absolute, relative -> internal on iOS/Android); default image via internal(); RGB565/no-mipmap card texture parameter; LRU-capped cardArtCache; mana-icon sprite sheet loaded with R/B channel swap (iOS/Metal PNG channel order made red mana render blue). - FSkin/FSkinFont: resolve bundled skin/fonts/translations via internal() relative paths (absolute bundle paths fail on device: /var symlink + sandbox realpath -> silent fallback_skin with no sprite sheets = blank buttons/icons); font version marker moved to writable local storage; Linear text filtering for Retina. - LibGDXImageFetcher: resolve Scryfall API URLs to direct CDN URLs via the JSON API (RoboVM's OkHttp can't follow Scryfall's 302 redirects), connection/read timeouts, zero-byte download guard, widened catch so a RuntimeException can't silently kill a downloader thread. - adventure Config: resPath() returns ASSETS_DIR on iOS (desktop relative-path probes never match the app bundle). - TextRenderer: BreakIterator fallback with simple word breaking (RoboVM ships no ICU break-iterator data). - CachedCardImage: guard fetch() so a fetch failure can't kill the render thread. - HostedMatch: per-game System.gc() for GUI-hosted matches only (keeps a 4GB iPad under its jetsam ceiling across a long match; headless sims bypass HostedMatch and skip it). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Runs UNMODIFIED upstream Java on MobiVM via the bytecode pipeline in forge-gui-ios/pipeline (JvmDowngrader -c 52 + MobiVmBridge -> streamsupport backports + app-supplied java.time/java.nio.file). Build against the transformed repo clone: mvn ... -Dmaven.repo.local=<repo>/tmp/ios-m2. - pom.xml: pipeline supply dependencies (streamsupport, streamsupport- cfuture, jvmdowngrader-java-api:1.3.6-mobivm, java-time-supply, java-nio-supply), sentry/jupnp/jetty exclusions, jupnp provided-scope for the IDeviceAdapter signature. - robovm.xml: force-link java8.**, xyz.wagyourtail.jvmdg.**, java.time.**, java.nio.file.**, forge.compat.**, UncheckedIOException, Assets$*/ImageCache$* nested classes; prune stale minlog/feature-only roots. - robovm.properties is now UNTRACKED (developer-specific bundle id); generated from robovm.properties.template + APP_ID in the untracked .env. Register your own App ID. - src/forge/compat: bytecode-rewrite targets for Java 8 members missing from MobiVM's Java-7-era runtime that streamsupport doesn't cover (String.join/chars, Math exact ops, boxed statics/unsigned, BufferedReader.lines, File.toPath, Files.walk, FileChannel.open, Matcher named groups, Date.from, ThreadLocal.withInitial, Predicate.isEqual, jvmdg's StringBuilder.append(byte) record-toString quirk) plus a java.lang.management remap suite (apfloat/tinylog probe it at boot). Referenced only from rewritten bytecode, never source. - Main.java: upstream API-drift updates (getApp signature, jupnp-typed UPnP stub, convertToPNG), audio-format override (interface default's Set.of field doesn't initialize on MobiVM), physical-RAM logging. - sentry-stub: Breadcrumb class + removeExtra/addBreadcrumb(Breadcrumb) (upstream now calls these on hot game paths). - Port from the ios-java8-compatibility fork: os_log wrapper + System.out/err redirection (iOS 26 log redaction), fallback skin, build-versioned card-cache clearing, NSTimeZone init, Info.plist. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Tracked home for the tooling that lets unmodified upstream Java run on MobiVM's Java-7-era runtime (previously living untracked in tmp/): - ios-pipeline.sh: single entry point (classpath | sim | device). Self-bootstraps third-party jars (JvmDowngrader, streamsupport, ThreeTen) on first run. No machine- or developer-specific values: device UDIDs, signing identity, provisioning profile, team id, and the bundle identifier all come from the untracked repo-root .env. - src/MobiVmBridge.java: ASM bytecode rewriter - whole-type remaps to streamsupport (java8.*), hierarchy-aware member redirection for Java 8 defaults/statics missing from MobiVM (rules in bridge.cfg), method-reference Handle rewriting in indy bootstrap args, jar entry renaming for relocations. - src/MobiVmLinkAudit.java: resolves every member reference in the final jars against the real runtime + supplies; its report is the complete porting workload after an upstream merge. - bridge.cfg / relocate-time.cfg: the rewrite rule sets. - java-supply/: minimal java.nio.file + java.io.UncheckedIOException implementations compiled with --patch-module (MobiVM lacks the package entirely; call sites are hot at boot). - README.md: architecture, usage, and the hard-won integration facts. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
forge-gui-ios/pom.xml depends on six local supply jars that a fresh clone could not previously build or resolve. Close the gap: - Track sentry-stub/build.sh (builds + installs the io.sentry no-op stubs; mvn now runs from the repo root so the tracked cwd-relative .mvn/maven.config resolves from anywhere) - Track the java.util.function stub sources (pipeline/java-function-stubs/): MobiVM's own Comparator signatures reference these types but its runtime omits the package, crashing serialization (EnumMap.readObject) - bootstrap() now builds both stub jars (mtime-guarded) and installs every pom supply into ~/.m2 idempotently: streamsupport, streamsupport-cfuture, the jvmdg api jar, java-nio-supply, java-function-stubs, and an unrelocated ThreeTen as the java-time-supply compile placeholder - README: document the fresh-clone bootstrap (JDK 17 + Maven + Xcode + .env is now the complete prerequisite list) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
On physical-device builds (never simulator), RoboVM runs Apple's `pngcrush -iphone` on every bundled .png, converting them to CgBI format: BGRA byte order + premultiplied alpha. libGDX decodes with stb_image, which does not un-swap CgBI, so every bundled PNG rendered with R and B exchanged on device - blue buttons/logo showed yellow/gold (#0A5C79 -> #795C0A, logo #10E0F0 -> #F0E010), and FSkinColor theme colors pixel-sampled from the crushed sprites were corrupted the same way (masked only because the default palette swatches are swap-invariant grays). Mana icons were the previously-visible symptom, patched at runtime by a per-file channel swap. Fix the cause instead: set skipPngCrush on the libGDX-consumed <resource> blocks in robovm.xml - the only place RoboVM honors it (Resource.isSkipPngCrush, consumed by IOSTarget.processFile; verified in robovm-dist-compiler 2.3.24 bytecode). The <skipPngCrush> that sat in pom.xml's plugin <configuration> was a silent no-op: robovm-maven-plugin declares no such parameter, so Maven ignored it. App icons (Icon*.png) stay crushed - SpringBoard consumes those and handles CgBI. Remove the now-harmful runtime mana-icon swap machinery from Assets.java (swapRedBlueChannels / getTextureWithColorFix / colorFixedPixmaps / colorFixedTexturePaths + getTexture/loadTexture interceptions): with uncrushed PNGs it would double-swap on device, and it keyed on ApplicationType.iOS, which had been silently inverting mana icons in the simulator (where PNGs were never crushed) all along. Skipping pngcrush also removes its alpha premultiplication, which no runtime swap ever undid. Verified: all 855 libGDX-consumed PNGs in the device bundle are free of the CgBI chunk (only the 10 Icon*.png remain crushed, intentionally); device shows blue buttons/logo and correct mana colors; simulator mana icons now render correctly too (red was showing as blue there pre-fix). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…emapping Two runtime bugs, both pipeline-level, found via an in-game crash on the "Report a crash" screen (AI v AI Commander, sim and device identically): 1. Every game with interacting static abilities crashed at start: ExceptionInInitializerError at org.jgrapht.graph.DefaultDirectedGraph (GameAction.findStaticAbilityToApply), caused by ClassCastException: SupplierUtil$$Lambda$14 cannot be cast to java.io.Serializable. RoboVM's AOT lambda plugin implements only FLAG_MARKERS/FLAG_BRIDGES of LambdaMetafactory.altMetafactory and silently drops FLAG_SERIALIZABLE (verified in robovm-dist-compiler 2.3.24 bytecode; no writeReplace/ SerializedLambda machinery exists in it at all), so serializable lambdas - `(Supplier<T> & Serializable) X::new`, all over ECJ-compiled jgrapht - come out NOT implementing Serializable and the compiler-emitted `checkcast java/io/Serializable` throws. Even untransformed upstream jgrapht would fail on RoboVM. MobiVmBridge now converts the ignored bit into an explicit FLAG_MARKERS java/io/Serializable marker, which RoboVM honors (192 sites fixed across the classpath; restores type-compatibility only - actually serializing such a lambda still throws, nothing does). 2. The crash screen's Save button silently did nothing: ZoneRulesException "No time-zone data files registered" from LocalDateTime.now() - the relocated ThreeTen java.time supply never registered its zone-rules provider because the META-INF/services entry kept its pre-relocation name and content, so ServiceLoader (which looks up the RELOCATED interface name) found nothing. MobiVmBridge now remaps services entry names and impl lines through the type rules (15 more entries fixed in the main pass). MobiVmLinkAudit gains matching gates so upstream merges can't silently regress either: it fails on any altMetafactory site with FLAG_SERIALIZABLE but no explicit Serializable marker, and flags META-INF/services entries naming classes that exist nowhere on the final classpath. The unrelocated java-time-supply compile placeholder is excluded from transform/audit (the relocated supply replaces it in the build clone). Verified in the simulator: Estrid/Adaptive Enchantment AI v AI Commander game (previously died at game start) runs past turn 5; user-verified multiple games to completion on device. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adventure mode had never run on iOS (the pre-pipeline fork predates upstream's controller support and never got past the splash either). Four independent blockers, found by walking the crash ladder in the simulator: 1. ClassNotFoundException: com.badlogic.gdx.controllers.IosControllerManager Forge.openAdventure -> Controllers.addListener reflectively loads the iOS controllers backend, but gdx-controllers-ios was never a dependency of forge-gui-ios (only gdx-controllers-core via forge-gui-mobile). Added 2.2.4 (Java-7 bytecode, lands in the pipeline KEEP set) + the GameController framework in robovm.xml. 2. NPE at Scene$SceneControllerListener.connected(Scene.java:19): IosControllerManager fires connected() during Controllers initialization for already-present controllers - before any scene exists, so Forge.getCurrentScene() is null. Null-guarded all five listener methods (upstreamable: any platform would crash if a controller connected before the first adventure scene). 3. ClassNotFoundException: com.ray3k.tenpatch.TenPatchDrawable while loading res/adventure/common/skin/ui_skin.json: the skin references TenPatchDrawable by NAME (libGDX skin JSON), invisible to RoboVM's reachability analysis -> tree-shaken. forceLink com.ray3k.tenpatch.**. 4. UnsatisfiedLinkError: c.b.g.physics.box2d.World.newWorld: the Box2D Java classes ship via forge-gui-mobile but the iOS NATIVES were never linked. Added gdx-box2d-platform:natives-ios, whose embedded META-INF/robovm/ios config + xcframework (device arm64 + sim arm64/x86_64) RoboVM merges automatically. Verified in the simulator end-to-end: mode selector -> adventure menu -> character creation -> world generation -> tutorial dungeon renders and the opening quest advances. Also: ios-pipeline.sh now lets explicitly exported env vars (SIM_UDID etc.) take precedence over .env values, so builds can target a secondary simulator without editing .env. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Audio was disabled on iOS (config.useAudio=false) to dodge an OpenAL-init crash in the fork's early Java-8 port. That reason no longer applies: ObjectAL.framework is now properly packaged (x86_64 + arm64) via gdx's natives, and the app boots cleanly with audio enabled. All Forge audio ships as .mp3, which ObjectAL decodes for both sound effects (preloadEffect via AudioToolbox) and music (OALAudioTrack via AVAudioPlayer). - Main.java: config.useAudio = true. - Main.java isSupportedAudioFormat: implement directly instead of the interface default (whose Set.of static field NPEs on MobiVM). Returns true for mp3/m4a/aac/wav/caf/aiff; excludes .ogg (no native iOS Vorbis decoder). This gate is what lets SoundSystem discover playable files - it had to return false while audio was off. Harden the one audio path that isn't already null-safe (it crashed the app during adventure testing: getMusic threw "iOS audio is not enabled" uncaught from the render loop when a dialog voice file loaded): - Assets.getMusic: wrap the synchronous load in try/catch, return null on failure instead of propagating. Callers already treat null as "no audio". - MapDialog: only build the audio pair when getMusic returns non-null (previously would NPE on the Pair's null Music). - AudioMusic.isPlaying: null-guard to match every other method. Verified in the simulator: app boots with audio enabled, a full Commander game and adventure run with no audio load failures or exceptions - and music is audible. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The iOS audio path (ObjectAL/AVAudioPlayer) renders mono MP3s as static noise; stereo files play correctly. Exactly 7 of 92 bundled audio files were mono, including all four Shandalar wizard-dialog voice clips (WizPAR1-4) - the 10s voice made the static obvious, while the mono SFX (button_press 0.17s, sprocket, daytime) were short enough to mask it. Re-encoded each in place to 2-channel stereo (mono signal duplicated to both channels; verified L/R RMS identical), same .mp3 filenames and comparable bitrates. Stereo is universal, so no change in behavior on desktop/Android. Device-independent content fix. Wizard voice verified clean on iOS (was static, now plays correctly). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ied) - Main.java: config.overrideRingerSwitch = true. ObjectAL honors the device silent/ringer switch by default (honorSilentSwitch = !overrideRingerSwitch), so game audio was completely silent on a device in silent mode - the simulator ignores the switch, which masked it. A game should play regardless of the ringer switch. (Device-verified: sound now plays.) - Forge.java: on iOS, exit/restart return to the main menu instead of the no-op that hung the app. iOS apps can't self-terminate (App Store guideline / HIG), so the device adapter's exit()/restart() are no-ops - the normal exit flow left the app stuck on the ClosingScreen, forcing a force-quit. Now adventure exit -> classic home (switchToClassic), classic exit -> home. (Device-verified: exiting works.) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…-verified progress) Hosting an online game crashed in sequence on the jvmdg/MobiVM gaps: 1. NoClassDefFoundError: xyz/wagyourtail/jvmdg/util/Utils. jvmdg's downgradeApi emits the runtime API stubs but omits the util/* runtime helpers those stubs call (Utils, Function, Pair, IOFunction); the pipeline bundled only exc/*. NetworkLogWriter was the first path to reach util/Utils. Bundle util/* too (15 Java-7 classes, no java-8-gap APIs, MobiVM-safe raw). NOT version//runtime/ (runtime-downgrader machinery that drags in ~850KB of shaded ASM, unused in static mode). 2. NoSuchMethodError: MethodHandles.lookup() via ProcessHandle.current(). NetworkLogWriter.writeSystemInfoHeader logs the PID (Java 9 ProcessHandle); jvmdg's stub needs MethodHandles trusted-lookup, which MobiVM lacks. The catch(IOException) missed this Error. Guard the PID lookup and make the whole diagnostic-header write catch Throwable - it's non-essential and must never break hosting. Both device-verified: hosting now progresses past these to later stages. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Keyboard (device-verified): on iOS the software keyboard delivers one
keyTyped per tap and routes backspace (0x08) / delete (0x7F) through
keyTyped rather than keyDown. Two upstream behaviors in
Forge.MainInputProcessor.keyTyped broke this:
- the printable-ASCII filter (' '..'~') dropped delete, so backspace did
nothing in text fields (Settings/Deck Manager search);
- the consecutive-character de-dup (lastKeyTyped/keyTyped) blocked repeated
letters ("aa" in "Kaalia") because iOS gives no keyUp to reset it.
Gate the iOS path to pass ' '..'~' plus \b/� straight through with no
de-dup; Android/desktop keep the existing de-dup unchanged. FTextField
.keyTyped now handles \b/� as deletion (mirrors its keyDown BACKSPACE).
UPnP (device-verified): hosting an online game crashed with
NoClassDefFoundError: org/jupnp/support/model/PortMapping. UPnP port
mapping via jupnp (provided scope) isn't available on iOS/MobiVM. It's
optional - LAN/direct hosting works without it - so wrap mapNatPort in
catch(Throwable) to degrade gracefully instead of killing hosting.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The background music (AVAudioPlayer) developed a slow periodic crackle (~14-32s, scaling with music volume, present even when paused) whenever the OpenAL sound-effects engine was active. Root cause: OpenAL and AVAudioPlayer are two independent CoreAudio clients whose render cycles beat against each other; matching sample rates only shifted the beat period, never removed it. Fix - collapse everything to one clock domain: - AudioClip: on iOS, play sound effects through libGDX Music (AVAudioPlayer / mediaserverd), the same engine as the music, instead of Sound (OpenAL). This is the only Gdx.audio.newSound call site, so it covers all effects. Also drop a re-trigger while the (single per-type) player is still playing to avoid machine-gun truncation, skip the 30ms OpenAL voice-reuse delay on iOS, and evaluate the iOS check per-instance so it can't strand effects on the suspended engine. - Main.didBecomeActive: permanently suspend the OpenAL engine libGDX auto-initializes (its active 3D-mixer render cycle is what beats against mediaserverd). OpenALManager.setManuallySuspended(true). - OpenALManager: minimal RoboVM ObjC binding to ObjectAL's OpenALManager (sharedInstance, manuallySuspended). iOS-only; desktop/Android keep the OpenAL Sound path unchanged. Device-verified (diagnostic reported manuallySuspended=true and the crackle is gone). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Nice work, obviously rather huge but hardly unexpected. A bit unfortunate there's still a few workarounds needed in the neighbour modules but tbh Android is probably just as guilty of that too 😆 Anyway three main points we need to think about: b) CI/CD - can your pipeline be integrated into our normal GitHub actions? if it's build time is way longer than the other platforms maybe only as release though c) Not having Sentry is a real loss, any chance to get that working somehow? |
… fix)
An online guest crashed the instant it tapped a discovered server to join.
Live device stack (idevicesyslog):
ExceptionInInitializerError
at NetConnectUtil.getPrioritizedServerUrls (Stream.toList())
at OnlineLobbyScreen.lambda$joinDiscoveredServer$0
Caused by: ClassNotFoundException: java.util.stream.ReferencePipeline
at xyz.wagyourtail.jvmdg.j16.stub.java_base.J_U_S_Stream.<clinit>
jvmdg's -c 52 downgrade rewrites every Stream.toList() (Java 16) into a call
on its J_U_S_Stream stub, whose static initializer reflectively loads
java.util.stream.ReferencePipeline and rethrows. MobiVM has no
java.util.stream.* (jvmdg relocates streams to java8.util.stream via
streamsupport), so the FIRST toList() actually executed on device dies. The
LAN/Tailscale discovery join path was the first to hit it; BondAi (soulbond)
is the other reachable caller. bridge.cfg's `type java/util/stream/ →
java8/util/stream/` rule remaps the stream TYPE but nothing mapped the jvmdg
J_U_S_Stream stub METHODS, so toList() fell through to the crashing stub.
Fix: add a bridge member rule redirecting J_U_S_Stream.toList to a
self-implemented forge.compat.JStream8.toList (drains the stream via its
iterator; must not call toList() itself or the bridge would rewrite it back
into a self-loop). Written against java8.util.stream.Stream so its descriptor
matches the redirected caller exactly. Verified: J_U_S_Stream.toList leaves the
bridge-unresolved report, and the device build links + installs cleanly (the
audit's MISSING-MEMBER for the app-side helper is the same benign false-positive
that the proven-working forge.compat.JString8.join shows).
An audit of the other forge-reachable unresolved jvmdg stubs (Predicate.not,
Optional.isEmpty, Objects.requireNonNullElseGet, CompletableFuture
.completeOnTimeout) found none with the broken-clinit shape — they have no
static initializer and are safe. The incomplete io.sentry.protocol Device/
OperatingSystem stubs are only invoked when GuiBase.hwInfo != null, which is
set on Android only, so they are inert on iOS. No other J_U_S_Stream-class
landmine remains.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…m forge code The J_U_S_Stream.toList crash (guest-join) was a jvmdg stub method reachable from forge code with no bridge rule — its <clinit> loaded a MobiVM-absent class and threw on first use. The linkage audit reported it but the pipeline ran with `|| true`, so it shipped. This adds a real gate. MobiVmBridge now scans its BRIDGE-UNRESOLVED set for jvmdg stub METHODs that have a forge/* referrer and are not on a small GATE_ALLOWLIST of verified-safe stubs (Predicate.not, Optional.isEmpty, Objects.requireNonNullElseGet, CompletableFuture.completeOnTimeout — audited: no broken clinit). Any other such method is emitted as BRIDGE-GATE-FAIL, and ios-pipeline.sh greps for that marker after the bridge step and exits 1 with the offending members + fix instructions (add a bridge.cfg member rule -> forge.compat helper, or allowlist after verifying the stub's clinit is MobiVM-safe). Verified: passes clean on the current (fixed) tree; trip-test removing the JStream8 rule fails the build naming exactly J_U_S_Stream.toList <- BondAi. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…rkflows
Integrates the iOS build pipeline with GitHub Actions, two-tier:
- ios-compat-gate.yml (ubuntu, every PR touching Java/poms, ~minutes): new
'audit' pipeline mode runs the full JvmDowngrader transform + MobiVmBridge +
linkage gate WITHOUT any Apple tooling and fails when code reachable from
Forge would crash on MobiVM (unbridged jvmdg stub / missing API). Lets anyone
catch 'modern Java API breaks iOS' regressions per-PR without owning a Mac.
- ios-build-ipa.yml (macos-15, manual dispatch / release-time): new 'ipa' mode
runs the full RoboVM AOT build with -Drobovm.iosSkipSigning=true and uploads
an UNSIGNED forge-ios.ipa artifact. No certificates or jailbreak anywhere:
users install it with Sideloadly/AltStore, which re-sign with their own
(free) Apple ID. RoboVM SDK + AOT cache are actions-cached; SKIP_CACHE_CLEAR
reuses the content-hashed AOT cache for fast rebuilds.
Pipeline script changes: portable in-place sed (GNU vs BSD) and APFS-clone
fallback to plain cp for Linux runners; the app-module bridge report now goes
through the same BRIDGE-GATE-FAIL check as the dependency pass; module install
includes the parent POM ('-pl .') so fresh CI clones resolve the ${revision}
parent.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ing; scope iOS file-path behavior to iOS only Addresses review feedback about iOS workarounds bleeding into neighbour modules. Adds GuiBase.setIsIOS()/isIOS() — the exact mirror of the existing Android launcher flag — set by forge.ios.Main, and replaces every raw Gdx.app.getType() == ApplicationType.iOS sniff in forge-gui-mobile with it (Forge exit-flow + keyTyped, adventure Config.resPath, AudioClip, Assets, FSkin). Several PR-added imports drop back out of shared files as a result. Also FIXES a latent Android regression this PR would have shipped: the internal()-path rerouting in FSkin.getFileHandle / Assets.getFileHandle / HybridFileHandleResolver applied to Android too, but Android's assets are extracted to storage and are NOT reachable via internal() APK paths — on master Android always resolved these absolutely. The internal() behavior is now scoped to iOS only (where the app bundle's /var symlink breaks absolute() inside the sandbox); Android behavior is bit-identical to master again. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…on GuiBase.isIOS() Continues the neighbour-module cleanup, fixing three more cases where iOS-motivated changes silently altered other platforms' behavior: - FSkinFont: translation-file and .fnt loading had been switched to internal() relative paths UNCONDITIONALLY — on Android those assets live in extracted storage (not APK-internal), so CJK glyph preloading and prebuilt-font lookup would silently fail. Both now route through FSkin.getFileHandle (iOS-only internal(), original absolute() elsewhere). The Nearest->Linear font-texture filter change is now iOS-only too (Retina smoothing); other platforms keep master's crisp Nearest filtering. - ImageCache.toRelativePath rewrote any path containing /Documents/ or /Library/ — on desktop macOS the cache legitimately lives under ~/Library, so mobile-dev image loading would corrupt paths. All Documents-cache texture paths (direct-Pixmap load, downloaded-texture cache) are now explicitly GuiBase.isIOS()-gated instead of path-sniffed. - LibGDXImageFetcher: the Scryfall JSON-API resolve (RoboVM can't follow the 302 redirects) ran for every platform, doubling Scryfall API requests per image on Android/desktop where redirects work natively. Now iOS-only. TextRenderer's BreakIterator fallback and FTextField's keyTyped handling stay ungated by design: they are adaptive/platform-neutral (the fallback only triggers where ICU is missing; control chars only arrive via keyTyped on iOS). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…/robovm/) Validated locally end-to-end: the ipa mode produces a 234MB forge.ios.Main.ipa with an ad-hoc (identity-less) signature — Signature=adhoc, TeamIdentifier not set, no embedded.mobileprovision — which is exactly the form Sideloadly/AltStore accept and re-sign with the user's own Apple ID. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Adds an iOS (MobiVM/RoboVM) build pipeline that downgrades/bridges upstream Java bytecode to a Java-7-era runtime, plus a set of iOS-specific runtime/platform fixes (filesystem, rendering, input, audio, networking, logging) to make Forge run on-device and in the simulator.
Changes:
- Introduce
forge-gui-ios/pipeline/tooling (JvmDowngrader + bridge + link audit) and CI workflows to continuously validate iOS/MobiVM compatibility. - Add iOS backend module code (launcher, logging, OpenAL suspension, compat shims/stubs) and RoboVM config/resources.
- Apply iOS-targeted fixes across shared/mobile GUI for paths, textures/memory, input behavior, audio routing, and optional networking diagnostics.
Reviewed changes
Copilot reviewed 96 out of 114 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
| forge-gui/src/main/java/forge/localinstance/properties/ForgeProfileProperties.java | Allow iOS launcher to supply writable user/cache dirs via system properties. |
| forge-gui/src/main/java/forge/gui/GuiBase.java | Add explicit iOS platform flag for shared branching. |
| forge-gui/src/main/java/forge/gamemodes/net/server/FServerManager.java | Make UPnP mapping degrade gracefully when UPnP classes/platform service are unavailable. |
| forge-gui/src/main/java/forge/gamemodes/net/NetworkLogWriter.java | Make diagnostic header resilient to missing modern APIs on iOS/MobiVM. |
| forge-gui/src/main/java/forge/gamemodes/match/HostedMatch.java | Add explicit GC at end of GUI-hosted games to mitigate iOS memory pressure. |
| forge-gui-mobile/src/forge/util/LibGDXImageFetcher.java | Add iOS Scryfall redirect workaround + improve download robustness/logging. |
| forge-gui-mobile/src/forge/toolbox/FTextField.java | Fix iOS delete/backspace delivered through keyTyped. |
| forge-gui-mobile/src/forge/sound/AudioMusic.java | Null-guard music playback state. |
| forge-gui-mobile/src/forge/sound/AudioClip.java | Route iOS SFX through AVAudioPlayer-backed Music to avoid OpenAL clock-domain crackle. |
| forge-gui-mobile/src/forge/Forge.java | Adjust iOS exit/restart flow + iOS keyboard keyTyped handling. |
| forge-gui-mobile/src/forge/CachedCardImage.java | Prevent image fetch failures from crashing the render thread on iOS. |
| forge-gui-mobile/src/forge/assets/TextRenderer.java | Add BreakIterator fallback when ICU resources are missing on iOS/RoboVM. |
| forge-gui-mobile/src/forge/assets/ImageCache.java | Add iOS texture/pixmap caching + downscaling and render-thread texture creation safeguards. |
| forge-gui-mobile/src/forge/assets/FSkinFont.java | Use iOS-compatible FileHandle lookups + adjust font texture filtering on iOS. |
| forge-gui-mobile/src/forge/assets/FSkin.java | Add iOS bundle/internal FileHandle support and iOS-specific skin/font marker handling. |
| forge-gui-mobile/src/forge/assets/Assets.java | Add hybrid resolver for internal vs absolute resources; add card texture parameter and LRU card art cache; harden music loading. |
| forge-gui-mobile/src/forge/adventure/util/MapDialog.java | Guard against null music when loading voice audio. |
| forge-gui-mobile/src/forge/adventure/util/Config.java | Use ASSETS_DIR on iOS (and Android) for resource path resolution. |
| forge-gui-mobile/src/forge/adventure/scene/Scene.java | Null-guard scene callbacks during early controller initialization on iOS. |
| forge-gui-ios/src/forge/ios/OSLogPrintStream.java | Add os_log / NSLog bridging so logs are visible on iOS 26+ and older iOS versions. |
| forge-gui-ios/src/forge/ios/OpenALManager.java | Add minimal ObjC binding to suspend OpenAL engine on iOS. |
| forge-gui-ios/src/forge/ios/Main.java | Implement iOS launcher setup (dirs, timezone, cache invalidation, memory warning handling, logging redirection, OpenAL suspension). |
| forge-gui-ios/src/forge/ios/ForgeOSLog.java | Provide native bridges for os_log entrypoints. |
| forge-gui-ios/src/forge/compat/package-info.java | Document compat rewrite targets for the iOS pipeline. |
| forge-gui-ios/src/forge/compat/mgmt/RuntimeMXBean.java | Provide java.lang.management whole-type remap interface. |
| forge-gui-ios/src/forge/compat/mgmt/package-info.java | Document java.lang.management remap strategy. |
| forge-gui-ios/src/forge/compat/mgmt/MemoryUsage.java | Provide java.lang.management.MemoryUsage remap class. |
| forge-gui-ios/src/forge/compat/mgmt/MemoryMXBean.java | Provide java.lang.management.MemoryMXBean remap interface. |
| forge-gui-ios/src/forge/compat/mgmt/ManagementFactory.java | Provide Runtime-backed management factory for boot-time probes. |
| forge-gui-ios/src/forge/compat/JThreadLocal8.java | Bridge target for ThreadLocal.withInitial(). |
| forge-gui-ios/src/forge/compat/JString8.java | Bridge targets for Java 8 String/CharSequence members. |
| forge-gui-ios/src/forge/compat/JStream8.java | Bridge target for Stream.toList() to avoid jvmdg stub crash on MobiVM. |
| forge-gui-ios/src/forge/compat/JRegex8.java | Bridge targets for Matcher named-group API. |
| forge-gui-ios/src/forge/compat/JMisc8.java | Bridge targets for assorted Java 8+ APIs used by upstream deps. |
| forge-gui-ios/src/forge/compat/JMath8.java | Bridge targets for Java 8 Math statics. |
| forge-gui-ios/src/forge/compat/JIo8.java | Bridge target for BufferedReader.lines(). |
| forge-gui-ios/src/forge/compat/JFunction8.java | Bridge target for missing Predicate.isEqual. |
| forge-gui-ios/src/forge/compat/JBoxed8.java | Bridge targets for boxed primitive statics/methods. |
| forge-gui-ios/sentry-stub/src/io/sentry/Sentry.java | Add no-op Sentry facade to satisfy shared code on iOS where real SDK is excluded. |
| forge-gui-ios/sentry-stub/src/io/sentry/ScopeType.java | Stub enum for Sentry API compatibility. |
| forge-gui-ios/sentry-stub/src/io/sentry/ScopeCallback.java | Stub interface for Sentry API compatibility. |
| forge-gui-ios/sentry-stub/src/io/sentry/protocol/SentryId.java | Stub SentryId type. |
| forge-gui-ios/sentry-stub/src/io/sentry/protocol/OperatingSystem.java | Stub OperatingSystem type. |
| forge-gui-ios/sentry-stub/src/io/sentry/protocol/Device.java | Stub Device type. |
| forge-gui-ios/sentry-stub/src/io/sentry/IScope.java | Stub IScope interface. |
| forge-gui-ios/sentry-stub/src/io/sentry/Hint.java | Stub Hint type. |
| forge-gui-ios/sentry-stub/src/io/sentry/Contexts.java | Stub Contexts type. |
| forge-gui-ios/sentry-stub/src/io/sentry/Breadcrumb.java | Stub Breadcrumb type. |
| forge-gui-ios/sentry-stub/build.sh | Script to build/install the Sentry stub jar into Maven local repo. |
| forge-gui-ios/sentry-stub/.gitignore | Ignore stub build artifacts. |
| forge-gui-ios/robovm.xml | Update RoboVM config for arm64, resources, force-linking, frameworks, and pngcrush behavior. |
| forge-gui-ios/robovm.properties.template | Add tracked template for untracked per-developer robovm.properties generation. |
| forge-gui-ios/robovm.properties | Remove tracked robovm.properties (now generated/untracked). |
| forge-gui-ios/pom.xml | Add RoboVM plugin + iOS deps (controllers/box2d/freetype natives) + exclude incompatible deps + add supplies/stubs. |
| forge-gui-ios/pipeline/src/MobiVmLinkAudit.java | Add linkage audit tool to gate missing classes/members and service registrations. |
| forge-gui-ios/pipeline/src/MobiVmBridge.java | Add bridge/rewriter tool to remap missing Java 8 APIs + fix serializable-lambda markers + remap META-INF/services. |
| forge-gui-ios/pipeline/relocate-time.cfg | Config for relocating ThreeTen into java.time.*. |
| forge-gui-ios/pipeline/README.md | Document the iOS modern-Java pipeline and usage. |
| forge-gui-ios/pipeline/java-supply/src/java/nio/file/SupportTypes.java | Anchor file for supplied java.nio.file package. |
| forge-gui-ios/pipeline/java-supply/src/java/nio/file/StandardOpenOption.java | Supply StandardOpenOption for MobiVM. |
| forge-gui-ios/pipeline/java-supply/src/java/nio/file/StandardCopyOption.java | Supply StandardCopyOption for MobiVM. |
| forge-gui-ios/pipeline/java-supply/src/java/nio/file/SimpleFileVisitor.java | Supply SimpleFileVisitor for MobiVM. |
| forge-gui-ios/pipeline/java-supply/src/java/nio/file/Paths.java | Supply Paths.get for MobiVM. |
| forge-gui-ios/pipeline/java-supply/src/java/nio/file/Path.java | Supply minimal Path interface for MobiVM. |
| forge-gui-ios/pipeline/java-supply/src/java/nio/file/OpenOption.java | Supply OpenOption interface. |
| forge-gui-ios/pipeline/java-supply/src/java/nio/file/NoSuchFileException.java | Supply NoSuchFileException. |
| forge-gui-ios/pipeline/java-supply/src/java/nio/file/LinkOption.java | Supply LinkOption enum. |
| forge-gui-ios/pipeline/java-supply/src/java/nio/file/InvalidPathException.java | Supply InvalidPathException. |
| forge-gui-ios/pipeline/java-supply/src/java/nio/file/ForgeFilePath.java | File-backed Path implementation. |
| forge-gui-ios/pipeline/java-supply/src/java/nio/file/FileVisitResult.java | Supply FileVisitResult. |
| forge-gui-ios/pipeline/java-supply/src/java/nio/file/FileVisitor.java | Supply FileVisitor. |
| forge-gui-ios/pipeline/java-supply/src/java/nio/file/FileVisitOption.java | Supply FileVisitOption. |
| forge-gui-ios/pipeline/java-supply/src/java/nio/file/FileSystems.java | Supply FileSystems.getDefault. |
| forge-gui-ios/pipeline/java-supply/src/java/nio/file/FileSystemException.java | Supply FileSystemException. |
| forge-gui-ios/pipeline/java-supply/src/java/nio/file/FileSystem.java | Supply minimal FileSystem. |
| forge-gui-ios/pipeline/java-supply/src/java/nio/file/Files.java | Supply key java.nio.file.Files operations backed by java.io.File. |
| forge-gui-ios/pipeline/java-supply/src/java/nio/file/FileAlreadyExistsException.java | Supply FileAlreadyExistsException. |
| forge-gui-ios/pipeline/java-supply/src/java/nio/file/CopyOption.java | Supply CopyOption interface. |
| forge-gui-ios/pipeline/java-supply/src/java/nio/file/attribute/UserPrincipalLookupService.java | Supply user principal lookup surface. |
| forge-gui-ios/pipeline/java-supply/src/java/nio/file/attribute/UserPrincipal.java | Supply UserPrincipal. |
| forge-gui-ios/pipeline/java-supply/src/java/nio/file/attribute/ForgeFileAttributes.java | File-backed BasicFileAttributes implementation. |
| forge-gui-ios/pipeline/java-supply/src/java/nio/file/attribute/FileTime.java | Supply FileTime. |
| forge-gui-ios/pipeline/java-supply/src/java/nio/file/attribute/FileAttribute.java | Supply FileAttribute. |
| forge-gui-ios/pipeline/java-supply/src/java/nio/file/attribute/BasicFileAttributes.java | Supply BasicFileAttributes. |
| forge-gui-ios/pipeline/java-supply/src/java/io/UncheckedIOException.java | Supply Java 8 UncheckedIOException. |
| forge-gui-ios/pipeline/java-function-stubs/src/java/util/function/ToLongFunction.java | Supply minimal java.util.function stub for MobiVM’s Comparator signature needs. |
| forge-gui-ios/pipeline/java-function-stubs/src/java/util/function/ToIntFunction.java | Supply minimal java.util.function stub. |
| forge-gui-ios/pipeline/java-function-stubs/src/java/util/function/ToDoubleFunction.java | Supply minimal java.util.function stub. |
| forge-gui-ios/pipeline/java-function-stubs/src/java/util/function/Function.java | Supply minimal java.util.function stub. |
| forge-gui-ios/pipeline/ios-pipeline.sh | Add full pipeline script (bootstrap, transform, audit, sim/device/ipa build flows). |
| forge-gui-ios/pipeline/bridge.cfg | Add bridge rules mapping missing Java 8 surface to streamsupport/compat supplies. |
| forge-gui-ios/oslog_wrapper/ForgeOSLog.m | Implement native os_log wrapper functions. |
| forge-gui-ios/oslog_wrapper/ForgeOSLog.h | Header for os_log wrapper functions. |
| forge-gui-ios/Info.plist.xml | Update capabilities/arm64 and add network/TLS and other iOS app settings. |
| .gitignore | Ignore per-developer robovm.properties. |
| .github/workflows/ios-compat-gate.yml | Add Linux CI job to run the iOS/MobiVM transform + bridge + link audit gate. |
| .github/workflows/ios-build-ipa.yml | Add manual macOS workflow to build an unsigned iOS IPA artifact. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Sentry was stubbed on iOS because the real io.sentry classes were never bundled/linked into the RoboVM build — every telemetry call from shared code threw NoClassDefFoundError, and on the choose-cards paths (Cultivate/Genesis Hydra library searches) that silently killed the game thread. That was a packaging gap, not a platform incompatibility: sentry-java targets Java 8 and its HttpURLConnection transport works on RoboVM. - forge-gui-ios now depends on the real sentry-java (8.21.1, the same artifact every other platform gets via forge-game) — the io.sentry exclusions and the forge.stubs:sentry-stub substitute are gone, stub sources deleted. - The jvmdg pipeline transforms the sentry jar like any other dependency; the iOS compatibility gate passes with zero sentry findings (no unbridged stubs, no missing MobiVM APIs reachable). - robovm.xml force-links io.sentry.** (the SDK wires integrations/transports reflectively, invisible to the tree-shaker — exactly how the original NoClassDefFoundError happened). - forge.ios.Main initializes Sentry the same way the desktop launcher does (same project DSN, release/environment tags, external-config enabled). Events are only sent when the user's USE_SENTRY preference allows it; init failure is caught so telemetry can never break app launch. Pending device verification: the previously-crashing choose-cards flows and a delivered event. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ad HTTPS fallback Device verification showed the desktop launcher's hardcoded fallback DSN (https://...@sentry.asgardsrealm.net//3, no port) answers HTTP 530 to every request — nothing serves the ingest API on 443. The checked-in sentry.properties files show the self-hosted Sentry actually listens on plain HTTP :9000, and that mobile platforms (Android's manifest, forge-gui-mobile-dev) report to project 2 while desktop uses project 3. Point iOS at the mobile project's real endpoint. RoboVM's Java sockets are not subject to ATS, so http:// is usable here (same trade-off the other platforms already accept, per the 'ideally this should be using HTTPS' note in sentry.properties). Side finding for upstream: any desktop install that loses sentry.properties (the Main.java comment suggests some do) falls back to the dead HTTPS URL and silently drops all crash reports. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… Cloudflare Tunnel Deeper endpoint investigation (thanks to reviewer skepticism) corrected the previous commit's conclusion: sentry.asgardsrealm.net is served through a Cloudflare Tunnel (the edge answers CF error 1033 'tunnel not connected' right now, from multiple networks and clients). Under a tunnel, 443 is the ONLY reachable path — the plain-HTTP :9000 DSNs in the checked-in sentry.properties files are pre-tunnel relics that time out at the Cloudflare edge and can never work again. The desktop launcher's hardcoded HTTPS fallback is actually the CORRECT form (and since sentry.properties isn't packaged into the desktop jar, it's what desktop uses at runtime). Point iOS at the mobile project (2) via the tunnel-served HTTPS form, matching Android's manifest. Delivery is verified client-side to the Cloudflare edge; events will flow once the upstream cloudflared connector reconnects (currently error 1033 — flagged to the maintainers, along with the stale :9000 sentry.properties). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
I'm gonna do a manual review after you're done with those Copilot findings 👍 Would you be able to support this port further (probably users will find more bugs) in case we decide to merge it? |
The ~120-line resolveScryfallUrl/extractImageUrl JSON-API path existed because "RoboVM's OkHttp can't follow Scryfall's 302 redirects" — an on-simulator probe proves that claim false: the bundled okhttp follows the api.scryfall.com -> cards.scryfall.io cross-host 302 fine (200, image bytes, correct final URL), and with the workaround deleted, card images verifiably download end-to-end through plain redirect-following. The workaround also doubled the Scryfall API request count per image. Kept from the old block: connect/read timeouts on the connection (a stalled connection shouldn't hang the download thread forever). Also fixes the setless-token retry logging the outer exception's message instead of its own (pre-existing upstream bug, flagged by Copilot). The real fetch failure the workaround was likely mis-attributed to is process-local getaddrinfo failures under the game-start fetch burst — which hit the workaround's JSON call identically (same host). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- FSkin.getFileHandle: reroute to internal() ONLY for paths under ASSETS_DIR (the read-only bundle). Writable cache/Documents paths keep absolute() — the old blanket reroute silently broke every cache-dir lookup on iOS: downloaded skins and planechase pics were invisible, and the generated .fnt font cache never hit (fonts re-generated from TTF on every launch). - HostedMatch: gate the per-game System.gc() to iOS — it exists for the iOS jetsam ceiling; desktop/Android shouldn't pay the GC stall. - NetworkLogWriter/FServerManager: narrow catch(Throwable) to catch(LinkageError | Exception) — still degrades gracefully on MobiVM-missing classes, no longer swallows OutOfMemoryError. - OSLogPrintStream: buffer UTF-8 bytes in print(String) instead of truncating chars to 8 bits, and decode the buffer explicitly as UTF-8 in flushBuffer() — non-ASCII (localized strings, accented card names) no longer garbles in os_log. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Booting parses ~32k card rules + builds ~100k PaperCards + loads skin assets — a transient allocation spike that leaves ~200MB of freed-but- unmapped bytes in the GC heap once the home screen is idle. Two full GCs return those pages to iOS (the collector needs the second pass to unmap what the first freed); device-measured on the feature branch: GC heap 690→492MB before a game starts. iOS-gated — other platforms don't sit against a per-process memory ceiling, so they skip the stall. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
iOS processes default to a 256-fd soft limit. The app's classpath jars get pinned open by ordinary classloader resource probes (87 jars x 2 fds ≈ 177 via the shared JarFile cache), match load holds ~70 more, and the game-start image-fetch burst opens dozens of sockets on top. Once past the limit, everything fails at once and none of it says "EMFILE" up front: lazy texture opens throw GdxRuntimeException "Couldn't load file" (card-frame/PW/nyx textures -> broken match board while the game itself keeps running), getaddrinfo returns "Unable to resolve host" (it needs a descriptor), and AssetManager errors on the default card image. The only honest signal was the nested cause in the saved crash log: java.io.FileNotFoundException: ... open failed: EMFILE. FileDescriptorLimit (new, Bro binding for getrlimit/setrlimit) raises the soft limit to min(hard, OPEN_MAX=10240) first thing in main(). Verified in the simulator: match load + full image-fetch burst runs at 276 fds with correctly rendered card frames and working downloads. Also stop Sentry's classpath config scans (external configuration + debug-meta probes) — hygiene, not the fix: there is no external sentry.properties on iOS (the DSN is set in code), so probing every bundled jar for it is pure waste. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Normal
I suppose because a different workflow wouldn't find any cached artifacts? |
…ofile forge-gui-ios depends on artifacts the iOS pipeline generates into the local Maven repo (streamsupport / JvmDowngrader / relocated-ThreeTen supplies) that are not published to Maven Central. Leaving the module in the default <modules> made a plain `mvn install` — and CI's normal test-build — fail to resolve those deps. Gate it behind a non-default profile instead: mvn -Pios ... The iOS pipeline builds the module directly (cd forge-gui-ios), never through the root reactor, so this is transparent to iOS builds; normal development and CI no longer touch it. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… detect The simulator ran x86_64 under Rosetta (RoboVM defaults the sim arch to the host JDK's arch, which is Intel here). Rosetta's CoreAudio translation floods HALC_ProxyIOContext overloads (~24/sec) — the audio static — and slows the whole sim. Devices were always native arm64 and unaffected. Now the pipeline builds a native arm64 simulator app on Apple Silicon (SIM_ARCH host-aware). Overloads drop ~24/sec -> ~0.1/sec (live-game verified). Pieces: - build_oslog: compile libForgeOSLog.a for the target flavor from source (arm64-device and arm64-simulator objects can't share one archive). sim() swaps in the sim flavor and restores the committed device lib on exit. The gdx/ObjectAL/freetype/box2d sim natives already ship as ios-arm64_x86_64-simulator xcframework slices in the -natives-ios jars. - assemble_arm64_sim_app: RoboVM 2.3.24's robovm:ipad-sim can't launch arm64 sims on Apple Silicon (its DeviceType table predates them) and aborts before bundling the .app. We finish with the standalone AppCompiler (-config + -properties + -skipsign -d), then fix the two platform tags it gets wrong for a simulator target: vtool re-stamps the main binary (device LC_VERSION_MIN_IPHONEOS -> BUILD_VERSION platform 7) and we swap each framework's device slice for the simulator slice, then ad-hoc sign. - JDK auto-detect: prefer a native arm64 JDK 17+ if installed, so the AOT compiler stops running under Rosetta. No-op until one is installed; the sim arch is pinned explicitly so a native JDK is otherwise harmless. Intel hosts keep the existing x86_64 path unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The native-JDK use is an optional accelerator, not a requirement — the build works on an x86_64 JDK, just slower under Rosetta. When on Apple Silicon with no arm64 JDK 17+ found, print a one-time, non-failing note pointing at the aarch64 Temurin download (and the gotcha that Homebrew's temurin@17 cask under an Intel brew installs the x86_64 build). No behavior change when a native JDK is present. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Thanks — catching up on your review, and I fixed the The <profiles>
<profile>
<id>ios</id>
<modules>
<module>forge-gui-ios</module>
</modules>
</profile>
</profiles>So On your three original points: a) User delivery — good news: no jailbreak is needed at any point. Three working paths, in increasing order of convenience:
b) CI/CD — done, two tiers so the runtime cost lands where you suggested:
c) Sentry — done and device-verified. The reason it was stubbed isn't a platform incompatibility: the Verified on hardware (iPad, iOS 26): the previously-crashing choose-cards flows (Cultivate/Genesis Hydra library searches) work with the real SDK linked, and init/capture/transport all function. I have iOS reporting to the mobile project (2), matching Android, rather than desktop's project 3 — let me know if that's not how you'd want mobile events routed. A couple of things I ran into while verifying delivery — quite possibly me holding it wrong, so I'd value your steer on how you want this set up:
iOS is wired to the mobile project's DSN ( On the "workarounds in the neighbour modules" aside: addressed. iOS-specific behavior in |
|
Addressed all the Copilot findings (plus one bonus simplification) — pushed as two commits: Copilot fixes:
Bonus: the Scryfall "can't follow 302 redirects" workaround in Verifying that also led me to the real image-fetch failure the workaround was mis-attributed to, and it turned out to be a bigger fish — a file-descriptor exhaustion (EMFILE) bug that I've now fixed (separate commit). iOS gives each process a 256-fd soft limit; the app's classpath jars get pinned open by ordinary classloader resource probes (~177 fds), a loaded match holds ~70 more, and the game-start image-fetch burst opens dozens of sockets on top. Once it tips past 256 everything fails at once, and none of it announces "EMFILE": lazy texture opens throw |
Yes — happy to. This port is what I play on weekly across two iPads. |
|
RE Sentry:
|
Both are relics of the old hand-backport approach, flagged in review: - The io.sentry.** entry carried a duplicate "No-op Sentry stubs / real SDK is excluded on iOS" comment — untrue since real Sentry was enabled; the correct entry + comment already exist lower in the file. Removed the stale comment and its duplicate pattern. - The "java.util.function is NOT available - use forge.util.function backports instead" NOTE is false under the jvmdg pipeline: the bridge remaps java/util/function -> streamsupport, unmodified upstream uses java.util.function in 155 files, and forge.util.function is used in 0. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Review flagged FSkin.getFileHandle as redundant to Assets.getFileHandle. They did the same iOS internal()/absolute() resolution; unify into one. The canonical helper now lives in Assets, next to HybridFileHandleResolver (file resolution is an Assets concern, and this respects the existing FSkin -> Assets dependency direction rather than inverting it). It keeps the startsWith(ASSETS_DIR) guard so writable cache/Documents paths keep absolute() on iOS. FSkin/FSkinFont now call Assets.getFileHandle; the old private Assets copy (unguarded, one caller) and the FSkin copy are gone. No behavior change — same-package access, no new imports. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ipeline # Conflicts: # forge-gui-mobile/src/forge/adventure/util/Config.java
Follow-up to the maintainer's "Minor clean up" — the same fqn-to-import tidy applied to our other iOS files, all where the type is (or is now) imported and there's no name clash. No behavior change. - Main.java: java.io.File param -> File - OSLogPrintStream.java: java.nio.charset.StandardCharsets (x2) -> import - ImageCache.java: java.util.IdentityHashMap / LinkedHashMap / java.io.FileInputStream -> imports - Assets.java: java.util.LinkedHashMap (x3) + java.util.Map.Entry -> imports Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ecation) GitHub is force-running the @v4 actions on Node 24 and warning that Node 20 is deprecated. Pin the lowest major of each that natively targets Node 24 (verified against each action's action.yml runs.using): actions/checkout v4 -> v5 actions/setup-java v4 -> v5 actions/cache v4 -> v5 actions/upload-artifact v4 -> v6 (v5 is still Node 20; v6 is the fix) No input changes — checkout takes none, and setup-java (distribution/ java-version/cache), cache (path/key) and upload-artifact (name/path/ if-no-files-found) keep the same inputs across these majors. Only our two iOS workflows are touched. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
@tool4ever , thanks for the feedback so far. Anything I can do to simplify the review? |
|
I'm fine with the code quality, we're just trying to test on a real device too |
|
Sounds good. I would like help verifying this on an android device. I run this on two physical iPads: an iPad (10th gen) on iOS 26.2 and an older iPad on iOS 16.7, so it's covered across a wide OS range. Full games vs. AI, adventure mode, and online multiplayer all work, and I've had this exact branch on-device (audio, card rendering, the memory fixes) throughout. |
Summary
Adds working iOS support for Forge. Instead of hand-backporting modern Java to MobiVM's Java‑7‑era runtime, it transforms the built jars so unmodified upstream Java runs on iOS:
-c 52) rewrites Java 9–17 bytecode (records,List.of,String.repeat, switch expressions, string‑concat indy, …) down to Java 8.streamsupport(java.util.function/stream/Optional), relocated ThreeTen asjava.time, a hand‑builtjava.nio.filesupply, andforge.compat.java/javaxmember reference in the final jars against the real runtime + supplies, so an upstream merge that introduces a new Java‑8 gap is caught at build time rather than as a device crash.The pipeline lives under
forge-gui-ios/pipeline/(ios-pipeline.sh [classpath|sim|device]) and bootstraps its third‑party jars automatically.iOS platform fixes
skipPngCrushper‑resource inrobovm.xml).keyTypedon iOS).util/*runtime helpers; guardProcessHandle/UPnP for MobiVM gaps.FLAG_SERIALIZABLE(was crashing games with interacting static abilities) +ServiceLoaderentry remapping (fixes time‑zone data registration).Shared/upstream source is otherwise unchanged except a few genuinely‑iOS platform hooks (adventure resource path when
Gdx.app.getType()==iOS, aTextRendererBreakIteratorfallback, and anIOSAdapteraudio‑format override).Testing
Device‑verified on physical iPads (iOS 16 and iOS 26): boots, plays full games vs. AI, adventure mode, audio, and online hosting (online hosting is buggy will fix in upcoming PR). Also runs in the iOS Simulator (same RoboVM build).
Notes for reviewers
.env;robovm.propertiesis generated fromrobovm.properties.templateon first pipeline run.🤖 Generated with Claude Code