quilt-loader
quilt-loader copied to clipboard
[RFC] Add a generic GameProvider for launching *any* valid application and game.
Why?
This largely spawned from wanting to jerryrig a Mixin-capable loader to ProxyFox in order to inject suppressed exceptions at kotlin.ResultKt#throwOnFailure to provide a much better stack trace over KTor's own, frankly useless, stack trace that immediately enters into itself rather than the application.
With committing the 7 sins of modding in order to achieve this, I've noted that it's really not difficult to get Quilt to launch for other applications (4 classes, 3 of which could've been simply omitted if the internal APIs for LibClassifier were "public"), and feel that providing an official method of launching any application with Quilt Loader could be of benefit for anyone who wishes to have an easy way to access widen & mixin into Java applications for any number of reasons, including the one I highlighted above.
Goals
- Provide a GameProvider that anyone can use by simply dropping it in and point to the correct jar or class.
Non-goals
- Replace the existing, working MinecraftGameProvider class. This cannot replace it as the Minecraft provider has special patches for branding, and hides arguments with secrets.
- Be a drop in where another modding platform exists (i.e. Forge, Paper, Velocity). This would open up such a possibility for the end user to be able to do so, but this is not an intended use case, and there's no guarantees that the classes would even pass through Knot as is without intervention.
Potential implementation
As a valid Java application will generally provide a META-INF/MANIFEST.MF that gives a classpath, version and main class, it's possible to read the manifest and get information on the application before attempting to launch it.
-
locateGame would have to use either an argument (say, first argument, ambiguous) to determine the boot class or jar, or use an environment variable. It would also have to bootstrap the metadata that can be fetched from the manifest, filling in values that may otherwise have to be left blank or at defaults.
-
LibClassifier would have to be extended to allow for easily excluding Quilt Loader and its dependencies from being loaded through itself. Ideally, it should be made public with a usable API to allow for other GameProvider implementations to use it to find the game or application they're providing, and to find which libraries are safe to throw into Loader's classpath. A flag maybe provided to add itself to the classpath anyways if it is for any reason shaded into the same jar as the application it's running.
-
Logger wrappers should be considered generic and not Minecraft-specific, as the end application may happen to provide SLF4J and Log4J. Note that the Log4J wrapper then can't assume that the application doesn't use advanced features of the logger this point. This can likely be done within an
AbstractGameProvidershould one be created for this purpose. -
Ideally should run last when no other provider matches. This is mainly to prevent specialised providers that may provide patches or specific arguments from being inadvertently suppressed by the generic implementation.
-
Potentially refactor the GameProvider setup a bit to provide an abstract implementation to more easily build custom and generic providers for applications.
There's likely more to account for, given that the generic implementation requires some thought to implement well, including UX for drop-in & go, and APIs that would have to be exposed to make for a better experience implementing custom providers should it be required for any reason.
Since this would be useful as reference: https://docs.oracle.com/en/java/javase/17/docs/specs/jar/jar.html#jar-manifest
Also, a thing to account for: Hot loading agents should be considered given that application jars may provide one and expect it to be applied by the JVM normally.
To be honest, I'm a bit unhappy with how game providers work, since I don't think we really need to have a single generalized version of quilt-loader that everyone installs - instead I'd rather move towards having a quilt-loader-bare.jar (which can't launch anything - instead looking for a class named org.quiltmc.loader.impl.game.TheGameProvider on the classpath) and a quilt-loader-for-minecraft.jar, which can load only minecraft itself and nothing else. The "generic" GameProvider could be in a similar situation, named something like quilt-loader-general.jar and only containing a game provider which launches standard java applications in the way you've described.
I do agree with the general idea of adding this though.
I'm not sure how agent loading works, but this is the sort of thing that isn't necessarily required for every usecase of this so I assume it could be looked into later?
Fair. It doesn't necessarily have to be bundled in the main jar, and is that way with the current architecture; one is able to write an arbitrary game provider and just have it in the classpath. The only problem with that approach is that GameProvider isn't considered stable API, so, it's possible to break across versions; a small aspect already changed with the built-in mod metadata creation between 0.17 and 0.18.
If a generally modular approach is to be done, it might be a good idea to stablise the APIs relating to GameProvider, but not necessarily expose it to the end mod, so that way the game provider doesn't immediately break if updated independent of the loader. If it still manages to break, a try/catch block for each provider could provide helpful information as to why it may have broken (i.e. for an older version of Quilt, which the provider may optionally embed which it was built for).
Agent loading can be looked into later, since it's not necessarily required for most applications to function, and it's possible the agent is optional as far as the application is concerned.
Game providers unfortunately touch (and are called by) a lot of loader internals, so it's not really possible to stabilize those APIs. That's okay though since game providers aren't expected to be very common, and will likely have much more control over when they update their underlying version of quilt-loader.
This particular (generic) game provider should be another sub-project of this repository - like how minecraft specific stuff is in a sub-project. My main point is just that it should probably be built into a different jar file at the end.
That's fair. It's very likely that if you're using your own GameProvider that either... A: You're most likely going to be fine with using an older version. B: You can update it yourself, if it is part of the main project (i.e. bot's).
It's just considering that if they'll be detached JARs, it might be a good idea to have some mechanism to at least say on crash that Hey, this GameProvider <name or class> broke, try ${relative ? up : down}grading to Loader <whichever version it was originally built for if found> or ${relative ? down : up}dating the provider, so that the end user, should be using such a setup, isn't confused as to why it broke.
It's just considering that if they'll be detached JARs,
Oh, er, not all game providers actually - quilt-provided ones would be separate (complete) jars containing bare quilt-loader plus the game-specific stuff.
It sounds really painful to isolate every use of game provider and blame errors specifically on that game provider, so I'd rather not try to do that - mismatched versions can already happen if the install mechanism doesn't mandate specific versions of libraries.
instead looking for a class named org.quiltmc.loader.impl.game.TheGameProvider on the classpath
Would sound much cleaner to me to use service providers instead. slf4j also used to do that and switched to service providers as well.
Oh, er, not all game providers actually - quilt-provided ones would be separate (complete) jars containing bare quilt-loader plus the game-specific stuff.
Oh, misinterpreted what you said then. Yeah, that should be pretty stable as is then.
It sounds really painful to isolate every use of game provider and blame errors specifically on that game provider, so I'd rather not try to do that - mismatched versions can already happen if the install mechanism doesn't mandate specific versions of libraries.
It might be feasible to at least say if the stack has a GameProvider class in it, that it can be blamed on it. Just depends on whether it'd be worth the time to at least account for that case.
~~Reminder that GitHub's quotes aren't like Discord's moment~~
Would sound much cleaner to me to use service providers instead. slf4j also used to do that and switched to service providers as well.
It's already in use by Quilt, so, nothing there needs to be done, making external providers just work if they define a game provider. The only thing special it has is short-circuiting with only trying its own Jar before going through the service loader.
Would sound much cleaner to me to use service providers instead.
We currently use service providers. I don't like using them since I want classes (like the environment enums and potentially others) to be in the game provider, but also returned (and directly used in) quilt-loader APIs. This requires us to have a single game provider on the classpath ever - so it would be somewhat meaningless to use a service loader.
A bit of a catch I ran into with developing on quilt-loader-0.18.1-beta.27-20230105.235638-8 is that SystemProperties.REMAP_CLASSPATH_FILE seems to be required, along with the intermediate_mappings field in the quilt.mod.json, neither of which makes much sense for this context.
The only real workaround I thought of without modifying the codebase for the remap classpath file was to create the file and set the property on initialise, which is about as hacky as you can get out of that.
A far better solution would likely be to query the game provider for a classpath, which it could then either give the remap classpath file, or its own made up one if applicable, which then could be later fed into Knot once ready.
As for intermediate_mappings, it does seem a bit out of place for when the 'game' in question isn't even obfuscated, making its requirement seem... very out of place. It might be good to have a none option of some sort for such mods that don't touch obfuscated code in any capacity, thus not requiring hashed or intermediary, or perhaps later on, let the game provider provide its own intermediary options, or let it try to auto-discover from the internal mappings folder.
Note that I'm not modifying Quilt Loader in any capacity in this instance, only adding a GameProvider and telling Quilt to try that via the service loader API.
One of the problems I've encountered using quilt loader for general applications is the fact that the library uses the net.fabricmc.api.EnvType enum. I currently working on an ASM Transformer to add an enum to net.fabricmc.api.EnvType for my own project.
For that we just said it was SERVER and called it good enough when using it for ProxyFox a while ago.
But yeah a dedicated enum type might be good?