Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
292 changes: 213 additions & 79 deletions .github/targets.json

Large diffs are not rendered by default.

8 changes: 6 additions & 2 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,14 +36,18 @@ jobs:
steps:
- uses: actions/checkout@v4

# The JVM the Gradle daemon itself runs on, not a compile target - every
# target sets its own `options.release`. Minecraft 26 refuses to set up
# under anything below 25, and Loom on the older versions has only ever
# been exercised on 21, so each target names the one it wants.
- uses: actions/setup-java@v4
with:
distribution: temurin
java-version: 21
java-version: ${{ matrix.target.java }}

- uses: gradle/actions/setup-gradle@v4

# Retried because Maven Central rate limits us: 79 jobs, each one
# Retried because Maven Central rate limits us: 90 jobs, each one
# fetching its own Minecraft and Forge artifacts from shared runner IPs,
# and no writable Actions cache on a pull request branch. Some of those
# downloads are Loom's own rather than Gradle's, so they get no retry or
Expand Down
8 changes: 5 additions & 3 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ jobs:
- uses: actions/checkout@v4

# A jar carries the version from gradle.properties, not the tag. Without
# this, tagging v7.0.1 while mod_version still says 7.0.0 publishes 80
# this, tagging v7.0.1 while mod_version still says 7.0.0 publishes 90
# files under the old version, and CurseForge files can't be replaced.
- name: Check the tag matches mod_version
if: github.event_name == 'release'
Expand All @@ -46,7 +46,7 @@ jobs:
# Uploads are independent; one bad target shouldn't cancel the rest.
fail-fast: false
# Each job decompiles its own Minecraft, so a target costs minutes, and
# there are 80 of them - serial uploads would run most of a day. Four at
# there are 90 of them - serial uploads would run most of a day. Four at
# a time matches the build workflow's headroom against Maven Central and
# stays well inside both platforms' upload rate limits.
max-parallel: 4
Expand All @@ -55,10 +55,12 @@ jobs:
steps:
- uses: actions/checkout@v4

# Per-target, for the same reason as the build workflow: Minecraft 26
# needs a Java 25 daemon, the rest are built on 21.
- uses: actions/setup-java@v4
with:
distribution: temurin
java-version: 21
java-version: ${{ matrix.target.java }}

- uses: gradle/actions/setup-gradle@v4

Expand Down
105 changes: 71 additions & 34 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,15 @@ on one.

Every loader and every Minecraft version is built from this one branch.

Every Minecraft release each loader ever shipped for, from 1.14.4 to 1.21.8
80 jars in all.
Every Minecraft release each loader ever shipped for, from 1.14.4 to 26.2
90 jars in all.

| Loader | Minecraft versions |
|----------|-------------------------------------------------------------------------------|
| Fabric | 1.14.4, 1.15–1.15.2, 1.16–1.16.5, 1.17–1.17.1, 1.18–1.18.2, 1.19–1.19.4, 1.20–1.20.6, 1.21–1.21.8 |
| Fabric | 1.14.4, 1.15–1.15.2, 1.16–1.16.5, 1.17–1.17.1, 1.18–1.18.2, 1.19–1.19.4, 1.20–1.20.6, 1.21–1.21.11 |
| Quilt | same jars as Fabric |
| Forge | 1.14.4, 1.15–1.15.2, 1.16.3–1.16.5, 1.17.1, 1.18–1.18.2, 1.19–1.19.4, 1.20–1.20.4, 1.20.6, 1.21, 1.21.1, 1.21.3–1.21.8 |
| NeoForge | 1.20.2–1.20.6, 1.21–1.21.8 |
| Forge | 1.14.4, 1.15–1.15.2, 1.16.3–1.16.5, 1.17.1, 1.18–1.18.2, 1.19–1.19.4, 1.20–1.20.4, 1.20.6, 1.21, 1.21.1, 1.21.3–1.21.11 |
| NeoForge | 1.20.2–1.20.6, 1.21–1.21.9, 26.1–26.1.2, 26.2 |

