Build every Minecraft version from one shared src/ with Stonecutter #31

Merged
selimaj-dev merged 16 commits from stonecutter into master 2026-09-27 04:50:16 +00:00
Owner

Closes #8.

Every supported version (1.21.4–1.21.11) now builds from one copy of the version-specific code in src/, with Stonecutter comments (//? if >=1.21.9 { … }) where Minecraft's API differs. It replaces eight near-identical copies in versions/*/src, about 27,700 lines of which ~78% were duplicates. Each step was reviewed, merged into the stonecutter integration branch and tested in game:

PR Step
#24 1.21.11 scaffold, Kotlin build scripts, remapJar limited to two at a time
#25 1.21.10
#26 1.21.9
#27 1.21.8 (before the 1.21.9 render queue)
#28 1.21.7 and 1.21.6 (before Fabric API render state data)
#29 1.21.5 and 1.21.4 (before the 1.21.6 GUI rewrite), and removing the old layout

What changes for development

  • Layout: src/ holds the version-specific code once. versions/<mc>/ only holds each version's gradle.properties (and build/, run/).
  • Projects are :<mc>, for example ./gradlew :1.21.11:runClient (was :mc-1.21.11). buildAll still produces build/allJars/saturn-client-<version>+<mc>.jar, so the Release and Modrinth workflows are unchanged.
  • Editing: edit with 1.21.11 active, switch with ./gradlew "Set active project to <mc>" (or the Stonecutter IntelliJ plugin), and run ./gradlew "Reset active project" before committing.
  • Build scripts:
    • settings.gradle.kts, stonecutter.gradle.kts (the root project) and build.gradle.kts (each version) are now Kotlin.
    • Loom's version is loom_version in the root gradle.properties.
    • scripts/port.sh is gone, since there's nothing to port between any more.
  • Docs: VERSION_GUIDE.md covers the layout, version comments, the conventions below, and adding a version (a gradle.properties plus an entry in stonecutterVersions).

Conventions the migration settled on

  • Keep features. When a newer version leaves out something an older one implements, the older code stays behind a condition and the newer side gets a TODO. The seven gaps this found are listed in #30.
  • Version-only mixins: NametagsMixin and MatrixStackMixin keep an empty class on the other versions and stay registered, so the mixin configs are the same for every version.
  • Big API splits: RenderScopeImpl and EntityDrawerImpl have two whole implementations, one per side of 1.21.6.
  • No nested block comments: inactive code is inside /* */, so it uses // comments, flat else if chains instead of nested multi-line blocks, and one-line conditions inside version-only blocks.

Behaviour changes

  • Hats render on 1.21.9 and 1.21.10. Their render-queue override was empty.
  • Freelook's zoom-out works on 1.21.5–1.21.11. It was only wired up on 1.21.4.
  • Cloak and hat player lookup on 1.21.8: by UUID from the render state instead of by name.
  • Null check: the display-name null check applies to every version.
  • buildAll runs at most two remapJar tasks at once. A cold build used to stall in garbage collection or fail on a 3 GB heap; it now takes about 1.5–2 minutes.

Checks

  • Jars: each step compared every affected version's jar with its previous one. Same entries and mixin configs, and bytecode differences only where intended (listed in each PR). Removed files are only dead classes that were never registered (NameTagMixin).
  • Builds: stonecutter builds all eight jars with buildAll, and compileJava passes; the Check workflow runs that on this PR too.
  • In game: every version was tested after its step, most recently 1.21.4, 1.21.5 and Freelook's zoom-out.

Follow-ups

  • #30: the features newer versions still lack.
  • Version properties: moving the versions' gradle.properties into one stonecutter.properties.toml.
  • After merge: the workspace CLAUDE.md gets the new commands and layout.

🤖 Generated with Claude Code

Closes #8. Every supported version (1.21.4–1.21.11) now builds from **one copy** of the version-specific code in `src/`, with [Stonecutter](https://stonecutter.kikugie.dev/) comments (`//? if >=1.21.9 { … }`) where Minecraft's API differs. It replaces eight near-identical copies in `versions/*/src`, about 27,700 lines of which ~78% were duplicates. Each step was reviewed, merged into the `stonecutter` integration branch and tested in game: | PR | Step | |---|---| | #24 | 1.21.11 scaffold, Kotlin build scripts, `remapJar` limited to two at a time | | #25 | 1.21.10 | | #26 | 1.21.9 | | #27 | 1.21.8 (before the 1.21.9 render queue) | | #28 | 1.21.7 and 1.21.6 (before Fabric API render state data) | | #29 | 1.21.5 and 1.21.4 (before the 1.21.6 GUI rewrite), and removing the old layout | ## What changes for development - **Layout:** `src/` holds the version-specific code once. `versions/<mc>/` only holds each version's `gradle.properties` (and `build/`, `run/`). - **Projects are `:<mc>`,** for example `./gradlew :1.21.11:runClient` (was `:mc-1.21.11`). `buildAll` still produces `build/allJars/saturn-client-<version>+<mc>.jar`, so the Release and Modrinth workflows are unchanged. - **Editing:** edit with 1.21.11 active, switch with `./gradlew "Set active project to <mc>"` (or the Stonecutter IntelliJ plugin), and run `./gradlew "Reset active project"` before committing. - **Build scripts:** - `settings.gradle.kts`, `stonecutter.gradle.kts` (the root project) and `build.gradle.kts` (each version) are now Kotlin. - Loom's version is `loom_version` in the root `gradle.properties`. - `scripts/port.sh` is gone, since there's nothing to port between any more. - **Docs:** `VERSION_GUIDE.md` covers the layout, version comments, the conventions below, and adding a version (a `gradle.properties` plus an entry in `stonecutterVersions`). ## Conventions the migration settled on - **Keep features.** When a newer version leaves out something an older one implements, the older code stays behind a condition and the newer side gets a `TODO`. The seven gaps this found are listed in #30. - **Version-only mixins:** `NametagsMixin` and `MatrixStackMixin` keep an empty class on the other versions and stay registered, so the mixin configs are the same for every version. - **Big API splits:** `RenderScopeImpl` and `EntityDrawerImpl` have two whole implementations, one per side of 1.21.6. - **No nested block comments:** inactive code is inside `/* */`, so it uses `//` comments, flat `else if` chains instead of nested multi-line blocks, and one-line conditions inside version-only blocks. ## Behaviour changes - **Hats render on 1.21.9 and 1.21.10.** Their render-queue override was empty. - **Freelook's zoom-out works on 1.21.5–1.21.11.** It was only wired up on 1.21.4. - **Cloak and hat player lookup on 1.21.8:** by UUID from the render state instead of by name. - **Null check:** the display-name null check applies to every version. - **`buildAll` runs at most two `remapJar` tasks at once.** A cold build used to stall in garbage collection or fail on a 3 GB heap; it now takes about 1.5–2 minutes. ## Checks - **Jars:** each step compared every affected version's jar with its previous one. Same entries and mixin configs, and bytecode differences only where intended (listed in each PR). Removed files are only dead classes that were never registered (`NameTagMixin`). - **Builds:** `stonecutter` builds all eight jars with `buildAll`, and `compileJava` passes; the Check workflow runs that on this PR too. - **In game:** every version was tested after its step, most recently 1.21.4, 1.21.5 and Freelook's zoom-out. ## Follow-ups - **#30:** the features newer versions still lack. - **Version properties:** moving the versions' `gradle.properties` into one `stonecutter.properties.toml`. - **After merge:** the workspace `CLAUDE.md` gets the new commands and layout. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
selimaj-dev added 16 commits 2026-09-27 04:49:35 +00:00
Step 1 of #8. 1.21.11's version-specific code moves from versions/1.21.11/src
to the top-level src/, built by Stonecutter as the project :1.21.11. The other
versions still build from their own copies as :mc-<v> until they're folded in.

- settings.gradle: applies Stonecutter (Groovy controller) with
  stonecutterVersions = ["1.21.11"] and skips those folders when including
  the old mc-<v> projects.
- The old root build.gradle becomes stonecutter.gradle (the root project's
  script, with the shared repositories, common wiring and buildAll), and
  1.21.11's build.gradle becomes the root build.gradle that Stonecutter runs
  for each version. versions/1.21.11 keeps its gradle.properties and run/.
- The jar has the same 415 entries, fabric.mod.json and mixin configs as
  before; the only class differences also appear when rebuilding an
  untouched version today.
- scripts/port.sh treats src/ as the active Stonecutter version, so changes
  port between it and the old copies both ways.
- README and VERSION_GUIDE describe the layout, version comments and how to
  add a version.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Switch the root build scripts to Kotlin and limit parallel remaps
Check / compile (pull_request) Successful in 7m47s
eebfe8fa4d
- settings.gradle, stonecutter.gradle and the root build.gradle become
  .kts. Loom's version for the Stonecutter versions moves to the root
  gradle.properties (loom_version), read by pluginManagement, since the
  Kotlin plugins block can't read properties. The old mc-<v> projects keep
  their Groovy scripts until they're folded in.
- buildAll ran all eight remapJar tasks at once. Each loads a Minecraft
  classpath and remaps a ~50 MB jar, which doesn't fit in the 3 GB daemon
  heap: a cold build stalled in garbage collection for 40+ minutes, and
  earlier runs failed. A shared build service now lets two remaps run
  together; a cold buildAll takes about 2 minutes.
- port.sh and VERSION_GUIDE follow the .kts file names.

The 1.21.11 jar is the same as with the Groovy scripts.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Reviewed-on: #24
Build 1.21.10 with Stonecutter from the shared src/
Check / compile (pull_request) Successful in 7m13s
95e4dff448
Step 2 of #8. versions/1.21.10/src is gone; 1.21.10 builds from src/ as
:1.21.10, with versions/1.21.10 keeping only gradle.properties and run/.

API differences go behind version comments: render layers (ShaderUtils),
Camera.update's World vs BlockView (CameraMixin), the text and textured-quad
render states (RenderScopeImpl), Screen.init (SplashOverlayMixin) and
jspecify's @NonNull (SaturnRenderState).

Features 1.21.11 leaves out stay on 1.21.10, with a TODO for 1.21.11:
WorldFeatureImpl.getWorldAge, SaturnScreenFabric.resize and
EntityDrawerImpl clearing the hitbox.

Hats now render on 1.21.10: its HatFeatureRenderer overrode the
render-queue method with an empty body, and the working code sat in an old
VertexConsumerProvider method nothing called. 1.21.10 now uses 1.21.11's
render-queue implementation (HatFeatureRenderer, ObjModel, ObjRenderer),
and also gets 1.21.11's null check on the player's display name.

The 1.21.10 jar has the same entries, fabric.mod.json and mixin configs as
before; only those four classes and ShaderUtils (a helper method for
entityAlpha) have different bytecode. The 1.21.11 jar only differs in
ShaderUtils.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Reviewed-on: #25
Build 1.21.9 with Stonecutter from the shared src/
Check / compile (pull_request) Successful in 7m8s
f2d30e6611
Step 3 of #8. 1.21.9's copy was identical to the old 1.21.10 one, so the
existing version comments already cover it and no code changes: 1.21.9 is
added to stonecutterVersions and versions/1.21.9/src is removed.

Like 1.21.10 in step 2, 1.21.9 now renders hats (1.21.11's render-queue
implementation) and gets the display-name null check. The jar has the same
entries, fabric.mod.json and mixin configs as before; only those classes
and ShaderUtils's entityAlpha helper differ in bytecode.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Reviewed-on: #26
Build 1.21.8 with Stonecutter from the shared src/
Check / compile (pull_request) Successful in 10m37s
0bf4fc6808
Step 4 of #8, the first version before the 1.21.9 render queue.
versions/1.21.8/src is gone; 1.21.8 builds from src/ as :1.21.8.

API differences behind version comments (<1.21.9 / >=1.21.9):
- Player cosmetics: FeatureRenderer.render takes a VertexConsumerProvider
  instead of the render queue. The cloak, hat and OBJ renderers name the
  render target "output" in both, so only the parameter type and the final
  draw call differ; the OBJ face loop moved into drawGroup.
- PlayerEntityRenderer.updateRenderState's entity type, the cape texture
  accessor, GameProfile getName()/getId() vs name()/id(), fonts in
  TextMixin, EntityRenderDispatcher vs EntityRenderManager, Screen input
  events (Click/KeyInput), and KeyBinding categories and isKeyPressed.

Features 1.21.9+ lacks stay on 1.21.8, with a TODO for 1.21.9+:
- NametagsMixin moves into src/. It hooks renderLabelIfPresent with a
  VertexConsumerProvider, which 1.21.9 changed, so on 1.21.9+ its body is
  empty; it stays registered, so features.mixins.json is the same for
  every version.
- The splash screen's loading-bar border, and the real drag deltas in
  SaturnScreenFabric.mouseDragged.
- Status effect icons keep 1.21.8's GUI atlas sprite; 1.21.9+ draws the
  texture instead (getIconId), as before.

Behaviour changes on 1.21.8: the cloak and hat read the Saturn player from
the render state (set by PlayerEntityRendererMixin, by UUID) instead of
looking it up by name, and the display-name null check applies.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Reviewed-on: #27
Step 5 of #8. versions/1.21.7/src is gone; 1.21.7 builds from src/ as
:1.21.7.

1.21.7's copy differed from 1.21.8's in the player render state: Fabric
API's RenderStateDataKey only exists from 1.21.8 (fabric-rendering-v1
12.6.0), so 1.21.7 had no updateRenderState hook, no SaturnRenderState,
and looked players up by name for the cloak and hat. That stays the case:
- The cloak and hat use getData on >=1.21.8 and SaturnPlayer.get(name)
  before that.
