Stonecutter 0.9 writes an inactive block inside another as /^ ... ^/, so nesting works (GlassRenderer relies on it since 26.1). else if is still clearer for several ranges of the same code. Co-Authored-By: Claude Opus 5.5 <[email protected]>
79 lines
7.8 KiB
Markdown
79 lines
7.8 KiB
Markdown
# 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, 26.1 and 26.2) builds from one copy of the version-specific code with [Stonecutter](https://stonecutter.kikugie.dev/):
|
|
|
|
- `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, 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 `loomx.loom_version` in the root `gradle.properties`.
|
|
|
|
Minecraft is obfuscated up to 1.21.11 and ships with Mojang's names from 26.1. The `loom-back-compat` plugin (`loomx`) applies the matching Loom for each version: the remapping one for 1.21.x, whose jar is `remapJar`'s output, and the plain one for 26.x, whose jar is `jar`'s. 26.x also compiles with Java 25 (21 before), set per version in `build.gradle.kts`; Gradle downloads a missing JDK through the foojay resolver, and CI installs both. An optional `minecraft_compat` in a version's `gradle.properties` widens the Minecraft versions its jar loads on, as 26.1's does for its hotfixes.
|
|
|
|
## Mappings
|
|
|
|
Every version is built with Mojang's official names (`loom.officialMojangMappings()`), the names Minecraft itself ships from 26.1 on. Yarn isn't used any more: 1.21.11 has the last Yarn release.
|
|
|
|
Mojang renames things between versions. `src/` uses the active version's (1.21.11's) names, and `stonecutter.gradle.kts` lists replacements that swap in each version's name when Stonecutter processes it: for example `Identifier`, which was `ResourceLocation` before 1.21.11, `AvatarRenderState`, which was `PlayerRenderState` before 1.21.9, and `GuiGraphics`, which is `GuiGraphicsExtractor` from 26.1. Add a replacement there when a rename touches many lines. From 26.2 the open screen and overlay live on `Gui` rather than `Minecraft`, so code goes through `impl.ui.Screens` for them. A class that only moved package needs just a version comment on its import, and a renamed method a version comment where it's called.
|
|
|
|
Released 1.21.x jars run on intermediary names (`method_1234`), not Mojang's, and Loom renames everything that refers to Minecraft when it builds them. So a mixin can't implement one of `common`'s Ref interfaces by relying on a vanilla method that happens to have the same name: the vanilla method is renamed in the jar, and the interface method is left without an implementation (`AbstractMethodError`). Shadow the vanilla method and implement the Ref method explicitly, as `WindowMixin` does. For the same reason, Ref methods mustn't share a name and parameters with a method of the class they're mixed into, or the mixin replaces the vanilla method in development.
|
|
|
|
## 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:
|
|
|
|
```java
|
|
//? if >=1.21.9 {
|
|
public void submit(PoseStack matrices, SubmitNodeCollector output, int light, AvatarRenderState state, float limbAngle, float limbDistance) {
|
|
//?} else
|
|
//public void render(PoseStack matrices, MultiBufferSource output, int light, AvatarRenderState 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 `/* … */`:
|
|
|
|
```java
|
|
//? if >=1.21.11 {
|
|
import net.minecraft.client.renderer.rendertype.RenderTypes;
|
|
import net.minecraft.client.renderer.rendertype.RenderSetup;
|
|
//?} else {
|
|
/*import net.minecraft.client.renderer.RenderStateShard.MultiTextureStateShard;
|
|
*///?}
|
|
```
|
|
|
|
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. The panorama in `SaturnScreenFabric.render` is an example. Search for `TODO` to find what newer versions still lack (tracked in #30), and remove the condition once a version gap is fixed.
|
|
|
|
A whole class that only exists for some versions (such as `MatrixStackMixin`, which only makes sense before 1.21.6's GUI rewrite) can keep an empty class body on the other versions. Before splitting a class like that, look for a hook that works on every version: nametags used to hook label rendering, which 1.21.9 changed, and now replace the name while the render state is built (`LivingEntityRendererMixin`), with no version condition at all. 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. Version blocks can nest (Stonecutter writes an inactive block inside another as `/^ … ^/`, as in `GlassRenderer`), but for more than two ranges of the same thing, prefer `//?} else if <condition> {`, 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:
|
|
|
|
```sh
|
|
./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`, `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 (or when its class first loads, such as in a world), 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.
|