All pages

Multi-version

How one source tree builds a jar for each supported Minecraft version, where version-specific code may live, what differs between the versions today, and the checklist for adding the next one.

Vantage ships one jar per Minecraft version it supports, and as of 2.0.1 that is 26.1.2 and 26.2. Both jars come from the same source tree at the same commit. This page is for anyone changing the code: how the build picks a version, where version-specific code is allowed to live, what actually differs between the two versions today, and what to touch when Minecraft releases the next one. If you only want a jar, Building is all you need.

One tree, two jars

Almost the whole mod is version-independent and lives in src/main/java. The handful of places where Minecraft moved something live beside the version they belong to, and the build picks the matching set. The point is that a bug is fixed once: there is no second branch to forget.

You choose the version with one Gradle property. Leave it out and you get the default, which is the minecraft_version line in gradle.properties (26.2 as of 2.0.1):

./gradlew build -PnoVersionBump                               # the default version
./gradlew build -PnoVersionBump -Pminecraft_version=26.1.2    # the other one

The jar is named vantage-<mod version>+<minecraft version>.jar, because build.gradle sets the project version to mod_version + "+" + minecraft_version. Two builds in a row therefore leave two differently named jars side by side in build/libs, which is exactly what the release job relies on.

What lives beside a version

Everything that is true of exactly one version lives in one of three places, named after the version:

WhereWhat it holdsAs of 2.0.1
gradle/versions/<v>.propertiesFacts about that version onlyTwo keys: fabric_api_version and minecraft_dependency
src/main/java-mc<v>/Source that cannot be written against both versionsFour compat classes and one mixin
src/main/resources-mc<v>/Resources that differ by versionThe access widener, vantage.accesswidener

The properties file is short. This is the one for 26.2, as it stands at 2.0.1:

fabric_api_version=0.158.0+26.2

# The range the jar advertises. Deliberately declared rather than derived from the version above: a
# build is only known to work on what it was compiled and tested against, and claiming more just turns
# a compile error into a crash on somebody else's machine.
minecraft_dependency=>=26.2 <26.3

The range is written by hand on purpose, and the comment says why. A jar claims only the versions it was built and tested against. Fabric Loader checks that range at start-up and refuses the jar outside it, so a player on the wrong version gets a clear message instead of a crash later.

Everything the two versions share stays in gradle.properties: the loader, Loom, the mod version, JUnit, the Hypixel Mod API and ModMenu. There is no per-version slot for those today.

How the build picks one

build.gradle turns minecraft_version into an overlay name, mc<version>, and does four things with it:

  1. Adds src/main/java-mc<version> and src/main/resources-mc<version> as extra source directories.
  2. Reads gradle/versions/<version>.properties for the Fabric API version and the declared range.
  3. Points Loom's access widener at src/main/resources-mc<version>/vantage.accesswidener.
  4. Expands ${minecraft_dependency} into fabric.mod.json's depends.minecraft while processing resources.

A version the tree does not know fails at configuration time, before a single class compiles. Without that check a typo would surface as a few hundred cannot find symbol errors. The message names what is missing and lists what exists; for a version that has no files it reads like this:

Vantage does not support Minecraft 26.3. It needs gradle/versions/26.3.properties and src/main/java-mc26.3. Supported: [26.1.2, 26.2]

The check looks for the properties file and the Java overlay directory. It does not look for the resources overlay, so create all three together.

What actually differs

As of 2.0.1, these are the differences between the two versions, and the class that absorbs each one. Everything here is read from the overlay classes' own documentation.

AreaOn 26.1.2On 26.2Absorbed by
Screens and the HUDOne Gui class; get-prefixed accessors; the hide-GUI flag on Options; legacy colour data on ChatFormattingGui split into a screen manager and Hud; most get prefixes dropped; the hide-GUI flag moved; colour data on TextColorMc
Dyed blocks and itemsOne constant per colour, such as Blocks.LIME_WOOLA colour collection, such as Blocks.WOOL.lime()Dyed
Slimes and magma cubesIn entity.monsterIn entity.monster.cubemobMobs
GPU pipelinesUniforms and samplers declared one call at a time; vertex layout from named constants; draw mode part of the layoutBind groups; vertex layout built from GpuFormat; draw mode separateGpu
GPU timer queriesOne query object per measurement, kept alive to be read a frame laterOne long-lived object that rotates its own slots and reports an averageGpuTimer, made by Gpu
The burning overlayScreenEffectRenderer.renderFire, drawing into a MultiBufferSourceScreenEffectRenderer.submitFire, submitting to a SubmitNodeCollectorScreenEffectRendererMixin

The shaders themselves did not change between the versions, which is why the GPU difference is a seam and not a rewrite. The burning overlay is what the Hide the fire overlay option of Weather and light removes, so its mixin has to exist in both shapes.

The overlay rule

