Skip to content

fix(launch): Forge 1.17+ on Java 17+ via launch-profile rework#8

Merged
objz merged 12 commits into
masterfrom
launch-args-rework
May 26, 2026
Merged

fix(launch): Forge 1.17+ on Java 17+ via launch-profile rework#8
objz merged 12 commits into
masterfrom
launch-args-rework

Conversation

@objz
Copy link
Copy Markdown
Owner

@objz objz commented May 25, 2026

Summary

Fixes the reported crash where Forge 1.17+ fails to launch on Java 17+ with IllegalAccessError. The root cause was structural: rmcl only understood Mojang's legacy minecraftArguments string format and silently dropped the modern arguments: {game, jvm} object that ships the required --add-opens flags.

The fix is a six-phase rework that adds a unified launch_profile module (rule evaluator, template engine, profile model, inheritsFrom resolver, render layer), then rewires the vanilla launch path, Forge/NeoForge installer paths, legacy Forge install_profile.json path, and Fabric/Quilt install paths to read and write upstream Mojang JSON byte-for-byte instead of re-serializing through narrow structs.

Two pre-existing latent bugs collapsed out as a side effect: the two divergent library-rule evaluators became one, and the missing template substitution layer became a real engine.

What this changes for users

  • Forge 1.17+ launches cleanly on Java 17+ (the reported bug).
  • NeoForge gets the same treatment on the same code path.
  • Vanilla modern versions (1.13+) now honor their own arguments.jvm.
  • Existing 0.3.0 instances auto-migrate on first launch:
    • vanilla: re-fetches meta.json from Mojang, with offline fallback to the stale cache
    • Forge/NeoForge: rebuilds from the installer JSON that's still at <instance>/.minecraft/versions/<name>/<name>.json
    • Fabric/Quilt: skipped (their upstream shape matches the legacy predicate but they need a fresh install)
  • Legacy Forge (1.7.10 era) still requires lwjgl3ify on Java 9+ as before; patches.rs is untouched.

Test plan

  • 266 unit tests pass
  • cargo clippy --all-targets -- -D warnings clean
  • cargo fmt --check clean for touched files
  • Manual: Forge 1.20.6 launches on Java 17+, includes upstream --add-opens in JVM args
  • Manual: Fabric 1.20.6 launches without ${quickPlay*} or ${version_type} warnings
  • Manual: GTNH (1.7.10 + lwjgl3ify) launches via patches.rs (unchanged)
  • Verify: NeoForge launches on Java 17+ (same code path as Forge, expected fine)

History

11 commits, conceptually grouped:

Commits Summary
22e4481 Phase 1: foundation primitives (rules, templates, model)
2029005 Phase 2: vanilla launch flow + raw-byte storage
6301472 Phase 3: inheritsFrom resolution with merge_into + chain walker
0d00678 Phase 4: modern Forge/NeoForge (the user-facing fix)
8d2b5b9 Phase 5: legacy Forge install_profile.json
a7b378b Phase 6: Fabric/Quilt raw-bytes storage + migration skip
1988764, 412e6ff, 363ada8 Code review fixes (critical / robustness / style)
d88ab08, f453518 Bugs surfaced by manual 1.20.6 testing

objz added 12 commits May 20, 2026 16:00
introduce the launch_profile module with the core types and pure functions
needed to parse, evaluate, and substitute against mojang-format version
JSON: Rule/OsCondition/FeatureSet with a single rule evaluator,
TemplateContext + substitute() for ${var} expansion, and the
LaunchProfile schema mirroring mojang's on-disk shape. no consumers yet.
…storage

vanilla launches now read upstream mojang JSON via the new LaunchProfile
path, render game/JVM args through launch_profile::render with template
substitution, and store the cached meta.json byte-for-byte instead of
re-serializing through a narrow struct. legacy stripped meta.json files
are re-fetched on first launch. adds HttpClient::get_bytes and
fetch_version_meta_with_raw; collapses the two library-rule evaluators
into one shared launch_profile::rules::evaluate.
new launch_profile::resolve module walks a profile's inheritsFrom chain,
loading parents from meta_dir/versions/<id>/meta.json and merging them
into a single flat LaunchProfile per mojang's documented semantics. pure
merge_into handles field-level merging; async resolve walks the chain
with HashSet-based cycle detection and an 8-level depth cap.
stop stripping the installer's version JSON in save_installer_profile -
copy it byte-for-byte so the upstream arguments.jvm (with the
--add-opens flags forge 1.17+ needs on java 17+) reaches launch time.
launch loads the loader profile as LaunchProfile, sets an implicit
inheritsFrom = game_version when absent, resolves through vanilla, then
renders args. classpath assembly unifies into one loop handling both
vanilla-style and loader-style libraries. lazy migration of pre-rework
loader-profiles from the installer's version JSON when still on disk.

