Skip to content

feat(gamebridge): launch an instance with devbridge switched on - #6

Merged
Flatts3000 merged 2 commits into
mainfrom
feat/gamebridge-launch
Aug 5, 2026
Merged

feat(gamebridge): launch an instance with devbridge switched on#6
Flatts3000 merged 2 commits into
mainfrom
feat/gamebridge-launch

Conversation

@Flatts3000

Copy link
Copy Markdown
Owner

What this changes

Adds gamebridge launch, which starts a CurseForge instance with devbridge switched on and a world
loaded. Closes #5.

gamebridge launch --instance "C:/.../Instances/Trashlands" --port 8604 --world "New World" \
                  --width 1280 --height 720 --wait

Route 1, and why it was cheaper than the issue estimated

The issue priced route 1 as "reimplements a launcher... realistically several PRs". That estimate
assumed resolving and downloading libraries. It turns out the CurseForge app already keeps a
vanilla-shaped install beside its instances - versions/, libraries/, assets/, natives/
and bundled JREs under Install/. So this parses manifests in a documented format and downloads
nothing.

It also only ever reads minecraftinstance.json. The issue warned that writing that file is a
known way to break an instance; that risk does not apply to a reader, which removes the main
objection to this route.

The work is: merge the loader manifest with what it inheritsFrom, apply library and argument
rules, build the classpath, substitute ${placeholders}, pick the JRE the manifest asks for, and
exec java with -Ddevbridge.port and --quickPlaySingleplayer.

Two bugs worth keeping in the record

Present-but-null is not the same as absent. The NeoForge manifest carries "assetIndex": null
and "jar": null, so get(key, default) returns the null instead of falling through to vanilla's
real value. Caught by a dry run; would have started a game with no assets.

Rule-gated arguments come from the manifest and nowhere else. Vanilla defines
--quickPlaySingleplayer, --width/--height and --quickPlayPath as feature-gated entries with
placeholders. Appending them by hand as well passed the flag twice, and jopt-simple rejects a
repeated single-value option with Found multiple arguments for option, which reads like a
malformed world name rather than a duplicated flag. Only found by actually launching - the dry
run looked fine to me until I diffed the flag counts.

Verified, end to end, against a real instance

This is the first thing in this repo proven against a running game rather than by inspection.

  • --dry-run produces a command whose 102 classpath entries all exist on disk (0 missing), with
    the bundled java-runtime-epsilon JRE and net.neoforged.fml.startup.Client.
  • A real launch booted the Trashlands pack, quick-played into "New World" as offline player Dev,
    and devbridge answered on [::1]:8604. netstat -p TCPv6 confirmed the listener's PID was the
    process this launched, not something already running - the wrong-game hazard the issue describes.
  • --wait reported up: integrated, protocol 1, so the version handshake works against a freshly
    built jar.
  • Full sequence through the launched game: cmd as @s returned
    Set minecraft:overworld to time marker minecraft:day; input, pause and hud all replied;
    screenshot wrote a valid PNG at exactly 1280x720, proving the resolution arguments took
    effect; stop closed it cleanly.
  • The stale jar already in the instance has no protocol field, and the client's version guard
    fired on it with both numbers named. That is the drift check from feat: bring gamebridge into this repo, and version the protocol #4 working on a real mismatch.

That run also retroactively verifies #3, which merged unverified: the ping reply showed
"pauseOnLostFocus": false and "inputLocked": true from a live client.

Note

The instance's mods/devbridge-26.1.2-0.1.0.jar was refreshed from build/libs during testing,
since the copy there predated the protocol field. That is ordinary after a rebuild, but worth
flagging as a change made outside this repo.

Closes #5.

The last manual step in the loop. Everything after launch already worked - commands,
screenshots, stop - but starting the game needed a person alt-tabbing to a launcher, which
is enough to stop the whole thing running unattended.

Route 1 from the issue, and much cheaper than the issue estimated. The CurseForge app
already keeps a vanilla-shaped install beside its instances: versions/, libraries/,
assets/, natives/ and bundled JREs. So this reads manifests in a documented format rather
than reverse-engineering a launcher, and downloads nothing. It also only ever reads
minecraftinstance.json - writing that file is a known way to break an instance, and the
risk the issue warned about does not apply to a reader.

What it does: merge the loader manifest with what it inherits from, apply library and
argument rules, build the classpath, fill in the placeholders, pick the JRE the manifest
asks for, and exec java with -Ddevbridge.port and --quickPlaySingleplayer.

Two details worth keeping:

Present-but-null is not the same as absent. The NeoForge manifest carries "assetIndex":
null and "jar": null, so a get() with a default silently loses vanilla's real values and
the game starts with no assets. Both now fall through to the parent.

Rule-gated arguments come from the manifest and nowhere else. Appending
--quickPlaySingleplayer by hand as well passed it twice, and jopt-simple rejects a repeated
single-value option with "Found multiple arguments for option", which reads like a
malformed world name rather than a duplicated flag. Found by launching, not by reading.

verify() checks every classpath entry exists before starting anything, because a missing
jar otherwise surfaces as a NoClassDefFoundError several seconds in, in a window that may
already have closed.
…gh failures

Review of this branch.

verify() tested Path(java).is_file(), which is false for a bare command name. Both the
--java override and _bundled_java's own fallback produce exactly that when a manifest names
no runtime, so a working java reported "java binary not found". Bare names now resolve
through shutil.which; anything with a separator in it is still checked as a file.

--dry-run exited before printing whenever verification failed, which is backwards: the flag
exists to show why a launch would not work, and the case somebody is debugging is precisely
the one where it withheld the command line. It now prints the command first and reports the
problems after.

offline_uuid passes usedforsecurity=False. md5 here has to match the algorithm the vanilla
server uses and is not a security choice; saying so keeps it working on a FIPS-mode
interpreter, where a plain md5() refuses to run at all.
@Flatts3000
Flatts3000 merged commit 36ab8d5 into main Aug 5, 2026
1 check passed
@Flatts3000
Flatts3000 deleted the feat/gamebridge-launch branch August 5, 2026 00:56
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.

gamebridge launch: start an instance and load a world with no human in the loop

1 participant