Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -99,3 +99,4 @@ __pycache__
.claude
# EDHREC commander matrix tool intermediates
forge-gui/tools/build
forge-gui-ios/robovm.properties
48 changes: 47 additions & 1 deletion forge-gui-ios/Info.plist.xml
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
</array>
<key>UIRequiredDeviceCapabilities</key>
<array>
<string>armv7</string>
<string>arm64</string>
</array>
<key>UISupportedInterfaceOrientations</key>
<array>
Expand All @@ -57,5 +57,51 @@
</array>
</dict>
</dict>
<key>UILaunchStoryboardName</key>
<string></string>
<key>UIRequiresFullScreen</key>
<true/>
<key>UIViewControllerBasedStatusBarAppearance</key>
<false/>
<key>UIStatusBarHidden</key>
<true/>
<key>UIFileSharingEnabled</key>
<true/>
<key>LSSupportsOpeningDocumentsInPlace</key>
<true/>
<key>UIUserInterfaceStyle</key>
<string>Light</string>
<key>NSLocalNetworkUsageDescription</key>
<string>Forge uses the local network to discover and connect to multiplayer game servers.</string>
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<false/>
<key>NSAllowsArbitraryLoadsInWebContent</key>
<false/>
<key>NSAllowsLocalNetworking</key>
<true/>
<key>NSExceptionDomains</key>
<dict>
<key>scryfall.com</key>
<dict>
<key>NSIncludesSubdomains</key>
<true/>
<key>NSTemporaryExceptionAllowsInsecureHTTPLoads</key>
<false/>
<key>NSTemporaryExceptionMinimumTLSVersion</key>
<string>TLSv1.2</string>
</dict>
<key>api.scryfall.com</key>
<dict>
<key>NSIncludesSubdomains</key>
<true/>
<key>NSTemporaryExceptionAllowsInsecureHTTPLoads</key>
<false/>
<key>NSTemporaryExceptionMinimumTLSVersion</key>
<string>TLSv1.2</string>
</dict>
</dict>
</dict>
</dict>
</plist>
Binary file added forge-gui-ios/fallback_skin/adv_bg_splash.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added forge-gui-ios/fallback_skin/adv_bg_texture.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added forge-gui-ios/fallback_skin/bg_splash.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added forge-gui-ios/fallback_skin/bg_texture.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added forge-gui-ios/fallback_skin/font1.ttf
Binary file not shown.
Binary file added forge-gui-ios/fallback_skin/title_bg_lq.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added forge-gui-ios/fallback_skin/transition.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added forge-gui-ios/libs/libForgeOSLog.a
Binary file not shown.
11 changes: 11 additions & 0 deletions forge-gui-ios/oslog_wrapper/ForgeOSLog.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// ForgeOSLog.h - Header for os_log wrapper

#ifndef ForgeOSLog_h
#define ForgeOSLog_h

void forge_os_log_public(const char *message);
void forge_os_log_info(const char *message);
void forge_os_log_debug(const char *message);
void forge_os_log_error(const char *message);

#endif /* ForgeOSLog_h */
23 changes: 23 additions & 0 deletions forge-gui-ios/oslog_wrapper/ForgeOSLog.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// ForgeOSLog.m - Simple os_log wrapper for RoboVM
// This wraps os_log since it's a C macro that can't be called directly from Java

#import <os/log.h>
#import <Foundation/Foundation.h>

// Simple function to log a public message using os_log
void forge_os_log_public(const char *message) {
os_log_with_type(OS_LOG_DEFAULT, OS_LOG_TYPE_DEFAULT, "%{public}s", message);
}

// Log with different levels
void forge_os_log_info(const char *message) {
os_log_with_type(OS_LOG_DEFAULT, OS_LOG_TYPE_INFO, "%{public}s", message);
}

void forge_os_log_debug(const char *message) {
os_log_with_type(OS_LOG_DEFAULT, OS_LOG_TYPE_DEBUG, "%{public}s", message);
}

void forge_os_log_error(const char *message) {
os_log_with_type(OS_LOG_DEFAULT, OS_LOG_TYPE_ERROR, "%{public}s", message);
}
Binary file added forge-gui-ios/oslog_wrapper/libForgeOSLog.a
Binary file not shown.
84 changes: 84 additions & 0 deletions forge-gui-ios/pipeline/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
# iOS modern-Java pipeline

