From 2a1eee072b56bdeadc5e5f3239edc01d4b8d702b Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 8 Jul 2026 04:23:18 +0000 Subject: [PATCH 01/15] Consolidate all launch testing into one version-independent test driver The launch tests previously shipped three test-driver flavours (modern gametest, legacy gametest with a near-identical copied source file, and a pre-gametest fallback). Replace them with the single fallback-style driver for every supported Minecraft version: the client joins a pre-generated survival world via --quickPlaySingleplayer, screenshots the HUD, and exits. One source file now compiles and runs against 1.21-26.2: - the auto-named Screenshot.grab overload is the only one whose signature is stable across all versions, so the driver renames its output instead of naming it up front - Minecraft.getMainRenderTarget() moved to GameRenderer in 26.2, so the accessor is resolved by name at runtime (Mojang name on unobfuscated 26.x, version-stable intermediary name on obfuscated 1.21.x) Every Minecraft symbol the driver uses was verified against the official Mojang mappings of all 12 obfuscated versions, and the source was compiled directly against the unobfuscated 26.1 and 26.2 client jars. This removes the clientGametest split everywhere: the fabric client gametest dependency and jvm args, the per-flavour source-set selection, the conditional pregen_world CI matrix field, and the title-screen screenshots (only the survival screenshots feed the PR report). Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01FpvdqYScz6nusUf9qioyg9 --- .github/scripts/generate-matrices.sh | 13 +-- .github/scripts/generate-test-world.sh | 15 +-- .github/scripts/publish-screenshots.sh | 13 +-- .github/workflows/build.yml | 19 +--- DEVELOPMENT.md | 9 +- launchtest/build.gradle | 103 ++++-------------- .../betterhud/gametest/LaunchTestClient.java | 68 ------------ .../src/fallback/resources/fabric.mod.json | 16 --- .../gametest/BetterHudClientGameTest.java | 44 -------- .../launchtest/LaunchTestClient.java | 97 +++++++++++++++++ launchtest/src/main/resources/fabric.mod.json | 16 +++ settings.gradle | 5 +- .../gametest/BetterHudClientGameTest.java | 39 ------- src/gametest/resources/fabric.mod.json | 16 --- supported-versions.json | 32 +++--- 15 files changed, 181 insertions(+), 324 deletions(-) delete mode 100644 launchtest/src/fallback/java/dsns/betterhud/gametest/LaunchTestClient.java delete mode 100644 launchtest/src/fallback/resources/fabric.mod.json delete mode 100644 launchtest/src/gametest-legacy/java/dsns/betterhud/gametest/BetterHudClientGameTest.java create mode 100644 launchtest/src/main/java/dsns/betterhud/launchtest/LaunchTestClient.java create mode 100644 launchtest/src/main/resources/fabric.mod.json delete mode 100644 src/gametest/java/dsns/betterhud/gametest/BetterHudClientGameTest.java delete mode 100644 src/gametest/resources/fabric.mod.json diff --git a/.github/scripts/generate-matrices.sh b/.github/scripts/generate-matrices.sh index ddf6d18..5de742d 100755 --- a/.github/scripts/generate-matrices.sh +++ b/.github/scripts/generate-matrices.sh @@ -2,10 +2,8 @@ # Derives the CI job matrices from supported-versions.json (the single source # of truth for supported Minecraft versions) and writes them to GITHUB_OUTPUT: # - build: one entry per build variant -> { mc, java } -# - launch: one entry per launchable version -> { mc, java, pregen_world } +# - launch: one entry per launchable version -> { mc, java } # - publish: one entry per build variant -> { mc, java, game_versions, range } -# pregen_world marks versions whose Fabric API has no client gametest module; -# their launch test needs a pre-generated world (see generate-test-world.sh). # game_versions is the newline-separated list of Minecraft versions the # variant's jar covers (fed to mc-publish), range is its human-readable form # for the release name (e.g. "1.21-1.21.5"). @@ -18,12 +16,11 @@ java_for_variant() { } launch="[]" -while IFS=$'\t' read -r mc variant gametest; do +while IFS=$'\t' read -r mc variant; do java="$(java_for_variant "$variant")" - pregen=$([ "$gametest" = "false" ] && echo true || echo false) - launch="$(jq -c --arg mc "$mc" --arg java "$java" --argjson pregen "$pregen" \ - '. + [{mc: $mc, java: $java, pregen_world: $pregen}]' <<<"$launch")" -done < <(jq -r 'to_entries[] | [.key, .value.variant, (.value.clientGametest | tostring)] | @tsv' "$JSON") + launch="$(jq -c --arg mc "$mc" --arg java "$java" \ + '. + [{mc: $mc, java: $java}]' <<<"$launch")" +done < <(jq -r 'to_entries[] | [.key, .value.variant] | @tsv' "$JSON") build="[]" publish="[]" diff --git a/.github/scripts/generate-test-world.sh b/.github/scripts/generate-test-world.sh index 8fcafe1..0001123 100755 --- a/.github/scripts/generate-test-world.sh +++ b/.github/scripts/generate-test-world.sh @@ -1,13 +1,8 @@ #!/usr/bin/env bash -# Generates a vanilla singleplayer world save for the given Minecraft version -# by briefly running that version's dedicated server, and places it where the -# launch test expects it (launchtest/run/saves/ci-world). -# -# Only needed for versions whose Fabric API has no client gametest module -# (before 1.21.4): their launch test cannot create a world in-game, so the -# client auto-joins this save via --quickPlaySingleplayer instead. Using the -# exact same Minecraft version to generate the world avoids any world-upgrade -# prompts when the client opens it. +# Generates the vanilla singleplayer world save the launch test auto-joins +# (launchtest/run/saves/ci-world) by briefly running the given Minecraft +# version's dedicated server. Using the exact same version avoids any +# world-upgrade prompts when the client opens it. set -euo pipefail MC_VERSION="${1:?usage: generate-test-world.sh }" @@ -23,7 +18,7 @@ curl -sSfo server.jar "$SERVER_URL" echo "eula=true" > eula.txt # A flat world keeps generation fast and renders instantly on the CI software -# renderer. The default gamemode (survival) matches the gametest-based runs. +# renderer; the default gamemode (survival) shows the full HUD. cat > server.properties <<'EOF' level-name=ci-world level-type=minecraft\:flat diff --git a/.github/scripts/publish-screenshots.sh b/.github/scripts/publish-screenshots.sh index cf77ee5..7946f0d 100755 --- a/.github/scripts/publish-screenshots.sh +++ b/.github/scripts/publish-screenshots.sh @@ -5,7 +5,7 @@ # URLs) in a PR comment and in the job summary. # # Leaves the normalized screenshots in screenshots-publish/ for the comment -# step: /mc--survival.png and /mc--title.png. +# step: /mc--survival.png. # # Required env: GITHUB_TOKEN, GITHUB_REPOSITORY, GITHUB_RUN_ID, GITHUB_SHA set -euo pipefail @@ -28,18 +28,15 @@ while IFS=: read -r id name; do unzip -oq "artifact-zips/$id.zip" -d "shots/$mc" done < <(jq -r '.artifacts[] | select(.name | startswith("launch-screenshots-mc")) | "\(.id):\(.name)"' artifacts.json) -# --- Normalize names (gametest screenshots carry a timestamp prefix) -------- PUBLISH="screenshots-publish/$GITHUB_SHA" mkdir -p "$PUBLISH" for dir in shots/*/; do mc="$(basename "$dir")" - for kind in survival-world title-screen; do - src="$(find "$dir" -name "*betterhud-$kind.png" -print -quit)" - if [ -n "$src" ]; then - cp "$src" "$PUBLISH/mc-$mc-${kind%%-*}.png" - fi - done + src="$(find "$dir" -name "betterhud-survival-world.png" -print -quit)" + if [ -n "$src" ]; then + cp "$src" "$PUBLISH/mc-$mc-survival.png" + fi done count="$(find "$PUBLISH" -name '*.png' | wc -l)" diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 20256d5..54c41d9 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,14 +1,13 @@ # Build the mod for every Stonecutter variant, then launch-test the built jars # on EVERY supported Minecraft version concurrently: each launch job boots a # real production Fabric client under XVFB with the covering variant's jar and -# version-matched runtime mods, enters a survival singleplayer world with the +# version-matched runtime mods, joins a pre-generated survival world with the # HUD active, screenshots it, and fails if anything crashes on the way. # The screenshot report job embeds all screenshots in a PR comment (via the -# ci-screenshots branch) and in the job summary, and the modrinth bundle job -# packages the release jars with their per-jar Minecraft version lists. +# ci-screenshots branch) and in the job summary. # -# Both job matrices are generated from supported-versions.json — add a version -# there (see README) and every job picks it up. +# All job matrices are generated from supported-versions.json — add a version +# there (see DEVELOPMENT.md) and every job picks it up. name: build on: push @@ -70,11 +69,6 @@ jobs: # One job per supported Minecraft version (not per build variant): the # 1.21 variant jar is launch-tested on 1.21-1.21.5, the 1.21.6 variant jar # on 1.21.6-1.21.11, and so on. - # - # pregen_world marks versions whose Fabric API has no client gametest module - # (before 1.21.4): for those a vanilla world save is generated first with - # that version's dedicated server, and the client auto-joins it instead of - # creating one through the gametest API. launch-test: name: launch mc${{ matrix.mc }} runs-on: ubuntu-24.04 @@ -105,11 +99,10 @@ jobs: run: sudo apt-get update && sudo apt-get install -y xvfb - name: generate singleplayer test world - if: ${{ matrix.pregen_world }} run: bash .github/scripts/generate-test-world.sh ${{ matrix.mc }} - name: launch minecraft ${{ matrix.mc }} with mod - run: ./gradlew :launchtest:runProductionClientGameTest -PtestMcVersion=${{ matrix.mc }} --stacktrace + run: ./gradlew :launchtest:runLaunchTest -PtestMcVersion=${{ matrix.mc }} --stacktrace - name: check survival world screenshot was taken run: test -n "$(find launchtest/run/screenshots -name '*betterhud-survival-world.png' -print -quit 2>/dev/null)" @@ -170,7 +163,7 @@ jobs: const marker = ''; const body = [ marker, - `The game launched and entered a survival singleplayer world with the HUD active, on every supported Minecraft version, for \`${context.sha.slice(0, 7)}\`. A ❌ means that version's launch test did not produce a screenshot — check its job. Title-screen screenshots and game logs are in the run artifacts.`, + `The game launched and entered a survival singleplayer world with the HUD active, on every supported Minecraft version, for \`${context.sha.slice(0, 7)}\`. A ❌ means that version's launch test did not produce a screenshot — check its job. Game logs are in the run artifacts.`, '', `\n${rows}
`, ].join('\n'); diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index 33cd133..b511820 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -16,14 +16,17 @@ Minecraft versions. - `fabricApi`, `modmenu`, `cloth`: the newest releases built for that Minecraft version (check each project's maven or Modrinth page — the launch test will catch runtime-incompatible picks). - - `clientGametest`: `true` (every version since 1.21.4 has the module). 2. If a new variant is needed: create `versions//gradle.properties` (copy the newest one and adjust the versions and `mc_dep` range), cap the previous variant's `mc_dep`, and add any `//?` Stonecutter guards the new Minecraft requires in the sources. -To launch-test one version locally: -`./gradlew :launchtest:runProductionClientGameTest -PtestMcVersion=` +To launch-test one version locally (generate the test world once per version): + +``` +bash .github/scripts/generate-test-world.sh +./gradlew :launchtest:runLaunchTest -PtestMcVersion= +``` ### Releasing to Modrinth and CurseForge diff --git a/launchtest/build.gradle b/launchtest/build.gradle index 49d52e8..83ef6a2 100644 --- a/launchtest/build.gradle +++ b/launchtest/build.gradle @@ -1,43 +1,24 @@ -// Production client launch test harness. +// Launch test harness: boots a real production Fabric client (fabric-loader + +// vanilla Minecraft + the built betterhud jar + its runtime dependency mods) +// for any supported Minecraft version, joins a pre-generated survival world +// (.github/scripts/generate-test-world.sh), screenshots it with the HUD +// active, and exits. Any crash on the way fails the build. // -// Boots a REAL production Fabric client (fabric-loader + vanilla Minecraft + -// the built betterhud jar + its runtime dependency mods) for ANY supported -// Minecraft version, verifies the game reaches the title screen and a survival -// singleplayer world with the HUD active, takes screenshots, and exits. Any -// crash on the way fails the build. The Minecraft version to launch is chosen -// with a Gradle property: -// -// ./gradlew :launchtest:runProductionClientGameTest -PtestMcVersion=1.21.3 +// ./gradlew :launchtest:runLaunchTest -PtestMcVersion=1.21.3 // // Loom's ClientProductionRunTask is hard-wired to the project's Minecraft -// version, so this project applies Loom with `testMcVersion` as its Minecraft +// version, so this project applies Loom with testMcVersion as its Minecraft // version, then launches the jar built by the Stonecutter variant that covers // that version. On Linux CI (CI env var set) Loom runs the game under XVFB. -// -// The test driver mod compiled by this project comes in three flavours, -// because Fabric's client gametest API only exists from 1.21.4: -// - 26.x: /src/gametest (Mojang-style gametest API names, 5.x) -// - 1.21.4-1.21.11: src/gametest-legacy (same test, Yarn-style API names) -// - 1.21-1.21.3: src/fallback (no gametest API: joins a pre-generated -// world via --quickPlaySingleplayer, screenshots, exits; -// see .github/scripts/generate-test-world.sh) plugins { id 'net.fabricmc.fabric-loom-remap' version "${loom_version}" apply false id 'net.fabricmc.fabric-loom' version "${loom_version}" apply false } -// --------------------------------------------------------------------------- -// supported-versions.json (repo root) is the single source of truth for the -// launchable Minecraft versions. Per version it pins: -// - variant: the Stonecutter build variant whose jar covers it -// - fabricApi: newest Fabric API built for exactly this version -// - modmenu: newest Mod Menu whose fabric.mod.json accepts this version -// - cloth: newest Cloth Config whose fabric.mod.json accepts this version -// - clientGametest: whether this Fabric API provides fabric-client-gametest-api-v1 -// (absent before 1.21.4; those versions use src/fallback) -// All mod versions were verified against each jar's declared `depends` range. -// --------------------------------------------------------------------------- +// supported-versions.json (repo root) pins, per launchable Minecraft version, +// the Stonecutter variant whose jar covers it and the newest fabricApi / +// modmenu / cloth releases built for exactly that version. def supportedVersions = new groovy.json.JsonSlurper().parse(new File(rootDir, 'supported-versions.json')) def testMcVersion = providers.gradleProperty('testMcVersion').getOrElse(supportedVersions.keySet().last()) @@ -46,15 +27,12 @@ if (entry == null) { throw new GradleException("Unsupported testMcVersion '${testMcVersion}'. Supported versions: ${supportedVersions.keySet().join(', ')}") } -// Reuse the covering variant's loader and Java versions so the launch matches -// what that variant is built and shipped for. def variantProps = new Properties() file("../versions/${entry.variant}/gradle.properties").withInputStream { variantProps.load(it) } def loaderVersion = variantProps.getProperty('loader_version') def javaVersionInt = Integer.parseInt(variantProps.getProperty('java_version')) -// Same plugin split as the variant builds: obfuscated 1.21.x needs the remap -// plugin (intermediary production namespace), 26.x runs Mojang names natively. +// Obfuscated 1.21.x needs the remap plugin; 26.x runs Mojang names natively. def isLegacy = entry.variant.startsWith('1.') apply plugin: isLegacy ? 'net.fabricmc.fabric-loom-remap' : 'net.fabricmc.fabric-loom' @@ -68,7 +46,7 @@ repositories { maven { url 'https://maven.terraformersmc.com/releases/' } } -// Matches the variant builds: MC 26+ rejects ASM coming from annotation processors. +// MC 26+ rejects ASM coming from annotation processors. if (!isLegacy) { configurations.configureEach { if (name.toLowerCase().contains('annotationprocessor')) { @@ -77,15 +55,6 @@ if (!isLegacy) { } } -// Pick the test driver source flavour for this Minecraft version (see header). -if (entry.clientGametest) { - sourceSets.main.java.srcDirs = [isLegacy ? 'src/gametest-legacy/java' : "${rootDir}/src/gametest/java"] - sourceSets.main.resources.srcDirs = ["${rootDir}/src/gametest/resources"] -} else { - sourceSets.main.java.srcDirs = ['src/fallback/java'] - sourceSets.main.resources.srcDirs = ['src/fallback/resources'] -} - dependencies { minecraft "com.mojang:minecraft:${testMcVersion}" @@ -93,28 +62,16 @@ dependencies { mappings loom.officialMojangMappings() modImplementation "net.fabricmc:fabric-loader:${loaderVersion}" modImplementation "net.fabricmc.fabric-api:fabric-api:${entry.fabricApi}" - if (entry.clientGametest) { - modImplementation fabricApi.module('fabric-client-gametest-api-v1', entry.fabricApi) - } } else { implementation "net.fabricmc:fabric-loader:${loaderVersion}" implementation "net.fabricmc.fabric-api:fabric-api:${entry.fabricApi}" - implementation fabricApi.module('fabric-client-gametest-api-v1', entry.fabricApi) } - // Mods loaded alongside the built betterhud jar by the production run. - // These are production (intermediary/official) jars straight off the mod - // mavens; transitive deps are excluded because each fat jar already nests - // everything it needs via jar-in-jar. + // Production jars loaded alongside the built betterhud jar; transitive + // deps are excluded because each fat jar nests its own via jar-in-jar. productionRuntimeMods("net.fabricmc.fabric-api:fabric-api:${entry.fabricApi}") { transitive = false } productionRuntimeMods("com.terraformersmc:modmenu:${entry.modmenu}") { transitive = false } productionRuntimeMods("me.shedaniel.cloth:cloth-config-fabric:${entry.cloth}") { transitive = false } - - if (entry.clientGametest) { - // Handles -Dfabric.client.gametest in the production client. Not part - // of the fabric-api fat jar, so it is loaded as its own mod. - productionRuntimeMods(fabricApi.module('fabric-client-gametest-api-v1', entry.fabricApi)) { transitive = false } - } } tasks.withType(JavaCompile).configureEach { @@ -129,35 +86,21 @@ java { evaluationDependsOn(":${entry.variant}") def variantProject = project(":${entry.variant}") -// 1.21.x projects ship the intermediary-remapped jar; 26.x projects ship the -// plain jar (no remapping for unobfuscated Minecraft). +// Unobfuscated 26.x projects have no remapJar; their plain jar ships as-is. def variantJarTask = variantProject.tasks.names.contains('remapJar') ? 'remapJar' : 'jar' def ownJarTask = tasks.names.contains('remapJar') ? 'remapJar' : 'jar' -// Launches a real production Fabric client and runs the launch test: reach the -// title screen, enter a survival singleplayer world with the HUD rendering, -// screenshot it (run/screenshots/), and shut down cleanly. -tasks.register('runProductionClientGameTest', net.fabricmc.loom.task.prod.ClientProductionRunTask) { +tasks.register('runLaunchTest', net.fabricmc.loom.task.prod.ClientProductionRunTask) { mods.setFrom(variantProject.tasks.named(variantJarTask), tasks.named(ownJarTask), configurations.productionRuntimeMods) - - if (entry.clientGametest) { - // Fabric's client gametest runner drives the test and shuts the game - // down after the registered gametest (src/gametest) finishes. - jvmArgs.add('-Dfabric.client.gametest') - jvmArgs.add('-Dfabric.client.gametest.disableNetworkSynchronizer=true') - } else if (file('run/saves/ci-world').exists()) { - // No gametest API on this version: auto-join the pre-generated world; - // the fallback testmod screenshots it and stops the client. Without a - // pre-generated world the testmod stops at the title screen instead. - jvmArgs.add('-Dbetterhud.launchtest.expectWorld=true') - programArgs.addAll('--quickPlaySingleplayer', 'ci-world') - } + programArgs.addAll('--quickPlaySingleplayer', 'ci-world') doFirst { - // On a fresh game dir Minecraft opens the accessibility onboarding - // screen instead of the title screen; disable it so the run always - // reaches the title screen. (The gametest module also forces this via - // mixin; the fallback testmod needs it done here.) + if (!file('run/saves/ci-world').exists()) { + throw new GradleException("Missing test world at launchtest/run/saves/ci-world. " + + "Generate it with: bash .github/scripts/generate-test-world.sh ${testMcVersion}") + } + // A fresh game dir opens the accessibility onboarding screen instead + // of joining the world; disable it. def options = new File(runDir.get().asFile, 'options.txt') if (!options.exists()) { options.parentFile.mkdirs() diff --git a/launchtest/src/fallback/java/dsns/betterhud/gametest/LaunchTestClient.java b/launchtest/src/fallback/java/dsns/betterhud/gametest/LaunchTestClient.java deleted file mode 100644 index cb21937..0000000 --- a/launchtest/src/fallback/java/dsns/betterhud/gametest/LaunchTestClient.java +++ /dev/null @@ -1,68 +0,0 @@ -package dsns.betterhud.gametest; - -import net.fabricmc.api.ClientModInitializer; -import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents; -import net.minecraft.client.Minecraft; -import net.minecraft.client.Screenshot; -import net.minecraft.client.gui.screens.TitleScreen; - -/** - * Fallback launch test driver for Minecraft versions whose Fabric API predates - * the client gametest module (before 1.21.4). - * - *