Quilt Loader reads `fabric.mod.json` directly, so the Fabric jar *is* the Quilt
jar — there's nothing extra to build, and releases are tagged for both.
Expand All @@ -41,6 +41,15 @@ dies remapping Minecraft on a name conflict, and on 1.16.2 the mixin annotation
processor is given no SRG mappings and so can't write a refmap. Fabric covers
both versions.

Nothing here ships a refmap any more. Current Loom defaults to
`useLegacyMixinAp = false`: instead of running Mixin's annotation processor to
emit a name map alongside the jar, it rewrites the mixin annotations themselves
into SRG inside `remapJar`. The jars are equivalent — the mapping is in the
class files rather than in a JSON file next to them — but it means
`cleancut.mixins.json` must *not* declare a `refmap`, and no build script here
may add Mixin as an `annotationProcessor`. Doing either points the build at
mapping data that is never generated.

On Forge 1.14.4, 1.15 and 1.15.1 the mod carries its own copy of Mixin, because
Forge only started bundling Mixin partway through 1.15.2. See
`forge/src/main/java/zone/rong/cleancut/bootstrap/`.
Expand All @@ -65,8 +74,8 @@ the Forge fork from before the rename and still lives under
`net.minecraftforge`, so the **Forge 1.20.1 jar is the NeoForge 1.20.1 jar** —
it's the same API, and building it twice would produce the same mod.