MobiVM/RoboVM ships a **Java-7-era runtime library** (no `java.util.function`,
`java.util.stream`, `Optional`, `java.time`, `java.nio.file`, and none of the
Java 8 default/static members on `Map`, `List`, `Comparator`, `String`, ...),
while upstream Forge freely uses Java 17. Instead of hand-backporting source
on every merge, this pipeline transforms the **built jars**:

| Stage | Tool | What it does |
|---|---|---|
| 1 | [JvmDowngrader](https://github.com/unimined/JvmDowngrader) `-c 52` | Java 9–17 → Java 8 bytecode: records de-sugared, `List.of`, `String.repeat`, switch expressions, string-concat indy. Lambdas stay `invokedynamic` (MobiVM handles those natively). |
| 2 | `MobiVmBridge` (`src/`) | The Java 8 library surface missing from MobiVM → [streamsupport](https://github.com/stefan-zobel/streamsupport) backports (`java8.*`), `forge.compat` rewrite targets, and app-supplied `java.*` classes. Rules in `bridge.cfg`. `java.time` is the ThreeTen backport relocated in-place (`relocate-time.cfg`); `java.nio.file` is the minimal supply in `java-supply/`. |
| 3 | `MobiVmLinkAudit` (`src/`) | Resolves **every** member reference in the final jars against the real runtime + supplies. Its report *is* the porting workload after an upstream merge — usually empty, occasionally one new `bridge.cfg` line. |

## Usage

```bash
# after every upstream merge / module rebuild:
mvn clean install -pl forge-core,forge-game,forge-gui,forge-gui-mobile,forge-ai -DskipTests
pipeline/ios-pipeline.sh classpath # transform -> tmp/ios-m2, prints audit
pipeline/ios-pipeline.sh sim # build + install + launch simulator
pipeline/ios-pipeline.sh device # build + sign + install to iPad
```

First run bootstraps third-party jars (JvmDowngrader, streamsupport,
ThreeTen) into `tmp/jvmdg/` automatically. Device IDs / signing identity are
env-overridable — see the header of `ios-pipeline.sh`.

## Key facts (hard-won — see git history of feature/ios-jvmdg-pipeline)

- JvmDowngrader `-c 51` (true Java 7 target) is a dead end: its 8→7 stub
bodies throw `MissingStubError`. `-c 52` is the mature path; the bridge
covers the 8→MobiVM gap with *real* implementations (streamsupport).
- streamsupport interfaces are bare SAMs: statics/defaults live in companion
classes (`RefStreams`, `Predicates`, `Comparators`, `Maps`, ...) — that's
what most `bridge.cfg` member rules encode, receiver-prepended.
- The bridge renames jar **entry paths** when whole-type remapping (RoboVM
locates classes by entry path) and rewrites method-reference `Handle`s
inside invokedynamic bootstrap args.
- App-class owners matter: forge collections inherit Java 8 defaults
(`CardCollection.forEach`), so the bridge resolves *every* indexed owner
through its hierarchy, not just `java/*`.
- `forge-gui-ios` sources compile against **untransformed** `~/.m2` jars,
then its `target/classes` go through the same downgrade+bridge — types line
up at the bytecode level.
- libGDX/RoboVM jars are excluded from transformation (MobiVM-clean by
construction); they participate in the resolution index only.
- The RoboVM AOT cache is content-hashed — `SKIP_CACHE_CLEAR=1` is safe and
saves ~15 min per build.
- RoboVM's AOT lambda plugin silently drops `altMetafactory`'s
`FLAG_SERIALIZABLE` (it only implements `FLAG_MARKERS`/`FLAG_BRIDGES`), so a
serializable lambda — `(Supplier<T> & Serializable) X::new`, ECJ-compiled
jgrapht is full of them — comes out NOT implementing `Serializable` and the
compiler-emitted `checkcast java/io/Serializable` throws at runtime. The
bridge converts the ignored bit into an explicit `FLAG_MARKERS` +
`java/io/Serializable` marker, which RoboVM honors; the audit fails on any
site that reaches the final jars without the marker. CAVEAT: this restores
type-compatibility only — RoboVM generates no `writeReplace`, so actually
Java-serializing such a lambda still throws `NotSerializableException`
(nothing does that today).
- `META-INF/services/` entries are class names in disguise: the bridge remaps
both the entry name and the impl lines through the type rules (miss this
and e.g. the relocated ThreeTen zone-rules provider never registers —
`java.time` works until the first `ZoneId.systemDefault()`, then throws
"No time-zone data files registered"). The audit flags any services entry
naming a class that exists nowhere on the final classpath.

## Bundle identifier (bring your own)

`forge-gui-ios/robovm.properties` is **untracked** because the bundle
identifier is developer-specific. Register your own App ID, put
`APP_ID=<your.bundle.id>` in the untracked `.env` at the repo root, and the
pipeline generates `robovm.properties` from `robovm.properties.template` on
first run (or copy the template manually and fill in `@APP_ID@`). Signing
settings (`SIGN_ID`, `PROFILE`, `TEAM_ID`) and device UDIDs also live in
`.env` — nothing machine- or identity-specific is tracked.

## Fresh-clone bootstrap

`ios-pipeline.sh` also builds and installs every `forge-gui-ios/pom.xml`
supply dependency into `~/.m2` on first run (streamsupport, the jvmdg api
jar, `java-nio-supply`, `java-function-stubs`, the sentry no-op stubs via
`sentry-stub/build.sh`, and a `java-time-supply` compile placeholder) — a
fresh clone needs only a JDK 17, Maven, Xcode, and an `.env`.
168 changes: 168 additions & 0 deletions forge-gui-ios/pipeline/bridge.cfg
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
# MobiVM bridge rules: map Java 8 library surface missing from MobiVM's
# Java-7-era runtime onto streamsupport / forge.compat backports.
# Applied AFTER JvmDowngrader -c 52 (which handles all Java 9+ surface).
#
# type <internalNamePrefix> <replacementPrefix> — whole-type remap
# member <declaringOwner> <name> <target> [PREPEND <rcv>] — unresolved-member redirect

# ---------- whole-type remaps: classes absent from robovm-rt ----------
type java/util/function/ java8/util/function/
type java/util/stream/ java8/util/stream/
type java/util/Optional java8/util/Optional
type java/util/Spliterator java8/util/Spliterator
type java/util/PrimitiveIterator java8/util/PrimitiveIterator
type java/util/StringJoiner java8/util/StringJoiner
type java/util/DoubleSummaryStatistics java8/util/DoubleSummaryStatistics
type java/util/IntSummaryStatistics java8/util/IntSummaryStatistics
type java/util/LongSummaryStatistics java8/util/LongSummaryStatistics
type java/util/concurrent/CompletableFuture java8/util/concurrent/CompletableFuture
type java/util/concurrent/CompletionStage java8/util/concurrent/CompletionStage
type java/util/concurrent/CompletionException java8/util/concurrent/CompletionException
# java.lang.management is absent on MobiVM; remap the whole package onto the
# Runtime-backed compat implementations (apfloat/tinylog probe it at boot)
type java/lang/management/ forge/compat/mgmt/

# ---------- Map + ConcurrentMap defaults ----------
member java/util/Map getOrDefault java8/util/Maps PREPEND Ljava/util/Map;
member java/util/Map putIfAbsent java8/util/Maps PREPEND Ljava/util/Map;
member java/util/Map computeIfAbsent java8/util/Maps PREPEND Ljava/util/Map;
member java/util/Map computeIfPresent java8/util/Maps PREPEND Ljava/util/Map;
member java/util/Map compute java8/util/Maps PREPEND Ljava/util/Map;
member java/util/Map merge java8/util/Maps PREPEND Ljava/util/Map;
member java/util/Map forEach java8/util/Maps PREPEND Ljava/util/Map;
member java/util/Map replaceAll java8/util/Maps PREPEND Ljava/util/Map;
member java/util/Map replace java8/util/Maps PREPEND Ljava/util/Map;
member java/util/Map remove java8/util/Maps PREPEND Ljava/util/Map;
member java/util/Map$Entry comparingByKey java8/util/Maps$Entry
member java/util/Map$Entry comparingByValue java8/util/Maps$Entry

# ---------- List / Collection / Iterable / Iterator defaults ----------
member java/util/List sort java8/util/Lists PREPEND Ljava/util/List;
member java/util/List replaceAll java8/util/Lists PREPEND Ljava/util/List;
member java/util/Collection stream java8/util/stream/StreamSupport PREPEND Ljava/util/Collection;
member java/util/Collection parallelStream java8/util/stream/StreamSupport PREPEND Ljava/util/Collection;
member java/util/Collection removeIf java8/lang/Iterables PREPEND Ljava/lang/Iterable;
member java/util/Collection spliterator java8/lang/Iterables PREPEND Ljava/lang/Iterable;
member java/lang/Iterable forEach java8/lang/Iterables PREPEND Ljava/lang/Iterable;
member java/lang/Iterable spliterator java8/lang/Iterables PREPEND Ljava/lang/Iterable;
member java/util/Iterator forEachRemaining java8/util/Iterators PREPEND Ljava/util/Iterator;

# ---------- Comparator statics + defaults ----------
member java/util/Comparator comparing java8/util/Comparators
member java/util/Comparator comparingInt java8/util/Comparators
member java/util/Comparator comparingLong java8/util/Comparators
member java/util/Comparator comparingDouble java8/util/Comparators
member java/util/Comparator naturalOrder java8/util/Comparators
member java/util/Comparator reverseOrder java8/util/Comparators
member java/util/Comparator nullsFirst java8/util/Comparators
member java/util/Comparator nullsLast java8/util/Comparators
member java/util/Comparator reversed java8/util/Comparators PREPEND Ljava/util/Comparator;
member java/util/Comparator thenComparing java8/util/Comparators PREPEND Ljava/util/Comparator;
member java/util/Comparator thenComparingInt java8/util/Comparators PREPEND Ljava/util/Comparator;
member java/util/Comparator thenComparingLong java8/util/Comparators PREPEND Ljava/util/Comparator;
member java/util/Comparator thenComparingDouble java8/util/Comparators PREPEND Ljava/util/Comparator;

# ---------- static interface methods (streamsupport keeps these in
# companion classes; applied as exact-owner rules before the type remap) ----------
member java/util/stream/Stream of java8/util/stream/RefStreams
member java/util/stream/Stream ofNullable java8/util/stream/RefStreams
member java/util/stream/Stream empty java8/util/stream/RefStreams
member java/util/stream/Stream concat java8/util/stream/RefStreams
member java/util/stream/Stream iterate java8/util/stream/RefStreams
member java/util/stream/Stream generate java8/util/stream/RefStreams
member java/util/stream/IntStream of java8/util/stream/IntStreams
member java/util/stream/IntStream range java8/util/stream/IntStreams
member java/util/stream/IntStream rangeClosed java8/util/stream/IntStreams
member java/util/stream/IntStream empty java8/util/stream/IntStreams
member java/util/stream/IntStream concat java8/util/stream/IntStreams
member java/util/stream/IntStream iterate java8/util/stream/IntStreams
member java/util/stream/IntStream generate java8/util/stream/IntStreams
member java/util/stream/LongStream of java8/util/stream/LongStreams
member java/util/stream/LongStream range java8/util/stream/LongStreams
member java/util/stream/LongStream rangeClosed java8/util/stream/LongStreams
member java/util/stream/LongStream empty java8/util/stream/LongStreams
member java/util/stream/LongStream concat java8/util/stream/LongStreams
member java/util/stream/DoubleStream of java8/util/stream/DoubleStreams
member java/util/stream/DoubleStream empty java8/util/stream/DoubleStreams
member java/util/stream/DoubleStream concat java8/util/stream/DoubleStreams
member java/util/function/Function identity java8/util/function/Functions
member java/util/function/Predicate isEqual forge/compat/JFunction8

# ---------- SAM-interface default methods (streamsupport interfaces are bare
# SAMs; defaults live in companion classes, receiver prepended) ----------
member java/util/function/Predicate and java8/util/function/Predicates PREPEND Ljava/util/function/Predicate;
member java/util/function/Predicate negate java8/util/function/Predicates PREPEND Ljava/util/function/Predicate;
member java/util/function/Predicate or java8/util/function/Predicates PREPEND Ljava/util/function/Predicate;
member java/util/function/Function andThen java8/util/function/Functions PREPEND Ljava/util/function/Function;
member java/util/function/Function compose java8/util/function/Functions PREPEND Ljava/util/function/Function;
member java/util/function/Consumer andThen java8/util/function/Consumers PREPEND Ljava/util/function/Consumer;
member java/util/function/BinaryOperator minBy java8/util/function/BinaryOperators
member java/util/function/BinaryOperator maxBy java8/util/function/BinaryOperators
member java/util/stream/Collector of java8/util/stream/Collectors

# ---------- Objects / Arrays ----------
member java/util/Objects isNull java8/util/Objects
member java/util/Objects nonNull java8/util/Objects
member java/util/Objects requireNonNull java8/util/Objects
member java/util/Arrays stream java8/util/J8Arrays
member java/util/Arrays setAll java8/util/J8Arrays
member java/util/Arrays parallelSort java8/util/J8Arrays
member java/util/Arrays spliterator java8/util/J8Arrays

# ---------- String / CharSequence ----------
member java/lang/String join forge/compat/JString8
member java/lang/String chars forge/compat/JString8 PREPEND Ljava/lang/CharSequence;
member java/lang/String codePoints forge/compat/JString8 PREPEND Ljava/lang/CharSequence;
member java/lang/CharSequence chars forge/compat/JString8 PREPEND Ljava/lang/CharSequence;
member java/lang/CharSequence codePoints forge/compat/JString8 PREPEND Ljava/lang/CharSequence;

# ---------- Math / boxed primitives (Java 8 statics) ----------
member java/lang/Math floorDiv forge/compat/JMath8
member java/lang/Math floorMod forge/compat/JMath8
member java/lang/Math toIntExact forge/compat/JMath8
member java/lang/Math addExact forge/compat/JMath8
member java/lang/Math subtractExact forge/compat/JMath8
member java/lang/Math multiplyExact forge/compat/JMath8
member java/lang/Integer sum forge/compat/JBoxed8
member java/lang/Integer max forge/compat/JBoxed8
member java/lang/Integer min forge/compat/JBoxed8
member java/lang/Integer hashCode forge/compat/JBoxed8
member java/lang/Long sum forge/compat/JBoxed8
member java/lang/Long max forge/compat/JBoxed8
member java/lang/Long min forge/compat/JBoxed8
member java/lang/Long hashCode forge/compat/JBoxed8
member java/lang/Double sum forge/compat/JBoxed8
member java/lang/Double max forge/compat/JBoxed8
member java/lang/Double min forge/compat/JBoxed8
member java/lang/Double hashCode forge/compat/JBoxed8
member java/lang/Float hashCode forge/compat/JBoxed8
member java/lang/Boolean hashCode forge/compat/JBoxed8
member java/lang/Boolean logicalAnd forge/compat/JBoxed8
member java/lang/Boolean logicalOr forge/compat/JBoxed8
member java/lang/ThreadLocal withInitial forge/compat/JThreadLocal8
member java/lang/Float sum forge/compat/JBoxed8
member java/lang/Float max forge/compat/JBoxed8
member java/lang/Float min forge/compat/JBoxed8
member java/lang/Float isFinite forge/compat/JBoxed8
member java/lang/Double isFinite forge/compat/JBoxed8
member java/lang/Integer compareUnsigned forge/compat/JBoxed8
member java/lang/Integer toUnsignedString forge/compat/JBoxed8
member java/lang/Integer toUnsignedLong forge/compat/JBoxed8
member java/lang/Long compareUnsigned forge/compat/JBoxed8
member java/lang/Long toUnsignedString forge/compat/JBoxed8
member java/lang/Byte toUnsignedInt forge/compat/JBoxed8
member java/lang/Short toUnsignedInt forge/compat/JBoxed8

# ---------- IO / reflection / nio bridge ----------
member java/io/BufferedReader lines forge/compat/JIo8 PREPEND Ljava/io/BufferedReader;
member java/io/File toPath forge/compat/JMisc8 PREPEND Ljava/io/File;
member java/lang/StringBuilder append forge/compat/JMisc8 PREPEND Ljava/lang/StringBuilder;
member java/lang/reflect/Type getTypeName forge/compat/JMisc8 PREPEND Ljava/lang/reflect/Type;
member java/lang/Class getTypeName forge/compat/JMisc8 PREPEND Ljava/lang/reflect/Type;
member java/nio/file/Files walk forge/compat/JMisc8
member java/nio/channels/FileChannel open forge/compat/JMisc8
member java/util/Date from forge/compat/JMisc8
member java/util/ResourceBundle getBaseBundleName forge/compat/JMisc8 PREPEND Ljava/util/ResourceBundle;
member java/util/regex/Matcher group forge/compat/JRegex8 PREPEND Ljava/util/regex/Matcher;
member java/util/regex/Matcher start forge/compat/JRegex8 PREPEND Ljava/util/regex/Matcher;
member java/util/regex/Matcher end forge/compat/JRegex8 PREPEND Ljava/util/regex/Matcher;
Loading