A file belongs in an overlay only when it genuinely cannot be written against both versions, and every overlay must present the same API. Nothing in src/main/java may be able to tell which version it is being compiled for.

In practice the differences funnel through one package, core.compat:

ClassCoversWhere it lives
McClient internals that moved: screens, the HUD, chat, the tab list, legacy text formattingEach overlay
DyedThe sixteen dyed variants of blocks and itemsEach overlay
MobsMob types whose package moved, which an import alias cannot paper overEach overlay
GpuPipeline construction and the few encoder calls that changed shapeEach overlay
GpuTimerThe one shape a GPU timer query takes; each version's Gpu.newTimer() supplies its ownsrc/main/java, shared

Mc describes itself as the only file in the mod allowed to know which Minecraft version it is being compiled against. Every method on it is a thin forward with no state of its own. A feature never touches the moved API; it asks the seam:

Screen open = Mc.screen();            // the screen currently open, or null in the world
boolean hidden = Mc.hudHidden();      // F1, wherever this version keeps the flag
Block wool = Dyed.wool(DyeColor.LIME);
if (Mobs.isSlime(entity)) { … }       // true for magma cubes too, as Minecraft's hierarchy says

When the next version moves something, prefer widening one of these classes over adding a new overlay file. When a whole file has to be duplicated, say why in its Javadoc, because the next person has to keep both copies in step.

Access wideners

Each version has its own access widener, in the accessWidener v2 official format, at src/main/resources-mc<v>/vantage.accesswidener. A member that common code reaches has to be widened in both files. The two files differ only where one version already makes a member public:

Widened inMemberWhy
BothThe tab list's header and footer, a display entity's scale, a container screen's bounds and hovered slotRead by the tab list reader, a foraging feature and the container overlays
26.1.2 onlyMinecraft.overlayThe UI smoke test waits for the loading overlay; 26.2 has a public accessor for it
26.2 onlyRenderPipelines.register, two pipeline snippets, RenderType.create26.2 made them non-public, and the world renderer builds its render types the way vanilla does

Mixins that may vanish

Every mixin config is declared not required, so a target that moves turns into a missing feature rather than a crash. That degradation is silent, which is the price. The tool that makes it loud again is tools/mixin_targets.py:

python tools/mixin_targets.py              # every version in gradle/versions/
python tools/mixin_targets.py 26.1.2       # just one

For each version it collects the common mixins plus that version's overlay mixins, finds each @Mixin target class and each injected method name in the merged Minecraft jar Loom has already downloaded, and exits non-zero if any is missing. It reads that jar from Loom's cache under ~/.gradle/caches/fabric-loom/<version>/, so build each version once before you run it. A healthy run ends with the line All mixin targets resolve.

A mixin whose target differs between versions lives in the overlays, as ScreenEffectRendererMixin does. Its config, vantage.qol.mixins.json, names the class once, and each overlay supplies a class by that name.

Adding the next version

No document lists these steps in one place; they follow from the build's error message, the access widener pair, the release tool and the pipeline. Work through them in order.

  1. Create gradle/versions/<v>.properties with fabric_api_version and minecraft_dependency. Declare only the range you will test.
  2. Create src/main/java-mc<v>/ by copying the nearest version's overlay, then change what Minecraft moved. Keep every public signature in Mc, Dyed, Mobs and Gpu identical to the other overlays.
  3. Create src/main/resources-mc<v>/vantage.accesswidener, starting from the nearest version's file. Drop what the new version made public; add what it hid.
  4. Compile and test the new target and the old ones: ./gradlew compileJava test -Pminecraft_version=<v>, then the same for every other version.
  5. Check the mixins: python tools/mixin_targets.py. A mixin whose target moved goes into the overlays.
  6. Look at it running: ./gradlew runUiSmokeTest -Pminecraft_version=<v>, then read the report. Testing says what a healthy report looks like.
  7. Teach the release tool: add the version to TARGETS in tools/release.py. It refuses to publish a release that lacks a jar for any target, and it writes the release notes' "Which jar" table from that tuple. The sentence under the table begins "Both require…", so change it once there are three.
  8. Teach the pipeline: add a ./gradlew build -Pminecraft_version=<v> -PnoVersionBump line to the release job in .gitlab-ci.yml, beside the existing two.
  9. Decide the default: minecraft_version in gradle.properties is what the compile and build-jar jobs build, and what a bare ./gradlew build builds.
  10. Regenerate the website's catalogue: python tools/sitedocs.py. It lists every version that has both a properties file and an overlay, so the version list on this site follows from step 1 and step 2.
  11. Update the prose that names versions by hand: the README and Install.

Dropping a version is the same list in reverse: delete its three files, its entry in TARGETS and its line in the release job.

  • Building: the JDK, the Gradle tasks and the version-bump flag.
  • Architecture: where core.compat sits among the other packages.
  • Testing: the unit tests and the smoke test you run on each target.
  • Releasing: how both jars become one release.