Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
2585e10
Drop support for versions before 26.1
Moggla Sep 20, 2026
411a90b
Add help dialog
Moggla Sep 20, 2026
fd0cca4
Add settings dialog
Moggla Sep 20, 2026
644699b
Remove text based settings menu
Moggla Sep 20, 2026
4107a31
Remove deprecated overlay formats key
Moggla Sep 20, 2026
816d428
Add toggle button and settings tooltip to help dialog
Moggla Sep 20, 2026
9d60c24
Rebuild the settings as a trigger based button menu
Moggla Sep 20, 2026
9de4a18
Update README screenshots and world options steps
Moggla Sep 20, 2026
490035f
Mention F3+F6 looking_at_block_state in player-placed trees tooltip
Moggla Sep 20, 2026
1cf5882
Add PackTest CI workflows and first test
Moggla Sep 20, 2026
b7c467b
Update README
Moggla Sep 21, 2026
be63cc7
Build tests on real worldgen trees with shared helpers
Moggla Sep 21, 2026
e4f275b
Isolate tests with an arena template and per-test dummies
Moggla Sep 21, 2026
bf3292c
Wait for the datapack to finish instead of a fixed delay
Moggla Sep 21, 2026
cdcba58
Add chop tests for 15 vanilla tree types
Moggla Sep 21, 2026
f4f528e
Test mangroves with a finder for the lowest log and warnings for know…
Moggla Sep 21, 2026
78f95d5
Add tests for nether fungi and huge mushrooms
Moggla Sep 21, 2026
0a85c4a
Add negative tests: structures the datapack must not chop
Moggla Sep 21, 2026
c33531a
Restore the persistent check for leaves
Moggla Sep 21, 2026
6f45ebb
Make the "not chopped" checks able to fail
Moggla Sep 21, 2026
29f8b83
Add settings tests, one test environment per setting
Moggla Sep 21, 2026
60f4196
Run the fungus tests in their own environment, test chop_fungi off
Moggla Sep 21, 2026
e958176
Test slow chop, its interval and its blocks per chop
Moggla Sep 21, 2026
00e7fd1
Test destroy_roots and start_chop_on_root on a hand built mangrove
Moggla Sep 21, 2026
ad7d05c
Test plant_sapling, drop_loot and stop_chopping, spread the tests ove…
Moggla Sep 21, 2026
af5d280
Test the settings menu: flips, guards, admins, sliders, player toggle
Moggla Sep 21, 2026
9fd0354
Test that uninstall removes every scoreboard and trigger of the datapack
Moggla Sep 21, 2026
c4755d8
Clear the schedules and remove the stored data in uninstall
Moggla Sep 21, 2026
43c8ddf
Make the uninstall check fail for storages and schedules too
Moggla Sep 21, 2026
ebcf65f
Read Unbreaking through the enchantments component without "levels"
Moggla Sep 21, 2026
9ff3a2d
Test the enchantments: Silk Touch and Unbreaking
Moggla Sep 21, 2026
3da68a8
Keep LF line endings in the repository
Moggla Sep 21, 2026
99eb40e
Don't let the uninstall test pass without its generated check
Moggla Sep 21, 2026
48a4a8d
Add CI badges, the one of the weekly check can be orange
Moggla Sep 21, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
16 changes: 16 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# Text files are stored with LF in the repository and checked out with LF, whatever the local Git setting (core.autocrlf on Windows)
# or the editor is. The shell scripts run under Linux, WSL and Git Bash, a CR at the end of a line breaks them, and Minecraft reads LF just fine.
* text=auto
*.sh text eol=lf
*.py text eol=lf
*.yml text eol=lf
*.md text eol=lf
*.mcfunction text eol=lf
*.json text eol=lf
*.mcmeta text eol=lf

