Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 70 additions & 0 deletions .github/scripts/gen-sapling-ground-tests.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
#!/usr/bin/env python3
"""Generates one autoplant test per block a sapling can be planted on, so a block that is added to (or dropped from) Minecraft is
covered without anybody keeping a list.

Usage: gen-sapling-ground-tests.py <server folder with the downloaded vanilla jar> <folder of the test datapack in the world, the one that contains data/>

The blocks are the resolved tag #minecraft:supports_vegetation of the vanilla server jar, the tag the game itself asks when a sapling is planted.
Writes data/timber_test/test/generated/plant_sapling_on_<block>.mcfunction. Fails when the tag is not found, so a rename can't leave a test that checks nothing.
"""
import glob
import io
import json
import os
import sys
import zipfile

server, dest = sys.argv[1], sys.argv[2]
TAG = "supports_vegetation"

jars = sorted(glob.glob(os.path.join(server, "versions", "*", "server-*.jar")))
if not jars:
sys.exit("gen-sapling-ground-tests: no vanilla server jar in " + server + "/versions")
z = zipfile.ZipFile(jars[-1])
# the vanilla server jar is a bundler, the game itself is a jar inside of it
for n in z.namelist():
if n.startswith("META-INF/versions/") and n.endswith(".jar"):
z = zipfile.ZipFile(io.BytesIO(z.read(n)))
break


def resolve(tag, seen=()):
if tag in seen:
return set()
entries = json.loads(z.read(f"data/minecraft/tags/block/{tag}.json"))["values"]
blocks = set()
for entry in entries:
entry = entry["id"] if isinstance(entry, dict) else entry
if entry.startswith("#minecraft:"):
blocks |= resolve(entry[len("#minecraft:"):], seen + (tag,))
else:
blocks.add(entry)
return blocks


try:
blocks = sorted(resolve(TAG))
except KeyError:
sys.exit(f"gen-sapling-ground-tests: block tag #minecraft:{TAG} not found in {jars[-1]}")
if not blocks:
sys.exit(f"gen-sapling-ground-tests: #minecraft:{TAG} is empty")

