Gradle plugin for Fabric ancient versions of Forge.
2.4
2.5-SNAPSHOT
(from CI every commit)disaster-time
rewrite branch started by quaternary Dec 2022 - Jun 2023.This branch is my playground and my domain. Here be dragons (it's me. I'm the dragon)
Voldeloom contains a forked copy of some code from FabricMC/stitch, moved into the net.fabricmc.loom.yoinked.stitch
package. Stitch is licensed under the Apache License 2.0, so its license has been reproduced in src/main/resources/STITCH_REDISTRIBUTION_NOTICE.md
.
Using the latest version of Minecraft Forge for each Minecraft version.
runClient
environment, with MCP names and IDE debugger, that you can use to test your mod without needing to build a release jar and copy it into a real Minecraft launcher.
Compile? | Release? | Deobf? | ||
---|---|---|---|---|
<=1.0 | ❌ | ❌ | ❌ | Predates files.minecraftforge.net . |
1.1 | ❌ | ❌ | ❌ | I don't think there is much interest in modding for this version. |
1.2.5 | ❌ | ❌ | ❌ | Mods don't load in dev. Remapping for release is dummied out. |
1.3.2 | ✅ | 🤔 | ✅ | |
1.4.7 | ✅ | ✅ | ✅ | Has received the most testing. Used for several production mods. |
1.5.2 | ✅ | ✅ | ✅ | |
1.6.4 | ✅ | 🤔 | ✅ | |
1.7.10 | ✅ | 🤔 | ✅ | |
>=1.8 | ❌ | ❌ | ❌ | Out of scope. |
Take this project with a grain of salt, especially the runClient
dev workspace.
This project implements a Forge modding toolchain from first principles, using a radically different approach than MCP/ForgeGradle ever did. They patch source code, we install Forge like a jarmod. They remap sources, we remap binaries. The MCP parsers and remappers and jar mergers and jar processors and access-transformers and other components used in this plugin share no lineage with anything Forge or MCP ever used. There are behavioral differences with just about all of these components. Additionally: there's a healthy dose of secret strips of duct-tape used to get things working, some important aspects of modding that you can't do yet, and a lot that's just plain WIP.
I strongly suggest testing the release version of your mod often in a "real" Forge production environment, like a Prism Launcher Forge instance. These workspaces are much more well-tested.
Also: this should go without saying, but do not bother the official Forge community with support requests.
Start with this build.gradle
. I would strongly suggest using modern tech - Java 17 and Gradle 7.6, to be specific. (Gradle 8 might work too, since 2.2-SNAPSHOT
.)
buildscript {
repositories {
mavenCentral()
maven { url "https://maven.fabricmc.net" }
maven { url "https://repo.sleeping.town" }
}
dependencies {
classpath "agency.highlysuspect:voldeloom:2.4"
}
}
apply plugin: "agency.highlysuspect.voldeloom"
java.toolchain.languageVersion = JavaLanguageVersion.of(11) //Last version able to set a --release as low as 6
compileJava.options.release.set(6) //Forge doesn't understand classes compiled to versions of the class-file format newer than Java 6's
String minecraftVersion = "1.4.7"
String forgeVersion = "1.4.7-6.6.2.534"
dependencies {
minecraft "com.mojang:minecraft:${minecraftVersion}"
forge "net.minecraftforge:forge:${forgeVersion}:universal@zip"
mappings "net.minecraftforge:forge:${forgeVersion}:src@zip"
}
volde {
//more configuration goes here...
}
(Infinite thanks to unascribed for hosting the maven.)
Now ./gradlew runClient --info --stacktrace
should perform a bunch of magic culminating in a Minecraft Forge 1.4.7 client window, and as an optional next step, genSources
will churn out a nice sources jar (with javadoc!) for you to attach in your IDE, like how Loom works.
Then just start modding. Don't worry too much about Java versions - the plugin will provision a Java 8 toolchain for launching the game.
Replace the mappings
line with:
mappings volde.layered {
importMCPBot("https://mcpbot.unascribed.com/", minecraftVersion, "stable", "12-1.7.10")
}
Voldeloom should work all the way down to Java 8 and Gradle 4. If using the sample buildscript above, replace the java.toolchain
/compileJava
lines with:
compileJava {
sourceCompatibility = "1.6"
targetCompatibility = "1.6"
}
and since old versions of Gradle are not Java 9-clean and lack the "toolchains" feature, remember to invoke Gradle using a Java 8 JDK.
Well I've gotta finish it first! Even this README
gets outdated worryingly quickly.
doc
folder.sample
folder for some sample projects. Some are compiled in CI; it should at least get that far.
LoomGradleExtension
class for a full list of things you can configure from volde { }
.If stuff isn't working:
genIdeaRuns
yet. It's broken. Just use the runClient
Gradle task for launching purposes.--info --stacktrace
for much more detailed log output--refresh-dependencies
(to enable the global refresh-dependencies mode) or -Pvoldeloom.refreshDependencies
(to refresh only Voldeloom artifacts)If you're looking for general 1.4 Forge development advice, try here.
What works:
metadataSources
forward-compat magic for gradle 5+binpatches.pack.lzma
gdiff archivegenSources
(sans methods with MCP messed-up switchmaps), attaching sources in intellij, browsing and find usages, MCP comments in source coderunClient
gets in-game (on at least 1.4.7 and 1.5.2)
shimForgeLibraries
task predownloads Forge's runtime-downloaded deps and places them in the location Forge expects, because the URLs hardcoded in forge are long deadshimResources
task will copy assets from your local assets cache into the run directory (because you can't configure --assetsDir
in this version of the game)-src
zip, an MCP download, tinyv2 archives (although tinyv2 will break binpatches)modImplementation
/etc works
coremodImplementation
/etc exists for coremods that exist at runtime, which need special handling (remappedConfigEntryFolderCopy
task handles it) modCompile
instead of modImplementation
, and drop the only
from modRuntimeOnly
(implementation
/runtimeOnly
are a gradle 7 convention)-Pvoldeloom.refreshDependencies
to get them to propagate through.What doesn't work:
runClient
task works, and is more of a priority because I get much more control over the startup processrunClient
and ide runs are, maybe i should write something up)migrateMappings
are (temporarily?) removed. If you're using retro mapping projects other than MCP....... have Funcoremods
folder where Forge wants to find them)Basically this uses a more Fabricy "do as much as possible with binaries" approach. This partially owes to the project's roots in Fabric Loom, which is a completely binary-based modding toolchain, but also because it's a good idea
genSources
is not required to compile a mod.We do miss out on the occasional line-comment that Forge's source patches add, but skipping Fernflower makes everything complete very fast.
One exception to this hierarchy is that we merge the client and server jars first (using FabricMC's JarMerger) and paste Forge's files on top of the merged jar, when the period-accurate installation process would probably paste Forge on top of merely a client jar or server jar. This is seamless because Forge's class-overwrites were evidently computed against a merged jar in the first place (see in.class
, which ships a SideOnly
annotation on a vanilla method).
Forge's period-accurate installation process is much more source-based - the game is immediately decompiled using a known Fernflower version (I think maybe some binary remapping is done using a tool called Retroguard), source-patches are applied to fill decompiler gaps + to patch in Forge's features, the rest of remapping is performed using textual find-and-replace, and the whole thing is fed back to javac
to produce the jar you run in development. This was done using some Python 2 scripts and binaries that you'd download alongside the forge/mcp install and trigger from your Ant build.
These days we have a much more well-rounded set of class binary-manipulation tools available straight off-the-shelf, like tiny-remapper
, JarMerger
, Java's ZipFileSystem
, etc, that make working with class binaries very expressive and fun. There isn't much reason to drop back to source files.
There doesn't seem to be a nice way to develop a Gradle plugin and actually use the plugin to see if it works (no, not "write automated tests for the plugin", actually use it) at the same time. This is because Gradle sucks.
Sample projects contain a line in settings.gradle
that includes the main voldeloom project as an "included build". This feels a bit backwards because the subfolder is "including" the parent folder. It is what it is.
Gotchas with this scheme:
./sample/1.4.7/.idea
directory, voldeloom will think it belongs to the root project and dump run configs into that, copypaste them back into ./.idea
, restart IDE. There's your run configs. (Or use runClient
.)IntelliJ users can right-click on each sample project's build.gradle
and press "Link Gradle Project", which is towards the bottom of the dropdown. The sample projects will then appear in the Gradle tool window for perusal. (It seems like code-completion in the editor uses the Gradle API that you last refreshed a project from.)
Do not press the "reload all gradle projects" buttons. Because of the multiple Gradle versions in play they will fail to read each other's lock files, so the gradles will stomp on each other, write to the cache at the same time, try to delete each other's output etc. Crap will end up in your Gradle cache too. Instead, to refresh projects, right-click on each sample project in the tool window you're interested in and refresh it individually.
Breakpoints don't work if you just hit the "refresh gradle" button, but if you select the task in the Select Run/Debug Configuration
bar, you can press the debug button.
General debugging stuff:
minecraft
, forge
and mappings
configurations, things will explode otherwise.~/.gradle/caches/fabric-loom
). If there are any obviously messed-up files like zero-byte files, corrupt/incomplete jars or zips, delete them and try again.
--info --stacktrace
. The plugin does spam --info
with quite a bit of useful stuff.I agree! There should be better error messages!
e04c5335922c5e457f0a7cd62c93c4a7f699f829
for a couple of dependency hashesThe shimForgeLibraries
task is intended to download the libraries Forge wants and place them in the locations it expects to find them before launching the game, since they were removed from the hardcoded URLs in Forge a long time ago (I think that's the sha1 of the Forge server's 404 page).
Either that task didn't run and the libraries aren't there (examine the Gradle log to see if it ran), or the minecraft.applet.TargetDirectory
system property did not get set on the client and it's trying to read libraries out of your real .minecraft
folder - if it's doing that, the rest of the game will also try to run out of that folder (check the path where Forge told you it saved the crash log)
I would recommend using a launcher that shims this process for you (like Prism Launcher) if you can, so you don't have to deal with this. If you can't, you will need to shim the libraries manually. To do this, check your .minecraft
folder for a logfile Forge produced, probably with a name like ForgeModLoader-client-0.log
. Open it, scroll to the bottom, and look for lines like:
There were errors during initial FML setup. Some files failed to download or were otherwise corrupted. You will need to manually obtain the following files from these download links and ensure your lib directory is clean.
*** Download http://files.minecraftforge.net/fmllibs/deobfuscation_data_1.5.2.zip
You can obtain the file from Prism Launcher's mirror by replacing http://files.minecraftforge.net/fmllibs/
with https://files.prismlauncher.org/fmllibs/
, then putting the URL into your web browser. You can also try putting the URL into the Internet Archive Wayback Machine.
Once you have the file, place it in .minecraft/lib
, using the filename at the end of the URL (in this example, make sure the file is named deobfuscation_data_1.5.2.zip
). Repeat for all URLs mentioned in the log file. The next time you start Forge, it should find these files, assume it already downloaded them, and won't make any attempts to contact the dead server.
FMLRelaunchLog
Forge assumes the .minecraft
directory exists without checking or creating it. If it doesn't exist an exception will be thrown when it creates its log file, but it silently swallows the exception, so you get an NPE shortly after when it tries to use the log. Because the plugin will try to create the run
directory if it doesn't exist, this is likely another "the game is not using the correct working directory" bug, so check that the minecraft.applet.TargetDirectory
system property is set.
Something compiled to Java 8's classfile format is on the classpath. Forge 1.4.7 only works with classes compiled for Java 6. (Not sure why this happens when using generated run configs, instead of the gradle runClient task, probably a classpath difference)
genSources
NPEs on ClassWrapper.getMethodWrapper
in methods like placeDoor
, getOptionOrdinalValue
, multiplyBy32AndRound
etcWhen desugaring a switch-over-enum, this version of Fernflower assumes its associated "switchmap" class is named with the same convention that javac
uses when compiling switch-over-enum, and will crash if it can't find it. Mojang proguarded the switchmap classes and MCP went back and renamed them, but gave them the "wrong" name, causing the bug. See this page on the CFR website for more information about switch-over-enum, and quat_notes/weird_enum_switch_methods.md
for some of my notes.
This is a binary-based toolchain where the decompiler output is just for show, so a method failing to decompile is not a big deal. If you need to see the body of the method you can try Quiltflower, the built-in IntelliJ decompiler, or CFR.
TODO: formalize these and put them into doc
See quat_notes/old notes.md
for stuff that used to be on this page but got outdated.
feb2023 oops might be outdated again
The entrypoint is LoomGradlePlugin
, which gets called upon writing the apply plugin
line.
java
, eclipse
, and idea
plugins are applied, as if you typed apply plugin: "eclipse"
GradleSupport.detectConfigurationNames
determines if you're on a compile
or implementation
-flavored version of Gradlevolde {
block you can type some settings into. I think more recent versions call this loom
project.afterEvaluate
blocks, and since tasks are executed after those, in task configuration and executionrepositories {
block:
minecraft
- extends compile
/implementation
minecraftDependencies
forge
forgeClient
, forgeServer
forgeDependencies
mappings
src
zip) or a file on your computer (if using volde.layered
mappings)accessTransformers
minecraftNamed
- extends minecraftDependencies
, forgeDependencies
compile
/implementation
extends from this (so you can code against it)modImplementation
and modImplementationNamed
implementation
extends modImplementationNamed
compile
instead of implementation
on Gradle 6-modCompileOnly
and modCompileOnlyNamed
compileOnly
extends modCompileOnlyNamed
modRuntimeOnly
and modRuntimeOnlyNamed
runtimeOnly
extends modRuntimeOnlyNamed
Only
suffix on Gradle 6-modLocalRuntime
and modLocalRuntimeNamed
runtimeOnly
extends modLocalRuntimeNamed
coremodImplementation
and coremodImplementationNamed
compileOnly
extends coremodImplementationNamed
coremodRuntimeOnly
and coremodRuntimeOnlyNamed
coremodLocalRuntime
and coremodLocalRuntimeNamed
idea { }
block in the scriptvolde.runs
will add a runXxx
task for it, then the client
and server
run configs are created which adds the runClient and runServer tasksThen we ask for an afterEvaluate
callback. The rest of the buildscript in your project runs first, so when the callback runs, it is able to access the settings configured in the volde { }
block:
ProviderGraph#trySetup()
. This is a deeply magical Does-It-All method. Described later.jar
and remapJarForRelease
tasks are wired up:
remapJarForRelease
doesn't have an input:
jar
is set to a classifier of "dev"
and remapJarForRelease
is set to a classifier of ""
.remapJarForRelease
's input is set to jar
's output.remapJar
's output is registered to the archives
artifact configuration.addUnmappedMod
called on it, set to jar
's output (idk)After doing all of that, the task execution phase may begin.
What happens in ProviderGraph#trySetup
:
minecraft
configuration. Reads its version number.VanillaJarFetcher
:
version_manifest.json
, locate the appropriate per-version manifest.VanillaDependencyFetcher
:
AssetDownloader
:
forge
configuration:
Binpatcher
:
Merger
:
-merged
jar.forgeClient
and forgeServer
jars:
ForgeDependencyFetcher
:
version.json
launcher profile. Handle those.Jarmodder
:
META-INF
.MappingsWrapper
:
mappings
configuration..srg
/.csv
files and construct McpMappings
from them.AccessTransformer
:
RemapperMcp
:
McpMappings
to the jar, creating an SRG-named jar (func, field, etc)NaiveRenamer
:
fields.csv
and methods.csv
names from the McpMappings
.DependencyRemapperMcp
:
modImplementation
/etc configurations and remaps them from the release namespace into the workspace names.GenSourcesTask.SourceGenerationJob
s.minecraft
configuration:
-linemapped
jar from the last genSources
execution, if one exists.NaiveRenamer
is used.remapJarForRelease
task.modImplementation
and friendsLoomGradleExtension
contains a NamedDomainObjectController
of RemappedConfigurationEntry
s. A RemappedConfigurationEntry
is a pair of configurations:
RemappedDependenciesProvider
dep provider will deposit remapped versions of the artifacts insideand some miscellaneous functionality:
So, let's say there's an entry with an input config of modImplementation
, an output of modImplementationNamed
, and a maven scope of "compile" (which there is, because LoomGradlePlugin
adds this one by default); and you add "vazkii:Botania:1.2.3"
to modImplementation
.
"vazkii:Botania:1.2.3"
dependency (since it's in the input configuration), remap the artifact into the current workspace names, and add the file to the modImplementationNamed
config (the output config).
implementation
was also set to extend from modImplementationNamed
in LoomGradlePlugin
, you are able to write code against the mod in your development environment.implementation
extends from runtimeClasspath
(regular gradle stuff), the mod will appear in your development runClient."vazkii:Botania:1.2.3"
as a "compile" dependency.Similar configurations exist under modCompileOnly
and modRuntimeOnly
(which map onto the corresponding standard Java Gradle configurations), and modLocalRuntime
(which is the same as modRuntimeOnly
but doesn't add to the "runtime" maven scope, intended for simply installing mods into your client workspace)
Forge 1.4.7 has a limitation where it cannot load coremods from the classpath; they must exist in the coremods folder only. If you set copyToFolder("coremods")
on a remapped dependency entry, a Gradle task that runs before any runXxxx
tasks (RemappedConfigEntryFolderCopyTask
) will notice, and copy the dependency into the coremods folder for you.
The predefined coremodImplementation
/coremodImplementationNamed
entry, for example, only sets coremodImplementationNamed
to extend from compileOnly
, not implementation
. This means it doesn't get put on the runtime classpath the usual way (by way of runtimeClasspath
extending implementation
). Forge picks up on the mod because the jar has been copied into the coremods
folder, though.
There are coremodImplementation
, coremodRuntimeOnly
, and coremodLocalRuntime
configurations predefined. coremodCompileOnly
does not exist because the folder-copy workaround is only required to load the coremod in the local development workspace; if it existed, it would be identical to modCompileOnly
, so just use that.