When the run is started with {@code --quickPlaySingleplayer ci-world} and - * {@code -Dbetterhud.launchtest.expectWorld=true} (see the launchtest Gradle - * project), the game joins the pre-generated survival world; once it has ticked - * long enough for chunks to render with the HUD active, this mod screenshots it - * and schedules a clean shutdown, making the client exit with code 0. Without a - * pre-generated world it screenshots the title screen and shuts down there - * instead. A crash anywhere on the way fails the run. - */ -public class LaunchTestClient implements ClientModInitializer { - private static final boolean EXPECT_WORLD = Boolean.getBoolean("betterhud.launchtest.expectWorld"); - - // 200 ticks (10s) gives the software renderer on CI time to draw chunks. - private static final int WORLD_SCREENSHOT_TICK = 200; - private static final int WORLD_STOP_TICK = 240; - private static final int TITLE_SCREENSHOT_TICK = 40; - private static final int TITLE_STOP_TICK = 80; - // 2400 ticks (2min) is plenty to join the pre-generated flat world; bail - // out with a failure instead of hanging until the CI job timeout. - private static final int WORLD_JOIN_TIMEOUT_TICKS = 2400; - - private int totalTicks; - private int worldTicks = -1; - private int titleTicks = -1; - - @Override - public void onInitializeClient() { - ClientTickEvents.END_CLIENT_TICK.register(client -> { - totalTicks++; - - if (EXPECT_WORLD && worldTicks < 0 && totalTicks >= WORLD_JOIN_TIMEOUT_TICKS) { - System.err.println("betterhud launch test: the game never joined the pre-generated world"); - System.exit(1); - } - - if (client.player != null && client.level != null) { - worldTicks++; - if (worldTicks == WORLD_SCREENSHOT_TICK) { - takeScreenshot(client, "betterhud-survival-world"); - } else if (worldTicks == WORLD_STOP_TICK) { - client.stop(); - } - } else if (!EXPECT_WORLD && client.screen instanceof TitleScreen) { - titleTicks++; - if (titleTicks == TITLE_SCREENSHOT_TICK) { - takeScreenshot(client, "betterhud-title-screen"); - } else if (titleTicks == TITLE_STOP_TICK) { - client.stop(); - } - } - }); - } - - private static void takeScreenshot(Minecraft client, String name) { - Screenshot.grab(client.gameDirectory, name + ".png", client.getMainRenderTarget(), message -> {}); - } -} diff --git a/launchtest/src/fallback/resources/fabric.mod.json b/launchtest/src/fallback/resources/fabric.mod.json deleted file mode 100644 index ec2bc93..0000000 --- a/launchtest/src/fallback/resources/fabric.mod.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "schemaVersion": 1, - "id": "betterhud-gametest", - "version": "1.0.0", - "name": "BetterHUD Launch Test", - "description": "Fallback launch test for BetterHUD on Minecraft versions without Fabric's client gametest module: screenshots the (pre-generated) survival world or the title screen, then exits.", - "license": "AGPL-3.0", - "environment": "client", - "entrypoints": { - "client": ["dsns.betterhud.gametest.LaunchTestClient"] - }, - "depends": { - "betterhud": "*", - "fabric-lifecycle-events-v1": "*" - } -} diff --git a/launchtest/src/gametest-legacy/java/dsns/betterhud/gametest/BetterHudClientGameTest.java b/launchtest/src/gametest-legacy/java/dsns/betterhud/gametest/BetterHudClientGameTest.java deleted file mode 100644 index 071f168..0000000 --- a/launchtest/src/gametest-legacy/java/dsns/betterhud/gametest/BetterHudClientGameTest.java +++ /dev/null @@ -1,44 +0,0 @@ -package dsns.betterhud.gametest; - -// Copy of /src/gametest/.../BetterHudClientGameTest.java for Minecraft -// 1.21.4-1.21.11, whose fabric-client-gametest-api-v1 (4.x) names the client -// world accessor getClientWorld() instead of getClientLevel() (5.x, 26.x). -// Keep the two files in sync when changing the test. - -import java.io.IOException; -import java.io.UncheckedIOException; -import java.nio.file.Files; -import java.nio.file.Path; - -import net.fabricmc.fabric.api.client.gametest.v1.FabricClientGameTest; -import net.fabricmc.fabric.api.client.gametest.v1.context.ClientGameTestContext; -import net.fabricmc.fabric.api.client.gametest.v1.context.TestSingleplayerContext; -import net.minecraft.client.gui.screens.worldselection.WorldCreationUiState; - -@SuppressWarnings("UnstableApiUsage") -public class BetterHudClientGameTest implements FabricClientGameTest { - @Override - public void runTest(ClientGameTestContext context) { - assertScreenshotSaved(context.takeScreenshot("betterhud-title-screen")); - - try (TestSingleplayerContext singleplayer = context.worldBuilder() - .adjustSettings(creator -> creator.setGameMode(WorldCreationUiState.SelectedGameMode.SURVIVAL)) - .create()) { - singleplayer.getClientWorld().waitForChunksRender(); - - // Let the world tick a little with the HUD rendering before capturing evidence. - context.waitTicks(40); - assertScreenshotSaved(context.takeScreenshot("betterhud-survival-world")); - } - } - - private static void assertScreenshotSaved(Path screenshot) { - try { - if (!Files.isRegularFile(screenshot) || Files.size(screenshot) == 0) { - throw new AssertionError("Screenshot was not saved: " + screenshot); - } - } catch (IOException e) { - throw new UncheckedIOException("Could not verify screenshot " + screenshot, e); - } - } -} diff --git a/launchtest/src/main/java/dsns/betterhud/launchtest/LaunchTestClient.java b/launchtest/src/main/java/dsns/betterhud/launchtest/LaunchTestClient.java new file mode 100644 index 0000000..ae71cdc --- /dev/null +++ b/launchtest/src/main/java/dsns/betterhud/launchtest/LaunchTestClient.java @@ -0,0 +1,97 @@ +package dsns.betterhud.launchtest; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.util.Comparator; +import java.util.stream.Stream; + +import com.mojang.blaze3d.pipeline.RenderTarget; + +import net.fabricmc.api.ClientModInitializer; +import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents; +import net.minecraft.client.Minecraft; +import net.minecraft.client.Screenshot; +import net.minecraft.client.renderer.GameRenderer; + +/** + * Drives the launch test on every supported Minecraft version: the client is + * started with {@code --quickPlaySingleplayer ci-world} (see the launchtest + * Gradle project), joins the pre-generated survival world, screenshots it with + * the HUD active, and exits cleanly. A crash anywhere on the way fails the run. + */ +public class LaunchTestClient implements ClientModInitializer { + private static final String SCREENSHOT_NAME = "betterhud-survival-world.png"; + // 200 ticks (10s) gives the software renderer on CI time to draw chunks; + // the screenshot is written asynchronously, so leave a few more seconds + // before renaming it and stopping the client. + private static final int SCREENSHOT_TICK = 200; + private static final int STOP_TICK = 300; + private static final int WORLD_JOIN_TIMEOUT_TICKS = 2400; + + private int totalTicks; + private int worldTicks = -1; + + @Override + public void onInitializeClient() { + ClientTickEvents.END_CLIENT_TICK.register(client -> { + totalTicks++; + if (worldTicks < 0 && totalTicks >= WORLD_JOIN_TIMEOUT_TICKS) { + fail("the game never joined the test world"); + } + if (client.player == null || client.level == null) { + return; + } + + worldTicks++; + if (worldTicks == SCREENSHOT_TICK) { + Screenshot.grab(client.gameDirectory, mainRenderTarget(client), message -> {}); + } else if (worldTicks == STOP_TICK) { + renameScreenshot(client.gameDirectory); + client.stop(); + } + }); + } + + // Minecraft.getMainRenderTarget() moved to GameRenderer in MC 26.2, so no + // direct call compiles against every supported version. This mod only runs + // in production, where 26.x uses plain Mojang names and 1.21.x is remapped + // to version-stable intermediary names, so resolve the accessor under + // whichever name the running game has. + private static RenderTarget mainRenderTarget(Minecraft client) { + for (String name : new String[] {"getMainRenderTarget", "method_1522"}) { + try { + return (RenderTarget) Minecraft.class.getMethod(name).invoke(client); + } catch (ReflectiveOperationException e) { + // try the next name + } + } + try { + return (RenderTarget) GameRenderer.class.getMethod("mainRenderTarget").invoke(client.gameRenderer); + } catch (ReflectiveOperationException e) { + throw new IllegalStateException("Could not resolve the main render target", e); + } + } + + // The Screenshot.grab overloads taking a file name differ across versions; + // only the auto-named one is stable, so rename its output afterwards. + private static void renameScreenshot(File gameDirectory) { + Path dir = gameDirectory.toPath().resolve("screenshots"); + try (Stream files = Files.list(dir)) { + Path newest = files.filter(f -> f.getFileName().toString().endsWith(".png")) + .max(Comparator.comparingLong(f -> f.toFile().lastModified())) + .orElseThrow(() -> new IOException("no screenshot in " + dir)); + Files.move(newest, dir.resolve(SCREENSHOT_NAME), StandardCopyOption.REPLACE_EXISTING); + } catch (IOException e) { + e.printStackTrace(); + fail("the screenshot was not saved"); + } + } + + private static void fail(String reason) { + System.err.println("betterhud launch test failed: " + reason); + System.exit(1); + } +} diff --git a/launchtest/src/main/resources/fabric.mod.json b/launchtest/src/main/resources/fabric.mod.json new file mode 100644 index 0000000..3cc81a5 --- /dev/null +++ b/launchtest/src/main/resources/fabric.mod.json @@ -0,0 +1,16 @@ +{ + "schemaVersion": 1, + "id": "betterhud-launchtest", + "version": "1.0.0", + "name": "BetterHUD Launch Test", + "description": "Joins the pre-generated test world, screenshots the HUD, and exits.", + "license": "AGPL-3.0", + "environment": "client", + "entrypoints": { + "client": ["dsns.betterhud.launchtest.LaunchTestClient"] + }, + "depends": { + "betterhud": "*", + "fabric-lifecycle-events-v1": "*" + } +} diff --git a/settings.gradle b/settings.gradle index 9b53c5b..1a48f96 100644 --- a/settings.gradle +++ b/settings.gradle @@ -42,7 +42,6 @@ stonecutter { rootProject.name = 'betterhud' -// Not a Stonecutter variant: a sourceless harness that launches a production -// Fabric client for any supported MC version (-PtestMcVersion) with the -// matching variant's built jar. Used by the CI launch-test matrix. +// Not a Stonecutter variant: launch-tests the built jar on any supported MC +// version (-PtestMcVersion). Used by the CI launch-test matrix. include 'launchtest' diff --git a/src/gametest/java/dsns/betterhud/gametest/BetterHudClientGameTest.java b/src/gametest/java/dsns/betterhud/gametest/BetterHudClientGameTest.java deleted file mode 100644 index 95c29ac..0000000 --- a/src/gametest/java/dsns/betterhud/gametest/BetterHudClientGameTest.java +++ /dev/null @@ -1,39 +0,0 @@ -package dsns.betterhud.gametest; - -import java.io.IOException; -import java.io.UncheckedIOException; -import java.nio.file.Files; -import java.nio.file.Path; - -import net.fabricmc.fabric.api.client.gametest.v1.FabricClientGameTest; -import net.fabricmc.fabric.api.client.gametest.v1.context.ClientGameTestContext; -import net.fabricmc.fabric.api.client.gametest.v1.context.TestSingleplayerContext; -import net.minecraft.client.gui.screens.worldselection.WorldCreationUiState; - -@SuppressWarnings("UnstableApiUsage") -public class BetterHudClientGameTest implements FabricClientGameTest { - @Override - public void runTest(ClientGameTestContext context) { - assertScreenshotSaved(context.takeScreenshot("betterhud-title-screen")); - - try (TestSingleplayerContext singleplayer = context.worldBuilder() - .adjustSettings(creator -> creator.setGameMode(WorldCreationUiState.SelectedGameMode.SURVIVAL)) - .create()) { - singleplayer.getClientLevel().waitForChunksRender(); - - // Let the world tick a little with the HUD rendering before capturing evidence. - context.waitTicks(40); - assertScreenshotSaved(context.takeScreenshot("betterhud-survival-world")); - } - } - - private static void assertScreenshotSaved(Path screenshot) { - try { - if (!Files.isRegularFile(screenshot) || Files.size(screenshot) == 0) { - throw new AssertionError("Screenshot was not saved: " + screenshot); - } - } catch (IOException e) { - throw new UncheckedIOException("Could not verify screenshot " + screenshot, e); - } - } -} diff --git a/src/gametest/resources/fabric.mod.json b/src/gametest/resources/fabric.mod.json deleted file mode 100644 index e432019..0000000 --- a/src/gametest/resources/fabric.mod.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "schemaVersion": 1, - "id": "betterhud-gametest", - "version": "1.0.0", - "name": "BetterHUD GameTest", - "description": "Client gametests for BetterHUD.", - "license": "AGPL-3.0", - "environment": "client", - "entrypoints": { - "fabric-client-gametest": ["dsns.betterhud.gametest.BetterHudClientGameTest"] - }, - "depends": { - "betterhud": "*", - "fabric-client-gametest-api-v1": "*" - } -} diff --git a/supported-versions.json b/supported-versions.json index 6ef4606..3fb1fac 100644 --- a/supported-versions.json +++ b/supported-versions.json @@ -1,18 +1,18 @@ { - "1.21": { "variant": "1.21", "fabricApi": "0.102.0+1.21", "modmenu": "11.0.4", "cloth": "15.0.140", "clientGametest": false }, - "1.21.1": { "variant": "1.21", "fabricApi": "0.116.13+1.21.1", "modmenu": "11.0.4", "cloth": "15.0.140", "clientGametest": false }, - "1.21.2": { "variant": "1.21", "fabricApi": "0.106.1+1.21.2", "modmenu": "12.0.1", "cloth": "16.0.143", "clientGametest": false }, - "1.21.3": { "variant": "1.21", "fabricApi": "0.114.1+1.21.3", "modmenu": "12.0.1", "cloth": "16.0.143", "clientGametest": false }, - "1.21.4": { "variant": "1.21", "fabricApi": "0.119.4+1.21.4", "modmenu": "13.0.4", "cloth": "17.0.144", "clientGametest": true }, - "1.21.5": { "variant": "1.21", "fabricApi": "0.128.2+1.21.5", "modmenu": "14.0.2", "cloth": "18.0.145", "clientGametest": true }, - "1.21.6": { "variant": "1.21.6", "fabricApi": "0.128.2+1.21.6", "modmenu": "15.0.2", "cloth": "19.0.147", "clientGametest": true }, - "1.21.7": { "variant": "1.21.6", "fabricApi": "0.129.0+1.21.7", "modmenu": "15.0.2", "cloth": "19.0.147", "clientGametest": true }, - "1.21.8": { "variant": "1.21.6", "fabricApi": "0.136.1+1.21.8", "modmenu": "15.0.2", "cloth": "19.0.147", "clientGametest": true }, - "1.21.9": { "variant": "1.21.6", "fabricApi": "0.134.1+1.21.9", "modmenu": "16.0.1", "cloth": "20.0.149", "clientGametest": true }, - "1.21.10": { "variant": "1.21.6", "fabricApi": "0.138.4+1.21.10", "modmenu": "16.0.1", "cloth": "21.11.153", "clientGametest": true }, - "1.21.11": { "variant": "1.21.6", "fabricApi": "0.141.4+1.21.11", "modmenu": "17.0.0", "cloth": "21.11.153", "clientGametest": true }, - "26.1": { "variant": "26.1", "fabricApi": "0.145.1+26.1", "modmenu": "18.0.0-beta.1", "cloth": "26.1.154", "clientGametest": true }, - "26.1.1": { "variant": "26.1", "fabricApi": "0.145.4+26.1.1", "modmenu": "18.0.0-beta.1", "cloth": "26.1.154", "clientGametest": true }, - "26.1.2": { "variant": "26.1", "fabricApi": "0.154.0+26.1.2", "modmenu": "18.0.0-beta.1", "cloth": "26.1.154", "clientGametest": true }, - "26.2": { "variant": "26.2", "fabricApi": "0.154.0+26.2", "modmenu": "20.0.0-beta.4", "cloth": "26.2.155", "clientGametest": true } + "1.21": { "variant": "1.21", "fabricApi": "0.102.0+1.21", "modmenu": "11.0.4", "cloth": "15.0.140" }, + "1.21.1": { "variant": "1.21", "fabricApi": "0.116.13+1.21.1", "modmenu": "11.0.4", "cloth": "15.0.140" }, + "1.21.2": { "variant": "1.21", "fabricApi": "0.106.1+1.21.2", "modmenu": "12.0.1", "cloth": "16.0.143" }, + "1.21.3": { "variant": "1.21", "fabricApi": "0.114.1+1.21.3", "modmenu": "12.0.1", "cloth": "16.0.143" }, + "1.21.4": { "variant": "1.21", "fabricApi": "0.119.4+1.21.4", "modmenu": "13.0.4", "cloth": "17.0.144" }, + "1.21.5": { "variant": "1.21", "fabricApi": "0.128.2+1.21.5", "modmenu": "14.0.2", "cloth": "18.0.145" }, + "1.21.6": { "variant": "1.21.6", "fabricApi": "0.128.2+1.21.6", "modmenu": "15.0.2", "cloth": "19.0.147" }, + "1.21.7": { "variant": "1.21.6", "fabricApi": "0.129.0+1.21.7", "modmenu": "15.0.2", "cloth": "19.0.147" }, + "1.21.8": { "variant": "1.21.6", "fabricApi": "0.136.1+1.21.8", "modmenu": "15.0.2", "cloth": "19.0.147" }, + "1.21.9": { "variant": "1.21.6", "fabricApi": "0.134.1+1.21.9", "modmenu": "16.0.1", "cloth": "20.0.149" }, + "1.21.10": { "variant": "1.21.6", "fabricApi": "0.138.4+1.21.10", "modmenu": "16.0.1", "cloth": "21.11.153" }, + "1.21.11": { "variant": "1.21.6", "fabricApi": "0.141.4+1.21.11", "modmenu": "17.0.0", "cloth": "21.11.153" }, + "26.1": { "variant": "26.1", "fabricApi": "0.145.1+26.1", "modmenu": "18.0.0-beta.1", "cloth": "26.1.154" }, + "26.1.1": { "variant": "26.1", "fabricApi": "0.145.4+26.1.1", "modmenu": "18.0.0-beta.1", "cloth": "26.1.154" }, + "26.1.2": { "variant": "26.1", "fabricApi": "0.154.0+26.1.2", "modmenu": "18.0.0-beta.1", "cloth": "26.1.154" }, + "26.2": { "variant": "26.2", "fabricApi": "0.154.0+26.2", "modmenu": "20.0.0-beta.4", "cloth": "26.2.155" } } From 38389ad3065442784ba6b9bd382a59a1806a6f31 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 8 Jul 2026 04:49:55 +0000 Subject: [PATCH 02/15] Derive supported versions from versions/, resolve deps from Modrinth MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit supported-versions.json is gone. The versions/ folder is now the single source of truth: CI expands each variant's mc_dep range against Mojang's version manifest to enumerate the Minecraft versions to build, launch-test, and publish for, so new patch releases are picked up with no repo change. The launch test resolves the latest Fabric API, Mod Menu, and Cloth Config releases for the tested Minecraft version from the Modrinth API at dependency-resolution time — the same versions a launcher would install — instead of hardcoding them per version. Verified to reproduce the previously pinned versions exactly. The launchtest project moves to src/launchtest since it is source code, and all comments outside src/main/java are removed. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01FpvdqYScz6nusUf9qioyg9 --- .github/scripts/generate-matrices.sh | 50 +++---- .github/scripts/generate-test-world.sh | 8 +- .github/scripts/publish-screenshots.sh | 12 -- .github/workflows/build.yml | 46 ++----- .github/workflows/publish.yml | 28 +--- .gitignore | 16 --- DEVELOPMENT.md | 35 ++--- build.gradle | 17 +-- gradle.properties | 6 - launchtest/build.gradle | 110 --------------- settings.gradle | 16 +-- src/launchtest/build.gradle | 125 ++++++++++++++++++ .../launchtest/LaunchTestClient.java | 26 +--- .../src/main/resources/fabric.mod.json | 0 stonecutter.gradle | 35 ++--- supported-versions.json | 18 --- versions/1.21.6/gradle.properties | 3 - versions/1.21/gradle.properties | 3 - versions/26.1/gradle.properties | 2 - versions/26.2/gradle.properties | 2 - 20 files changed, 205 insertions(+), 353 deletions(-) delete mode 100644 launchtest/build.gradle create mode 100644 src/launchtest/build.gradle rename {launchtest => src/launchtest}/src/main/java/dsns/betterhud/launchtest/LaunchTestClient.java (65%) rename {launchtest => src/launchtest}/src/main/resources/fabric.mod.json (100%) delete mode 100644 supported-versions.json diff --git a/.github/scripts/generate-matrices.sh b/.github/scripts/generate-matrices.sh index 5de742d..f5f3926 100755 --- a/.github/scripts/generate-matrices.sh +++ b/.github/scripts/generate-matrices.sh @@ -1,41 +1,41 @@ #!/usr/bin/env bash -# Derives the CI job matrices from supported-versions.json (the single source -# of truth for supported Minecraft versions) and writes them to GITHUB_OUTPUT: -# - build: one entry per build variant -> { mc, java } -# - launch: one entry per launchable version -> { mc, java } -# - publish: one entry per build variant -> { mc, java, game_versions, range } -# game_versions is the newline-separated list of Minecraft versions the -# variant's jar covers (fed to mc-publish), range is its human-readable form -# for the release name (e.g. "1.21-1.21.5"). set -euo pipefail -JSON=supported-versions.json +releases="$(curl -sSf https://piston-meta.mojang.com/mc/game/version_manifest_v2.json \ + | jq -c '[.versions[] | select(.type == "release") | .id] | reverse')" -java_for_variant() { - grep -oP '^java_version=\K.+' "versions/$1/gradle.properties" +expand_mc_dep() { + jq -nc --argjson releases "$releases" --arg dep "$1" ' + def key: split(".") | map(tonumber); + [$releases[] | . as $id | select( + all($dep | split(" ")[] | capture("(?>=|<=|<|>)(?.+)"); + if .op == ">=" then ($id | key) >= (.ver | key) + elif .op == "<=" then ($id | key) <= (.ver | key) + elif .op == ">" then ($id | key) > (.ver | key) + else ($id | key) < (.ver | key) end))]' } -launch="[]" -while IFS=$'\t' read -r mc variant; do - java="$(java_for_variant "$variant")" - launch="$(jq -c --arg mc "$mc" --arg java "$java" \ - '. + [{mc: $mc, java: $java}]' <<<"$launch")" -done < <(jq -r 'to_entries[] | [.key, .value.variant] | @tsv' "$JSON") +prop() { + grep -oP "^$2=\K.+" "versions/$1/gradle.properties" +} build="[]" +launch="[]" publish="[]" -while read -r variant; do - java="$(java_for_variant "$variant")" +for variant in $(ls versions | sort -V); do + java="$(prop "$variant" java_version)" + mc_versions="$(expand_mc_dep "$(prop "$variant" mc_dep)")" + build="$(jq -c --arg mc "$variant" --arg java "$java" \ '. + [{mc: $mc, java: $java}]' <<<"$build")" - game_versions="$(jq -r --arg v "$variant" \ - '[to_entries[] | select(.value.variant == $v) | .key] | join("\n")' "$JSON")" - range="$(jq -r --arg v "$variant" \ - '[to_entries[] | select(.value.variant == $v) | .key] - | if length == 1 then .[0] else "\(first)-\(last)" end' "$JSON")" + launch="$(jq -c --argjson mcs "$mc_versions" --arg java "$java" \ + '. + [$mcs[] | {mc: ., java: $java}]' <<<"$launch")" + + game_versions="$(jq -r 'join("\n")' <<<"$mc_versions")" + range="$(jq -r 'if length == 1 then .[0] else "\(first)-\(last)" end' <<<"$mc_versions")" publish="$(jq -c --arg mc "$variant" --arg java "$java" --arg gv "$game_versions" --arg range "$range" \ '. + [{mc: $mc, java: $java, game_versions: $gv, range: $range}]' <<<"$publish")" -done < <(jq -r '[.[].variant] | reduce .[] as $v ([]; if index($v) then . else . + [$v] end) | .[]' "$JSON") +done echo "build=$build" >> "$GITHUB_OUTPUT" echo "launch=$launch" >> "$GITHUB_OUTPUT" diff --git a/.github/scripts/generate-test-world.sh b/.github/scripts/generate-test-world.sh index 0001123..99ae0cb 100755 --- a/.github/scripts/generate-test-world.sh +++ b/.github/scripts/generate-test-world.sh @@ -1,12 +1,8 @@ #!/usr/bin/env bash -# Generates the vanilla singleplayer world save the launch test auto-joins -# (launchtest/run/saves/ci-world) by briefly running the given Minecraft -# version's dedicated server. Using the exact same version avoids any -# world-upgrade prompts when the client opens it. set -euo pipefail MC_VERSION="${1:?usage: generate-test-world.sh }" -DEST="$(pwd)/launchtest/run/saves/ci-world" +DEST="$(pwd)/src/launchtest/run/saves/ci-world" WORK="$(mktemp -d)" cd "$WORK" @@ -17,8 +13,6 @@ SERVER_URL="$(curl -sSf "$MANIFEST_URL" | jq -r '.downloads.server.url')" curl -sSfo server.jar "$SERVER_URL" echo "eula=true" > eula.txt -# A flat world keeps generation fast and renders instantly on the CI software -# renderer; the default gamemode (survival) shows the full HUD. cat > server.properties <<'EOF' level-name=ci-world level-type=minecraft\:flat diff --git a/.github/scripts/publish-screenshots.sh b/.github/scripts/publish-screenshots.sh index 7946f0d..e58f89f 100755 --- a/.github/scripts/publish-screenshots.sh +++ b/.github/scripts/publish-screenshots.sh @@ -1,20 +1,10 @@ #!/usr/bin/env bash -# Collects the launch-test screenshots uploaded as artifacts by every matrix -# job of the current workflow run, normalizes their names, and commits them to -# the ci-screenshots branch so they can be embedded (as raw.githubusercontent -# URLs) in a PR comment and in the job summary. -# -# Leaves the normalized screenshots in screenshots-publish/ for the comment -# step: /mc--survival.png. -# -# Required env: GITHUB_TOKEN, GITHUB_REPOSITORY, GITHUB_RUN_ID, GITHUB_SHA set -euo pipefail api() { curl -sSf -H "Authorization: Bearer $GITHUB_TOKEN" -H "Accept: application/vnd.github+json" "$@" } -# --- Download every launch-screenshots-mc* artifact of this run ------------- mkdir -p artifact-zips shots api "https://api.github.com/repos/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID/artifacts?per_page=100" > artifacts.json @@ -46,7 +36,6 @@ if [ "$count" -eq 0 ]; then exit 0 fi -# --- Commit to the ci-screenshots branch ------------------------------------ REMOTE="https://x-access-token:${GITHUB_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" if ! git clone --quiet --depth 1 --branch ci-screenshots "$REMOTE" ci-screenshots-branch 2>/dev/null; then mkdir -p ci-screenshots-branch @@ -65,7 +54,6 @@ fi git -c user.name="github-actions[bot]" -c user.email="41898282+github-actions[bot]@users.noreply.github.com" \ commit --quiet -m "CI screenshots for $GITHUB_SHA" -# Retry the push in case a concurrent run updated the branch first. for _ in 1 2 3; do if git push --quiet origin ci-screenshots; then echo "Published screenshots to ci-screenshots/$GITHUB_SHA" diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 54c41d9..699db12 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,19 +1,6 @@ -# Build the mod for every Stonecutter variant, then launch-test the built jars -# on EVERY supported Minecraft version concurrently: each launch job boots a -# real production Fabric client under XVFB with the covering variant's jar and -# version-matched runtime mods, joins a pre-generated survival world with the -# HUD active, screenshots it, and fails if anything crashes on the way. -# The screenshot report job embeds all screenshots in a PR comment (via the -# ci-screenshots branch) and in the job summary. -# -# All job matrices are generated from supported-versions.json — add a version -# there (see DEVELOPMENT.md) and every job picks it up. - name: build on: push -# contents: write lets the screenshot report push to the ci-screenshots branch, -# pull-requests: write lets it comment on the associated pull request. permissions: contents: write pull-requests: write @@ -29,7 +16,7 @@ jobs: - name: checkout repository uses: actions/checkout@v6 - - name: generate matrices from supported-versions.json + - name: generate matrices id: gen run: bash .github/scripts/generate-matrices.sh @@ -66,9 +53,6 @@ jobs: name: betterhud-mc${{ matrix.mc }} path: versions/${{ matrix.mc }}/build/libs/*.jar - # One job per supported Minecraft version (not per build variant): the - # 1.21 variant jar is launch-tested on 1.21-1.21.5, the 1.21.6 variant jar - # on 1.21.6-1.21.11, and so on. launch-test: name: launch mc${{ matrix.mc }} runs-on: ubuntu-24.04 @@ -95,7 +79,6 @@ jobs: run: chmod +x ./gradlew - name: install xvfb - # Provides a virtual display so the Minecraft client can launch headlessly. run: sudo apt-get update && sudo apt-get install -y xvfb - name: generate singleplayer test world @@ -105,15 +88,14 @@ jobs: run: ./gradlew :launchtest:runLaunchTest -PtestMcVersion=${{ matrix.mc }} --stacktrace - name: check survival world screenshot was taken - run: test -n "$(find launchtest/run/screenshots -name '*betterhud-survival-world.png' -print -quit 2>/dev/null)" + run: test -n "$(find src/launchtest/run/screenshots -name 'betterhud-survival-world.png' -print -quit 2>/dev/null)" - name: upload screenshots - # Consumed by the screenshot-report job below. if: always() uses: actions/upload-artifact@v7 with: name: launch-screenshots-mc${{ matrix.mc }} - path: launchtest/run/screenshots/ + path: src/launchtest/run/screenshots/ if-no-files-found: ignore - name: upload game logs @@ -121,7 +103,7 @@ jobs: uses: actions/upload-artifact@v7 with: name: launch-logs-mc${{ matrix.mc }} - path: launchtest/run/logs/ + path: src/launchtest/run/logs/ if-no-files-found: ignore screenshot-report: @@ -144,16 +126,19 @@ jobs: script: | const fs = require('fs'); - const versions = Object.keys(JSON.parse(fs.readFileSync('supported-versions.json', 'utf8'))); const dir = `screenshots-publish/${context.sha}`; const base = `https://raw.githubusercontent.com/${context.repo.owner}/${context.repo.repo}/ci-screenshots/${context.sha}`; + const versions = fs.existsSync(dir) + ? fs.readdirSync(dir) + .filter(f => f.startsWith('mc-') && f.endsWith('-survival.png')) + .map(f => f.slice(3, -13)) + .sort((a, b) => a.localeCompare(b, undefined, { numeric: true })) + : []; + const cells = versions.map(v => { const file = `mc-${v}-survival.png`; - const ok = fs.existsSync(`${dir}/${file}`); - return ok - ? `${v}
BetterHUD on Minecraft ${v}` - : `${v}
❌ no screenshot`; + return `${v}
BetterHUD on Minecraft ${v}`; }); let rows = ''; for (let i = 0; i < cells.length; i += 4) { @@ -163,12 +148,11 @@ jobs: const marker = ''; const body = [ marker, - `The game launched and entered a survival singleplayer world with the HUD active, on every supported Minecraft version, for \`${context.sha.slice(0, 7)}\`. A ❌ means that version's launch test did not produce a screenshot — check its job. Game logs are in the run artifacts.`, + `The game launched and entered a survival singleplayer world with the HUD active, for \`${context.sha.slice(0, 7)}\`. A missing version means its launch test did not produce a screenshot — check its job. Game logs are in the run artifacts.`, '', `\n${rows}
`, ].join('\n'); - // Always show the grid in the job summary, even without a PR. await core.summary.addRaw(body).write(); const { data: prs } = await github.rest.repos.listPullRequestsAssociatedWithCommit({ @@ -182,7 +166,6 @@ jobs: return; } - // Keep a single comment up to date instead of posting one per push. const { data: comments } = await github.rest.issues.listComments({ owner: context.repo.owner, repo: context.repo.repo, issue_number: pr.number, per_page: 100, @@ -200,9 +183,6 @@ jobs: }); } - # Builds every variant and bundles the release jars together with UPLOAD.md, - # which lists exactly which Minecraft versions to select for each jar when - # uploading to Modrinth. Download the "modrinth-upload" artifact and follow it. modrinth-bundle: name: modrinth bundle runs-on: ubuntu-24.04 diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 9ab1350..918059d 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -1,20 +1,3 @@ -# Publishes the mod to Modrinth and CurseForge on demand: bump `mod_version` -# in gradle.properties, push, then run this workflow from the Actions tab. -# It builds every Stonecutter variant and uploads each release jar as its own -# version on both platforms, selecting exactly the Minecraft versions that jar -# covers — driven by supported-versions.json, the same single source of truth -# the build and launch-test matrices use. -# -# Required repository secrets (Settings -> Secrets and variables -> Actions): -# - MODRINTH_TOKEN: Modrinth PAT with the "Create versions" scope -# (https://modrinth.com/settings/pats) -# - CURSEFORGE_TOKEN: CurseForge API token -# (https://legacy.curseforge.com/account/api-tokens) -# -# This workflow only builds; correctness on every supported Minecraft version -# is already covered by the launch tests that run on every push (build.yml), -# so make sure the commit you release from is green there first. - name: publish on: workflow_dispatch: @@ -45,7 +28,6 @@ on: permissions: contents: read -# Never let two release runs interleave their uploads. concurrency: group: publish cancel-in-progress: false @@ -61,7 +43,7 @@ jobs: - name: checkout repository uses: actions/checkout@v6 - - name: generate matrices from supported-versions.json + - name: generate matrices id: gen run: bash .github/scripts/generate-matrices.sh @@ -73,9 +55,6 @@ jobs: echo "Releasing BetterHUD $version" - name: check the required tokens are configured - # mc-publish silently skips a platform whose token is empty, which - # would make a run "succeed" while publishing nothing — fail early - # instead if a selected platform has no token. env: PLATFORMS: ${{ inputs.platforms }} MODRINTH_TOKEN: ${{ secrets.MODRINTH_TOKEN }} @@ -93,9 +72,6 @@ jobs: runs-on: ubuntu-24.04 needs: matrices strategy: - # Publish one variant at a time, oldest first (supported-versions.json - # order), so the newest-Minecraft version ends up as the most recently - # uploaded — and a failure stops before uploading the rest. max-parallel: 1 fail-fast: true matrix: @@ -122,8 +98,6 @@ jobs: - name: publish to modrinth and curseforge uses: Kir-Antipov/mc-publish@v3.3 with: - # An empty token makes mc-publish skip that platform, which is how - # the "platforms" input narrows the run to a single site. modrinth-id: betterhudfabric modrinth-token: ${{ inputs.platforms != 'curseforge' && secrets.MODRINTH_TOKEN || '' }} curseforge-id: 1375095 diff --git a/.gitignore b/.gitignore index 8c52ed7..911b5a3 100644 --- a/.gitignore +++ b/.gitignore @@ -1,47 +1,31 @@ -# gradle - .gradle/ build/ out/ classes/ -# eclipse - *.launch -# idea - .idea/ *.iml *.ipr *.iws -# vscode - .settings/ .vscode/ bin/ .classpath .project -# macos - *.DS_Store -# fabric - run/ -# stonecutter - versions/*/.gradle/ versions/*/build/ versions/*/run/ versions/*/src/ *.stonecutter.tmp -# java - hs_err_*.log replay_*.log *.hprof diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index b511820..be96f99 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -2,24 +2,25 @@ BetterHUD is built to support several Minecraft versions from one codebase with [Stonecutter](https://stonecutter.kikugie.dev/). Each build variant -(`versions//gradle.properties`) produces one jar covering a range of -Minecraft versions. +(`versions//gradle.properties`) produces one jar covering the range of +Minecraft versions in its `mc_dep` property. -### Adding a new Minecraft version +The `versions/` folder is the single source of truth for what is supported: + +- CI expands each variant's `mc_dep` range against Mojang's version manifest to + get the exact Minecraft versions to build, launch-test, and publish for. +- The launch tests resolve the latest Fabric API, Mod Menu, and Cloth Config + releases for each Minecraft version from Modrinth — the same versions a + launcher would install alongside the mod. -[`supported-versions.json`](supported-versions.json) is the source of truth for every supported Minecraft version. +### Adding a new Minecraft version -1. Add an entry to `supported-versions.json` (keep the order oldest → newest): - - `variant`: the build variant whose jar covers the new version. Reuse the - newest variant if the mod still compiles and runs on it; create a new - variant only when the new Minecraft breaks the current code. - - `fabricApi`, `modmenu`, `cloth`: the newest releases built for that - Minecraft version (check each project's maven or Modrinth page — the - launch test will catch runtime-incompatible picks). -2. If a new variant is needed: create `versions//gradle.properties` - (copy the newest one and adjust the versions and `mc_dep` range), cap the - previous variant's `mc_dep`, and add any `//?` Stonecutter guards the new - Minecraft requires in the sources. +- If the newest variant's `mc_dep` range already covers it (for example a new + patch release), there is nothing to do — CI picks it up automatically. +- Otherwise create `versions//gradle.properties` (copy the newest one + and adjust the versions and the `mc_dep` range), cap the previous variant's + `mc_dep`, and add any `//?` Stonecutter guards the new Minecraft requires in + the sources. To launch-test one version locally (generate the test world once per version): @@ -41,5 +42,5 @@ scope) and `CURSEFORGE_TOKEN` (a For a manual upload instead, run `./gradlew modrinthBundle` (or download the `modrinth-upload` artifact from any CI run). It produces `build/modrinth/` -containing every release jar plus `UPLOAD.md`, which lists exactly which -Minecraft versions to select for each jar. +containing every release jar plus `UPLOAD.md`, which lists each jar's +Minecraft version range. diff --git a/build.gradle b/build.gradle index 9972fe4..85fba51 100644 --- a/build.gradle +++ b/build.gradle @@ -1,20 +1,15 @@ plugins { - // Loom 1.14+ splits into two sibling plugins: - // - `net.fabricmc.fabric-loom-remap` handles obfuscated MC (1.21.x with intermediary) - // - `net.fabricmc.fabric-loom` handles unobfuscated MC (26.1+, Mojang names native) - // We declare both with `apply false` and pick the right one for the current variant. id 'net.fabricmc.fabric-loom-remap' version "${loom_version}" apply false id 'net.fabricmc.fabric-loom' version "${loom_version}" apply false id 'maven-publish' } def mcVersion = stonecutter.current.version -def isLegacy = stonecutter.eval(mcVersion, '<26') // 1.21.x family -def hasHudElementRegistry = stonecutter.eval(mcVersion, '>=1.21.6') // 1.21.6+ and 26+ +def isLegacy = stonecutter.eval(mcVersion, '<26') +def hasHudElementRegistry = stonecutter.eval(mcVersion, '>=1.21.6') apply plugin: isLegacy ? 'net.fabricmc.fabric-loom-remap' : 'net.fabricmc.fabric-loom' -// Per-MC Java version (21 for 1.21.x, 25 for 26.1+). def javaVersionInt = Integer.parseInt(project.java_version) def javaLang = JavaVersion.toVersion(javaVersionInt) def mixinJavaCompat = "JAVA_${javaVersionInt}" @@ -31,8 +26,6 @@ repositories { maven { url 'https://maven.terraformersmc.com/releases/' } } -// MC 26+ uses fabric-rendering-v1's transitive deps but rejects ASM coming from -// annotation processors. if (!isLegacy) { configurations.configureEach { if (name.toLowerCase().contains('annotationprocessor')) { @@ -41,9 +34,6 @@ if (!isLegacy) { } } -// Every variant declares fabric-api, modmenu, and cloth-config as runtime deps -// (not bundled). 1.21.x uses modImplementation for intermediary remapping; 26.x -// uses plain implementation against unobfuscated MC. dependencies { minecraft "com.mojang:minecraft:${project.minecraft_version}" @@ -117,7 +107,4 @@ publishing { from components.java } } - repositories { - // Add publish repos here if/when needed. - } } diff --git a/gradle.properties b/gradle.properties index 0ce008e..2345a67 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,18 +1,12 @@ -# Done to increase the memory available to gradle. org.gradle.jvmargs=-Xmx2G org.gradle.parallel=true -# IntelliJ IDEA is not yet fully compatible with configuration cache, see: https://github.com/FabricMC/fabric-loom/issues/1349 org.gradle.configuration-cache=false -# Acknowledge using Groovy DSL with Stonecutter (Kotlin DSL is recommended but Groovy works). dev.kikugie.stonecutter.hard_mode=true -# Single loom version covers all MC variants. The `fabric-loom-remap` plugin id -# handles obfuscated MC (1.21.x), while `fabric-loom` handles unobfuscated MC (26.2+). loom_version=1.17-SNAPSHOT -# Mod Properties (shared) mod_version=2.1.1 maven_group=dsns.betterhud archives_base_name=betterhud diff --git a/launchtest/build.gradle b/launchtest/build.gradle deleted file mode 100644 index 83ef6a2..0000000 --- a/launchtest/build.gradle +++ /dev/null @@ -1,110 +0,0 @@ -// Launch test harness: boots a real production Fabric client (fabric-loader + -// vanilla Minecraft + the built betterhud jar + its runtime dependency mods) -// for any supported Minecraft version, joins a pre-generated survival world -// (.github/scripts/generate-test-world.sh), screenshots it with the HUD -// active, and exits. Any crash on the way fails the build. -// -// ./gradlew :launchtest:runLaunchTest -PtestMcVersion=1.21.3 -// -// Loom's ClientProductionRunTask is hard-wired to the project's Minecraft -// version, so this project applies Loom with testMcVersion as its Minecraft -// version, then launches the jar built by the Stonecutter variant that covers -// that version. On Linux CI (CI env var set) Loom runs the game under XVFB. - -plugins { - id 'net.fabricmc.fabric-loom-remap' version "${loom_version}" apply false - id 'net.fabricmc.fabric-loom' version "${loom_version}" apply false -} - -// supported-versions.json (repo root) pins, per launchable Minecraft version, -// the Stonecutter variant whose jar covers it and the newest fabricApi / -// modmenu / cloth releases built for exactly that version. -def supportedVersions = new groovy.json.JsonSlurper().parse(new File(rootDir, 'supported-versions.json')) - -def testMcVersion = providers.gradleProperty('testMcVersion').getOrElse(supportedVersions.keySet().last()) -def entry = supportedVersions[testMcVersion] -if (entry == null) { - throw new GradleException("Unsupported testMcVersion '${testMcVersion}'. Supported versions: ${supportedVersions.keySet().join(', ')}") -} - -def variantProps = new Properties() -file("../versions/${entry.variant}/gradle.properties").withInputStream { variantProps.load(it) } -def loaderVersion = variantProps.getProperty('loader_version') -def javaVersionInt = Integer.parseInt(variantProps.getProperty('java_version')) - -// Obfuscated 1.21.x needs the remap plugin; 26.x runs Mojang names natively. -def isLegacy = entry.variant.startsWith('1.') -apply plugin: isLegacy ? 'net.fabricmc.fabric-loom-remap' : 'net.fabricmc.fabric-loom' - -version = '1.0.0' -base { - archivesName = 'betterhud-launchtest' -} - -repositories { - maven { url 'https://maven.shedaniel.me/' } - maven { url 'https://maven.terraformersmc.com/releases/' } -} - -// MC 26+ rejects ASM coming from annotation processors. -if (!isLegacy) { - configurations.configureEach { - if (name.toLowerCase().contains('annotationprocessor')) { - exclude group: 'org.ow2.asm' - } - } -} - -dependencies { - minecraft "com.mojang:minecraft:${testMcVersion}" - - if (isLegacy) { - mappings loom.officialMojangMappings() - modImplementation "net.fabricmc:fabric-loader:${loaderVersion}" - modImplementation "net.fabricmc.fabric-api:fabric-api:${entry.fabricApi}" - } else { - implementation "net.fabricmc:fabric-loader:${loaderVersion}" - implementation "net.fabricmc.fabric-api:fabric-api:${entry.fabricApi}" - } - - // Production jars loaded alongside the built betterhud jar; transitive - // deps are excluded because each fat jar nests its own via jar-in-jar. - productionRuntimeMods("net.fabricmc.fabric-api:fabric-api:${entry.fabricApi}") { transitive = false } - productionRuntimeMods("com.terraformersmc:modmenu:${entry.modmenu}") { transitive = false } - productionRuntimeMods("me.shedaniel.cloth:cloth-config-fabric:${entry.cloth}") { transitive = false } -} - -tasks.withType(JavaCompile).configureEach { - it.options.release = javaVersionInt -} - -java { - toolchain { - languageVersion = JavaLanguageVersion.of(javaVersionInt) - } -} - -evaluationDependsOn(":${entry.variant}") -def variantProject = project(":${entry.variant}") -// Unobfuscated 26.x projects have no remapJar; their plain jar ships as-is. -def variantJarTask = variantProject.tasks.names.contains('remapJar') ? 'remapJar' : 'jar' -def ownJarTask = tasks.names.contains('remapJar') ? 'remapJar' : 'jar' - -tasks.register('runLaunchTest', net.fabricmc.loom.task.prod.ClientProductionRunTask) { - mods.setFrom(variantProject.tasks.named(variantJarTask), tasks.named(ownJarTask), configurations.productionRuntimeMods) - programArgs.addAll('--quickPlaySingleplayer', 'ci-world') - - doFirst { - if (!file('run/saves/ci-world').exists()) { - throw new GradleException("Missing test world at launchtest/run/saves/ci-world. " + - "Generate it with: bash .github/scripts/generate-test-world.sh ${testMcVersion}") - } - // A fresh game dir opens the accessibility onboarding screen instead - // of joining the world; disable it. - def options = new File(runDir.get().asFile, 'options.txt') - if (!options.exists()) { - options.parentFile.mkdirs() - options.text = 'onboardAccessibility:false\n' - } - } -} diff --git a/settings.gradle b/settings.gradle index 1a48f96..fcab7ea 100644 --- a/settings.gradle +++ b/settings.gradle @@ -21,27 +21,17 @@ plugins { id 'dev.kikugie.stonecutter' version '0.9.3' } -// Every supported Minecraft version, and the build variant that covers it, -// lives in supported-versions.json — the single source of truth also used by -// the launch tests, the CI matrices, and the modrinthBundle task. -def supportedVersions = new groovy.json.JsonSlurper().parse(file('supported-versions.json')) -def variants = supportedVersions.values()*.variant.unique() +def versionKey = { String v -> v.tokenize('.').collect { it.padLeft(4, '0') }.join('.') } +def variants = file('versions').listFiles().findAll { it.directory }*.name.sort { versionKey(it) } stonecutter { create(rootProject) { - // Each variant is one buildable subproject; the string is both the - // subproject name and the MC version it compiles against (see - // versions//gradle.properties). versions(*variants) - - // vcsVersion is the variant whose form the sources are committed in - // (the "active" version when nothing else is selected). vcsVersion = variants.last() } } rootProject.name = 'betterhud' -// Not a Stonecutter variant: launch-tests the built jar on any supported MC -// version (-PtestMcVersion). Used by the CI launch-test matrix. include 'launchtest' +project(':launchtest').projectDir = file('src/launchtest') diff --git a/src/launchtest/build.gradle b/src/launchtest/build.gradle new file mode 100644 index 0000000..aebfdef --- /dev/null +++ b/src/launchtest/build.gradle @@ -0,0 +1,125 @@ +plugins { + id 'net.fabricmc.fabric-loom-remap' version "${loom_version}" apply false + id 'net.fabricmc.fabric-loom' version "${loom_version}" apply false +} + +def versionKey = { String v -> v.tokenize('.').collect { it.padLeft(4, '0') }.join('.') } +def variantsDir = new File(rootDir, 'versions') +def variants = variantsDir.listFiles().findAll { it.directory }*.name.sort { versionKey(it) } + +def variantProps = { String variant -> + def props = new Properties() + new File(variantsDir, "${variant}/gradle.properties").withInputStream { props.load(it) } + props +} + +def testMcVersion = providers.gradleProperty('testMcVersion') + .getOrElse(variantProps(variants.last()).getProperty('minecraft_version')) + +def coveringVariants = variants.findAll { versionKey(it) <= versionKey(testMcVersion) } +if (coveringVariants.isEmpty()) { + throw new GradleException("No build variant covers Minecraft ${testMcVersion}") +} +def variant = coveringVariants.last() + +def props = variantProps(variant) +def loaderVersion = props.getProperty('loader_version') +def javaVersionInt = Integer.parseInt(props.getProperty('java_version')) + +def isLegacy = variant.startsWith('1.') +apply plugin: isLegacy ? 'net.fabricmc.fabric-loom-remap' : 'net.fabricmc.fabric-loom' + +version = '1.0.0' +base { + archivesName = 'betterhud-launchtest' +} + +repositories { + maven { url 'https://maven.shedaniel.me/' } + maven { url 'https://maven.terraformersmc.com/releases/' } +} + +if (!isLegacy) { + configurations.configureEach { + if (name.toLowerCase().contains('annotationprocessor')) { + exclude group: 'org.ow2.asm' + } + } +} + +def modrinthCache = [:] +def modrinthLatest = { String slug -> + modrinthCache.computeIfAbsent(slug) { + def query = "https://api.modrinth.com/v2/project/${slug}/version" + + "?game_versions=%5B%22${testMcVersion}%22%5D&loaders=%5B%22fabric%22%5D" + def found = new groovy.json.JsonSlurper().parse(new URI(query).toURL()) + if (found.isEmpty()) { + throw new GradleException("No ${slug} release on Modrinth supports Minecraft ${testMcVersion}") + } + found.first().version_number + } +} + +def modrinthVersions = [ + 'net.fabricmc.fabric-api:fabric-api' : { modrinthLatest('fabric-api') }, + 'com.terraformersmc:modmenu' : { modrinthLatest('modmenu') }, + 'me.shedaniel.cloth:cloth-config-fabric': { modrinthLatest('cloth-config').replace('+fabric', '') } +] + +configurations.configureEach { + resolutionStrategy.eachDependency { details -> + def key = "${details.requested.group}:${details.requested.name}".toString() + if (details.requested.version == 'modrinth' && modrinthVersions.containsKey(key)) { + details.useVersion(modrinthVersions[key]()) + } + } +} + +dependencies { + minecraft "com.mojang:minecraft:${testMcVersion}" + + if (isLegacy) { + mappings loom.officialMojangMappings() + modImplementation "net.fabricmc:fabric-loader:${loaderVersion}" + modImplementation 'net.fabricmc.fabric-api:fabric-api:modrinth' + } else { + implementation "net.fabricmc:fabric-loader:${loaderVersion}" + implementation 'net.fabricmc.fabric-api:fabric-api:modrinth' + } + + productionRuntimeMods('net.fabricmc.fabric-api:fabric-api:modrinth') { transitive = false } + productionRuntimeMods('com.terraformersmc:modmenu:modrinth') { transitive = false } + productionRuntimeMods('me.shedaniel.cloth:cloth-config-fabric:modrinth') { transitive = false } +} + +tasks.withType(JavaCompile).configureEach { + it.options.release = javaVersionInt +} + +java { + toolchain { + languageVersion = JavaLanguageVersion.of(javaVersionInt) + } +} + +evaluationDependsOn(":${variant}") +def variantProject = project(":${variant}") +def variantJarTask = variantProject.tasks.names.contains('remapJar') ? 'remapJar' : 'jar' +def ownJarTask = tasks.names.contains('remapJar') ? 'remapJar' : 'jar' + +tasks.register('runLaunchTest', net.fabricmc.loom.task.prod.ClientProductionRunTask) { + mods.setFrom(variantProject.tasks.named(variantJarTask), tasks.named(ownJarTask), configurations.productionRuntimeMods) + programArgs.addAll('--quickPlaySingleplayer', 'ci-world') + + doFirst { + if (!file('run/saves/ci-world').exists()) { + throw new GradleException("Missing test world at src/launchtest/run/saves/ci-world. " + + "Generate it with: bash .github/scripts/generate-test-world.sh ${testMcVersion}") + } + def options = new File(runDir.get().asFile, 'options.txt') + if (!options.exists()) { + options.parentFile.mkdirs() + options.text = 'onboardAccessibility:false\n' + } + } +} diff --git a/launchtest/src/main/java/dsns/betterhud/launchtest/LaunchTestClient.java b/src/launchtest/src/main/java/dsns/betterhud/launchtest/LaunchTestClient.java similarity index 65% rename from launchtest/src/main/java/dsns/betterhud/launchtest/LaunchTestClient.java rename to src/launchtest/src/main/java/dsns/betterhud/launchtest/LaunchTestClient.java index ae71cdc..5d206ed 100644 --- a/launchtest/src/main/java/dsns/betterhud/launchtest/LaunchTestClient.java +++ b/src/launchtest/src/main/java/dsns/betterhud/launchtest/LaunchTestClient.java @@ -16,17 +16,10 @@ import net.minecraft.client.Screenshot; import net.minecraft.client.renderer.GameRenderer; -/** - * Drives the launch test on every supported Minecraft version: the client is - * started with {@code --quickPlaySingleplayer ci-world} (see the launchtest - * Gradle project), joins the pre-generated survival world, screenshots it with - * the HUD active, and exits cleanly. A crash anywhere on the way fails the run. - */ public class LaunchTestClient implements ClientModInitializer { private static final String SCREENSHOT_NAME = "betterhud-survival-world.png"; - // 200 ticks (10s) gives the software renderer on CI time to draw chunks; - // the screenshot is written asynchronously, so leave a few more seconds - // before renaming it and stopping the client. + private static final String[] MINECRAFT_RENDER_TARGET_GETTERS = {"getMainRenderTarget", "method_1522"}; + private static final String GAME_RENDERER_RENDER_TARGET_GETTER = "mainRenderTarget"; private static final int SCREENSHOT_TICK = 200; private static final int STOP_TICK = 300; private static final int WORLD_JOIN_TIMEOUT_TICKS = 2400; @@ -55,28 +48,21 @@ public void onInitializeClient() { }); } - // Minecraft.getMainRenderTarget() moved to GameRenderer in MC 26.2, so no - // direct call compiles against every supported version. This mod only runs - // in production, where 26.x uses plain Mojang names and 1.21.x is remapped - // to version-stable intermediary names, so resolve the accessor under - // whichever name the running game has. private static RenderTarget mainRenderTarget(Minecraft client) { - for (String name : new String[] {"getMainRenderTarget", "method_1522"}) { + for (String getter : MINECRAFT_RENDER_TARGET_GETTERS) { try { - return (RenderTarget) Minecraft.class.getMethod(name).invoke(client); + return (RenderTarget) Minecraft.class.getMethod(getter).invoke(client); } catch (ReflectiveOperationException e) { - // try the next name + continue; } } try { - return (RenderTarget) GameRenderer.class.getMethod("mainRenderTarget").invoke(client.gameRenderer); + return (RenderTarget) GameRenderer.class.getMethod(GAME_RENDERER_RENDER_TARGET_GETTER).invoke(client.gameRenderer); } catch (ReflectiveOperationException e) { throw new IllegalStateException("Could not resolve the main render target", e); } } - // The Screenshot.grab overloads taking a file name differ across versions; - // only the auto-named one is stable, so rename its output afterwards. private static void renameScreenshot(File gameDirectory) { Path dir = gameDirectory.toPath().resolve("screenshots"); try (Stream files = Files.list(dir)) { diff --git a/launchtest/src/main/resources/fabric.mod.json b/src/launchtest/src/main/resources/fabric.mod.json similarity index 100% rename from launchtest/src/main/resources/fabric.mod.json rename to src/launchtest/src/main/resources/fabric.mod.json diff --git a/stonecutter.gradle b/stonecutter.gradle index 755ca07..231a20d 100644 --- a/stonecutter.gradle +++ b/stonecutter.gradle @@ -2,30 +2,14 @@ plugins { id 'dev.kikugie.stonecutter' } -// The "active" version determines which variant the source tree currently shows -// uncommented. Switch with `./gradlew "Set active version to "` (Stonecutter task). stonecutter.active('26.2') /* [SC] DO NOT EDIT */ -// Stonecutter 0.7+ no longer needs explicit `registerChiseled` task: running -// `./gradlew build` from the root automatically runs `build` for every version -// subproject. Per-version commands like `./gradlew :1.21:build` also work. - -// Groups every supported Minecraft version (supported-versions.json) by the -// build variant whose jar covers it, e.g. '1.21' -> ['1.21', ..., '1.21.5']. -def supportedVersions = new groovy.json.JsonSlurper().parse(file('supported-versions.json')) -def gameVersionsByVariant = [:] -supportedVersions.each { mc, info -> - gameVersionsByVariant.computeIfAbsent(info.variant) { [] } << mc -} - -// Builds every variant and collects the release jars in build/modrinth/ along -// with UPLOAD.md, which lists exactly which Minecraft versions to select for -// each jar when uploading to Modrinth. CI publishes the same directory as the -// "modrinth-upload" artifact on every push. tasks.register('modrinthBundle') { group = 'publishing' - description = 'Collects the release jars and the per-jar Minecraft version lists for Modrinth.' - gameVersionsByVariant.keySet().each { dependsOn(":${it}:build") } + description = 'Collects the release jars and the per-jar Minecraft version ranges for Modrinth.' + + def variants = file('versions').listFiles().findAll { it.directory }*.name.sort() + variants.each { dependsOn(":${it}:build") } doLast { def outDir = file('build/modrinth') @@ -33,7 +17,7 @@ tasks.register('modrinthBundle') { outDir.mkdirs() def rows = [] - gameVersionsByVariant.each { variant, gameVersions -> + variants.each { variant -> def jar = file("versions/${variant}/build/libs").listFiles().find { it.name.endsWith('.jar') && !it.name.contains('-sources') } @@ -41,15 +25,18 @@ tasks.register('modrinthBundle') { throw new GradleException("No release jar found for variant ${variant} in versions/${variant}/build/libs") } java.nio.file.Files.copy(jar.toPath(), new File(outDir, jar.name).toPath()) - rows << "| `${jar.name}` | ${gameVersions.join(', ')} |" + + def props = new Properties() + file("versions/${variant}/gradle.properties").withInputStream { props.load(it) } + rows << "| `${jar.name}` | `${props.getProperty('mc_dep')}` |" } def manifest = new File(outDir, 'UPLOAD.md') manifest.text = [ "# Modrinth upload — betterhud ${project.mod_version}", '', - 'Upload each jar below as its own Modrinth version, selecting exactly', - 'the listed Minecraft versions for it. Loader: Fabric. Required', + 'Upload each jar below as its own Modrinth version, selecting every', + 'Minecraft release in its range. Loader: Fabric. Required', 'dependencies: Fabric API, Cloth Config, Mod Menu.', '', '| File | Minecraft versions |', diff --git a/supported-versions.json b/supported-versions.json deleted file mode 100644 index 3fb1fac..0000000 --- a/supported-versions.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "1.21": { "variant": "1.21", "fabricApi": "0.102.0+1.21", "modmenu": "11.0.4", "cloth": "15.0.140" }, - "1.21.1": { "variant": "1.21", "fabricApi": "0.116.13+1.21.1", "modmenu": "11.0.4", "cloth": "15.0.140" }, - "1.21.2": { "variant": "1.21", "fabricApi": "0.106.1+1.21.2", "modmenu": "12.0.1", "cloth": "16.0.143" }, - "1.21.3": { "variant": "1.21", "fabricApi": "0.114.1+1.21.3", "modmenu": "12.0.1", "cloth": "16.0.143" }, - "1.21.4": { "variant": "1.21", "fabricApi": "0.119.4+1.21.4", "modmenu": "13.0.4", "cloth": "17.0.144" }, - "1.21.5": { "variant": "1.21", "fabricApi": "0.128.2+1.21.5", "modmenu": "14.0.2", "cloth": "18.0.145" }, - "1.21.6": { "variant": "1.21.6", "fabricApi": "0.128.2+1.21.6", "modmenu": "15.0.2", "cloth": "19.0.147" }, - "1.21.7": { "variant": "1.21.6", "fabricApi": "0.129.0+1.21.7", "modmenu": "15.0.2", "cloth": "19.0.147" }, - "1.21.8": { "variant": "1.21.6", "fabricApi": "0.136.1+1.21.8", "modmenu": "15.0.2", "cloth": "19.0.147" }, - "1.21.9": { "variant": "1.21.6", "fabricApi": "0.134.1+1.21.9", "modmenu": "16.0.1", "cloth": "20.0.149" }, - "1.21.10": { "variant": "1.21.6", "fabricApi": "0.138.4+1.21.10", "modmenu": "16.0.1", "cloth": "21.11.153" }, - "1.21.11": { "variant": "1.21.6", "fabricApi": "0.141.4+1.21.11", "modmenu": "17.0.0", "cloth": "21.11.153" }, - "26.1": { "variant": "26.1", "fabricApi": "0.145.1+26.1", "modmenu": "18.0.0-beta.1", "cloth": "26.1.154" }, - "26.1.1": { "variant": "26.1", "fabricApi": "0.145.4+26.1.1", "modmenu": "18.0.0-beta.1", "cloth": "26.1.154" }, - "26.1.2": { "variant": "26.1", "fabricApi": "0.154.0+26.1.2", "modmenu": "18.0.0-beta.1", "cloth": "26.1.154" }, - "26.2": { "variant": "26.2", "fabricApi": "0.154.0+26.2", "modmenu": "20.0.0-beta.4", "cloth": "26.2.155" } -} diff --git a/versions/1.21.6/gradle.properties b/versions/1.21.6/gradle.properties index e3951b0..a6cb6ef 100644 --- a/versions/1.21.6/gradle.properties +++ b/versions/1.21.6/gradle.properties @@ -2,12 +2,9 @@ minecraft_version=1.21.6 loader_version=0.17.3 java_version=21 -# fabric.mod.json range mc_dep=>=1.21.6 <1.22 loader_dep=>=0.16.10 -# Fabric API + ecosystem (compile-time versions for this variant; the launch -# tests pin per-version runtime versions in supported-versions.json). fabric_api_version=0.128.2+1.21.6 modmenu_version=15.0.2 cloth_config_version=19.0.147 diff --git a/versions/1.21/gradle.properties b/versions/1.21/gradle.properties index c84e2bf..ed95574 100644 --- a/versions/1.21/gradle.properties +++ b/versions/1.21/gradle.properties @@ -2,12 +2,9 @@ minecraft_version=1.21 loader_version=0.17.3 java_version=21 -# fabric.mod.json range mc_dep=>=1.21 <=1.21.5 loader_dep=>=0.15.11 -# Fabric API + ecosystem (compile-time versions for this variant; the launch -# tests pin per-version runtime versions in supported-versions.json). fabric_api_version=0.102.0+1.21 modmenu_version=14.0.2 cloth_config_version=19.0.147 diff --git a/versions/26.1/gradle.properties b/versions/26.1/gradle.properties index dfd7869..ce6fc20 100644 --- a/versions/26.1/gradle.properties +++ b/versions/26.1/gradle.properties @@ -2,11 +2,9 @@ minecraft_version=26.1 loader_version=0.19.2 java_version=25 -# fabric.mod.json range mc_dep=>=26.1 <26.2 loader_dep=>=0.19 -# Fabric API + ecosystem (runtime deps, not bundled) fabric_api_version=0.145.1+26.1 modmenu_version=18.0.0-alpha.8 cloth_config_version=26.1.154 diff --git a/versions/26.2/gradle.properties b/versions/26.2/gradle.properties index d5bee7a..0bb6973 100644 --- a/versions/26.2/gradle.properties +++ b/versions/26.2/gradle.properties @@ -2,11 +2,9 @@ minecraft_version=26.2 loader_version=0.19.3 java_version=25 -# fabric.mod.json range mc_dep=>=26.2 loader_dep=>=0.19 -# Fabric API + ecosystem (26.2 requires these as runtime deps, not bundled) fabric_api_version=0.154.0+26.2 modmenu_version=18.0.0-alpha.8 cloth_config_version=26.1.154 From df19d32d7f98e1c534547d5cb04868ad723d5dfe Mon Sep 17 00:00:00 2001 From: Dominic Seung Date: Tue, 7 Jul 2026 21:56:59 -0700 Subject: [PATCH 03/15] Refactor Modrinth dependency resolution in build.gradle --- src/launchtest/build.gradle | 42 +++++++++++++------------------------ 1 file changed, 14 insertions(+), 28 deletions(-) diff --git a/src/launchtest/build.gradle b/src/launchtest/build.gradle index aebfdef..1c4d203 100644 --- a/src/launchtest/build.gradle +++ b/src/launchtest/build.gradle @@ -47,33 +47,19 @@ if (!isLegacy) { } } -def modrinthCache = [:] def modrinthLatest = { String slug -> - modrinthCache.computeIfAbsent(slug) { - def query = "https://api.modrinth.com/v2/project/${slug}/version" + - "?game_versions=%5B%22${testMcVersion}%22%5D&loaders=%5B%22fabric%22%5D" - def found = new groovy.json.JsonSlurper().parse(new URI(query).toURL()) - if (found.isEmpty()) { - throw new GradleException("No ${slug} release on Modrinth supports Minecraft ${testMcVersion}") - } - found.first().version_number + def query = "https://api.modrinth.com/v2/project/${slug}/version" + + "?game_versions=%5B%22${testMcVersion}%22%5D&loaders=%5B%22fabric%22%5D" + def found = new groovy.json.JsonSlurper().parse(new URI(query).toURL()) + if (found.isEmpty()) { + throw new GradleException("No ${slug} release on Modrinth supports Minecraft ${testMcVersion}") } + found.first().version_number } -def modrinthVersions = [ - 'net.fabricmc.fabric-api:fabric-api' : { modrinthLatest('fabric-api') }, - 'com.terraformersmc:modmenu' : { modrinthLatest('modmenu') }, - 'me.shedaniel.cloth:cloth-config-fabric': { modrinthLatest('cloth-config').replace('+fabric', '') } -] - -configurations.configureEach { - resolutionStrategy.eachDependency { details -> - def key = "${details.requested.group}:${details.requested.name}".toString() - if (details.requested.version == 'modrinth' && modrinthVersions.containsKey(key)) { - details.useVersion(modrinthVersions[key]()) - } - } -} +def fabricApiVersion = modrinthLatest('fabric-api') +def modmenuVersion = modrinthLatest('modmenu') +def clothConfigVersion = modrinthLatest('cloth-config').replace('+fabric', '') dependencies { minecraft "com.mojang:minecraft:${testMcVersion}" @@ -81,15 +67,15 @@ dependencies { if (isLegacy) { mappings loom.officialMojangMappings() modImplementation "net.fabricmc:fabric-loader:${loaderVersion}" - modImplementation 'net.fabricmc.fabric-api:fabric-api:modrinth' + modImplementation "net.fabricmc.fabric-api:fabric-api:${fabricApiVersion}" } else { implementation "net.fabricmc:fabric-loader:${loaderVersion}" - implementation 'net.fabricmc.fabric-api:fabric-api:modrinth' + implementation "net.fabricmc.fabric-api:fabric-api:${fabricApiVersion}" } - productionRuntimeMods('net.fabricmc.fabric-api:fabric-api:modrinth') { transitive = false } - productionRuntimeMods('com.terraformersmc:modmenu:modrinth') { transitive = false } - productionRuntimeMods('me.shedaniel.cloth:cloth-config-fabric:modrinth') { transitive = false } + productionRuntimeMods("net.fabricmc.fabric-api:fabric-api:${fabricApiVersion}") { transitive = false } + productionRuntimeMods("com.terraformersmc:modmenu:${modmenuVersion}") { transitive = false } + productionRuntimeMods("me.shedaniel.cloth:cloth-config-fabric:${clothConfigVersion}") { transitive = false } } tasks.withType(JavaCompile).configureEach { From 2d00c4c714670cd0b8304906abe5bb2fe349eb52 Mon Sep 17 00:00:00 2001 From: Dominic Seung Date: Tue, 7 Jul 2026 21:57:09 -0700 Subject: [PATCH 04/15] Delete DEVELOPMENT.md --- DEVELOPMENT.md | 46 ---------------------------------------------- 1 file changed, 46 deletions(-) delete mode 100644 DEVELOPMENT.md diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md deleted file mode 100644 index be96f99..0000000 --- a/DEVELOPMENT.md +++ /dev/null @@ -1,46 +0,0 @@ -# Development - -BetterHUD is built to support several Minecraft versions from one codebase with -[Stonecutter](https://stonecutter.kikugie.dev/). Each build variant -(`versions//gradle.properties`) produces one jar covering the range of -Minecraft versions in its `mc_dep` property. - -The `versions/` folder is the single source of truth for what is supported: - -- CI expands each variant's `mc_dep` range against Mojang's version manifest to - get the exact Minecraft versions to build, launch-test, and publish for. -- The launch tests resolve the latest Fabric API, Mod Menu, and Cloth Config - releases for each Minecraft version from Modrinth — the same versions a - launcher would install alongside the mod. - -### Adding a new Minecraft version - -- If the newest variant's `mc_dep` range already covers it (for example a new - patch release), there is nothing to do — CI picks it up automatically. -- Otherwise create `versions//gradle.properties` (copy the newest one - and adjust the versions and the `mc_dep` range), cap the previous variant's - `mc_dep`, and add any `//?` Stonecutter guards the new Minecraft requires in - the sources. - -To launch-test one version locally (generate the test world once per version): - -``` -bash .github/scripts/generate-test-world.sh -./gradlew :launchtest:runLaunchTest -PtestMcVersion= -``` - -### Releasing to Modrinth and CurseForge - -Bump `mod_version` in `gradle.properties`, push, wait for CI to go green, -then run the **publish** workflow from the Actions tab (Run workflow). -The workflow asks for a changelog, a release channel (release/beta/alpha), and which platforms to publish to. - -It needs two repository secrets: `MODRINTH_TOKEN` (a -[Modrinth PAT](https://modrinth.com/settings/pats) with the "Create versions" -scope) and `CURSEFORGE_TOKEN` (a -[CurseForge API token](https://legacy.curseforge.com/account/api-tokens)). - -For a manual upload instead, run `./gradlew modrinthBundle` (or download the -`modrinth-upload` artifact from any CI run). It produces `build/modrinth/` -containing every release jar plus `UPLOAD.md`, which lists each jar's -Minecraft version range. From b65f04fd492a5bacbc026e82d32bbd2be820c618 Mon Sep 17 00:00:00 2001 From: Dominic Seung Date: Tue, 7 Jul 2026 22:07:08 -0700 Subject: [PATCH 05/15] bring back old warning when no screenshot --- .github/workflows/build.yml | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 699db12..791c37b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -138,6 +138,10 @@ jobs: const cells = versions.map(v => { const file = `mc-${v}-survival.png`; + const ok = fs.existsSync(`${dir}/${file}`); + return ok + ? `${v}
BetterHUD on Minecraft ${v}` + : `${v}
❌ no screenshot`; return `${v}
BetterHUD on Minecraft ${v}`; }); let rows = ''; @@ -145,10 +149,8 @@ jobs: rows += `${cells.slice(i, i + 4).join('')}\n`; } - const marker = ''; const body = [ - marker, - `The game launched and entered a survival singleplayer world with the HUD active, for \`${context.sha.slice(0, 7)}\`. A missing version means its launch test did not produce a screenshot — check its job. Game logs are in the run artifacts.`, + `# BetterHUD Launch Screenshots`, '', `\n${rows}
`, ].join('\n'); From 5131e25d18aa5bdb8e72965593c22c31b0a3d284 Mon Sep 17 00:00:00 2001 From: Dominic Seung Date: Tue, 7 Jul 2026 22:24:12 -0700 Subject: [PATCH 06/15] start over, remove useless variables --- .github/scripts/generate-matrices.sh | 45 ---- .github/scripts/generate-test-world.sh | 51 ----- .github/scripts/publish-screenshots.sh | 65 ------ .github/workflows/build.yml | 216 ------------------ .github/workflows/publish.yml | 121 ---------- build.gradle | 24 +- src/launchtest/build.gradle | 111 --------- .../launchtest/LaunchTestClient.java | 83 ------- .../src/main/resources/fabric.mod.json | 16 -- 9 files changed, 9 insertions(+), 723 deletions(-) delete mode 100755 .github/scripts/generate-matrices.sh delete mode 100755 .github/scripts/generate-test-world.sh delete mode 100755 .github/scripts/publish-screenshots.sh delete mode 100644 .github/workflows/build.yml delete mode 100644 .github/workflows/publish.yml delete mode 100644 src/launchtest/build.gradle delete mode 100644 src/launchtest/src/main/java/dsns/betterhud/launchtest/LaunchTestClient.java delete mode 100644 src/launchtest/src/main/resources/fabric.mod.json diff --git a/.github/scripts/generate-matrices.sh b/.github/scripts/generate-matrices.sh deleted file mode 100755 index f5f3926..0000000 --- a/.github/scripts/generate-matrices.sh +++ /dev/null @@ -1,45 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -releases="$(curl -sSf https://piston-meta.mojang.com/mc/game/version_manifest_v2.json \ - | jq -c '[.versions[] | select(.type == "release") | .id] | reverse')" - -expand_mc_dep() { - jq -nc --argjson releases "$releases" --arg dep "$1" ' - def key: split(".") | map(tonumber); - [$releases[] | . as $id | select( - all($dep | split(" ")[] | capture("(?>=|<=|<|>)(?.+)"); - if .op == ">=" then ($id | key) >= (.ver | key) - elif .op == "<=" then ($id | key) <= (.ver | key) - elif .op == ">" then ($id | key) > (.ver | key) - else ($id | key) < (.ver | key) end))]' -} - -prop() { - grep -oP "^$2=\K.+" "versions/$1/gradle.properties" -} - -build="[]" -launch="[]" -publish="[]" -for variant in $(ls versions | sort -V); do - java="$(prop "$variant" java_version)" - mc_versions="$(expand_mc_dep "$(prop "$variant" mc_dep)")" - - build="$(jq -c --arg mc "$variant" --arg java "$java" \ - '. + [{mc: $mc, java: $java}]' <<<"$build")" - launch="$(jq -c --argjson mcs "$mc_versions" --arg java "$java" \ - '. + [$mcs[] | {mc: ., java: $java}]' <<<"$launch")" - - game_versions="$(jq -r 'join("\n")' <<<"$mc_versions")" - range="$(jq -r 'if length == 1 then .[0] else "\(first)-\(last)" end' <<<"$mc_versions")" - publish="$(jq -c --arg mc "$variant" --arg java "$java" --arg gv "$game_versions" --arg range "$range" \ - '. + [{mc: $mc, java: $java, game_versions: $gv, range: $range}]' <<<"$publish")" -done - -echo "build=$build" >> "$GITHUB_OUTPUT" -echo "launch=$launch" >> "$GITHUB_OUTPUT" -echo "publish=$publish" >> "$GITHUB_OUTPUT" -echo "build matrix: $build" -echo "launch matrix: $launch" -echo "publish matrix: $publish" diff --git a/.github/scripts/generate-test-world.sh b/.github/scripts/generate-test-world.sh deleted file mode 100755 index 99ae0cb..0000000 --- a/.github/scripts/generate-test-world.sh +++ /dev/null @@ -1,51 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -MC_VERSION="${1:?usage: generate-test-world.sh }" -DEST="$(pwd)/src/launchtest/run/saves/ci-world" - -WORK="$(mktemp -d)" -cd "$WORK" - -MANIFEST_URL="$(curl -sSf https://piston-meta.mojang.com/mc/game/version_manifest_v2.json \ - | jq -r --arg v "$MC_VERSION" '.versions[] | select(.id == $v) | .url')" -SERVER_URL="$(curl -sSf "$MANIFEST_URL" | jq -r '.downloads.server.url')" -curl -sSfo server.jar "$SERVER_URL" - -echo "eula=true" > eula.txt -cat > server.properties <<'EOF' -level-name=ci-world -level-type=minecraft\:flat -view-distance=4 -simulation-distance=4 -online-mode=false -spawn-protection=0 -EOF - -mkfifo console -java -Xmx1G -jar server.jar nogui < console > server-log.txt 2>&1 & -SERVER_PID=$! -exec 3> console - -for _ in $(seq 1 300); do - grep -q 'Done (' server-log.txt && break - if ! kill -0 "$SERVER_PID" 2>/dev/null; then - cat server-log.txt - echo "::error::Dedicated server for $MC_VERSION exited before finishing world generation" >&2 - exit 1 - fi - sleep 1 -done -if ! grep -q 'Done (' server-log.txt; then - cat server-log.txt - echo "::error::Dedicated server for $MC_VERSION did not finish starting within 300s" >&2 - exit 1 -fi - -echo "stop" >&3 -wait "$SERVER_PID" -exec 3>&- - -mkdir -p "$(dirname "$DEST")" -cp -r ci-world "$DEST" -echo "Generated test world at $DEST" diff --git a/.github/scripts/publish-screenshots.sh b/.github/scripts/publish-screenshots.sh deleted file mode 100755 index e58f89f..0000000 --- a/.github/scripts/publish-screenshots.sh +++ /dev/null @@ -1,65 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -api() { - curl -sSf -H "Authorization: Bearer $GITHUB_TOKEN" -H "Accept: application/vnd.github+json" "$@" -} - -mkdir -p artifact-zips shots -api "https://api.github.com/repos/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID/artifacts?per_page=100" > artifacts.json - -while IFS=: read -r id name; do - mc="${name#launch-screenshots-mc}" - echo "Downloading $name" - curl -sSfL -H "Authorization: Bearer $GITHUB_TOKEN" \ - -o "artifact-zips/$id.zip" \ - "https://api.github.com/repos/$GITHUB_REPOSITORY/actions/artifacts/$id/zip" - mkdir -p "shots/$mc" - unzip -oq "artifact-zips/$id.zip" -d "shots/$mc" -done < <(jq -r '.artifacts[] | select(.name | startswith("launch-screenshots-mc")) | "\(.id):\(.name)"' artifacts.json) - -PUBLISH="screenshots-publish/$GITHUB_SHA" -mkdir -p "$PUBLISH" - -for dir in shots/*/; do - mc="$(basename "$dir")" - src="$(find "$dir" -name "betterhud-survival-world.png" -print -quit)" - if [ -n "$src" ]; then - cp "$src" "$PUBLISH/mc-$mc-survival.png" - fi -done - -count="$(find "$PUBLISH" -name '*.png' | wc -l)" -echo "Collected $count screenshots" -if [ "$count" -eq 0 ]; then - echo "No screenshots to publish, skipping branch update" - exit 0 -fi - -REMOTE="https://x-access-token:${GITHUB_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" -if ! git clone --quiet --depth 1 --branch ci-screenshots "$REMOTE" ci-screenshots-branch 2>/dev/null; then - mkdir -p ci-screenshots-branch - git -C ci-screenshots-branch init --quiet -b ci-screenshots - git -C ci-screenshots-branch remote add origin "$REMOTE" -fi - -mkdir -p "ci-screenshots-branch/$GITHUB_SHA" -cp "$PUBLISH"/*.png "ci-screenshots-branch/$GITHUB_SHA/" -cd ci-screenshots-branch -git add . -if git diff --cached --quiet; then - echo "Screenshots already published for $GITHUB_SHA" - exit 0 -fi -git -c user.name="github-actions[bot]" -c user.email="41898282+github-actions[bot]@users.noreply.github.com" \ - commit --quiet -m "CI screenshots for $GITHUB_SHA" - -for _ in 1 2 3; do - if git push --quiet origin ci-screenshots; then - echo "Published screenshots to ci-screenshots/$GITHUB_SHA" - exit 0 - fi - git pull --quiet --rebase origin ci-screenshots || true -done -echo "::error::Could not push screenshots to the ci-screenshots branch" >&2 -exit 1 diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml deleted file mode 100644 index 791c37b..0000000 --- a/.github/workflows/build.yml +++ /dev/null @@ -1,216 +0,0 @@ -name: build -on: push - -permissions: - contents: write - pull-requests: write - -jobs: - matrices: - name: compute matrices - runs-on: ubuntu-24.04 - outputs: - build: ${{ steps.gen.outputs.build }} - launch: ${{ steps.gen.outputs.launch }} - steps: - - name: checkout repository - uses: actions/checkout@v6 - - - name: generate matrices - id: gen - run: bash .github/scripts/generate-matrices.sh - - build: - name: build mc${{ matrix.mc }} - runs-on: ubuntu-24.04 - needs: matrices - strategy: - fail-fast: false - matrix: - include: ${{ fromJson(needs.matrices.outputs.build) }} - steps: - - name: checkout repository - uses: actions/checkout@v6 - - - name: validate gradle wrapper - uses: gradle/actions/wrapper-validation@v6 - - - name: setup jdk ${{ matrix.java }} - uses: actions/setup-java@v5 - with: - java-version: ${{ matrix.java }} - distribution: 'microsoft' - - - name: make gradle wrapper executable - run: chmod +x ./gradlew - - - name: build :${{ matrix.mc }}:build - run: ./gradlew :${{ matrix.mc }}:build --stacktrace - - - name: upload jar - uses: actions/upload-artifact@v7 - with: - name: betterhud-mc${{ matrix.mc }} - path: versions/${{ matrix.mc }}/build/libs/*.jar - - launch-test: - name: launch mc${{ matrix.mc }} - runs-on: ubuntu-24.04 - needs: matrices - timeout-minutes: 40 - strategy: - fail-fast: false - matrix: - include: ${{ fromJson(needs.matrices.outputs.launch) }} - steps: - - name: checkout repository - uses: actions/checkout@v6 - - - name: validate gradle wrapper - uses: gradle/actions/wrapper-validation@v6 - - - name: setup jdk ${{ matrix.java }} - uses: actions/setup-java@v5 - with: - java-version: ${{ matrix.java }} - distribution: 'microsoft' - - - name: make gradle wrapper executable - run: chmod +x ./gradlew - - - name: install xvfb - run: sudo apt-get update && sudo apt-get install -y xvfb - - - name: generate singleplayer test world - run: bash .github/scripts/generate-test-world.sh ${{ matrix.mc }} - - - name: launch minecraft ${{ matrix.mc }} with mod - run: ./gradlew :launchtest:runLaunchTest -PtestMcVersion=${{ matrix.mc }} --stacktrace - - - name: check survival world screenshot was taken - run: test -n "$(find src/launchtest/run/screenshots -name 'betterhud-survival-world.png' -print -quit 2>/dev/null)" - - - name: upload screenshots - if: always() - uses: actions/upload-artifact@v7 - with: - name: launch-screenshots-mc${{ matrix.mc }} - path: src/launchtest/run/screenshots/ - if-no-files-found: ignore - - - name: upload game logs - if: always() - uses: actions/upload-artifact@v7 - with: - name: launch-logs-mc${{ matrix.mc }} - path: src/launchtest/run/logs/ - if-no-files-found: ignore - - screenshot-report: - name: screenshot report - runs-on: ubuntu-24.04 - needs: launch-test - if: ${{ always() }} - steps: - - name: checkout repository - uses: actions/checkout@v6 - - - name: publish screenshots to the ci-screenshots branch - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: bash .github/scripts/publish-screenshots.sh - - - name: post screenshot grid to pull request and job summary - uses: actions/github-script@v8 - with: - script: | - const fs = require('fs'); - - const dir = `screenshots-publish/${context.sha}`; - const base = `https://raw.githubusercontent.com/${context.repo.owner}/${context.repo.repo}/ci-screenshots/${context.sha}`; - - const versions = fs.existsSync(dir) - ? fs.readdirSync(dir) - .filter(f => f.startsWith('mc-') && f.endsWith('-survival.png')) - .map(f => f.slice(3, -13)) - .sort((a, b) => a.localeCompare(b, undefined, { numeric: true })) - : []; - - const cells = versions.map(v => { - const file = `mc-${v}-survival.png`; - const ok = fs.existsSync(`${dir}/${file}`); - return ok - ? `${v}
BetterHUD on Minecraft ${v}` - : `${v}
❌ no screenshot`; - return `${v}
BetterHUD on Minecraft ${v}`; - }); - let rows = ''; - for (let i = 0; i < cells.length; i += 4) { - rows += `${cells.slice(i, i + 4).join('')}\n`; - } - - const body = [ - `# BetterHUD Launch Screenshots`, - '', - `\n${rows}
`, - ].join('\n'); - - await core.summary.addRaw(body).write(); - - const { data: prs } = await github.rest.repos.listPullRequestsAssociatedWithCommit({ - owner: context.repo.owner, - repo: context.repo.repo, - commit_sha: context.sha, - }); - const pr = prs.find(p => p.state === 'open'); - if (!pr) { - core.info('No open pull request for this commit, skipping comment'); - return; - } - - const { data: comments } = await github.rest.issues.listComments({ - owner: context.repo.owner, repo: context.repo.repo, - issue_number: pr.number, per_page: 100, - }); - const existing = comments.find(c => c.body && c.body.includes(marker)); - if (existing) { - await github.rest.issues.updateComment({ - owner: context.repo.owner, repo: context.repo.repo, - comment_id: existing.id, body, - }); - } else { - await github.rest.issues.createComment({ - owner: context.repo.owner, repo: context.repo.repo, - issue_number: pr.number, body, - }); - } - - modrinth-bundle: - name: modrinth bundle - runs-on: ubuntu-24.04 - steps: - - name: checkout repository - uses: actions/checkout@v6 - - - name: validate gradle wrapper - uses: gradle/actions/wrapper-validation@v6 - - - name: setup jdk 21 and 25 - uses: actions/setup-java@v5 - with: - java-version: | - 21 - 25 - distribution: 'microsoft' - - - name: make gradle wrapper executable - run: chmod +x ./gradlew - - - name: build all variants and write the upload guide - run: ./gradlew modrinthBundle --stacktrace "-Porg.gradle.java.installations.paths=$JAVA_HOME_21_X64,$JAVA_HOME_25_X64" - - - name: upload modrinth bundle - uses: actions/upload-artifact@v7 - with: - name: modrinth-upload - path: build/modrinth/ diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml deleted file mode 100644 index 918059d..0000000 --- a/.github/workflows/publish.yml +++ /dev/null @@ -1,121 +0,0 @@ -name: publish -on: - workflow_dispatch: - inputs: - changelog: - description: 'Changelog (Markdown, shown on both platforms)' - required: true - type: string - version_type: - description: 'Release channel' - required: true - type: choice - default: release - options: - - release - - beta - - alpha - platforms: - description: 'Where to publish' - required: true - type: choice - default: both - options: - - both - - modrinth - - curseforge - -permissions: - contents: read - -concurrency: - group: publish - cancel-in-progress: false - -jobs: - matrices: - name: compute publish matrix - runs-on: ubuntu-24.04 - outputs: - publish: ${{ steps.gen.outputs.publish }} - mod_version: ${{ steps.version.outputs.mod_version }} - steps: - - name: checkout repository - uses: actions/checkout@v6 - - - name: generate matrices - id: gen - run: bash .github/scripts/generate-matrices.sh - - - name: read mod version from gradle.properties - id: version - run: | - version="$(grep -oP '^mod_version=\K.+' gradle.properties)" - echo "mod_version=$version" >> "$GITHUB_OUTPUT" - echo "Releasing BetterHUD $version" - - - name: check the required tokens are configured - env: - PLATFORMS: ${{ inputs.platforms }} - MODRINTH_TOKEN: ${{ secrets.MODRINTH_TOKEN }} - CURSEFORGE_TOKEN: ${{ secrets.CURSEFORGE_TOKEN }} - run: | - if [ "$PLATFORMS" != "curseforge" ] && [ -z "$MODRINTH_TOKEN" ]; then - echo "::error::MODRINTH_TOKEN secret is not set"; exit 1 - fi - if [ "$PLATFORMS" != "modrinth" ] && [ -z "$CURSEFORGE_TOKEN" ]; then - echo "::error::CURSEFORGE_TOKEN secret is not set"; exit 1 - fi - - publish: - name: publish mc${{ matrix.mc }} - runs-on: ubuntu-24.04 - needs: matrices - strategy: - max-parallel: 1 - fail-fast: true - matrix: - include: ${{ fromJson(needs.matrices.outputs.publish) }} - steps: - - name: checkout repository - uses: actions/checkout@v6 - - - name: validate gradle wrapper - uses: gradle/actions/wrapper-validation@v6 - - - name: setup jdk ${{ matrix.java }} - uses: actions/setup-java@v5 - with: - java-version: ${{ matrix.java }} - distribution: 'microsoft' - - - name: make gradle wrapper executable - run: chmod +x ./gradlew - - - name: build :${{ matrix.mc }}:build - run: ./gradlew :${{ matrix.mc }}:build --stacktrace - - - name: publish to modrinth and curseforge - uses: Kir-Antipov/mc-publish@v3.3 - with: - modrinth-id: betterhudfabric - modrinth-token: ${{ inputs.platforms != 'curseforge' && secrets.MODRINTH_TOKEN || '' }} - curseforge-id: 1375095 - curseforge-token: ${{ inputs.platforms != 'modrinth' && secrets.CURSEFORGE_TOKEN || '' }} - - files: versions/${{ matrix.mc }}/build/libs/!(*-@(dev|sources|javadoc)).jar - name: BetterHUD ${{ needs.matrices.outputs.mod_version }} (Minecraft ${{ matrix.range }}) - version: ${{ needs.matrices.outputs.mod_version }}+mc${{ matrix.mc }} - version-type: ${{ inputs.version_type }} - changelog: ${{ inputs.changelog }} - - loaders: fabric - game-versions: ${{ matrix.game_versions }} - java: ${{ matrix.java }} - dependencies: | - fabric-api(required) - cloth-config(required) - modmenu(required) - - retry-attempts: 2 - retry-delay: 10000 diff --git a/build.gradle b/build.gradle index 85fba51..b083774 100644 --- a/build.gradle +++ b/build.gradle @@ -4,17 +4,11 @@ plugins { id 'maven-publish' } -def mcVersion = stonecutter.current.version -def isLegacy = stonecutter.eval(mcVersion, '<26') -def hasHudElementRegistry = stonecutter.eval(mcVersion, '>=1.21.6') +def isLegacy = stonecutter.eval(stonecutter.current.version, '<26') apply plugin: isLegacy ? 'net.fabricmc.fabric-loom-remap' : 'net.fabricmc.fabric-loom' -def javaVersionInt = Integer.parseInt(project.java_version) -def javaLang = JavaVersion.toVersion(javaVersionInt) -def mixinJavaCompat = "JAVA_${javaVersionInt}" - -version = "${project.mod_version}+mc${mcVersion}" +version = "${project.mod_version}+mc${stonecutter.current.version}" group = project.maven_group base { @@ -66,8 +60,8 @@ processResources { version : project.version, minecraftReq : project.mc_dep, loaderReq : project.loader_dep, - javaReq : ">=${javaVersionInt}", - javaCompat : mixinJavaCompat, + javaReq : ">=${project.java_version}", + javaCompat : "JAVA_${project.java_version}", dependsExtra : dependsExtra ] inputs.properties expansion @@ -81,15 +75,15 @@ processResources { } tasks.withType(JavaCompile).configureEach { - it.options.release = javaVersionInt + it.options.release = Integer.parseInt(project.java_version) } java { withSourcesJar() - sourceCompatibility = javaLang - targetCompatibility = javaLang + sourceCompatibility = JavaVersion.toVersion(Integer.parseInt(project.java_version)) + targetCompatibility = JavaVersion.toVersion(Integer.parseInt(project.java_version)) toolchain { - languageVersion = JavaLanguageVersion.of(javaVersionInt) + languageVersion = JavaLanguageVersion.of(Integer.parseInt(project.java_version)) } } @@ -103,7 +97,7 @@ jar { publishing { publications { create('mavenJava', MavenPublication) { - artifactId = "${project.archives_base_name}-mc${mcVersion}" + artifactId = "${project.archives_base_name}-mc${stonecutter.current.version}" from components.java } } diff --git a/src/launchtest/build.gradle b/src/launchtest/build.gradle deleted file mode 100644 index 1c4d203..0000000 --- a/src/launchtest/build.gradle +++ /dev/null @@ -1,111 +0,0 @@ -plugins { - id 'net.fabricmc.fabric-loom-remap' version "${loom_version}" apply false - id 'net.fabricmc.fabric-loom' version "${loom_version}" apply false -} - -def versionKey = { String v -> v.tokenize('.').collect { it.padLeft(4, '0') }.join('.') } -def variantsDir = new File(rootDir, 'versions') -def variants = variantsDir.listFiles().findAll { it.directory }*.name.sort { versionKey(it) } - -def variantProps = { String variant -> - def props = new Properties() - new File(variantsDir, "${variant}/gradle.properties").withInputStream { props.load(it) } - props -} - -def testMcVersion = providers.gradleProperty('testMcVersion') - .getOrElse(variantProps(variants.last()).getProperty('minecraft_version')) - -def coveringVariants = variants.findAll { versionKey(it) <= versionKey(testMcVersion) } -if (coveringVariants.isEmpty()) { - throw new GradleException("No build variant covers Minecraft ${testMcVersion}") -} -def variant = coveringVariants.last() - -def props = variantProps(variant) -def loaderVersion = props.getProperty('loader_version') -def javaVersionInt = Integer.parseInt(props.getProperty('java_version')) - -def isLegacy = variant.startsWith('1.') -apply plugin: isLegacy ? 'net.fabricmc.fabric-loom-remap' : 'net.fabricmc.fabric-loom' - -version = '1.0.0' -base { - archivesName = 'betterhud-launchtest' -} - -repositories { - maven { url 'https://maven.shedaniel.me/' } - maven { url 'https://maven.terraformersmc.com/releases/' } -} - -if (!isLegacy) { - configurations.configureEach { - if (name.toLowerCase().contains('annotationprocessor')) { - exclude group: 'org.ow2.asm' - } - } -} - -def modrinthLatest = { String slug -> - def query = "https://api.modrinth.com/v2/project/${slug}/version" + - "?game_versions=%5B%22${testMcVersion}%22%5D&loaders=%5B%22fabric%22%5D" - def found = new groovy.json.JsonSlurper().parse(new URI(query).toURL()) - if (found.isEmpty()) { - throw new GradleException("No ${slug} release on Modrinth supports Minecraft ${testMcVersion}") - } - found.first().version_number -} - -def fabricApiVersion = modrinthLatest('fabric-api') -def modmenuVersion = modrinthLatest('modmenu') -def clothConfigVersion = modrinthLatest('cloth-config').replace('+fabric', '') - -dependencies { - minecraft "com.mojang:minecraft:${testMcVersion}" - - if (isLegacy) { - mappings loom.officialMojangMappings() - modImplementation "net.fabricmc:fabric-loader:${loaderVersion}" - modImplementation "net.fabricmc.fabric-api:fabric-api:${fabricApiVersion}" - } else { - implementation "net.fabricmc:fabric-loader:${loaderVersion}" - implementation "net.fabricmc.fabric-api:fabric-api:${fabricApiVersion}" - } - - productionRuntimeMods("net.fabricmc.fabric-api:fabric-api:${fabricApiVersion}") { transitive = false } - productionRuntimeMods("com.terraformersmc:modmenu:${modmenuVersion}") { transitive = false } - productionRuntimeMods("me.shedaniel.cloth:cloth-config-fabric:${clothConfigVersion}") { transitive = false } -} - -tasks.withType(JavaCompile).configureEach { - it.options.release = javaVersionInt -} - -java { - toolchain { - languageVersion = JavaLanguageVersion.of(javaVersionInt) - } -} - -evaluationDependsOn(":${variant}") -def variantProject = project(":${variant}") -def variantJarTask = variantProject.tasks.names.contains('remapJar') ? 'remapJar' : 'jar' -def ownJarTask = tasks.names.contains('remapJar') ? 'remapJar' : 'jar' - -tasks.register('runLaunchTest', net.fabricmc.loom.task.prod.ClientProductionRunTask) { - mods.setFrom(variantProject.tasks.named(variantJarTask), tasks.named(ownJarTask), configurations.productionRuntimeMods) - programArgs.addAll('--quickPlaySingleplayer', 'ci-world') - - doFirst { - if (!file('run/saves/ci-world').exists()) { - throw new GradleException("Missing test world at src/launchtest/run/saves/ci-world. " + - "Generate it with: bash .github/scripts/generate-test-world.sh ${testMcVersion}") - } - def options = new File(runDir.get().asFile, 'options.txt') - if (!options.exists()) { - options.parentFile.mkdirs() - options.text = 'onboardAccessibility:false\n' - } - } -} diff --git a/src/launchtest/src/main/java/dsns/betterhud/launchtest/LaunchTestClient.java b/src/launchtest/src/main/java/dsns/betterhud/launchtest/LaunchTestClient.java deleted file mode 100644 index 5d206ed..0000000 --- a/src/launchtest/src/main/java/dsns/betterhud/launchtest/LaunchTestClient.java +++ /dev/null @@ -1,83 +0,0 @@ -package dsns.betterhud.launchtest; - -import java.io.File; -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.StandardCopyOption; -import java.util.Comparator; -import java.util.stream.Stream; - -import com.mojang.blaze3d.pipeline.RenderTarget; - -import net.fabricmc.api.ClientModInitializer; -import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents; -import net.minecraft.client.Minecraft; -import net.minecraft.client.Screenshot; -import net.minecraft.client.renderer.GameRenderer; - -public class LaunchTestClient implements ClientModInitializer { - private static final String SCREENSHOT_NAME = "betterhud-survival-world.png"; - private static final String[] MINECRAFT_RENDER_TARGET_GETTERS = {"getMainRenderTarget", "method_1522"}; - private static final String GAME_RENDERER_RENDER_TARGET_GETTER = "mainRenderTarget"; - private static final int SCREENSHOT_TICK = 200; - private static final int STOP_TICK = 300; - private static final int WORLD_JOIN_TIMEOUT_TICKS = 2400; - - private int totalTicks; - private int worldTicks = -1; - - @Override - public void onInitializeClient() { - ClientTickEvents.END_CLIENT_TICK.register(client -> { - totalTicks++; - if (worldTicks < 0 && totalTicks >= WORLD_JOIN_TIMEOUT_TICKS) { - fail("the game never joined the test world"); - } - if (client.player == null || client.level == null) { - return; - } - - worldTicks++; - if (worldTicks == SCREENSHOT_TICK) { - Screenshot.grab(client.gameDirectory, mainRenderTarget(client), message -> {}); - } else if (worldTicks == STOP_TICK) { - renameScreenshot(client.gameDirectory); - client.stop(); - } - }); - } - - private static RenderTarget mainRenderTarget(Minecraft client) { - for (String getter : MINECRAFT_RENDER_TARGET_GETTERS) { - try { - return (RenderTarget) Minecraft.class.getMethod(getter).invoke(client); - } catch (ReflectiveOperationException e) { - continue; - } - } - try { - return (RenderTarget) GameRenderer.class.getMethod(GAME_RENDERER_RENDER_TARGET_GETTER).invoke(client.gameRenderer); - } catch (ReflectiveOperationException e) { - throw new IllegalStateException("Could not resolve the main render target", e); - } - } - - private static void renameScreenshot(File gameDirectory) { - Path dir = gameDirectory.toPath().resolve("screenshots"); - try (Stream files = Files.list(dir)) { - Path newest = files.filter(f -> f.getFileName().toString().endsWith(".png")) - .max(Comparator.comparingLong(f -> f.toFile().lastModified())) - .orElseThrow(() -> new IOException("no screenshot in " + dir)); - Files.move(newest, dir.resolve(SCREENSHOT_NAME), StandardCopyOption.REPLACE_EXISTING); - } catch (IOException e) { - e.printStackTrace(); - fail("the screenshot was not saved"); - } - } - - private static void fail(String reason) { - System.err.println("betterhud launch test failed: " + reason); - System.exit(1); - } -} diff --git a/src/launchtest/src/main/resources/fabric.mod.json b/src/launchtest/src/main/resources/fabric.mod.json deleted file mode 100644 index 3cc81a5..0000000 --- a/src/launchtest/src/main/resources/fabric.mod.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "schemaVersion": 1, - "id": "betterhud-launchtest", - "version": "1.0.0", - "name": "BetterHUD Launch Test", - "description": "Joins the pre-generated test world, screenshots the HUD, and exits.", - "license": "AGPL-3.0", - "environment": "client", - "entrypoints": { - "client": ["dsns.betterhud.launchtest.LaunchTestClient"] - }, - "depends": { - "betterhud": "*", - "fabric-lifecycle-events-v1": "*" - } -} From a24b16a1088df0da15832bd48064def0ac5b2b09 Mon Sep 17 00:00:00 2001 From: Dominic Seung Date: Tue, 7 Jul 2026 22:52:09 -0700 Subject: [PATCH 07/15] faithful use of template https://github.com/stonecutter-versioning/stonecutter-template-fabric --- build.gradle | 104 -------------------- build.gradle.kts | 115 +++++++++++++++++++++++ gradle.properties | 14 +-- gradle/wrapper/gradle-wrapper.properties | 6 +- settings.gradle | 37 -------- settings.gradle.kts | 34 +++++++ src/main/resources/betterhud.mixins.json | 2 +- src/main/resources/fabric.mod.json | 12 ++- stonecutter.gradle | 52 ---------- stonecutter.gradle.kts | 23 +++++ stonecutter.properties.toml | 42 +++++++++ versions/1.21.6/gradle.properties | 10 -- versions/1.21/gradle.properties | 10 -- versions/26.1/gradle.properties | 10 -- versions/26.2/gradle.properties | 10 -- 15 files changed, 229 insertions(+), 252 deletions(-) delete mode 100644 build.gradle create mode 100644 build.gradle.kts delete mode 100644 settings.gradle create mode 100644 settings.gradle.kts delete mode 100644 stonecutter.gradle create mode 100644 stonecutter.gradle.kts create mode 100644 stonecutter.properties.toml delete mode 100644 versions/1.21.6/gradle.properties delete mode 100644 versions/1.21/gradle.properties delete mode 100644 versions/26.1/gradle.properties delete mode 100644 versions/26.2/gradle.properties diff --git a/build.gradle b/build.gradle deleted file mode 100644 index b083774..0000000 --- a/build.gradle +++ /dev/null @@ -1,104 +0,0 @@ -plugins { - id 'net.fabricmc.fabric-loom-remap' version "${loom_version}" apply false - id 'net.fabricmc.fabric-loom' version "${loom_version}" apply false - id 'maven-publish' -} - -def isLegacy = stonecutter.eval(stonecutter.current.version, '<26') - -apply plugin: isLegacy ? 'net.fabricmc.fabric-loom-remap' : 'net.fabricmc.fabric-loom' - -version = "${project.mod_version}+mc${stonecutter.current.version}" -group = project.maven_group - -base { - archivesName = project.archives_base_name -} - -repositories { - maven { url 'https://maven.shedaniel.me/' } - maven { url 'https://maven.terraformersmc.com/releases/' } -} - -if (!isLegacy) { - configurations.configureEach { - if (name.toLowerCase().contains('annotationprocessor')) { - exclude group: 'org.ow2.asm' - } - } -} - -dependencies { - minecraft "com.mojang:minecraft:${project.minecraft_version}" - - if (isLegacy) { - mappings loom.officialMojangMappings() - - modImplementation "net.fabricmc:fabric-loader:${project.loader_version}" - modImplementation "net.fabricmc.fabric-api:fabric-api:${project.fabric_api_version}" - modImplementation "com.terraformersmc:modmenu:${project.modmenu_version}" - modImplementation("me.shedaniel.cloth:cloth-config-fabric:${project.cloth_config_version}") { - exclude(group: 'net.fabricmc.fabric-api') - } - } else { - implementation "net.fabricmc:fabric-loader:${project.loader_version}" - implementation "net.fabricmc.fabric-api:fabric-api:${project.fabric_api_version}" - implementation "com.terraformersmc:modmenu:${project.modmenu_version}" - implementation("me.shedaniel.cloth:cloth-config-fabric:${project.cloth_config_version}") { - exclude(group: 'net.fabricmc.fabric-api') - } - } -} - -processResources { - def dependsExtra = ''', - "fabric-api": "*", - "modmenu": "*", - "cloth-config2": "*"''' - - def expansion = [ - version : project.version, - minecraftReq : project.mc_dep, - loaderReq : project.loader_dep, - javaReq : ">=${project.java_version}", - javaCompat : "JAVA_${project.java_version}", - dependsExtra : dependsExtra - ] - inputs.properties expansion - - filesMatching('fabric.mod.json') { - expand expansion - } - filesMatching('betterhud.mixins.json') { - expand expansion - } -} - -tasks.withType(JavaCompile).configureEach { - it.options.release = Integer.parseInt(project.java_version) -} - -java { - withSourcesJar() - sourceCompatibility = JavaVersion.toVersion(Integer.parseInt(project.java_version)) - targetCompatibility = JavaVersion.toVersion(Integer.parseInt(project.java_version)) - toolchain { - languageVersion = JavaLanguageVersion.of(Integer.parseInt(project.java_version)) - } -} - -jar { - def archiveName = base.archivesName.get() - from('../../LICENSE') { - rename { "${it}_${archiveName}" } - } -} - -publishing { - publications { - create('mavenJava', MavenPublication) { - artifactId = "${project.archives_base_name}-mc${stonecutter.current.version}" - from components.java - } - } -} diff --git a/build.gradle.kts b/build.gradle.kts new file mode 100644 index 0000000..eadcc70 --- /dev/null +++ b/build.gradle.kts @@ -0,0 +1,115 @@ +plugins { + // This plugin applies the correct loom variant based on the Minecraft version + id("dev.kikugie.loom-back-compat") +} + +// DO NOT set group = ...! +version = "${property("mod.version")}+${sc.current.version}" +base.archivesName = property("mod.id") as String + +val requiredJava: JavaVersion = when { + sc.current.parsed >= "26.1" -> JavaVersion.VERSION_25 + sc.current.parsed >= "1.20.5" -> JavaVersion.VERSION_21 + sc.current.parsed >= "1.18" -> JavaVersion.VERSION_17 + sc.current.parsed >= "1.17" -> JavaVersion.VERSION_16 + else -> JavaVersion.VERSION_1_8 +} + +// This can be used for publishing on Modrinth and Curseforge +val compatibleVersions: List = sc.properties.rawOrNull("mod", "mc_releases") + ?.asList().orEmpty().map { it.toString() } + +repositories { + /** + * Restricts dependency search of the given [groups] to the [maven URL][url], + * improving the setup speed. + */ + fun strictMaven(url: String, alias: String, vararg groups: String) = exclusiveContent { + forRepository { maven(url) { name = alias } } + filter { groups.forEach(::includeGroup) } + } + strictMaven("https://www.cursemaven.com", "CurseForge", "curse.maven") + strictMaven("https://api.modrinth.com/maven", "Modrinth", "maven.modrinth") + maven("https://maven.shedaniel.me/") + maven("https://maven.terraformersmc.com/releases/") +} + +dependencies { + /** + * Fetches only the required Fabric API modules to not waste time downloading all of them for each version. + * @see List of Fabric API modules + */ + fun fapi(vararg modules: String) { + for (it in modules) modImplementation(fabricApi.module(it, sc.properties["deps.fabric_api"])) + } + + minecraft("com.mojang:minecraft:${sc.current.version}") + // Applies Mojang Mappings on obfuscated versions + loomx.applyMojangMappings() + + // Use `mod{dependency type}` even on 26.1+ - loom-back-compat converts them + modImplementation("net.fabricmc:fabric-loader:${property("deps.fabric_loader")}") + fapi("fabric-lifecycle-events-v1", "fabric-rendering-v1") + modImplementation("com.terraformersmc:modmenu:${property("deps.modmenu")}") + modImplementation("me.shedaniel.cloth:cloth-config-fabric:${property("deps.cloth_config")}") { + exclude(group = "net.fabricmc.fabric-api") + } +} + +loom { + fabricModJsonPath = rootProject.file("src/main/resources/fabric.mod.json") // Useful for interface injection + + decompilerOptions.named("vineflower") { + options.put("mark-corresponding-synthetics", "1") // Adds names to lambdas - useful for mixins + } + + runConfigs.all { + preferGradleTask = true + generateRunConfig = true + runDirectory = rootProject.file("run") // Shares the run directory between versions + jvmArguments.add("-Dmixin.debug.export=true") // Exports transformed classes for debugging + } +} + +java { + withSourcesJar() + targetCompatibility = requiredJava + sourceCompatibility = requiredJava + + toolchain { + vendor = JvmVendorSpec.ADOPTIUM + languageVersion = JavaLanguageVersion.of(requiredJava.majorVersion) + } +} + +tasks { + processResources { + fun MutableMap.register(key: String, property: String) { + val value: String = sc.properties[property] + inputs.property(key, value) + set(key, value) + } + + val props = buildMap { + register("id", "mod.id") + register("name", "mod.name") + register("version", "mod.version") + register("minecraft", "mod.mc_compat") + } + + filesMatching("fabric.mod.json") { expand(props) } + + val mixinJava = "JAVA_${requiredJava.majorVersion}" + filesMatching("*.mixins.json") { expand("java" to mixinJava) } + } + + register("buildAndCollect") { + group = "build" + description = "Builds mod jars and copies results to `build/libs/{mod version}/`" + + inputs.property("version", project.property("mod.version")) + // loomx.mod(Sources)Jar returns the jar task for the applied loom variant + from(loomx.modJar.flatMap { it.archiveFile }, loomx.modSourcesJar.flatMap { it.archiveFile }) + into(rootProject.layout.buildDirectory.file("libs/${project.property("mod.version")}")) + } +} \ No newline at end of file diff --git a/gradle.properties b/gradle.properties index 2345a67..16f1881 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,12 +1,4 @@ -org.gradle.jvmargs=-Xmx2G +# This file is used for Gradle options, for the rest use stonecutter.properties.toml +org.gradle.jvmargs=-Xmx1G org.gradle.parallel=true - -org.gradle.configuration-cache=false - -dev.kikugie.stonecutter.hard_mode=true - -loom_version=1.17-SNAPSHOT - -mod_version=2.1.1 -maven_group=dsns.betterhud -archives_base_name=betterhud +org.gradle.configuration-cache=true \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 5dd3c01..d19bd2a 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,7 +1,9 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-9.5.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.6.0-bin.zip networkTimeout=10000 +retries=0 +retryBackOffMs=500 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME -zipStorePath=wrapper/dists +zipStorePath=wrapper/dists \ No newline at end of file diff --git a/settings.gradle b/settings.gradle deleted file mode 100644 index fcab7ea..0000000 --- a/settings.gradle +++ /dev/null @@ -1,37 +0,0 @@ -pluginManagement { - repositories { - maven { - name = 'Fabric' - url = 'https://maven.fabricmc.net/' - } - maven { - name = 'Stonecutter Releases' - url = 'https://maven.kikugie.dev/releases' - } - maven { - name = 'Stonecutter Snapshots' - url = 'https://maven.kikugie.dev/snapshots' - } - mavenCentral() - gradlePluginPortal() - } -} - -plugins { - id 'dev.kikugie.stonecutter' version '0.9.3' -} - -def versionKey = { String v -> v.tokenize('.').collect { it.padLeft(4, '0') }.join('.') } -def variants = file('versions').listFiles().findAll { it.directory }*.name.sort { versionKey(it) } - -stonecutter { - create(rootProject) { - versions(*variants) - vcsVersion = variants.last() - } -} - -rootProject.name = 'betterhud' - -include 'launchtest' -project(':launchtest').projectDir = file('src/launchtest') diff --git a/settings.gradle.kts b/settings.gradle.kts new file mode 100644 index 0000000..aaaf7b2 --- /dev/null +++ b/settings.gradle.kts @@ -0,0 +1,34 @@ +pluginManagement { + repositories { + mavenLocal() + mavenCentral() + gradlePluginPortal() + maven("https://maven.fabricmc.net/") + maven("https://maven.kikugie.dev/releases") { name = "KikuGie Releases" } + maven("https://maven.kikugie.dev/snapshots") { name = "KikuGie Snapshots" } + } +} + +plugins { + // Check the latest version on https://stonecutter.kikugie.dev/blog/changes/0.9 + id("dev.kikugie.stonecutter") version "0.9.6" + + // Used for cross-compat for 26.1+ and older versions (https://codeberg.org/KikuGie/loom-back-compat) + id("dev.kikugie.loom-back-compat") version "0.3" + + // Sometimes it is needed to make Gradle run at all, so it doesn't hurt to have + // (https://github.com/gradle/foojay-toolchains) + id("org.gradle.toolchains.foojay-resolver-convention") version "1.0.0" +} + +stonecutter { + create(rootProject) { + // See https://stonecutter.kikugie.dev/wiki/start/#choosing-minecraft-versions + versions("1.21", "1.21.6") + version("26.1.x", "26.1") + version("26.2.x", "26.2") + vcsVersion = "26.2.x" + } +} + +rootProject.name = "BetterHUD" \ No newline at end of file diff --git a/src/main/resources/betterhud.mixins.json b/src/main/resources/betterhud.mixins.json index c35b39c..b314554 100644 --- a/src/main/resources/betterhud.mixins.json +++ b/src/main/resources/betterhud.mixins.json @@ -1,7 +1,7 @@ { "required": true, "package": "dsns.betterhud.mixin", - "compatibilityLevel": "${javaCompat}", + "compatibilityLevel": "${java}", "client": [], "injectors": { "defaultRequire": 1 diff --git a/src/main/resources/fabric.mod.json b/src/main/resources/fabric.mod.json index 6900660..58fd55e 100644 --- a/src/main/resources/fabric.mod.json +++ b/src/main/resources/fabric.mod.json @@ -1,8 +1,8 @@ { "schemaVersion": 1, - "id": "betterhud", + "id": "${id}", "version": "${version}", - "name": "BetterHUD", + "name": "${name}", "description": "A simple HUD with essential information easily accessible for Minecraft Fabric.", "authors": [ "DSNS" @@ -21,8 +21,10 @@ "betterhud.mixins.json" ], "depends": { - "fabricloader": "${loaderReq}", - "minecraft": "${minecraftReq}", - "java": "${javaReq}"${dependsExtra} + "fabricloader": "*", + "minecraft": "${minecraft}", + "fabric-api": "*", + "modmenu": "*", + "cloth-config2": "*" } } diff --git a/stonecutter.gradle b/stonecutter.gradle deleted file mode 100644 index 231a20d..0000000 --- a/stonecutter.gradle +++ /dev/null @@ -1,52 +0,0 @@ -plugins { - id 'dev.kikugie.stonecutter' -} - -stonecutter.active('26.2') /* [SC] DO NOT EDIT */ - -tasks.register('modrinthBundle') { - group = 'publishing' - description = 'Collects the release jars and the per-jar Minecraft version ranges for Modrinth.' - - def variants = file('versions').listFiles().findAll { it.directory }*.name.sort() - variants.each { dependsOn(":${it}:build") } - - doLast { - def outDir = file('build/modrinth') - outDir.deleteDir() - outDir.mkdirs() - - def rows = [] - variants.each { variant -> - def jar = file("versions/${variant}/build/libs").listFiles().find { - it.name.endsWith('.jar') && !it.name.contains('-sources') - } - if (jar == null) { - throw new GradleException("No release jar found for variant ${variant} in versions/${variant}/build/libs") - } - java.nio.file.Files.copy(jar.toPath(), new File(outDir, jar.name).toPath()) - - def props = new Properties() - file("versions/${variant}/gradle.properties").withInputStream { props.load(it) } - rows << "| `${jar.name}` | `${props.getProperty('mc_dep')}` |" - } - - def manifest = new File(outDir, 'UPLOAD.md') - manifest.text = [ - "# Modrinth upload — betterhud ${project.mod_version}", - '', - 'Upload each jar below as its own Modrinth version, selecting every', - 'Minecraft release in its range. Loader: Fabric. Required', - 'dependencies: Fabric API, Cloth Config, Mod Menu.', - '', - '| File | Minecraft versions |', - '| --- | --- |', - *rows, - '' - ].join('\n') - - println() - println manifest.text - println "Bundle written to ${outDir}" - } -} diff --git a/stonecutter.gradle.kts b/stonecutter.gradle.kts new file mode 100644 index 0000000..422b33d --- /dev/null +++ b/stonecutter.gradle.kts @@ -0,0 +1,23 @@ +plugins { + id("dev.kikugie.stonecutter") +} + +stonecutter active "26.2.x" + +// See https://stonecutter.kikugie.dev/wiki/config/params +stonecutter parameters { + swaps["mod_version"] = "\"${property("mod.version")}\";" + swaps["minecraft"] = "\"${node.metadata.version}\";" + constants["release"] = property("mod.id") != "template" + dependencies["fapi"] = node.project.property("deps.fabric_api") as String + + replacements { + string(current.parsed >= "1.21.11") { + replace("ResourceLocation", "Identifier") + } + + string(current.parsed >= "26.1") { + replace("classTweaker v2 named", "classTweaker v2 official") + } + } +} \ No newline at end of file diff --git a/stonecutter.properties.toml b/stonecutter.properties.toml new file mode 100644 index 0000000..ae91cd5 --- /dev/null +++ b/stonecutter.properties.toml @@ -0,0 +1,42 @@ +# See https://stonecutter.kikugie.dev/wiki/config/properties#centralized-properties +mod.id = "betterhud" +mod.name = "BetterHUD" +mod.group = "dsns.betterhud" +mod.version = "2.1.1" + +deps.fabric_loader = "0.19.3" +loomx.loom_version = "1.17-SNAPSHOT" + +["1.21"] +mod.mc_compat = ">=1.21 <=1.21.5" +mod.mc_releases = ["1.21", "1.21.1", "1.21.2", "1.21.3", "1.21.4", "1.21.5"] + +deps.fabric_loader = "0.17.3" +deps.fabric_api = "0.102.0+1.21" +deps.modmenu = "14.0.2" +deps.cloth_config = "19.0.147" + +["1.21.6"] +mod.mc_compat = ">=1.21.6 <1.22" +mod.mc_releases = ["1.21.6", "1.21.7", "1.21.8", "1.21.9", "1.21.10", "1.21.11"] + +deps.fabric_api = "0.128.2+1.21.6" +deps.modmenu = "15.0.2" +deps.cloth_config = "19.0.147" + +["26.1.x"] +mod.mc_compat = ">=26.1 <26.2" +mod.mc_releases = ["26.1"] + +deps.fabric_loader = "0.19.2" +deps.fabric_api = "0.145.1+26.1" +deps.modmenu = "18.0.0-alpha.8" +deps.cloth_config = "26.1.154" + +["26.2.x"] +mod.mc_compat = ">=26.2" +mod.mc_releases = ["26.2"] + +deps.fabric_api = "0.152.2+26.2" +deps.modmenu = "18.0.0-alpha.8" +deps.cloth_config = "26.1.154" diff --git a/versions/1.21.6/gradle.properties b/versions/1.21.6/gradle.properties deleted file mode 100644 index a6cb6ef..0000000 --- a/versions/1.21.6/gradle.properties +++ /dev/null @@ -1,10 +0,0 @@ -minecraft_version=1.21.6 -loader_version=0.17.3 -java_version=21 - -mc_dep=>=1.21.6 <1.22 -loader_dep=>=0.16.10 - -fabric_api_version=0.128.2+1.21.6 -modmenu_version=15.0.2 -cloth_config_version=19.0.147 diff --git a/versions/1.21/gradle.properties b/versions/1.21/gradle.properties deleted file mode 100644 index ed95574..0000000 --- a/versions/1.21/gradle.properties +++ /dev/null @@ -1,10 +0,0 @@ -minecraft_version=1.21 -loader_version=0.17.3 -java_version=21 - -mc_dep=>=1.21 <=1.21.5 -loader_dep=>=0.15.11 - -fabric_api_version=0.102.0+1.21 -modmenu_version=14.0.2 -cloth_config_version=19.0.147 diff --git a/versions/26.1/gradle.properties b/versions/26.1/gradle.properties deleted file mode 100644 index ce6fc20..0000000 --- a/versions/26.1/gradle.properties +++ /dev/null @@ -1,10 +0,0 @@ -minecraft_version=26.1 -loader_version=0.19.2 -java_version=25 - -mc_dep=>=26.1 <26.2 -loader_dep=>=0.19 - -fabric_api_version=0.145.1+26.1 -modmenu_version=18.0.0-alpha.8 -cloth_config_version=26.1.154 diff --git a/versions/26.2/gradle.properties b/versions/26.2/gradle.properties deleted file mode 100644 index 0bb6973..0000000 --- a/versions/26.2/gradle.properties +++ /dev/null @@ -1,10 +0,0 @@ -minecraft_version=26.2 -loader_version=0.19.3 -java_version=25 - -mc_dep=>=26.2 -loader_dep=>=0.19 - -fabric_api_version=0.154.0+26.2 -modmenu_version=18.0.0-alpha.8 -cloth_config_version=26.1.154 From 999d134c8c587c83b1baf2ef3637e7618bfbcc09 Mon Sep 17 00:00:00 2001 From: Dominic Seung Date: Tue, 7 Jul 2026 22:58:13 -0700 Subject: [PATCH 08/15] implement workflows from https://github.com/rotgruengelb/stonecutter-mod-template --- .github/workflows/build.yml | 6 + .github/workflows/build_reusable.yml | 59 ++++++++ .github/workflows/release.yml | 215 +++++++++++++++++++++++++++ 3 files changed, 280 insertions(+) create mode 100644 .github/workflows/build.yml create mode 100644 .github/workflows/build_reusable.yml create mode 100644 .github/workflows/release.yml diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..14bfb4e --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,6 @@ +name: build +on: [ push, pull_request ] + +jobs: + build: + uses: ./.github/workflows/build_reusable.yml diff --git a/.github/workflows/build_reusable.yml b/.github/workflows/build_reusable.yml new file mode 100644 index 0000000..8ae6a71 --- /dev/null +++ b/.github/workflows/build_reusable.yml @@ -0,0 +1,59 @@ +name: reusable-build +on: + workflow_call: + inputs: + release: + description: "Whether this is a release build" + type: boolean + default: false + outputs: + artifact-name: + description: "Name of the uploaded build artifact" + value: ${{ jobs.build.outputs.artifact-name }} + +jobs: + build: + name: Build Project + runs-on: ubuntu-latest + outputs: + artifact-name: ${{ steps.upload.outputs.artifact-name }} + + env: + MOD_IS_RELEASE: ${{ inputs.release }} + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 1 + + - name: Setup JDK + uses: actions/setup-java@v4 + with: + java-version: 25 + distribution: 'microsoft' + + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v4 + with: + gradle-version: wrapper + + - name: Cache Gradle packages + uses: actions/cache@v4 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} + restore-keys: | + ${{ runner.os }}-gradle- + + - name: Build with Gradle + run: ./gradlew buildAndCollect --no-daemon + + - name: Upload artifacts + id: upload + uses: actions/upload-artifact@v4 + with: + name: build-artifacts + path: build/libs/ diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..3f67850 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,215 @@ +name: release + +on: + push: + tags: + - '*' + +permissions: + contents: write + +jobs: + validate: + name: Validate Tag + runs-on: ubuntu-latest + permissions: + contents: write + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Validate tag matches version + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + TAG="${GITHUB_REF_NAME}" + VERSION=$(grep '^mod\.version' stonecutter.properties.toml | cut -d'"' -f2) + CHANNEL_TAG=$(grep '^mod\.channel_tag' stonecutter.properties.toml | cut -d'"' -f2) + FULL_VERSION="${VERSION}${CHANNEL_TAG}" + + echo "Validating tag '$TAG' against version '$FULL_VERSION'..." + if [[ "$TAG" != "$FULL_VERSION" ]]; then + echo "::error::Tag '$TAG' does not match version '$FULL_VERSION' in gradle.properties." + + if gh release view "$TAG" &>/dev/null; then + echo "Deleting GitHub release for $TAG..." + gh release delete "$TAG" -y + fi + + git push origin ":refs/tags/$TAG" + exit 1 + fi + echo "✓ Tag matches version." + + build: + needs: validate + uses: ./.github/workflows/build_reusable.yml + with: + release: true + + prepare: + name: Prepare Release + runs-on: ubuntu-latest + needs: build + permissions: + contents: write + + outputs: + publish_github: ${{ steps.check.outputs.publish_github }} + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Prepare git-cliff + run: | + REPO_URL="https://github.com/${{ github.repository }}" + sed -i "s|%%%repo_url%%%|${REPO_URL}|g" cliff.toml + + - name: Generate changelog with git-cliff + uses: orhun/git-cliff-action@v4 + with: + args: --latest --strip header --output CHANGELOG.md + version: 'v2.10.1' + + - name: Check GitHub releases toggle + id: check + run: | + if [[ "${{ vars.PUB_GITHUB_RELEASES }}" == "true" ]]; then + echo "publish_github=true" >> "$GITHUB_OUTPUT" + else + echo "publish_github=false" >> "$GITHUB_OUTPUT" + fi + + - name: Upload changelog + uses: actions/upload-artifact@v4 + with: + name: changelog + path: CHANGELOG.md + + github-release: + name: Release to GitHub + runs-on: ubuntu-latest + needs: prepare + if: needs.prepare.outputs.publish_github == 'true' + permissions: + contents: write + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Download build artifacts + uses: actions/download-artifact@v4 + with: + name: build-artifacts + path: build/libs/ + + - name: Download changelog + uses: actions/download-artifact@v4 + with: + name: changelog + path: . + + - name: Create or update GitHub release + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + TAG="${GITHUB_REF_NAME}" + echo "Creating or updating release for $TAG" + # Try to create the release first + if ! gh release create "$TAG" \ + --title "$TAG" \ + --notes-file CHANGELOG.md \ + --latest \ + --verify-tag \ + ./build/libs/$TAG/*; then + + echo "Release already exists. Updating existing release..." + + # Update release notes and mark as latest + gh release edit "$TAG" \ + --title "$TAG" \ + --notes-file CHANGELOG.md \ + --latest + + # Re-upload artifacts (delete first to avoid duplicates) + for FILE in ./build/libs/$TAG/*; do + NAME=$(basename "$FILE") + gh release delete-asset "$TAG" "$NAME" -y || true + gh release upload "$TAG" "$FILE" --clobber + done + fi + + platform-release: + name: Release to Platforms + runs-on: ubuntu-latest + needs: prepare + continue-on-error: true + + env: + MOD_IS_RELEASE: true + PUB_DRY_RUN: ${{ vars.PUB_DRY_RUN }} + PUB_MODS_ENABLE: ${{ vars.PUB_MODS_ENABLE }} + PUB_MAVEN_ENABLE: ${{ vars.PUB_MAVEN_ENABLE }} + PUB_MAVEN_CENTRAL_ENABLE: ${{ vars.PUB_MAVEN_CENTRAL_ENABLE }} + PUB_MAVEN_CENTRAL_SNAPSHOTS: ${{ vars.PUB_MAVEN_CENTRAL_SNAPSHOTS }} + PUB_MODRINTH_STAGING: ${{ vars.PUB_MODRINTH_STAGING }} + PUB_MODRINTH_PROJECT_ID: ${{ vars.PUB_MODRINTH_PROJECT_ID }} + PUB_CURSEFORGE_PROJECT_ID: ${{ vars.PUB_CURSEFORGE_PROJECT_ID }} + PUB_MODRINTH_TOKEN: ${{ secrets.PUB_MODRINTH_TOKEN }} + PUB_CURSEFORGE_TOKEN: ${{ secrets.PUB_CURSEFORGE_TOKEN }} + PUB_SIGNING_KEY: ${{ secrets.PUB_SIGNING_KEY }} + PUB_SIGNING_ID: ${{ secrets.PUB_SIGNING_ID }} + PUB_SIGNING_PASSWORD: ${{ secrets.PUB_SIGNING_PASSWORD }} + PUB_MAVEN_CENTRAL_USERNAME: ${{ secrets.PUB_MAVEN_CENTRAL_USERNAME }} + PUB_MAVEN_CENTRAL_PASSWORD: ${{ secrets.PUB_MAVEN_CENTRAL_PASSWORD }} + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 1 + + - name: Download build artifacts + uses: actions/download-artifact@v4 + with: + name: build-artifacts + path: build/libs/ + + - name: Download changelog + uses: actions/download-artifact@v4 + with: + name: changelog + path: . + + - name: Setup JDK + uses: actions/setup-java@v4 + with: + java-version: 21 + distribution: 'microsoft' + + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v4 + with: + gradle-version: wrapper + + - name: Cache Gradle packages + uses: actions/cache@v4 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} + restore-keys: | + ${{ runner.os }}-gradle- + + - name: Publish to Platforms + run: ./gradlew publishMods --stacktrace From d05a542d669e3494d708f5c2af3e8d0f96f937fd Mon Sep 17 00:00:00 2001 From: Dominic Seung Date: Tue, 7 Jul 2026 23:00:06 -0700 Subject: [PATCH 09/15] stop building twice --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 14bfb4e..0c25503 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,5 +1,5 @@ name: build -on: [ push, pull_request ] +on: push jobs: build: From 6967c3e0cd549faa3c8860dc50e2ba7d103a6565 Mon Sep 17 00:00:00 2001 From: Dominic Seung Date: Tue, 7 Jul 2026 23:06:17 -0700 Subject: [PATCH 10/15] update all github actions to latest --- .github/workflows/build_reusable.yml | 10 +++++----- .github/workflows/release.yml | 26 +++++++++++++------------- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/.github/workflows/build_reusable.yml b/.github/workflows/build_reusable.yml index 8ae6a71..3314b3d 100644 --- a/.github/workflows/build_reusable.yml +++ b/.github/workflows/build_reusable.yml @@ -23,23 +23,23 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@latest with: fetch-depth: 1 - name: Setup JDK - uses: actions/setup-java@v4 + uses: actions/setup-java@latest with: java-version: 25 distribution: 'microsoft' - name: Setup Gradle - uses: gradle/actions/setup-gradle@v4 + uses: gradle/actions/setup-gradle@latest with: gradle-version: wrapper - name: Cache Gradle packages - uses: actions/cache@v4 + uses: actions/cache@latest with: path: | ~/.gradle/caches @@ -53,7 +53,7 @@ jobs: - name: Upload artifacts id: upload - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@latest with: name: build-artifacts path: build/libs/ diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 3f67850..b0feb29 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -17,7 +17,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@latest with: fetch-depth: 0 @@ -62,7 +62,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@latest with: fetch-depth: 0 @@ -72,7 +72,7 @@ jobs: sed -i "s|%%%repo_url%%%|${REPO_URL}|g" cliff.toml - name: Generate changelog with git-cliff - uses: orhun/git-cliff-action@v4 + uses: orhun/git-cliff-action@latest with: args: --latest --strip header --output CHANGELOG.md version: 'v2.10.1' @@ -87,7 +87,7 @@ jobs: fi - name: Upload changelog - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@latest with: name: changelog path: CHANGELOG.md @@ -102,18 +102,18 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@latest with: fetch-depth: 0 - name: Download build artifacts - uses: actions/download-artifact@v4 + uses: actions/download-artifact@latest with: name: build-artifacts path: build/libs/ - name: Download changelog - uses: actions/download-artifact@v4 + uses: actions/download-artifact@latest with: name: changelog path: . @@ -174,35 +174,35 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@latest with: fetch-depth: 1 - name: Download build artifacts - uses: actions/download-artifact@v4 + uses: actions/download-artifact@latest with: name: build-artifacts path: build/libs/ - name: Download changelog - uses: actions/download-artifact@v4 + uses: actions/download-artifact@latest with: name: changelog path: . - name: Setup JDK - uses: actions/setup-java@v4 + uses: actions/setup-java@latest with: java-version: 21 distribution: 'microsoft' - name: Setup Gradle - uses: gradle/actions/setup-gradle@v4 + uses: gradle/actions/setup-gradle@latest with: gradle-version: wrapper - name: Cache Gradle packages - uses: actions/cache@v4 + uses: actions/cache@latest with: path: | ~/.gradle/caches From e895d5d6674acd4e68516c5d07bf100ff968ca3f Mon Sep 17 00:00:00 2001 From: Dominic Seung Date: Tue, 7 Jul 2026 23:08:48 -0700 Subject: [PATCH 11/15] consolidate build.yml --- .github/workflows/build.yml | 39 ++++- .github/workflows/build_reusable.yml | 59 -------- .github/workflows/release.yml | 215 --------------------------- 3 files changed, 38 insertions(+), 275 deletions(-) delete mode 100644 .github/workflows/build_reusable.yml delete mode 100644 .github/workflows/release.yml diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 0c25503..7563e63 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -3,4 +3,41 @@ on: push jobs: build: - uses: ./.github/workflows/build_reusable.yml + name: Build Project + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@latest + with: + fetch-depth: 1 + + - name: Setup JDK + uses: actions/setup-java@latest + with: + java-version: 25 + distribution: 'microsoft' + + - name: Setup Gradle + uses: gradle/actions/setup-gradle@latest + with: + gradle-version: wrapper + + - name: Cache Gradle packages + uses: actions/cache@latest + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} + restore-keys: | + ${{ runner.os }}-gradle- + + - name: Build with Gradle + run: ./gradlew buildAndCollect --no-daemon + + - name: Upload artifacts + uses: actions/upload-artifact@latest + with: + name: build-artifacts + path: build/libs/ diff --git a/.github/workflows/build_reusable.yml b/.github/workflows/build_reusable.yml deleted file mode 100644 index 3314b3d..0000000 --- a/.github/workflows/build_reusable.yml +++ /dev/null @@ -1,59 +0,0 @@ -name: reusable-build -on: - workflow_call: - inputs: - release: - description: "Whether this is a release build" - type: boolean - default: false - outputs: - artifact-name: - description: "Name of the uploaded build artifact" - value: ${{ jobs.build.outputs.artifact-name }} - -jobs: - build: - name: Build Project - runs-on: ubuntu-latest - outputs: - artifact-name: ${{ steps.upload.outputs.artifact-name }} - - env: - MOD_IS_RELEASE: ${{ inputs.release }} - - steps: - - name: Checkout repository - uses: actions/checkout@latest - with: - fetch-depth: 1 - - - name: Setup JDK - uses: actions/setup-java@latest - with: - java-version: 25 - distribution: 'microsoft' - - - name: Setup Gradle - uses: gradle/actions/setup-gradle@latest - with: - gradle-version: wrapper - - - name: Cache Gradle packages - uses: actions/cache@latest - with: - path: | - ~/.gradle/caches - ~/.gradle/wrapper - key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} - restore-keys: | - ${{ runner.os }}-gradle- - - - name: Build with Gradle - run: ./gradlew buildAndCollect --no-daemon - - - name: Upload artifacts - id: upload - uses: actions/upload-artifact@latest - with: - name: build-artifacts - path: build/libs/ diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml deleted file mode 100644 index b0feb29..0000000 --- a/.github/workflows/release.yml +++ /dev/null @@ -1,215 +0,0 @@ -name: release - -on: - push: - tags: - - '*' - -permissions: - contents: write - -jobs: - validate: - name: Validate Tag - runs-on: ubuntu-latest - permissions: - contents: write - - steps: - - name: Checkout repository - uses: actions/checkout@latest - with: - fetch-depth: 0 - - - name: Validate tag matches version - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - TAG="${GITHUB_REF_NAME}" - VERSION=$(grep '^mod\.version' stonecutter.properties.toml | cut -d'"' -f2) - CHANNEL_TAG=$(grep '^mod\.channel_tag' stonecutter.properties.toml | cut -d'"' -f2) - FULL_VERSION="${VERSION}${CHANNEL_TAG}" - - echo "Validating tag '$TAG' against version '$FULL_VERSION'..." - if [[ "$TAG" != "$FULL_VERSION" ]]; then - echo "::error::Tag '$TAG' does not match version '$FULL_VERSION' in gradle.properties." - - if gh release view "$TAG" &>/dev/null; then - echo "Deleting GitHub release for $TAG..." - gh release delete "$TAG" -y - fi - - git push origin ":refs/tags/$TAG" - exit 1 - fi - echo "✓ Tag matches version." - - build: - needs: validate - uses: ./.github/workflows/build_reusable.yml - with: - release: true - - prepare: - name: Prepare Release - runs-on: ubuntu-latest - needs: build - permissions: - contents: write - - outputs: - publish_github: ${{ steps.check.outputs.publish_github }} - - steps: - - name: Checkout repository - uses: actions/checkout@latest - with: - fetch-depth: 0 - - - name: Prepare git-cliff - run: | - REPO_URL="https://github.com/${{ github.repository }}" - sed -i "s|%%%repo_url%%%|${REPO_URL}|g" cliff.toml - - - name: Generate changelog with git-cliff - uses: orhun/git-cliff-action@latest - with: - args: --latest --strip header --output CHANGELOG.md - version: 'v2.10.1' - - - name: Check GitHub releases toggle - id: check - run: | - if [[ "${{ vars.PUB_GITHUB_RELEASES }}" == "true" ]]; then - echo "publish_github=true" >> "$GITHUB_OUTPUT" - else - echo "publish_github=false" >> "$GITHUB_OUTPUT" - fi - - - name: Upload changelog - uses: actions/upload-artifact@latest - with: - name: changelog - path: CHANGELOG.md - - github-release: - name: Release to GitHub - runs-on: ubuntu-latest - needs: prepare - if: needs.prepare.outputs.publish_github == 'true' - permissions: - contents: write - - steps: - - name: Checkout repository - uses: actions/checkout@latest - with: - fetch-depth: 0 - - - name: Download build artifacts - uses: actions/download-artifact@latest - with: - name: build-artifacts - path: build/libs/ - - - name: Download changelog - uses: actions/download-artifact@latest - with: - name: changelog - path: . - - - name: Create or update GitHub release - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - TAG="${GITHUB_REF_NAME}" - echo "Creating or updating release for $TAG" - # Try to create the release first - if ! gh release create "$TAG" \ - --title "$TAG" \ - --notes-file CHANGELOG.md \ - --latest \ - --verify-tag \ - ./build/libs/$TAG/*; then - - echo "Release already exists. Updating existing release..." - - # Update release notes and mark as latest - gh release edit "$TAG" \ - --title "$TAG" \ - --notes-file CHANGELOG.md \ - --latest - - # Re-upload artifacts (delete first to avoid duplicates) - for FILE in ./build/libs/$TAG/*; do - NAME=$(basename "$FILE") - gh release delete-asset "$TAG" "$NAME" -y || true - gh release upload "$TAG" "$FILE" --clobber - done - fi - - platform-release: - name: Release to Platforms - runs-on: ubuntu-latest - needs: prepare - continue-on-error: true - - env: - MOD_IS_RELEASE: true - PUB_DRY_RUN: ${{ vars.PUB_DRY_RUN }} - PUB_MODS_ENABLE: ${{ vars.PUB_MODS_ENABLE }} - PUB_MAVEN_ENABLE: ${{ vars.PUB_MAVEN_ENABLE }} - PUB_MAVEN_CENTRAL_ENABLE: ${{ vars.PUB_MAVEN_CENTRAL_ENABLE }} - PUB_MAVEN_CENTRAL_SNAPSHOTS: ${{ vars.PUB_MAVEN_CENTRAL_SNAPSHOTS }} - PUB_MODRINTH_STAGING: ${{ vars.PUB_MODRINTH_STAGING }} - PUB_MODRINTH_PROJECT_ID: ${{ vars.PUB_MODRINTH_PROJECT_ID }} - PUB_CURSEFORGE_PROJECT_ID: ${{ vars.PUB_CURSEFORGE_PROJECT_ID }} - PUB_MODRINTH_TOKEN: ${{ secrets.PUB_MODRINTH_TOKEN }} - PUB_CURSEFORGE_TOKEN: ${{ secrets.PUB_CURSEFORGE_TOKEN }} - PUB_SIGNING_KEY: ${{ secrets.PUB_SIGNING_KEY }} - PUB_SIGNING_ID: ${{ secrets.PUB_SIGNING_ID }} - PUB_SIGNING_PASSWORD: ${{ secrets.PUB_SIGNING_PASSWORD }} - PUB_MAVEN_CENTRAL_USERNAME: ${{ secrets.PUB_MAVEN_CENTRAL_USERNAME }} - PUB_MAVEN_CENTRAL_PASSWORD: ${{ secrets.PUB_MAVEN_CENTRAL_PASSWORD }} - - steps: - - name: Checkout repository - uses: actions/checkout@latest - with: - fetch-depth: 1 - - - name: Download build artifacts - uses: actions/download-artifact@latest - with: - name: build-artifacts - path: build/libs/ - - - name: Download changelog - uses: actions/download-artifact@latest - with: - name: changelog - path: . - - - name: Setup JDK - uses: actions/setup-java@latest - with: - java-version: 21 - distribution: 'microsoft' - - - name: Setup Gradle - uses: gradle/actions/setup-gradle@latest - with: - gradle-version: wrapper - - - name: Cache Gradle packages - uses: actions/cache@latest - with: - path: | - ~/.gradle/caches - ~/.gradle/wrapper - key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} - restore-keys: | - ${{ runner.os }}-gradle- - - - name: Publish to Platforms - run: ./gradlew publishMods --stacktrace From 301610ac6a15002a4819be161f218d05f57a2e72 Mon Sep 17 00:00:00 2001 From: Dominic Seung Date: Tue, 7 Jul 2026 23:10:17 -0700 Subject: [PATCH 12/15] use latest version of each action --- .github/workflows/build.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 7563e63..d1254f5 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -8,23 +8,23 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@latest + uses: actions/checkout@v7 with: fetch-depth: 1 - name: Setup JDK - uses: actions/setup-java@latest + uses: actions/setup-java@v5 with: java-version: 25 distribution: 'microsoft' - name: Setup Gradle - uses: gradle/actions/setup-gradle@latest + uses: gradle/actions/setup-gradle@v6 with: gradle-version: wrapper - name: Cache Gradle packages - uses: actions/cache@latest + uses: actions/cache@v6 with: path: | ~/.gradle/caches @@ -37,7 +37,7 @@ jobs: run: ./gradlew buildAndCollect --no-daemon - name: Upload artifacts - uses: actions/upload-artifact@latest + uses: actions/upload-artifact@v7 with: name: build-artifacts path: build/libs/ From ce382e501af5838156612e51b9ad39850746c856 Mon Sep 17 00:00:00 2001 From: Dominic Seung Date: Tue, 7 Jul 2026 23:13:34 -0700 Subject: [PATCH 13/15] fix gitignore and remove more useless comments --- .gitignore | 5 +---- src/main/java/dsns/betterhud/BetterHUD.java | 4 ---- 2 files changed, 1 insertion(+), 8 deletions(-) diff --git a/.gitignore b/.gitignore index 911b5a3..6c2c554 100644 --- a/.gitignore +++ b/.gitignore @@ -20,10 +20,7 @@ bin/ run/ -versions/*/.gradle/ -versions/*/build/ -versions/*/run/ -versions/*/src/ +versions/ *.stonecutter.tmp hs_err_*.log diff --git a/src/main/java/dsns/betterhud/BetterHUD.java b/src/main/java/dsns/betterhud/BetterHUD.java index 39bd452..9d2ffeb 100644 --- a/src/main/java/dsns/betterhud/BetterHUD.java +++ b/src/main/java/dsns/betterhud/BetterHUD.java @@ -23,10 +23,6 @@ @Environment(EnvType.CLIENT) public class BetterHUD implements ClientModInitializer { - - // This logger is used to write text to the console and the log file. - // It is considered best practice to use your mod id as the logger's name. - // That way, it's clear which mod wrote info, warnings, and errors. public static final Logger LOGGER = LoggerFactory.getLogger("betterhud"); public static ArrayList mods = new ArrayList<>( From bb059df8239fe71e41ac3781261524ea63f32143 Mon Sep 17 00:00:00 2001 From: Dominic Seung Date: Tue, 7 Jul 2026 23:17:19 -0700 Subject: [PATCH 14/15] remove useless name --- .github/workflows/build.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index d1254f5..ac303f8 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -3,9 +3,7 @@ on: push jobs: build: - name: Build Project runs-on: ubuntu-latest - steps: - name: Checkout repository uses: actions/checkout@v7 From 2964dea353d25f34185d3c9a9add6562180966d3 Mon Sep 17 00:00:00 2001 From: Dominic Seung Date: Tue, 7 Jul 2026 23:48:36 -0700 Subject: [PATCH 15/15] get gametest working --- build.gradle.kts | 10 +++++++ settings.gradle.kts | 3 ++- .../gametest/BetterHudClientGameTest.java | 26 +++++++++++++++++++ src/gametest/resources/fabric.mod.json | 16 ++++++++++++ stonecutter.properties.toml | 8 +++--- 5 files changed, 58 insertions(+), 5 deletions(-) create mode 100644 src/gametest/java/dsns/betterhud/gametest/BetterHudClientGameTest.java create mode 100644 src/gametest/resources/fabric.mod.json diff --git a/build.gradle.kts b/build.gradle.kts index eadcc70..66c2804 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -34,6 +34,16 @@ repositories { maven("https://maven.terraformersmc.com/releases/") } +fabricApi { + configureTests { + createSourceSet = false + modId = "betterhud-gametest" + enableGameTests = false + enableClientGameTests = true + eula = true + } +} + dependencies { /** * Fetches only the required Fabric API modules to not waste time downloading all of them for each version. diff --git a/settings.gradle.kts b/settings.gradle.kts index aaaf7b2..f9f33c3 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -24,7 +24,8 @@ plugins { stonecutter { create(rootProject) { // See https://stonecutter.kikugie.dev/wiki/start/#choosing-minecraft-versions - versions("1.21", "1.21.6") + versions("1.21.4") + versions("1.21.6") version("26.1.x", "26.1") version("26.2.x", "26.2") vcsVersion = "26.2.x" diff --git a/src/gametest/java/dsns/betterhud/gametest/BetterHudClientGameTest.java b/src/gametest/java/dsns/betterhud/gametest/BetterHudClientGameTest.java new file mode 100644 index 0000000..5c2a537 --- /dev/null +++ b/src/gametest/java/dsns/betterhud/gametest/BetterHudClientGameTest.java @@ -0,0 +1,26 @@ +package dsns.betterhud.gametest; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.file.Files; +import java.nio.file.Path; + +import net.fabricmc.fabric.api.client.gametest.v1.FabricClientGameTest; +import net.fabricmc.fabric.api.client.gametest.v1.context.ClientGameTestContext; +import net.fabricmc.fabric.api.client.gametest.v1.context.TestSingleplayerContext; +import net.minecraft.client.gui.screens.worldselection.WorldCreationUiState; + +@SuppressWarnings("UnstableApiUsage") +public class BetterHudClientGameTest implements FabricClientGameTest { + @Override + public void runTest(ClientGameTestContext context) { + TestSingleplayerContext singleplayer = context.worldBuilder() + .adjustSettings(creator -> creator.setGameMode(WorldCreationUiState.SelectedGameMode.SURVIVAL)) + .create(); + + singleplayer.getClientLevel().waitForChunksRender(); + + context.waitTicks(40); + context.takeScreenshot("betterhud-survival-world"); + } +} diff --git a/src/gametest/resources/fabric.mod.json b/src/gametest/resources/fabric.mod.json new file mode 100644 index 0000000..e432019 --- /dev/null +++ b/src/gametest/resources/fabric.mod.json @@ -0,0 +1,16 @@ +{ + "schemaVersion": 1, + "id": "betterhud-gametest", + "version": "1.0.0", + "name": "BetterHUD GameTest", + "description": "Client gametests for BetterHUD.", + "license": "AGPL-3.0", + "environment": "client", + "entrypoints": { + "fabric-client-gametest": ["dsns.betterhud.gametest.BetterHudClientGameTest"] + }, + "depends": { + "betterhud": "*", + "fabric-client-gametest-api-v1": "*" + } +} diff --git a/stonecutter.properties.toml b/stonecutter.properties.toml index ae91cd5..5430246 100644 --- a/stonecutter.properties.toml +++ b/stonecutter.properties.toml @@ -7,14 +7,14 @@ mod.version = "2.1.1" deps.fabric_loader = "0.19.3" loomx.loom_version = "1.17-SNAPSHOT" -["1.21"] +["1.21.4"] mod.mc_compat = ">=1.21 <=1.21.5" mod.mc_releases = ["1.21", "1.21.1", "1.21.2", "1.21.3", "1.21.4", "1.21.5"] deps.fabric_loader = "0.17.3" -deps.fabric_api = "0.102.0+1.21" -deps.modmenu = "14.0.2" -deps.cloth_config = "19.0.147" +deps.fabric_api = "0.119.4+1.21.4" +deps.modmenu = "13.0.4" +deps.cloth_config = "17.0.144" ["1.21.6"] mod.mc_compat = ">=1.21.6 <1.22"