out = os.path.join(dest, "data", "timber_test", "test", "generated")
os.makedirs(out, exist_ok=True)
for block in blocks:
name = block.split(":")[1]
with open(os.path.join(out, f"plant_sapling_on_{name}.mcfunction"), "w", newline="\n") as f:
f.write(f"""#> GENERATED by .github/scripts/gen-sapling-ground-tests.py, do not edit. With plant_sapling on, a sapling item that lies on {block} gets planted
# @environment timber_test:plant_sapling_on
# @template timber_test:arena
# @timeout 400
# @skyaccess true

function timber_test:platform
setblock ~10 ~-1 ~10 {block}
summon minecraft:item ~10.5 ~0.5 ~10.5 {{Item:{{id:"minecraft:oak_sapling",count:1}}}}

# the loop runs every 2 seconds
await block ~10 ~ ~10 minecraft:oak_sapling
""")
print(f"gen-sapling-ground-tests: {len(blocks)} blocks: {', '.join(b.split(':')[1] for b in blocks)}")
9 changes: 8 additions & 1 deletion .github/scripts/setup-test-server.sh
Original file line number Diff line number Diff line change
Expand Up @@ -20,17 +20,24 @@ LOADER=$(curl -fsSL https://meta.fabricmc.net/v2/versions/loader | jq -r '[.[] |
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)
# the vanilla server jar: the Fabric server only downloads it on its first start, gen-sapling-ground-tests.py needs it before that
VANILLA_META=$(curl -fsSL https://piston-meta.mojang.com/mc/game/version_manifest_v2.json | jq -r --arg v "$MC" '.versions[] | select(.id == $v) | .url')
VANILLA=""
[ -z "$VANILLA_META" ] || VANILLA=$(curl -fsSL "$VANILLA_META" | jq -r '.downloads.server.url // empty')

MISSING=""
[ -n "$SERVER" ] || MISSING="$MISSING Fabric server,"
[ -n "$VANILLA" ] || MISSING="$MISSING vanilla 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
mkdir -p mods "versions/$MC"
# where the Fabric server puts it too (it replaces it with its own copy on the first start, gen-sapling-ground-tests.py reads both)
curl -fsSL -o "versions/$MC/server-$MC.jar" "$VANILLA"
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"
Expand Down
5 changes: 5 additions & 0 deletions .github/workflows/latest.yml
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,11 @@ jobs:
if: steps.setup.outputs.ready == 'true'
run: python3 .github/scripts/gen-uninstall-check.py . world/datapacks/timber_test

# one autoplant test per block a sapling can be planted on, read from the tag of the vanilla jar, so new blocks are covered
- name: Generate the sapling ground tests
if: steps.setup.outputs.ready == 'true'
run: python3 .github/scripts/gen-sapling-ground-tests.py . world/datapacks/timber_test

- name: Run tests
id: tests
if: steps.setup.outputs.ready == 'true'
Expand Down
4 changes: 4 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,10 @@ jobs:
- name: Generate the uninstall check
run: python3 .github/scripts/gen-uninstall-check.py . world/datapacks/timber_test

# one autoplant test per block a sapling can be planted on, read from the tag of the vanilla jar, so new blocks are covered
- name: Generate the sapling ground tests
run: python3 .github/scripts/gen-sapling-ground-tests.py . world/datapacks/timber_test

- name: Run tests
run: |
set -o pipefail
Expand Down
17 changes: 14 additions & 3 deletions 121/data/timber/function/autoplant/plant.mcfunction
Original file line number Diff line number Diff line change
@@ -1,7 +1,18 @@
# replace item-sapling with block-sapling (special case for mud, because fallingblock get destroyed. 0.125 is the height when you sink into the mud)
execute unless block ~ ~ ~ minecraft:air run summon marker ~ ~ ~ {Tags:["timber_mud","global.ignore"]}
execute unless block ~ ~ ~ minecraft:air run schedule function timber:autoplant/mud_fix 1t
execute unless block ~ ~ ~ minecraft:air run setblock ~ ~ ~ minecraft:muddy_mangrove_roots
execute if block ~ ~ ~ minecraft:mud run summon marker ~ ~ ~ {Tags:["timber_mud","global.ignore"]}
execute if block ~ ~ ~ minecraft:mud run schedule function timber:autoplant/mud_fix 1t
execute if block ~ ~ ~ minecraft:mud run setblock ~ ~ ~ minecraft:muddy_mangrove_roots
# same for farmland (a bit lower than a full block, the falling block would land inside of it): a dirt block for a moment, so the falling block has something to land on. The marker remembers the moisture, farmland_fix sets it again
execute if block ~ ~ ~ minecraft:farmland[moisture=0] run summon marker ~ ~ ~ {Tags:["timber_farmland","timber_moisture_0","global.ignore"]}
execute if block ~ ~ ~ minecraft:farmland[moisture=1] run summon marker ~ ~ ~ {Tags:["timber_farmland","timber_moisture_1","global.ignore"]}
execute if block ~ ~ ~ minecraft:farmland[moisture=2] run summon marker ~ ~ ~ {Tags:["timber_farmland","timber_moisture_2","global.ignore"]}
execute if block ~ ~ ~ minecraft:farmland[moisture=3] run summon marker ~ ~ ~ {Tags:["timber_farmland","timber_moisture_3","global.ignore"]}
execute if block ~ ~ ~ minecraft:farmland[moisture=4] run summon marker ~ ~ ~ {Tags:["timber_farmland","timber_moisture_4","global.ignore"]}
execute if block ~ ~ ~ minecraft:farmland[moisture=5] run summon marker ~ ~ ~ {Tags:["timber_farmland","timber_moisture_5","global.ignore"]}
execute if block ~ ~ ~ minecraft:farmland[moisture=6] run summon marker ~ ~ ~ {Tags:["timber_farmland","timber_moisture_6","global.ignore"]}
execute if block ~ ~ ~ minecraft:farmland[moisture=7] run summon marker ~ ~ ~ {Tags:["timber_farmland","timber_moisture_7","global.ignore"]}
execute if block ~ ~ ~ minecraft:farmland run schedule function timber:autoplant/farmland_fix 3t
execute if block ~ ~ ~ minecraft:farmland run setblock ~ ~ ~ minecraft:dirt
summon minecraft:falling_block ~ ~0.125 ~ {BlockState:"minecraft:barrier",Time:1,DropItem:0b,Tags:["timber_item_to_block"]}
execute positioned ~ ~0.125 ~ run data modify entity @e[type=minecraft:falling_block,tag=timber_item_to_block,distance=...1,sort=arbitrary,limit=1] BlockState set from entity @s Item.id

Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@


This is a really useful addition for those who don't want to spend expensive time on gathering resources! After installing, the Timber Datapack allows you to instantly chop down a tree just by breaking one log with any axe.
By default, sneaking while breaking will not chop the tree.

Optionally, you can adjust everything as you wish.

# Under the Hood Features
Expand All @@ -15,7 +15,7 @@ Optionally, you can adjust everything as you wish.
- Takes account of any enchantment
- Many options for customization

¹ Trees are defined as logs that are next to naturally generated leaves.
¹ Trees are defined as logs with naturally generated leaves within 2 blocks of the trunk.
² Logs in the tag #minecraft:logs.
³ All kind of custom trees like from **BiomesO'Plenty**, **The Biome Overhaul**, **Terrestria and Traverse**.

Expand Down
5 changes: 3 additions & 2 deletions data/timber/function/algorithm/tree/chop_roots.mcfunction
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,6 @@ execute if score stop_chopping timber matches 1.. unless score unbreakable timbe
# mark block that will get destroyed
execute if score durability_okay timber matches 1.. if score tree_size timber < max_tree_size timber run summon marker ~ ~ ~ {Tags:["timber_root"]}

# search for next log/root
execute if score durability_okay timber matches 1.. if score tree_size timber < max_tree_size timber run function timber:algorithm/tree/search_1
# search for next root, only sideways and downwards and never into a log: roots of trees next to each other touch,
# going up from a root would climb into the trunk of the neighbor and chop the whole forest
execute if score durability_okay timber matches 1.. if score tree_size timber < max_tree_size timber if score destroy_roots timber matches 1.. run function timber:algorithm/tree/roots/search_1
19 changes: 19 additions & 0 deletions data/timber/function/algorithm/tree/find_leaves_2.mcfunction
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# run at every log of the tree: marks the leaves 2 blocks away from it (straight or around an edge), for trees with too few leaves right at the trunk
execute positioned ~ ~ ~2 if predicate timber:block/leaf unless entity @e[type=minecraft:marker,tag=timber_leaves_found,distance=...1,sort=nearest,limit=1] run summon marker ~ ~ ~ {Tags:["timber_leaves_found"]}
execute positioned ~ ~ ~-2 if predicate timber:block/leaf unless entity @e[type=minecraft:marker,tag=timber_leaves_found,distance=...1,sort=nearest,limit=1] run summon marker ~ ~ ~ {Tags:["timber_leaves_found"]}
execute positioned ~2 ~ ~ if predicate timber:block/leaf unless entity @e[type=minecraft:marker,tag=timber_leaves_found,distance=...1,sort=nearest,limit=1] run summon marker ~ ~ ~ {Tags:["timber_leaves_found"]}
execute positioned ~-2 ~ ~ if predicate timber:block/leaf unless entity @e[type=minecraft:marker,tag=timber_leaves_found,distance=...1,sort=nearest,limit=1] run summon marker ~ ~ ~ {Tags:["timber_leaves_found"]}
execute positioned ~ ~2 ~ if predicate timber:block/leaf unless entity @e[type=minecraft:marker,tag=timber_leaves_found,distance=...1,sort=nearest,limit=1] run summon marker ~ ~ ~ {Tags:["timber_leaves_found"]}
execute positioned ~ ~-2 ~ if predicate timber:block/leaf unless entity @e[type=minecraft:marker,tag=timber_leaves_found,distance=...1,sort=nearest,limit=1] run summon marker ~ ~ ~ {Tags:["timber_leaves_found"]}
execute positioned ~1 ~ ~1 if predicate timber:block/leaf unless entity @e[type=minecraft:marker,tag=timber_leaves_found,distance=...1,sort=nearest,limit=1] run summon marker ~ ~ ~ {Tags:["timber_leaves_found"]}
execute positioned ~1 ~ ~-1 if predicate timber:block/leaf unless entity @e[type=minecraft:marker,tag=timber_leaves_found,distance=...1,sort=nearest,limit=1] run summon marker ~ ~ ~ {Tags:["timber_leaves_found"]}
execute positioned ~-1 ~ ~1 if predicate timber:block/leaf unless entity @e[type=minecraft:marker,tag=timber_leaves_found,distance=...1,sort=nearest,limit=1] run summon marker ~ ~ ~ {Tags:["timber_leaves_found"]}
execute positioned ~-1 ~ ~-1 if predicate timber:block/leaf unless entity @e[type=minecraft:marker,tag=timber_leaves_found,distance=...1,sort=nearest,limit=1] run summon marker ~ ~ ~ {Tags:["timber_leaves_found"]}
execute positioned ~1 ~1 ~ if predicate timber:block/leaf unless entity @e[type=minecraft:marker,tag=timber_leaves_found,distance=...1,sort=nearest,limit=1] run summon marker ~ ~ ~ {Tags:["timber_leaves_found"]}
execute positioned ~-1 ~1 ~ if predicate timber:block/leaf unless entity @e[type=minecraft:marker,tag=timber_leaves_found,distance=...1,sort=nearest,limit=1] run summon marker ~ ~ ~ {Tags:["timber_leaves_found"]}
execute positioned ~ ~1 ~1 if predicate timber:block/leaf unless entity @e[type=minecraft:marker,tag=timber_leaves_found,distance=...1,sort=nearest,limit=1] run summon marker ~ ~ ~ {Tags:["timber_leaves_found"]}
execute positioned ~ ~1 ~-1 if predicate timber:block/leaf unless entity @e[type=minecraft:marker,tag=timber_leaves_found,distance=...1,sort=nearest,limit=1] run summon marker ~ ~ ~ {Tags:["timber_leaves_found"]}
execute positioned ~1 ~-1 ~ if predicate timber:block/leaf unless entity @e[type=minecraft:marker,tag=timber_leaves_found,distance=...1,sort=nearest,limit=1] run summon marker ~ ~ ~ {Tags:["timber_leaves_found"]}
execute positioned ~-1 ~-1 ~ if predicate timber:block/leaf unless entity @e[type=minecraft:marker,tag=timber_leaves_found,distance=...1,sort=nearest,limit=1] run summon marker ~ ~ ~ {Tags:["timber_leaves_found"]}
execute positioned ~ ~-1 ~1 if predicate timber:block/leaf unless entity @e[type=minecraft:marker,tag=timber_leaves_found,distance=...1,sort=nearest,limit=1] run summon marker ~ ~ ~ {Tags:["timber_leaves_found"]}
execute positioned ~ ~-1 ~-1 if predicate timber:block/leaf unless entity @e[type=minecraft:marker,tag=timber_leaves_found,distance=...1,sort=nearest,limit=1] run summon marker ~ ~ ~ {Tags:["timber_leaves_found"]}
20 changes: 20 additions & 0 deletions data/timber/function/algorithm/tree/roots/chop_start.mcfunction
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# a root on the way from the mined root to the trunk (start_chop_on_root)
# resets
scoreboard players set durability_okay timber 1

# keep track of tree size
scoreboard players add tree_size timber 1

# wear out tool
function timber:tool/durability_count

# tool durability check
execute if score stop_chopping timber matches 1.. unless score unbreakable timber matches 1 run function timber:tool/durability_check

# mark block that will get destroyed
execute if score durability_okay timber matches 1.. if score tree_size timber < max_tree_size timber run summon marker ~ ~ ~ {Tags:["timber_root"]}

# search upwards and sideways for the trunk, roots can run sideways for a long way before they reach it
execute if score durability_okay timber matches 1.. if score tree_size timber < max_tree_size timber run function timber:algorithm/tree/roots/search_start_1
# search for roots downwards
execute if score durability_okay timber matches 1.. if score tree_size timber < max_tree_size timber if score destroy_roots timber matches 1.. run function timber:algorithm/tree/roots/search_1
20 changes: 20 additions & 0 deletions data/timber/function/algorithm/tree/roots/search_1.mcfunction
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# search for next root sideways
execute positioned ~ ~ ~1 run function timber:algorithm/tree/roots/search_2
execute positioned ~ ~ ~-1 run function timber:algorithm/tree/roots/search_2
execute positioned ~1 ~ ~ run function timber:algorithm/tree/roots/search_2
execute positioned ~1 ~ ~1 run function timber:algorithm/tree/roots/search_2
execute positioned ~1 ~ ~-1 run function timber:algorithm/tree/roots/search_2
execute positioned ~-1 ~ ~ run function timber:algorithm/tree/roots/search_2
execute positioned ~-1 ~ ~1 run function timber:algorithm/tree/roots/search_2
execute positioned ~-1 ~ ~-1 run function timber:algorithm/tree/roots/search_2

# search for next root downwards
execute positioned ~ ~-1 ~ run function timber:algorithm/tree/roots/search_2
execute positioned ~ ~-1 ~1 run function timber:algorithm/tree/roots/search_2
execute positioned ~ ~-1 ~-1 run function timber:algorithm/tree/roots/search_2
execute positioned ~1 ~-1 ~ run function timber:algorithm/tree/roots/search_2
execute positioned ~1 ~-1 ~1 run function timber:algorithm/tree/roots/search_2
execute positioned ~1 ~-1 ~-1 run function timber:algorithm/tree/roots/search_2
execute positioned ~-1 ~-1 ~ run function timber:algorithm/tree/roots/search_2
execute positioned ~-1 ~-1 ~1 run function timber:algorithm/tree/roots/search_2
execute positioned ~-1 ~-1 ~-1 run function timber:algorithm/tree/roots/search_2
4 changes: 4 additions & 0 deletions data/timber/function/algorithm/tree/roots/search_2.mcfunction
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
scoreboard players add log_probes timber_bench 1

# if root (a log is never followed from here)
execute if block ~ ~ ~ minecraft:mangrove_roots unless entity @e[type=minecraft:marker,tag=timber_root,distance=...1,sort=arbitrary,limit=1] run function timber:algorithm/tree/chop_roots
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# search for next log/root upwards
execute positioned ~ ~1 ~ run function timber:algorithm/tree/roots/search_start_2
execute positioned ~ ~1 ~1 run function timber:algorithm/tree/roots/search_start_2
execute positioned ~ ~1 ~-1 run function timber:algorithm/tree/roots/search_start_2
execute positioned ~1 ~1 ~ run function timber:algorithm/tree/roots/search_start_2
execute positioned ~1 ~1 ~1 run function timber:algorithm/tree/roots/search_start_2
execute positioned ~1 ~1 ~-1 run function timber:algorithm/tree/roots/search_start_2
execute positioned ~-1 ~1 ~ run function timber:algorithm/tree/roots/search_start_2
execute positioned ~-1 ~1 ~1 run function timber:algorithm/tree/roots/search_start_2
execute positioned ~-1 ~1 ~-1 run function timber:algorithm/tree/roots/search_start_2

# search for next log/root sideways
execute positioned ~ ~ ~1 run function timber:algorithm/tree/roots/search_start_2
execute positioned ~ ~ ~-1 run function timber:algorithm/tree/roots/search_start_2
execute positioned ~1 ~ ~ run function timber:algorithm/tree/roots/search_start_2
execute positioned ~1 ~ ~1 run function timber:algorithm/tree/roots/search_start_2
execute positioned ~1 ~ ~-1 run function timber:algorithm/tree/roots/search_start_2
execute positioned ~-1 ~ ~ run function timber:algorithm/tree/roots/search_start_2
execute positioned ~-1 ~ ~1 run function timber:algorithm/tree/roots/search_start_2
execute positioned ~-1 ~ ~-1 run function timber:algorithm/tree/roots/search_start_2
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
scoreboard players add log_probes timber_bench 1

# if log: the trunk is found, from now on roots are only followed sideways and downwards (chop_roots), so the walk doesn't go on to the next tree
execute if predicate timber:block/log unless entity @e[type=minecraft:marker,tag=timber_log,distance=...1,sort=arbitrary,limit=1] run scoreboard players set root_walk timber 0
execute if predicate timber:block/log unless entity @e[type=minecraft:marker,tag=timber_log,distance=...1,sort=arbitrary,limit=1] run function timber:algorithm/tree/chop
# if root
execute if score root_walk timber matches 1 if block ~ ~ ~ minecraft:mangrove_roots unless entity @e[type=minecraft:marker,tag=timber_root,distance=...1,sort=arbitrary,limit=1] run function timber:algorithm/tree/roots/chop_start
execute if score root_walk timber matches 0 if block ~ ~ ~ minecraft:mangrove_roots unless entity @e[type=minecraft:marker,tag=timber_root,distance=...1,sort=arbitrary,limit=1] run function timber:algorithm/tree/chop_roots
Loading
Loading