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:
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.
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)
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]>
- 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]>
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]>
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]>
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]>
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]>
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]>
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]>
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]>
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
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 inversions/*/src, about 27,700 lines of which ~78% were duplicates. Each step was reviewed, merged into thestonecutterintegration branch and tested in game:remapJarlimited to two at a timeWhat changes for development
src/holds the version-specific code once.versions/<mc>/only holds each version'sgradle.properties(andbuild/,run/).:<mc>, for example./gradlew :1.21.11:runClient(was:mc-1.21.11).buildAllstill producesbuild/allJars/saturn-client-<version>+<mc>.jar, so the Release and Modrinth workflows are unchanged../gradlew "Set active project to <mc>"(or the Stonecutter IntelliJ plugin), and run./gradlew "Reset active project"before committing.settings.gradle.kts,stonecutter.gradle.kts(the root project) andbuild.gradle.kts(each version) are now Kotlin.loom_versionin the rootgradle.properties.scripts/port.shis gone, since there's nothing to port between any more.VERSION_GUIDE.mdcovers the layout, version comments, the conventions below, and adding a version (agradle.propertiesplus an entry instonecutterVersions).Conventions the migration settled on
TODO. The seven gaps this found are listed in #30.NametagsMixinandMatrixStackMixinkeep an empty class on the other versions and stay registered, so the mixin configs are the same for every version.RenderScopeImplandEntityDrawerImplhave two whole implementations, one per side of 1.21.6./* */, so it uses//comments, flatelse ifchains instead of nested multi-line blocks, and one-line conditions inside version-only blocks.Behaviour changes
buildAllruns at most tworemapJartasks 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
NameTagMixin).stonecutterbuilds all eight jars withbuildAll, andcompileJavapasses; the Check workflow runs that on this PR too.Follow-ups
gradle.propertiesinto onestonecutter.properties.toml.CLAUDE.mdgets the new commands and layout.🤖 Generated with Claude Code