diff --git a/.github/scripts/generate-matrices.sh b/.github/scripts/generate-matrices.sh deleted file mode 100755 index ddf6d18..0000000 --- a/.github/scripts/generate-matrices.sh +++ /dev/null @@ -1,48 +0,0 @@ -#!/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 - -java_for_variant() { - grep -oP '^java_version=\K.+' "versions/$1/gradle.properties" -} - -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") - -build="[]" -publish="[]" -while read -r variant; do - java="$(java_for_variant "$variant")" - 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")" - 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") - -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 8fcafe1..0000000 --- a/.github/scripts/generate-test-world.sh +++ /dev/null @@ -1,62 +0,0 @@ -#!/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" - -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 -# 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 -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 cf77ee5..0000000 --- a/.github/scripts/publish-screenshots.sh +++ /dev/null @@ -1,80 +0,0 @@ -#!/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 - -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) - -# --- 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 -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 - -# --- 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 - 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" - -# 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" - 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 index 20256d5..ac303f8 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,241 +1,41 @@ -# 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 - 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 from supported-versions.json - 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) }} + runs-on: ubuntu-latest 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 + - name: Checkout repository + uses: actions/checkout@v7 with: - 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 - 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 + fetch-depth: 1 - - name: setup jdk ${{ matrix.java }} + - name: Setup JDK uses: actions/setup-java@v5 with: - java-version: ${{ matrix.java }} + java-version: 25 distribution: 'microsoft' - - name: make gradle wrapper executable - 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 - - - name: check survival world screenshot was taken - run: test -n "$(find 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 + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v6 with: - name: launch-screenshots-mc${{ matrix.mc }} - path: launchtest/run/screenshots/ - if-no-files-found: ignore + gradle-version: wrapper - - name: upload game logs - if: always() - uses: actions/upload-artifact@v7 + - name: Cache Gradle packages + uses: actions/cache@v6 with: - name: launch-logs-mc${{ matrix.mc }} - path: 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 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 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`; - }); - let rows = ''; - for (let i = 0; i < cells.length; i += 4) { - 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, 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.`, - '', - `\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({ - 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; - } - - // 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, - }); - 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, - }); - } - - # 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 - 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 + path: | + ~/.gradle/caches + ~/.gradle/wrapper + key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} + restore-keys: | + ${{ runner.os }}-gradle- - - 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: Build with Gradle + run: ./gradlew buildAndCollect --no-daemon - - name: upload modrinth bundle + - name: Upload artifacts uses: actions/upload-artifact@v7 with: - name: modrinth-upload - path: build/modrinth/ + name: build-artifacts + path: build/libs/ diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml deleted file mode 100644 index 9ab1350..0000000 --- a/.github/workflows/publish.yml +++ /dev/null @@ -1,147 +0,0 @@ -# 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: - 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 - -# Never let two release runs interleave their uploads. -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 from supported-versions.json - 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 - # 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 }} - 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: - # 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: - 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: - # 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 - 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/.gitignore b/.gitignore index 8c52ed7..6c2c554 100644 --- a/.gitignore +++ b/.gitignore @@ -1,47 +1,28 @@ -# 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/ +versions/ *.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 deleted file mode 100644 index 9972fe4..0000000 --- a/build.gradle +++ /dev/null @@ -1,123 +0,0 @@ -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+ - -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}" - -version = "${project.mod_version}+mc${mcVersion}" -group = project.maven_group - -base { - archivesName = project.archives_base_name -} - -repositories { - maven { url 'https://maven.shedaniel.me/' } - 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')) { - exclude group: 'org.ow2.asm' - } - } -} - -// 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}" - - 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 : ">=${javaVersionInt}", - javaCompat : mixinJavaCompat, - dependsExtra : dependsExtra - ] - inputs.properties expansion - - filesMatching('fabric.mod.json') { - expand expansion - } - filesMatching('betterhud.mixins.json') { - expand expansion - } -} - -tasks.withType(JavaCompile).configureEach { - it.options.release = javaVersionInt -} - -java { - withSourcesJar() - sourceCompatibility = javaLang - targetCompatibility = javaLang - toolchain { - languageVersion = JavaLanguageVersion.of(javaVersionInt) - } -} - -jar { - def archiveName = base.archivesName.get() - from('../../LICENSE') { - rename { "${it}_${archiveName}" } - } -} - -publishing { - publications { - create('mavenJava', MavenPublication) { - artifactId = "${project.archives_base_name}-mc${mcVersion}" - from components.java - } - } - repositories { - // Add publish repos here if/when needed. - } -} diff --git a/build.gradle.kts b/build.gradle.kts new file mode 100644 index 0000000..66c2804 --- /dev/null +++ b/build.gradle.kts @@ -0,0 +1,125 @@ +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/") +} + +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. + * @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 0ce008e..16f1881 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,18 +1,4 @@ -# Done to increase the memory available to gradle. -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 - -# 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 +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/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 deleted file mode 100644 index 9b53c5b..0000000 --- a/settings.gradle +++ /dev/null @@ -1,48 +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' -} - -// 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() - -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' diff --git a/settings.gradle.kts b/settings.gradle.kts new file mode 100644 index 0000000..f9f33c3 --- /dev/null +++ b/settings.gradle.kts @@ -0,0 +1,35 @@ +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.4") + versions("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/gametest/java/dsns/betterhud/gametest/BetterHudClientGameTest.java b/src/gametest/java/dsns/betterhud/gametest/BetterHudClientGameTest.java index 95c29ac..5c2a537 100644 --- a/src/gametest/java/dsns/betterhud/gametest/BetterHudClientGameTest.java +++ b/src/gametest/java/dsns/betterhud/gametest/BetterHudClientGameTest.java @@ -14,26 +14,13 @@ public class BetterHudClientGameTest implements FabricClientGameTest { @Override public void runTest(ClientGameTestContext context) { - assertScreenshotSaved(context.takeScreenshot("betterhud-title-screen")); + TestSingleplayerContext singleplayer = context.worldBuilder() + .adjustSettings(creator -> creator.setGameMode(WorldCreationUiState.SelectedGameMode.SURVIVAL)) + .create(); - try (TestSingleplayerContext singleplayer = context.worldBuilder() - .adjustSettings(creator -> creator.setGameMode(WorldCreationUiState.SelectedGameMode.SURVIVAL)) - .create()) { - singleplayer.getClientLevel().waitForChunksRender(); + 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); - } + context.waitTicks(40); + context.takeScreenshot("betterhud-survival-world"); } } 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<>( 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 755ca07..0000000 --- a/stonecutter.gradle +++ /dev/null @@ -1,65 +0,0 @@ -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") } - - doLast { - def outDir = file('build/modrinth') - outDir.deleteDir() - outDir.mkdirs() - - def rows = [] - gameVersionsByVariant.each { variant, gameVersions -> - 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()) - rows << "| `${jar.name}` | ${gameVersions.join(', ')} |" - } - - 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', - '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..5430246 --- /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.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.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" +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/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 deleted file mode 100644 index e3951b0..0000000 --- a/versions/1.21.6/gradle.properties +++ /dev/null @@ -1,13 +0,0 @@ -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 deleted file mode 100644 index c84e2bf..0000000 --- a/versions/1.21/gradle.properties +++ /dev/null @@ -1,13 +0,0 @@ -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 deleted file mode 100644 index dfd7869..0000000 --- a/versions/26.1/gradle.properties +++ /dev/null @@ -1,12 +0,0 @@ -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 deleted file mode 100644 index d5bee7a..0000000 --- a/versions/26.2/gradle.properties +++ /dev/null @@ -1,12 +0,0 @@ -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