1.21.8 is the upper limit for every loader, for the toolchain reason described
under [Newer Minecraft](#newer-minecraft).
The ceilings differ per loader — 1.21.11 on Fabric and Forge, 26.2 on NeoForge —
for the reasons described under [Newer Minecraft](#newer-minecraft).

## How the repository is laid out

Expand Down Expand Up @@ -104,11 +113,20 @@ the logic, and a mixin that decides where vanilla gets interrupted.
./gradlew :neoforge:1.21.4:build
```

Jars land in `<loader>/versions/<version>/build/libs/`. Build with **JDK 21** —
each target compiles down to the release level its Minecraft version needs, so
one JDK covers all 82. It has to be 21 and not something newer: this build is on
Gradle 8, which rejects JDK 25 with `Unsupported class file major version 69`.
Set `JAVA_HOME` if your default `java` is a later release.
Jars land in `<loader>/versions/<version>/build/libs/`. Build with **JDK 21**,
except for the Minecraft 26 targets, which need **JDK 25**:

```sh
./gradlew :neoforge:26.2:build # needs JAVA_HOME on a JDK 25
```

That is the JVM the Gradle daemon runs on, not what the mod compiles against —
each target sets its own `options.release`, so one daemon covers every release
level from 8 upwards. Loom refuses to set Minecraft 26 up under anything below
25 (`Minecraft 26.1 requires Java 25 but Gradle is using 21`), and the older
targets have only been exercised on 21. `.github/targets.json` carries the
version each target wants, and CI installs it per job. Set `JAVA_HOME` to
switch locally.

`./gradlew build` with no arguments builds every target in sequence, which takes
a while — the per-version Minecraft decompile dominates. CI builds them in
Expand All @@ -132,35 +150,54 @@ every target, which is slow — they're for IDE work, not part of building.

## Newer Minecraft

1.21.8 is where this build stops, and the reason is the toolchain rather than
the mod. From 1.21.9 Yarn ships unpick v3 data, which needs Fabric Loom 1.17,
which needs Gradle 9 — and the Gradle wrapper and the Stonecutter version are
properties of the whole build, not of one branch, so this can't be mixed with
the Gradle 8 stack the older versions are on.

Forge and NeoForge do have 1.21.9+ builds, and being Mojang-mapped they never
touch unpick. They stop at 1.21.8 anyway so that all three loaders cover the
same range, and because the Architectury Loom pinned here predates those
versions. Raising their ceiling is a row in the `versionData` table plus
whatever the compiler then complains about — worth doing on its own, rather
than mixed into a Gradle 9 migration.

The same wall stands in front of Minecraft's new version scheme (26.1.2 and
later), which additionally has no Yarn mappings at all and would have to be
built against Mojang's.

Getting there means either moving the entire build to Gradle 9, Stonecutter
0.9 and current Loom — which risks the 1.14-1.16 targets, whose support in
current Loom is unverified — or keeping this build as it is and adding a
second, modern Gradle build beside it in the same branch.
This build is on Gradle 9, Stonecutter 0.9 and current Loom, because Minecraft
26 needs a Java 25 daemon and Gradle 8 cannot run on Java 25 at all — it rejects
the class files. The wrapper and the Stonecutter version are properties of the
whole build rather than of one branch, so that migration was all-or-nothing.

The obvious worry about moving the whole build forward is that current Loom
drops the oldest targets, and it very nearly did. Forge 1.14.4, 1.15 and 1.15.1
— the three that shade their own Mixin — failed with `Unable to locate
obfuscation mapping for @Redirect target`. The cause was on this side, not
Loom's: those three added Mixin as an `annotationProcessor` by hand, and current
Loom no longer passes the processor any mapping arguments because it no longer
uses the processor. Dropping those lines fixes all three. See the note on
refmaps under [Supported versions](#supported-versions), and don't reintroduce
them.

Every other target from 1.14.4 up builds unchanged, so there is one build here,
not a legacy one and a modern one.

What is left is per-loader, and none of it is a toolchain problem:

**NeoForge skips 1.21.10 and 1.21.11.** From 21.10 its published artifact no
longer carries `data/server.lzma` where Architectury Loom expects it. The
packaging moved out from under Loom, so a version bump on this side doesn't fix
it — building those two means NeoForge's own toolchain instead of Loom, which is
a different build rather than a different number. It picks back up at 26.1.
Fabric and Forge cover 1.21.10 and 1.21.11.

**Forge stops at 1.21.11.** On 26.x Loom throws an NPE setting Minecraft up on
the Forge platform. NeoForge 26.x goes through the same Loom and works, so this
is Forge-platform-specific.

**Fabric stops at 1.21.11 because 26.x has no Yarn.** Intermediary exists, Yarn
does not. The Fabric sources here are written in Yarn names, so 26.x on Fabric
means either Mojang mappings — and the class names in `fabric/src` change
wholesale, since `MinecraftClient` becomes `Minecraft` and so on — or nothing.
Forge and NeoForge are already Mojang-mapped and don't have this problem.

Yarn's unpick v3 is *not* a wall, despite an earlier note here saying so.

## Adding a Minecraft version

1. Add it to the right `branch(...)` in `settings.gradle`.
2. Add a row to that loader's `versionData` table in `<loader>/build.gradle`
with the mappings or loader build and the Java release level. The dependency
ranges that go into the mod metadata are derived from those, not written out.
3. Add it to `.github/targets.json` so CI builds it.
3. Add it to `.github/targets.json` so CI builds it, with the `java` the Gradle
daemon needs for it — 21 for everything so far except Minecraft 26, which
needs 25.

Then build it. If an API moved, the compiler will say so, and the fix is another
`//? if` block around the two alternatives.
Expand Down
36 changes: 32 additions & 4 deletions fabric/build.gradle
Original file line number Diff line number Diff line change
@@ -1,8 +1,17 @@
plugins {
id 'fabric-loom' version '1.10-SNAPSHOT'
// Minecraft 26 ships unobfuscated, and Fabric Loom has no mode for that -
// it requires a mappings dependency that no longer exists for those
// versions. Architectury Loom's no-remap plugin does, and it drives the
// Fabric platform as well as the other two.
id 'fabric-loom' version '1.17.17' apply false
id 'dev.architectury.loom-no-remap' version '1.17.485' apply false
id 'me.modmuss50.mod-publish-plugin' version '2.2.0'
}

apply plugin: stonecutter.current.version.startsWith('26')
? 'dev.architectury.loom-no-remap'
: 'fabric-loom'

// Everything that differs between Minecraft versions on this loader. Adding a
// version means adding a row here and listing it in settings.gradle.
// `mappings` is the Yarn build; `java` is the release level that version of
Expand Down Expand Up @@ -44,9 +53,14 @@ def versionData = [
'1.21.6': [mappings: '1.21.6+build.1', java: 21],
'1.21.7': [mappings: '1.21.7+build.8', java: 21],
'1.21.8': [mappings: '1.21.8+build.1', java: 21],
'1.21.9' : [mappings: '1.21.9+build.1', java: 21],
'1.21.10': [mappings: '1.21.10+build.3', java: 21],
'1.21.11': [mappings: '1.21.11+build.6', java: 21],
'26.2' : [mappings: null, java: 25],
]

def minecraftVersion = stonecutter.current.version
def unobfuscated = minecraftVersion.startsWith('26')
def target = versionData[minecraftVersion]
if (target == null) {
throw new GradleException("No Fabric build data for Minecraft ${minecraftVersion}; add a row to fabric/build.gradle")
Expand All @@ -61,8 +75,20 @@ base {

dependencies {
minecraft "com.mojang:minecraft:${minecraftVersion}"
mappings "net.fabricmc:yarn:${target.mappings}:v2"
modImplementation "net.fabricmc:fabric-loader:${fabric_loader_version}"
// Minecraft 26 is unobfuscated: no Yarn, no Mojang mappings, nothing to
// declare. Yarn existed to give obfuscated names meaning, and the names
// now come in the jar.
if (target.mappings != null) {
mappings "net.fabricmc:yarn:${target.mappings}:v2"
}
// modImplementation exists to remap a dependency out of intermediary on
// the way in. With nothing obfuscated the no-remap plugin doesn't register
// it, and a plain dependency is already the right thing.
if (unobfuscated) {
implementation "net.fabricmc:fabric-loader:${fabric_loader_version}"
} else {
modImplementation "net.fabricmc:fabric-loader:${fabric_loader_version}"
}
}

processResources {
Expand Down Expand Up @@ -100,7 +126,9 @@ jar {
}

publishMods {
file = tasks.remapJar.archiveFile
// No remapJar where nothing is obfuscated: the jar off the compiler is
// already the jar that ships.
file = (unobfuscated ? tasks.jar : tasks.remapJar).archiveFile
displayName = "${mod_name} ${mod_version} - Fabric ${minecraftVersion}"
version = "${mod_version}+${minecraftVersion}-fabric"
changelog = providers.environmentVariable('CHANGELOG')
Expand Down
67 changes: 50 additions & 17 deletions forge/build.gradle
Original file line number Diff line number Diff line change
@@ -1,8 +1,17 @@
plugins {
id 'dev.architectury.loom' version '1.10-SNAPSHOT'
// Minecraft 26 ships unobfuscated, and Loom takes a different plugin for
// that - remapping a jar that was never obfuscated is not a no-op, it is a
// step that has nothing to work from. Both are declared and one is applied
// below, once the version being built is known.
id 'dev.architectury.loom' version '1.17.485' apply false
id 'dev.architectury.loom-no-remap' version '1.17.485' apply false
id 'me.modmuss50.mod-publish-plugin' version '2.2.0'
}

apply plugin: stonecutter.current.version.startsWith('26')
? 'dev.architectury.loom-no-remap'
: 'dev.architectury.loom'

// Everything that differs between Minecraft versions on this loader. Adding a
// version means adding a row here and listing it in settings.gradle.
// Forge/NeoForge build numbers are pinned: Architectury Loom resolves the
Expand Down Expand Up @@ -48,9 +57,17 @@ def versionData = [
'1.21.6': [forge: '1.21.6-56.0.9', java: 21],
'1.21.7': [forge: '1.21.7-57.0.3', java: 21],
'1.21.8': [forge: '1.21.8-58.1.0', java: 21],
'1.21.9': [forge: '1.21.9-59.0.5', java: 21],
'1.21.10': [forge: '1.21.10-60.1.13', java: 21],
'1.21.11': [forge: '1.21.11-61.1.14', java: 21],
'26.1' : [forge: '26.1-62.0.9', java: 25],
'26.1.1': [forge: '26.1.1-63.0.2', java: 25],
'26.1.2': [forge: '26.1.2-64.1.0', java: 25],
'26.2' : [forge: '26.2-65.1.0', java: 25],
]

def minecraftVersion = stonecutter.current.version
def unobfuscated = minecraftVersion.startsWith('26')
def target = versionData[minecraftVersion]
if (target == null) {
throw new GradleException("No Forge build data for Minecraft ${minecraftVersion}; add a row to forge/build.gradle")
Expand Down Expand Up @@ -109,30 +126,44 @@ configurations {

loom {
silentMojangMappingsLicense()
mixin {
// Must match the "refmap" entry in cleancut.mixins.json - Forge reads
// the name from the config rather than having it injected.
defaultRefmapName = "${mod_id}.refmap.json"
}
forge {
mixinConfig "${mod_id}.mixins.json"
// No refmap anywhere in here, on any version. A refmap maps mixin targets
// back to obfuscated names, and this Loom doesn't produce one: it defaults
// to `useLegacyMixinAp = false` and rewrites the annotations themselves
// into SRG inside remapJar. Declaring one in cleancut.mixins.json would
// just point Mixin at a file the build never writes.
//
// mixinConfig still matters - it puts the config in the jar manifest,
// which is how Forge finds it. The no-remap plugin used for Minecraft 26
// sets that up itself before this script runs.
if (!unobfuscated) {
forge {
mixinConfig "${mod_id}.mixins.json"
}
}
}

dependencies {
minecraft "com.mojang:minecraft:${minecraftVersion}"
mappings loom.officialMojangMappings()
// Minecraft 26 ships unobfuscated - the jar already carries real names,
// and Mojang stopped publishing mappings because nothing is left to map.
// Declaring any mappings there is an error, not a no-op.
if (!unobfuscated) {
mappings loom.officialMojangMappings()
}
forge "net.minecraftforge:forge:${target.forge}"

if (shadesMixin) {
// Compile against Mixin and run the annotation processor over it, so
// the refmap still gets generated, then put Mixin in the jar.
// Compile against Mixin, then put it in the jar.
//
// Deliberately no `annotationProcessor` for Mixin here. This Loom runs
// with `useLegacyMixinAp = false`: it rewrites mixin annotations into
// SRG names inside remapJar instead of having Mixin's processor emit a
// refmap. Adding the processor by hand puts it on the compile path
// without any of the mapping arguments Loom used to pass with it, and
// it then fails outright with "Unable to locate obfuscation mapping
// for @Redirect target". Every other Forge version here already builds
// through the remapper; these three are no different.
compileOnly "org.spongepowered:mixin:${mixinVersion}"
annotationProcessor "org.spongepowered:mixin:${mixinVersion}"
// Mixin's POM marks these provided, so the annotation processor would
// otherwise start up and immediately fail on NoClassDefFoundError.
annotationProcessor 'com.google.code.gson:gson:2.8.9'
annotationProcessor 'com.google.guava:guava:31.1-jre'
shaded "org.spongepowered:mixin:${mixinVersion}"
// Just asm-util, not the ASM it was built against - taking its
// transitives would put a whole second copy of ASM in the jar.
Expand Down Expand Up @@ -213,7 +244,9 @@ jar {
}

publishMods {
file = tasks.remapJar.archiveFile
// No remapJar where nothing is obfuscated: the jar off the compiler is
// already the jar that ships.
file = (unobfuscated ? tasks.jar : tasks.remapJar).archiveFile
displayName = "${mod_name} ${mod_version} - Forge ${minecraftVersion}"
version = "${mod_version}+${minecraftVersion}-forge"
changelog = providers.environmentVariable('CHANGELOG')
Expand Down
1 change: 0 additions & 1 deletion forge/src/main/resources/cleancut.mixins.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
"minVersion": "0.8",
"package": "zone.rong.cleancut.mixin",
"compatibilityLevel": "${mixin_compat}",
"refmap": "${mod_id}.refmap.json",
"client": [
"MinecraftMixin"
],
Expand Down
Binary file modified gradle/wrapper/gradle-wrapper.jar
Binary file not shown.
5 changes: 3 additions & 2 deletions gradle/wrapper/gradle-wrapper.properties
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#Multi-version CleanCut build
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-9.6.1-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
Loading
Loading