fixes the reported Forge 1.17+ on java 17+ IllegalAccessError.
install_forge_from_profile now writes the upstream versionInfo as compact
JSON (no key reordering, no field stripping) instead of building a custom
{mainClass, libraries, gameArguments} shape. legacy 1.7.10-era Forge
profiles ride the same LaunchProfile + resolve + render pipeline as
modern Forge. drops the no-longer-needed extract_extra_game_args helper.
stop re-serializing FabricProfile/QuiltProfile through narrow structs;
new fetch_X_profile_with_raw helpers return both the parsed shape (for
library downloads) and the upstream bytes (written byte-for-byte to the
loader-profiles cache). also fixes a latent issue surfaced by the
change: the legacy-loader migration predicate now skips Fabric and Quilt
explicitly since their upstream shape matches the predicate but they
have no installer JSON to recover from.
…ary dedup

extra_args from the patches module was being passed to build_game_args
(which appended it to game_args) AND emitted again via cmd.args before
game_args - a regression that doubled legacy Forge + lwjgl3ify shim
args. extra_args is now passed only once. also, merge_into now dedups
libraries by group:artifact, preferring the child's entry - without
this, Forge/NeoForge library overrides of vanilla (e.g. log4j) would
silently lose to vanilla because the JVM picks the first classpath
match.
several smaller fixes from the code review:
 - add os_version to RuleContext + OsCondition for mojang rules that
   constrain natives selection by host version (rare, but in the spec).
 - factor mojang_os_name/arch/version into a shared launch_profile::system
   module instead of duplicating between net/mojang.rs and launch/mod.rs.
 - get_json and get_bytes now retry transient failures (5xx, connect,
   body decode) via a shared get_with_retry envelope; previously only
   download_file had retry logic.
 - migrate_legacy_meta_if_needed falls back to the stale cached profile
   with a warn log on network failure instead of erroring out (offline
   launches now work).
 - tightened legacy-loader detection by requiring the unique-to-rmcl
   gameArguments field; clearer error when loader_version is absent.
 - forge legacy versionInfo write uses compact serialization (no
   pretty-print key reorder).
 - deleted dead fetch_version_meta and save_profile_json.
 - documented quick-play templates omission in templates::lookup.
 - added end-to-end test that loads a vanilla profile from disk, loads a
   loader profile with inheritsFrom, resolves, renders, and snapshots.
 - drop stale phase planning comments from launch_profile module headers.
 - simplify merge_libraries to HashSet<&str> (no string clones).
 - rewrite coord_key with match_indices (clearer intent).
 - read /proc/sys/kernel/osrelease on linux for mojang_os_version
   instead of shelling out to uname; return empty elsewhere.
 - add comment explaining why is_retryable excludes Parse.
 - shorten the launch_profile::... paths in launch/mod.rs with top-level
   use statements.
 - derive Default on LaunchProfile + nested types so test fixtures use
   ..Default::default() instead of listing every None field.
 - inline account_can_launch at its single call site, delete the helper
   and three tests that exercised trivially-correct one-line logic.
 - extract get_json_with_raw on HttpClient and save_profile_bytes in
   instance::loader; collapses three near-identical fetch_X_profile_with_raw
   helpers and three near-identical 'create dir + write bytes' blocks.
…t on classpath

modern forge's installer places some libraries (notably the bootstrap
library that defines ForgeBootstrap / BootstrapLauncher) into the
instance's .minecraft/libraries/ rather than the shared meta cache. the
unified classpath loop introduced in the rework checked downloads.artifact
first and pushed meta_dir/libraries/<path> without ever consulting the
local installer dir for libraries that had downloads.artifact set.

resolve a single relative path per library (downloads.artifact.path if
present, else maven_coord_to_path(name)), then for has_local_libs check
the local installer dir first regardless of which path source we used.
fixes ClassNotFoundException at launch on forge 1.20.6 and likely
similar versions.
two related bugs surfaced by manual testing on 1.20.6 vanilla and fabric:

1. FeatureSet missing has_quick_plays_support / is_quick_play_singleplayer /
   is_quick_play_multiplayer / is_quick_play_realms. mojang's profiles gate
   the quick-play arguments behind these features. serde silently dropped
   the unknown fields during deserialization, leaving an empty FeatureSet
   that matched any context, so the conditional arguments emitted raw
   ${quickPlay*} placeholders to the JVM that template substitution then
   warned about. add the fields explicitly and extend features_match to
   reject any required feature the context doesn't set.

2. TemplateContext missing version_type. ${version_type} is an
   unconditional template in modern vanilla's arguments.game (renders the
   profile's "type" field, e.g. "release"). without it, the substitution
   emitted a literal ${version_type} to minecraft and warned. add the
   field, plumb the merged profile's type_ value through, default to
   "release" when absent.
@objz objz self-assigned this May 25, 2026
@objz objz merged commit efcef1c into master May 26, 2026
13 checks passed
@objz objz deleted the launch-args-rework branch May 26, 2026 18:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

LoaderProfileJson does not parse "arguments" object format, missing arguments.jvm and arguments.game

1 participant