# never touch these
*.png binary
*.gif binary
*.pdn binary
*.nbt binary
Binary file modified .github/resources/help.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified .github/resources/settings1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified .github/resources/settings2.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
92 changes: 92 additions & 0 deletions .github/scripts/gen-uninstall-check.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
#!/usr/bin/env python3
"""Generates the checks of the uninstall test from the code of the datapack, so nobody has to keep a list.

Usage: gen-uninstall-check.py <repo root> <folder of the test datapack in the world, the one that contains data/>

Writes two functions into <folder>/data/timber_test/function/generated/:
- uninstall_prepare: puts something into every storage key the datapack writes to, so that the removal is something to check.
- uninstall_objectives: runs after timber:uninstall and FAILS, with the names, if something of the datapack is left:
* a scoreboard objective (triggers are objectives) that any function creates with "scoreboard objectives add"
* a storage key that any function writes to (storage timber:x key)
* a scheduled function (schedule function) that uninstall doesn't clear with schedule clear. A schedule can't be looked
at from a function, so that one is compared with the code of uninstall.mcfunction.
A new scoreboard, storage key or schedule that is added to the datapack and forgotten in uninstall makes the test fail.
"""
import os
import re
import sys

repo, dest = sys.argv[1], sys.argv[2]

# 121/ holds the overlay of the newest pack format, both are scanned
folders = [os.path.join(repo, "data", "timber", "function"), os.path.join(repo, "121", "data", "timber", "function")]
uninstall_text = ""
objectives, schedules, storage_keys = set(), set(), set()
for folder in folders:
for root, _, files in os.walk(folder):
for name in files:
if not name.endswith(".mcfunction"):
continue
path = os.path.join(root, name)
text = open(path, encoding="utf-8", errors="replace").read()
if os.path.relpath(path, folder).replace(os.sep, "/") == "uninstall.mcfunction":
uninstall_text += text + "\n"
continue
objectives.update(re.findall(r"scoreboard objectives add (\S+)", text))
schedules.update(re.findall(r"schedule function (timber:[\w/]+)", text))
# storage timber:settings dialog.inputs[0].initial -> (timber:settings, dialog), the top level key is what gets removed
storage_keys.update(re.findall(r"storage (timber:\w+) (\w+)", text))

if not objectives:
sys.exit("gen-uninstall-check: no 'scoreboard objectives add' found in " + str(folders) + ", is the path right?")

out = os.path.join(dest, "data", "timber_test", "function", "generated")
os.makedirs(out, exist_ok=True)


def write(name, lines):
with open(os.path.join(out, name), "w", newline="\n") as f:
f.write("\n".join(lines) + "\n")


HEADER = "#> GENERATED by .github/scripts/gen-uninstall-check.py from the code of the datapack, do not edit."

# --- before the uninstall: something in every storage key
# The first line of both functions sets a marker that the test checks: PackTest doesn't complain about a function that doesn't
# exist, without the marker a missing or broken generation step would leave a test that checks nothing and passes.
write("uninstall_prepare.mcfunction", [HEADER, "#> Puts a value into every storage key the datapack writes to.",
"scoreboard objectives add timber_test dummy", "scoreboard players set #uninstall_prepare_ran timber_test 1"]
+ [f"data modify storage {sid} {key} set value 1" for sid, key in sorted(storage_keys)])

# --- after the uninstall
lines = [
HEADER,
"#> Fails if anything of the datapack is left after timber:uninstall, the names are in the message.",
"scoreboard objectives add timber_test dummy",
"scoreboard players set #uninstall_check_ran timber_test 1",
"data modify storage timber_test:uninstall left set value []",
]
for name in sorted(objectives):
# adding 0 to a holder works when the objective exists and fails when it doesn't.
# The names go into a list, a fail in the loop would end this function at the first one.
lines += [
f"execute store success score #exists timber_test run scoreboard players add #uninstall_probe {name} 0",
f'execute if score #exists timber_test matches 1 run data modify storage timber_test:uninstall left append value "scoreboard {name}"',
f"execute if score #exists timber_test matches 1 run scoreboard players reset #uninstall_probe {name}",
]
for sid, key in sorted(storage_keys):
# data get fails when the key is not there
lines += [
f"execute store success score #exists timber_test run data get storage {sid} {key}",
f'execute if score #exists timber_test matches 1 run data modify storage timber_test:uninstall left append value "storage {sid} {key}"',
]
cleared_schedules = set(re.findall(r"schedule clear (timber:[\w/]+)", uninstall_text))
for target in sorted(schedules - cleared_schedules):
lines.append(f'data modify storage timber_test:uninstall left append value "schedule {target} (not cleared in uninstall.mcfunction)"')