- PlayerEntityRendererMixin's updateRenderState exists only on >=1.21.8,
  as flat ranges (>=1.21.9, else if >=1.21.8) calling a shared
  saturn$applyPlayer helper, instead of nested version blocks.
- SaturnRenderState's key only exists on >=1.21.8; the class is empty
  before.
- TODO: 1.21.6 and 1.21.7 still don't show the role icon in names.

1.21.7's NameTagMixin isn't carried over: it wasn't registered in any
mixin config, so it never ran.

The 1.21.7 jar differs from the old one only as 1.21.8's did in step 4
(entityAlpha and drawGroup helpers, getIconId, and the cloak getting its
buffer at the draw call), plus the empty SaturnRenderState and the
missing dead NameTagMixin. 1.21.8-1.21.11 only differ in
PlayerEntityRendererMixin (the helper).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Build 1.21.6 with Stonecutter from the shared src/
Check / compile (pull_request) Successful in 8m3s
a949c18ec1
1.21.6's copy only differed from 1.21.7's in RenderScopeImpl: item
rendering uses ItemRenderState, which 1.21.7 renamed to
KeyedItemRenderState. That's behind a version comment; 1.21.6 keeps its
older loader_version (0.18.5) in its gradle.properties.

Moving a version over leaves the old mc-<v> project's compiled classes in
the shared build/ folder, so a removed class (1.21.6's dead NameTagMixin)
still got packaged until a clean. VERSION_GUIDE now says to run
:<v>:clean once after moving a version.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Reviewed-on: #28
1.21.5 is the first version before the 1.21.6 GUI rewrite, which moved
GUI drawing from vertex consumers and a MatrixStack to render states and
a 2D Matrix3x2fStack.

- RenderScopeImpl and EntityDrawerImpl have two whole implementations,
  one per side of 1.21.6 (a single >=1.21.6 / else split), instead of
  conditionals scattered through them. Callers use RenderScopeImpl(context)
  in every version; 1.21.5's did the same thing through its
  (matrices, vertexConsumers) constructor.
- MatrixStackMixin (MatrixStack as a MatrixStackRef) is real before 1.21.6
  and empty from 1.21.6, registered everywhere like NametagsMixin.
- DrawContextAccessor exposes the vertex consumers before 1.21.6 and the
  GUI render state from it.
- Smaller changes: fog (BackgroundRenderer vs FogRenderer), the end
  portal texture arguments (ShaderUtils, now a flat three-way chain), the
  status effect sprite manager, and getIconId only from 1.21.6.

Features 1.21.6+ lacks stay on 1.21.5, with a TODO: the title panorama
behind Saturn screens and the background blur (screen.backgroundBlur,
see #23).

The 1.21.5 jar differs from the old one only in the helpers from earlier
steps, the RenderScopeImpl(context) calls, the empty SaturnRenderState
and unused Matrix3x2fStackRef, and 1.21.5's dead NameTagMixin (never
registered) being gone. 1.21.6-1.21.11 only gain the empty
MatrixStackMixin and its impl.mixins.json entry.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Build 1.21.4 with Stonecutter; every version now shares src/
Check / compile (pull_request) Successful in 7m44s
25a19d7951
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]>
Reviewed-on: #29
Fix the Release workflow's comment on how buildAll remaps
Check / compile (pull_request) Successful in 2m22s
a8e6f14022
Co-Authored-By: Claude Opus 5.5 <[email protected]>
selimaj-dev added the area/buildarea/versions
type
refactor
labels 2026-09-27 04:49:36 +00:00
selimaj-dev merged commit 771ed93992 into master 2026-09-27 04:50:16 +00:00
selimaj-dev deleted branch stonecutter 2026-09-27 04:50:20 +00:00
selimaj-dev added this to the Saturn Client project 2026-09-27 04:52:11 +00:00
selimaj-dev moved this to Done in Saturn Client on 2026-09-27 04:52:29 +00:00
Sign in to join this conversation.