feat(gamebridge): launch an instance with devbridge switched on - #6
Merged
Conversation
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.
This was referenced Aug 5, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What this changes
Adds
gamebridge launch, which starts a CurseForge instance with devbridge switched on and a worldloaded. Closes #5.
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 downloadsnothing.
It also only ever reads
minecraftinstance.json. The issue warned that writing that file is aknown 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 argumentrules, build the classpath, substitute
${placeholders}, pick the JRE the manifest asks for, andexec java with
-Ddevbridge.portand--quickPlaySingleplayer.Two bugs worth keeping in the record
Present-but-null is not the same as absent. The NeoForge manifest carries
"assetIndex": nulland
"jar": null, soget(key, default)returns the null instead of falling through to vanilla'sreal 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/--heightand--quickPlayPathas feature-gated entries withplaceholders. 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 amalformed 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-runproduces a command whose 102 classpath entries all exist on disk (0 missing), withthe bundled
java-runtime-epsilonJRE andnet.neoforged.fml.startup.Client.Dev,and devbridge answered on
[::1]:8604.netstat -p TCPv6confirmed the listener's PID was theprocess this launched, not something already running - the wrong-game hazard the issue describes.
--waitreportedup: integrated, protocol 1, so the version handshake works against a freshlybuilt jar.
cmdas@sreturnedSet minecraft:overworld to time marker minecraft:day;input,pauseandhudall replied;screenshotwrote a valid PNG at exactly 1280x720, proving the resolution arguments tookeffect;
stopclosed it cleanly.protocolfield, and the client's version guardfired 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": falseand"inputLocked": truefrom a live client.Note
The instance's
mods/devbridge-26.1.2-0.1.0.jarwas refreshed frombuild/libsduring 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.