lines.append('execute if data storage timber_test:uninstall left[0] run fail [{"text":"left after uninstall: "},{"nbt":"left","storage":"timber_test:uninstall"}]')
lines.append("# (no assert after the fail: it would replace the message with an \"Expected ...\" without the names)")
write("uninstall_objectives.mcfunction", lines)

print(f"gen-uninstall-check: {len(objectives)} objectives, {len(storage_keys)} storage keys, {len(schedules)} schedules "
f"({len(schedules - cleared_schedules)} not cleared by uninstall)")
53 changes: 53 additions & 0 deletions .github/scripts/publish-latest-badge.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
#!/usr/bin/env bash
# Writes the badge of the weekly check (latest.json) to the branch "badges", shields.io reads it as an endpoint badge.
#
# GitHub's own workflow badge only knows passing, failing and no status. This one can be orange when the test tools (Fabric,
# PackTest) are not there for a new Minecraft release yet, and its file changes every run, so the "last commit" badge of the
# branch turns orange and red by itself when the schedule stops (GitHub switches schedules off after 60 days without activity).
#
# Input (environment): REMOTE (url to push to), and what the workflow knows:
# VERSION_OUTCOME outcome of the step that looks up the latest release (success = it worked)
# MC the latest release
# NEWER true if MC is newer than the highest version of the test matrix
# READY true if the test tools exist for MC
# TESTS outcome of the tests on MC
set -euo pipefail

: "${REMOTE:?REMOTE is the url of the repository to push the branch badges to}"
MC="${MC:-}"; NEWER="${NEWER:-}"; READY="${READY:-}"; TESTS="${TESTS:-}"; VERSION_OUTCOME="${VERSION_OUTCOME:-}"

if [ "$VERSION_OUTCOME" != "success" ] || [ -z "$MC" ]; then
MESSAGE="check did not run"; COLOR="lightgrey"
elif [ "$NEWER" != "true" ]; then
MESSAGE="$MC is in the test matrix"; COLOR="brightgreen"
elif [ "$READY" != "true" ]; then
MESSAGE="$MC, test tools not ready yet"; COLOR="orange"
elif [ "$TESTS" = "success" ]; then
MESSAGE="$MC works"; COLOR="brightgreen"
else
MESSAGE="$MC breaks"; COLOR="red"
fi
CHECKED=$(date -u +%Y-%m-%dT%H:%MZ)
echo "Badge: $MESSAGE ($COLOR)"

WORK=$(mktemp -d)
trap 'rm -rf "$WORK"' EXIT
cd "$WORK"
git init -q
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git remote add origin "$REMOTE"
if git ls-remote --exit-code --heads origin badges > /dev/null 2>&1; then
git fetch -q --depth 1 origin badges
git checkout -q -B badges FETCH_HEAD
else
git checkout -q --orphan badges
fi

# "checked" is not read by shields, it makes the file (and with it the last commit of the branch) change every run
printf '{\n "schemaVersion": 1,\n "label": "latest Minecraft",\n "message": "%s",\n "color": "%s",\n "cacheSeconds": 3600,\n "checked": "%s"\n}\n' \
"$MESSAGE" "$COLOR" "$CHECKED" > latest.json
git add latest.json
# two runs in the same minute with the same result give the same file
git diff --cached --quiet || git commit -q -m "Weekly check: $MESSAGE"
git push -q origin badges
37 changes: 37 additions & 0 deletions .github/scripts/setup-test-server.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
#!/usr/bin/env bash
# Sets up a Fabric server with Fabric API and PackTest for the given Minecraft version in the current directory.
# Exit code 3 means the tooling is not available for that version (yet), any other non-zero code is a real error.
set -euo pipefail

