Files
saturnclient/VERSION_GUIDE.md
T
selimaj-devandclaude 25a19d7951
Check / compile (pull_request) Successful in 7m44s
Build 1.21.4 with Stonecutter; every version now shares src/
Step 6 of #8. versions/1.21.4/src is gone, so all eight versions (1.21.4 to
1.21.11) build from src/.

1.21.4's differences from 1.21.5 are behind version comments:
- The end gateway render layer has no render pipeline before 1.21.5
  (ShaderUtils: the pipeline field only exists from 1.21.5, and the layer
  is a four-way chain).
- Hat head rotation, the NativeImageBackedTexture constructor, armor slots
  (inventory.armor vs getEquippedStack), MatrixStackMixin's Quaternionf,
  and inside the pre-1.21.6 implementations: prevHeadYaw, GUI lighting,
  ModelTransformationMode, item depth, setShaderTexture and the blur
  uniform call. Those are one-line version comments, since the pre-1.21.6
  blocks are already inside /* */ on newer versions.

1.21.5's drawItem wrapped its body in a second isEmpty check after already
returning early for empty stacks; the redundant check is gone, so 1.21.4
and 1.21.5 share the body.

Freelook's zoom-out (FreelookMod.shouldZoomOut in GameRendererMixin) only
existed on 1.21.4. It doesn't depend on the Minecraft version, so it's
back on every version.

With no old copies left, the mc-<v> fallback in settings.gradle.kts and
scripts/port.sh are removed, and README and VERSION_GUIDE describe the
single layout.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-27 06:34:32 +02:00

5.2 KiB

Version guide

How Saturn Client supports several Minecraft versions, and how to change or add one.

Layout

Every supported Minecraft version (1.21.4 to 1.21.11) builds from one copy of the version-specific code with Stonecutter:

  • common/: all version-independent code (mods, UI, cosmetics, the server client). It has no Minecraft dependency and reaches the game only through the interfaces in org.saturnclient.common.
  • src/: the version-specific code (providers, refs, mixins and the three mixin configs), one copy for every version. Where Minecraft's API differs between versions, the code goes behind version comments (see below).
  • versions/<mc>/: each version's gradle.properties (Minecraft, Yarn, Fabric Loader and Fabric API versions), plus its build/ and run/ folders. Each version is the Gradle project :<mc>.

settings.gradle.kts lists the versions in stonecutterVersions. The root build.gradle.kts builds each version, and stonecutter.gradle.kts configures the root project (shared repositories, the common wiring and buildAll). Loom's version is loom_version in the root gradle.properties.

Version-specific code with Stonecutter

The code in src/ is always plain Java for the active version (1.21.11, set in stonecutter.gradle.kts). Code for other versions sits in comments that Stonecutter swaps before building each version:

//? if >=1.21.9 {
public void render(MatrixStack matrices, OrderedRenderCommandQueue queue, int light, PlayerEntityRenderState state, float limbAngle, float limbDistance) {
//?} else
//public void render(MatrixStack matrices, VertexConsumerProvider consumers, int light, PlayerEntityRenderState state, float limbAngle, float limbDistance) {

A condition without braces covers the next line only. For several lines, use braces and wrap the inactive block in /* … */:

//? if >=1.21.11 {
import net.minecraft.client.render.RenderLayers;
import net.minecraft.client.render.RenderSetup;
//?} else {
/*import net.minecraft.client.render.RenderPhase.Textures;
*///?}

Stonecutter rewrites inactive code into its own layout when switching versions (a single inactive line becomes //code), so write it that way to begin with, or run a switch and a reset before committing.

When a newer version leaves something out that an older one implements, keep the older version's code behind a condition and mark the newer branch with a TODO, rather than dropping it for every version. WorldFeatureImpl.getWorldAge and SaturnScreenFabric.resize are examples. Search for TODO to find what newer versions still lack.

A whole class that only exists for some versions (such as NametagsMixin, which hooks a method that 1.21.9 changed) can keep an empty class body on the other versions. An empty mixin changes nothing, and it keeps the mixin configs the same for every version, since JSON can't hold version comments.

Inactive code sits inside a /* … */ comment, so it can't contain /* … */ comments itself: use // comments in code that's only active for some versions. For the same reason, don't nest multi-line version blocks; use //?} else if <condition> { for more than two version ranges, as in PlayerEntityRendererMixin and SaturnRenderState.

  • ./gradlew "Set active project to <mc>" (or the Stonecutter IntelliJ plugin) rewrites src/ so that version's code is the uncommented one, for editing it in the IDE.
  • ./gradlew "Reset active project" switches back to 1.21.11. Run it before committing, so src/ is always committed with 1.21.11 active.
  • Each version's processed copy is compiled from versions/<mc>/build/generated/stonecutter/, which is also where to look when a non-active version fails to compile.

The emotes repository uses the same setup, and its BentCubeRenderer and PlayerModelMixin are small examples of version comments.

Checking a change on every version

Since every version shares src/, a change applies to all of them at once. Code that uses the Minecraft API may still only compile on some, so build them all:

./gradlew compileJava

The Check workflow runs the same compile on every pull request, so a version a change doesn't compile on fails before merging. Mixins whose target changed fail at startup rather than at compile time, so launch the versions a change touches with ./gradlew :<mc>:runClient.

For bigger changes, ./gradlew buildAll builds every version's jar into build/allJars/.

Adding a Minecraft version

  1. Create versions/<new>/gradle.properties with minecraft_version, yarn_mappings, loader_version and fabric_api_version (copy 1.21.11's and change the values; they're listed at https://fabricmc.net/develop). Loom's version is shared, in the root gradle.properties.
  2. Add <new> to stonecutterVersions in settings.gradle.kts.
  3. Build it with ./gradlew :<new>:compileJava and put what Minecraft changed behind version comments. Check the mixin configs in src/main/resources/*.mixins.json as well: a mixin whose target changed fails at startup, not at compile time, so launch it with ./gradlew :<new>:runClient.
  4. The release workflow builds everything through buildAll, and "Publish to Modrinth" takes the game version from each jar's name, so neither needs changing.