diff --git a/.github/scripts/generate-matrices.sh b/.github/scripts/generate-matrices.sh index ddf6d18..f5f3926 100755 --- a/.github/scripts/generate-matrices.sh +++ b/.github/scripts/generate-matrices.sh @@ -1,44 +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, pregen_world } -# - 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"). 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 gametest; 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") +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 8fcafe1..99ae0cb 100755 --- a/.github/scripts/generate-test-world.sh +++ b/.github/scripts/generate-test-world.sh @@ -1,17 +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. 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" @@ -22,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) matches the gametest-based runs. 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..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 and /mc--title.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 @@ -28,18 +18,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)" @@ -49,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 @@ -68,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 20256d5..f362b7c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,20 +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, enters a survival singleplayer 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. -# -# Both job matrices are generated from supported-versions.json — add a version -# there (see README) 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 @@ -30,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 @@ -67,14 +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. - # - # 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 @@ -101,26 +79,23 @@ 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 - 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)" + 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 @@ -128,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: @@ -151,10 +126,16 @@ 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}`); @@ -170,12 +151,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. Title-screen screenshots and game logs are in the run artifacts.`, + `# BetterHUD Launch Screenshots`, '', `\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({ @@ -189,7 +169,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, @@ -207,9 +186,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 deleted file mode 100644 index 33cd133..0000000 --- a/DEVELOPMENT.md +++ /dev/null @@ -1,42 +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 a range of -Minecraft versions. - -### Adding a new Minecraft version - -[`supported-versions.json`](supported-versions.json) is the source of truth for every supported 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). - - `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=` - -### 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 exactly which -Minecraft versions to select for each jar. 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 49d52e8..0000000 --- a/launchtest/build.gradle +++ /dev/null @@ -1,167 +0,0 @@ -// Production client 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, 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 -// -// 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. -// -// 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. -// --------------------------------------------------------------------------- -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(', ')}") -} - -// 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. -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/' } -} - -// Matches the variant builds: MC 26+ rejects ASM coming from annotation processors. -if (!isLegacy) { - configurations.configureEach { - if (name.toLowerCase().contains('annotationprocessor')) { - exclude group: 'org.ow2.asm' - } - } -} - -// 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}" - - if (isLegacy) { - 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. - 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 { - it.options.release = javaVersionInt -} - -java { - toolchain { - languageVersion = JavaLanguageVersion.of(javaVersionInt) - } -} - -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). -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) { - 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') - } - - 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.) - 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/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/settings.gradle b/settings.gradle index 9b53c5b..fcab7ea 100644 --- a/settings.gradle +++ b/settings.gradle @@ -21,28 +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: 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. include 'launchtest' +project(':launchtest').projectDir = file('src/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/src/launchtest/build.gradle b/src/launchtest/build.gradle new file mode 100644 index 0000000..1c4d203 --- /dev/null +++ b/src/launchtest/build.gradle @@ -0,0 +1,111 @@ +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 new file mode 100644 index 0000000..5d206ed --- /dev/null +++ b/src/launchtest/src/main/java/dsns/betterhud/launchtest/LaunchTestClient.java @@ -0,0 +1,83 @@ +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 new file mode 100644 index 0000000..3cc81a5 --- /dev/null +++ b/src/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/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 6ef4606..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", "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 } -} 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