MC="$1"
UA="Moggla/Timber-Datapack CI"

# prints the download url of the newest release of a Modrinth project for $MC, or nothing if there is none
modrinth_url() {
curl -fsSL -A "$UA" -G "https://api.modrinth.com/v2/project/$1/version" \
--data-urlencode "game_versions=[\"$MC\"]" \
--data-urlencode 'loaders=["fabric"]' \
| jq -r '[.[] | select(.version_type == "release")][0].files[0].url // empty'
}

FABRIC_API=$(modrinth_url fabric-api)
PACKTEST=$(modrinth_url packtest)
LOADER=$(curl -fsSL https://meta.fabricmc.net/v2/versions/loader | jq -r '[.[] | select(.stable)][0].version')
INSTALLER=$(curl -fsSL https://meta.fabricmc.net/v2/versions/installer | jq -r '[.[] | select(.stable)][0].version')
# the meta api answers unknown versions with an error or an empty list
SERVER=$(curl -sSL "https://meta.fabricmc.net/v2/versions/loader/$MC" | jq -r 'if type == "array" and length > 0 then "yes" else empty end' 2>/dev/null || true)

MISSING=""
[ -n "$SERVER" ] || MISSING="$MISSING Fabric server,"
[ -n "$FABRIC_API" ] || MISSING="$MISSING Fabric API,"
[ -n "$PACKTEST" ] || MISSING="$MISSING PackTest,"
if [ -n "$MISSING" ]; then
echo "Not available for Minecraft $MC yet:${MISSING%,}" >&2
exit 3
fi

mkdir -p mods
curl -fsSL -o server.jar "https://meta.fabricmc.net/v2/versions/loader/$MC/$LOADER/$INSTALLER/server/jar"
curl -fsSL -o mods/fabric-api.jar "$FABRIC_API"
curl -fsSL -o mods/packtest.jar "$PACKTEST"
echo "eula=true" > eula.txt
138 changes: 138 additions & 0 deletions .github/workflows/latest.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
name: Latest Minecraft

# Checks whether the datapack works on a Minecraft release newer than the ones in the test matrix
# and opens an issue with the result, so that nobody has to look at the runs.
# Not a required check: Fabric, Fabric API and PackTest usually lag a few days behind a new release.
on:
schedule:
- cron: "0 6 * * 1"
workflow_dispatch:

permissions:
contents: write # the badge goes to the branch "badges"
issues: write
actions: write # to enable this workflow again, see the last step

jobs:
latest:
name: Latest Minecraft release
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Find latest release
id: version
run: |
MANIFEST=$(curl -fsSL https://piston-meta.mojang.com/mc/game/version_manifest_v2.json)
MC=$(jq -r '.latest.release' <<< "$MANIFEST")
URL=$(jq -r --arg id "$MC" '.versions[] | select(.id == $id) | .url' <<< "$MANIFEST")
JAVA=$(curl -fsSL "$URL" | jq -r '.javaVersion.majorVersion')

# highest version that the test matrix already covers
SUPPORTED=$(sed -n 's/.*minecraft: "\([^"]*\)".*/\1/p' .github/workflows/test.yml | sort -V | tail -n 1)

echo "mc=$MC" >> "$GITHUB_OUTPUT"
echo "java=$JAVA" >> "$GITHUB_OUTPUT"
if [ "$MC" != "$SUPPORTED" ] && [ "$(printf '%s\n%s\n' "$SUPPORTED" "$MC" | sort -V | tail -n 1)" = "$MC" ]; then
echo "newer=true" >> "$GITHUB_OUTPUT"
echo "Latest Minecraft release $MC is newer than the supported $SUPPORTED (Java $JAVA)"
else
echo "newer=false" >> "$GITHUB_OUTPUT"
echo "Latest Minecraft release $MC is already covered by the test matrix (highest: $SUPPORTED)." | tee -a "$GITHUB_STEP_SUMMARY"
fi

- uses: actions/setup-java@v4
if: steps.version.outputs.newer == 'true'
with:
distribution: temurin
java-version: ${{ steps.version.outputs.java }}

- name: Set up Fabric server, Fabric API and PackTest
id: setup
if: steps.version.outputs.newer == 'true'
run: |
set +e
bash .github/scripts/setup-test-server.sh "${{ steps.version.outputs.mc }}"
CODE=$?
if [ $CODE -eq 3 ]; then
echo "ready=false" >> "$GITHUB_OUTPUT"
echo "Minecraft ${{ steps.version.outputs.mc }} can't be tested yet, the test tooling isn't available for it. Trying again next time." | tee -a "$GITHUB_STEP_SUMMARY"
exit 0
fi
[ $CODE -eq 0 ] || exit $CODE
echo "ready=true" >> "$GITHUB_OUTPUT"

- name: Install datapacks
if: steps.setup.outputs.ready == 'true'
run: |
mkdir -p world/datapacks/timber world/datapacks/timber_test
cp -r data pack.mcmeta 121 world/datapacks/timber/
cp -r tests/. world/datapacks/timber_test/

# the uninstall test checks the scoreboards the code of the datapack creates, without this step it fails
- name: Generate the uninstall check
if: steps.setup.outputs.ready == 'true'
run: python3 .github/scripts/gen-uninstall-check.py . world/datapacks/timber_test

- name: Run tests
id: tests
if: steps.setup.outputs.ready == 'true'
continue-on-error: true
run: java -Xmx2G -Dpacktest.auto -Dpacktest.auto.annotations -jar server.jar nogui

- name: Open an issue with the result
if: steps.setup.outputs.ready == 'true'
env:
GH_TOKEN: ${{ github.token }}
MC: ${{ steps.version.outputs.mc }}
OUTCOME: ${{ steps.tests.outcome }}
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
run: |
if [ "$OUTCOME" = "success" ]; then
TITLE="Minecraft $MC works with the datapack"
BODY="The tests pass on Minecraft $MC ([run]($RUN_URL)).

To support it:
- bump \`max_format\` in \`pack.mcmeta\`
- add \`$MC\` to the matrix in \`.github/workflows/test.yml\`
- release the update"
else
TITLE="Minecraft $MC breaks the datapack"
BODY="The tests fail on Minecraft $MC ([run]($RUN_URL)). See the annotations of the run for the failed tests."
fi

# closed issues count too, so a handled version doesn't get reported again
EXISTING=$(gh issue list --state all --search "\"$TITLE\" in:title" --json title --jq "[.[] | select(.title == \"$TITLE\")] | length")
if [ "$EXISTING" -gt 0 ]; then
echo "Issue \"$TITLE\" already exists." | tee -a "$GITHUB_STEP_SUMMARY"
else
gh issue create --title "$TITLE" --body "$BODY" --label "CI/CD"
echo "Opened issue \"$TITLE\"." | tee -a "$GITHUB_STEP_SUMMARY"
fi

# The badge of the README: orange when the test tools don't exist for the new release yet, see publish-latest-badge.sh
- name: Publish the badge
if: always()
continue-on-error: true
env:
REMOTE: https://x-access-token:${{ github.token }}@github.com/${{ github.repository }}.git
VERSION_OUTCOME: ${{ steps.version.outcome }}
MC: ${{ steps.version.outputs.mc }}
NEWER: ${{ steps.version.outputs.newer }}
READY: ${{ steps.setup.outputs.ready }}
TESTS: ${{ steps.tests.outcome }}
run: bash .github/scripts/publish-latest-badge.sh

# GitHub switches schedules off after 60 days without activity in the repository. Enabling the workflow again is reported to
# start those 60 days over (not confirmed by GitHub's documentation). It does no harm when it doesn't, and the "last check" badge
# turns orange and red by itself when the schedule stops anyway.
- name: Keep the schedule alive
if: always()
continue-on-error: true
env:
GH_TOKEN: ${{ github.token }}
run: gh api --method PUT "repos/${{ github.repository }}/actions/workflows/latest.yml/enable"

- name: Fail if the tests failed
if: steps.tests.outcome == 'failure'
run: exit 1
Loading
Loading