From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: William Blake Galbreath <blake.galbreath@gmail.com>
Date: Fri, 2 Jul 2021 20:54:29 -0500
Subject: [PATCH] Tool actionable options


diff --git a/src/main/java/net/minecraft/world/item/AxeItem.java b/src/main/java/net/minecraft/world/item/AxeItem.java
index 734276a74ad11f117a816b214c62415818c27dcc..b2fdfc62f1f273fd6f78ae6153444e9c1a863b00 100644
--- a/src/main/java/net/minecraft/world/item/AxeItem.java
+++ b/src/main/java/net/minecraft/world/item/AxeItem.java
@@ -32,22 +32,22 @@ public class AxeItem extends DiggerItem {
         BlockPos blockPos = context.getClickedPos();
         Player player = context.getPlayer();
         BlockState blockState = level.getBlockState(blockPos);
-        Optional<BlockState> optional = this.getStripped(blockState);
-        Optional<BlockState> optional2 = WeatheringCopper.getPrevious(blockState);
-        Optional<BlockState> optional3 = Optional.ofNullable(HoneycombItem.WAX_OFF_BY_BLOCK.get().get(blockState.getBlock())).map((block) -> {
-            return block.withPropertiesOf(blockState);
-        });
+        // Purpur start
+        Optional<net.pl3x.purpur.tool.Actionable> optional = Optional.ofNullable(level.purpurConfig.axeStrippables.get(blockState.getBlock()));
+        Optional<net.pl3x.purpur.tool.Actionable> optional2 = Optional.ofNullable(level.purpurConfig.axeWeatherables.get(blockState.getBlock()));
+        Optional<net.pl3x.purpur.tool.Actionable> optional3 = Optional.ofNullable(level.purpurConfig.axeWaxables.get(blockState.getBlock()));
+        // Purpur end
         ItemStack itemStack = context.getItemInHand();
-        Optional<BlockState> optional4 = Optional.empty();
+        Optional<net.pl3x.purpur.tool.Actionable> optional4 = Optional.empty(); // Purpur
         if (optional.isPresent()) {
-            level.playSound(player, blockPos, SoundEvents.AXE_STRIP, SoundSource.BLOCKS, 1.0F, 1.0F);
+            level.playSound(null, blockPos, SoundEvents.AXE_STRIP, SoundSource.BLOCKS, 1.0F, 1.0F); // Purpur - force sound
             optional4 = optional;
         } else if (optional2.isPresent()) {
-            level.playSound(player, blockPos, SoundEvents.AXE_SCRAPE, SoundSource.BLOCKS, 1.0F, 1.0F);
+            level.playSound(null, blockPos, SoundEvents.AXE_SCRAPE, SoundSource.BLOCKS, 1.0F, 1.0F); // Purpur - force sound
             level.levelEvent(player, 3005, blockPos, 0);
             optional4 = optional2;
         } else if (optional3.isPresent()) {
-            level.playSound(player, blockPos, SoundEvents.AXE_WAX_OFF, SoundSource.BLOCKS, 1.0F, 1.0F);
+            level.playSound(null, blockPos, SoundEvents.AXE_WAX_OFF, SoundSource.BLOCKS, 1.0F, 1.0F); // Purpur - force sound
             level.levelEvent(player, 3004, blockPos, 0);
             optional4 = optional3;
         }
@@ -57,14 +57,22 @@ public class AxeItem extends DiggerItem {
                 CriteriaTriggers.ITEM_USED_ON_BLOCK.trigger((ServerPlayer)player, blockPos, itemStack);
             }
 
-            level.setBlock(blockPos, optional4.get(), 11);
+            // Purpur start
+            net.pl3x.purpur.tool.Actionable actionable = optional4.get();
+            level.setBlock(blockPos, actionable.into().withPropertiesOf(blockState), 11);
+            actionable.drops().forEach((drop, chance) -> {
+                if (level.random.nextDouble() < chance) {
+                    Block.popResourceFromFace(level, blockPos, context.getClickedFace(), new ItemStack(drop));
+                }
+            });
+            // Purpur end
             if (player != null) {
                 itemStack.hurtAndBreak(1, player, (p) -> {
                     p.broadcastBreakEvent(context.getHand());
                 });
             }
 
-            return InteractionResult.sidedSuccess(level.isClientSide);
+            return InteractionResult.SUCCESS; // Purpur - force arm swing
         } else {
             return InteractionResult.PASS;
         }
diff --git a/src/main/java/net/minecraft/world/item/HoeItem.java b/src/main/java/net/minecraft/world/item/HoeItem.java
index 009dd6b59e27a9413b3ef115468a6d1dd0746d56..fdbc1378bc148f5a16b414a3e5867cdce91c73b4 100644
--- a/src/main/java/net/minecraft/world/item/HoeItem.java
+++ b/src/main/java/net/minecraft/world/item/HoeItem.java
@@ -33,15 +33,22 @@ public class HoeItem extends DiggerItem {
     public InteractionResult useOn(UseOnContext context) {
         Level level = context.getLevel();
         BlockPos blockPos = context.getClickedPos();
-        Pair<Predicate<UseOnContext>, Consumer<UseOnContext>> pair = TILLABLES.get(level.getBlockState(blockPos).getBlock());
-        if (pair == null) {
-            return InteractionResult.PASS;
-        } else {
-            Predicate<UseOnContext> predicate = pair.getFirst();
-            Consumer<UseOnContext> consumer = pair.getSecond();
+        // Purpur start
+        var tillable = level.purpurConfig.hoeTillables.get(level.getBlockState(blockPos).getBlock());
+        if (tillable == null) { return InteractionResult.PASS; } else {
+            Predicate<UseOnContext> predicate = tillable.condition().predicate();
+            Consumer<UseOnContext> consumer = (ctx) -> {
+                level.setBlock(blockPos, tillable.into().defaultBlockState(), 11);
+                tillable.drops().forEach((drop, chance) -> {
+                    if (level.random.nextDouble() < chance) {
+                        Block.popResourceFromFace(level, blockPos, ctx.getClickedFace(), new ItemStack(drop));
+                    }
+                });
+            };
+            // Purpur end
             if (predicate.test(context)) {
                 Player player = context.getPlayer();
-                level.playSound(player, blockPos, SoundEvents.HOE_TILL, SoundSource.BLOCKS, 1.0F, 1.0F);
+                level.playSound(null, blockPos, SoundEvents.HOE_TILL, SoundSource.BLOCKS, 1.0F, 1.0F); // Purpur - force sound
                 if (!level.isClientSide) {
                     consumer.accept(context);
                     if (player != null) {
@@ -51,7 +58,7 @@ public class HoeItem extends DiggerItem {
                     }
                 }
 
-                return InteractionResult.sidedSuccess(level.isClientSide);
+                return InteractionResult.SUCCESS; // Purpur - force arm swing
             } else {
                 return InteractionResult.PASS;
             }
diff --git a/src/main/java/net/pl3x/purpur/PurpurWorldConfig.java b/src/main/java/net/pl3x/purpur/PurpurWorldConfig.java
index 2ba590032d6d7c5278cc2bf5774be2c278e99fc0..2eb8beff069fea2cff9b95bffc7adaef331197f9 100644
--- a/src/main/java/net/pl3x/purpur/PurpurWorldConfig.java
+++ b/src/main/java/net/pl3x/purpur/PurpurWorldConfig.java
@@ -459,6 +459,153 @@ public class PurpurWorldConfig {
         });
     }
 
+    public Map<Block, Strippable> axeStrippables = new HashMap<>();
+    public Map<Block, Waxable> axeWaxables = new HashMap<>();
+    public Map<Block, Weatherable> axeWeatherables = new HashMap<>();
+    public Map<Block, Tillable> hoeTillables = new HashMap<>();
+    private void toolSettings() {
+        axeStrippables.clear();
+        axeWaxables.clear();
+        axeWeatherables.clear();
+        hoeTillables.clear();
+        if (PurpurConfig.version < 18) {
+            ConfigurationSection section = PurpurConfig.config.getConfigurationSection("world-settings." + worldName + ".tools.hoe.tilling");
+            if (section != null) {
+                PurpurConfig.config.set("world-settings." + worldName + ".tools.hoe.tillables", section);
+                PurpurConfig.config.set("world-settings." + worldName + ".tools.hoe.tilling", null);
+            }
+            section = PurpurConfig.config.getConfigurationSection("world-settings.default.tools.hoe.tilling");
+            if (section != null) {
+                PurpurConfig.config.set("world-settings.default.tools.hoe.tillables", section);
+                PurpurConfig.config.set("world-settings.default.tools.hoe.tilling", null);
+            }
+        }
+        getMap("tools.axe.strippables", Map.ofEntries(
+                Map.entry("minecraft:oak_wood", Map.of("into", "minecraft:stripped_oak_wood", "drops", new HashMap<String, Double>())),
+                Map.entry("minecraft:oak_log", Map.of("into", "minecraft:stripped_oak_log", "drops", new HashMap<String, Double>())),
+                Map.entry("minecraft:dark_oak_wood", Map.of("into", "minecraft:stripped_dark_oak_wood", "drops", new HashMap<String, Double>())),
+                Map.entry("minecraft:dark_oak_log", Map.of("into", "minecraft:stripped_dark_oak_log", "drops", new HashMap<String, Double>())),
+                Map.entry("minecraft:acacia_wood", Map.of("into", "minecraft:stripped_acacia_wood", "drops", new HashMap<String, Double>())),
+                Map.entry("minecraft:acacia_log", Map.of("into", "minecraft:stripped_acacia_log", "drops", new HashMap<String, Double>())),
+                Map.entry("minecraft:birch_wood", Map.of("into", "minecraft:stripped_birch_wood", "drops", new HashMap<String, Double>())),
+                Map.entry("minecraft:birch_log", Map.of("into", "minecraft:stripped_birch_log", "drops", new HashMap<String, Double>())),
+                Map.entry("minecraft:jungle_wood", Map.of("into", "minecraft:stripped_jungle_wood", "drops", new HashMap<String, Double>())),
+                Map.entry("minecraft:jungle_log", Map.of("into", "minecraft:stripped_jungle_log", "drops", new HashMap<String, Double>())),
+                Map.entry("minecraft:spruce_wood", Map.of("into", "minecraft:stripped_spruce_wood", "drops", new HashMap<String, Double>())),
+                Map.entry("minecraft:spruce_log", Map.of("into", "minecraft:stripped_spruce_log", "drops", new HashMap<String, Double>())),
+                Map.entry("minecraft:warped_stem", Map.of("into", "minecraft:stripped_warped_stem", "drops", new HashMap<String, Double>())),
+                Map.entry("minecraft:warped_hyphae", Map.of("into", "minecraft:stripped_warped_hyphae", "drops", new HashMap<String, Double>())),
+                Map.entry("minecraft:crimson_stem", Map.of("into", "minecraft:stripped_crimson_stem", "drops", new HashMap<String, Double>())),
+                Map.entry("minecraft:crimson_hyphae", Map.of("into", "minecraft:stripped_crimson_hyphae", "drops", new HashMap<String, Double>())))
+        ).forEach((blockId, obj) -> {
+            Block block = Registry.BLOCK.get(new ResourceLocation(blockId));
+            if (block == Blocks.AIR) { PurpurConfig.log(Level.SEVERE, "Invalid block for `tools.axe.strippables`: " + blockId); return; }
+            if (!(obj instanceof Map<?, ?> map)) { PurpurConfig.log(Level.SEVERE, "Invalid yaml for `tools.axe.strippables." + blockId + "`"); return; }
+            String intoId = (String) map.get("into");
+            Block into = Registry.BLOCK.get(new ResourceLocation(intoId));
+            if (into == Blocks.AIR) { PurpurConfig.log(Level.SEVERE, "Invalid block for `tools.axe.strippables." + blockId + ".into`: " + intoId); return; }
+            Object dropsObj = map.get("drops");
+            if (!(dropsObj instanceof Map<?, ?> dropsMap)) { PurpurConfig.log(Level.SEVERE, "Invalid yaml for `tools.axe.strippables." + blockId + ".drops`"); return; }
+            Map<Item, Double> drops = new HashMap<>();
+            dropsMap.forEach((itemId, chance) -> {
+                Item item = Registry.ITEM.get(new ResourceLocation(itemId.toString()));
+                if (item == Items.AIR) { PurpurConfig.log(Level.SEVERE, "Invalid item for `tools.axe.strippables." + blockId + ".drops`: " + itemId); return; }
+                drops.put(item, (double) chance);
+            });
+            axeStrippables.put(block, new Strippable(into, drops));
+        });
+        getMap("tools.axe.waxables", Map.ofEntries(
+                Map.entry("minecraft:waxed_copper_block", Map.of("into", "minecraft:copper_block", "drops", new HashMap<String, Double>())),
+                Map.entry("minecraft:waxed_exposed_copper", Map.of("into", "minecraft:exposed_copper", "drops", new HashMap<String, Double>())),
+                Map.entry("minecraft:waxed_weathered_copper", Map.of("into", "minecraft:weathered_copper", "drops", new HashMap<String, Double>())),
+                Map.entry("minecraft:waxed_oxidized_copper", Map.of("into", "minecraft:oxidized_copper", "drops", new HashMap<String, Double>())),
+                Map.entry("minecraft:waxed_cut_copper", Map.of("into", "minecraft:cut_copper", "drops", new HashMap<String, Double>())),
+                Map.entry("minecraft:waxed_exposed_cut_copper", Map.of("into", "minecraft:exposed_cut_copper", "drops", new HashMap<String, Double>())),
+                Map.entry("minecraft:waxed_weathered_cut_copper", Map.of("into", "minecraft:weathered_cut_copper", "drops", new HashMap<String, Double>())),
+                Map.entry("minecraft:waxed_oxidized_cut_copper", Map.of("into", "minecraft:oxidized_cut_copper", "drops", new HashMap<String, Double>())),
+                Map.entry("minecraft:waxed_cut_copper_slab", Map.of("into", "minecraft:cut_copper_slab", "drops", new HashMap<String, Double>())),
+                Map.entry("minecraft:waxed_exposed_cut_copper_slab", Map.of("into", "minecraft:exposed_cut_copper_slab", "drops", new HashMap<String, Double>())),
+                Map.entry("minecraft:waxed_weathered_cut_copper_slab", Map.of("into", "minecraft:weathered_cut_copper_slab", "drops", new HashMap<String, Double>())),
+                Map.entry("minecraft:waxed_oxidized_cut_copper_slab", Map.of("into", "minecraft:oxidized_cut_copper_slab", "drops", new HashMap<String, Double>())),
+                Map.entry("minecraft:waxed_cut_copper_stairs", Map.of("into", "minecraft:cut_copper_stairs", "drops", new HashMap<String, Double>())),
+                Map.entry("minecraft:waxed_exposed_cut_copper_stairs", Map.of("into", "minecraft:exposed_cut_copper_stairs", "drops", new HashMap<String, Double>())),
+                Map.entry("minecraft:waxed_weathered_cut_copper_stairs", Map.of("into", "minecraft:weathered_cut_copper_stairs", "drops", new HashMap<String, Double>())),
+                Map.entry("minecraft:waxed_oxidized_cut_copper_stairs", Map.of("into", "minecraft:oxidized_cut_copper_stairs", "drops", new HashMap<String, Double>())))
+        ).forEach((blockId, obj) -> {
+            Block block = Registry.BLOCK.get(new ResourceLocation(blockId));
+            if (block == Blocks.AIR) { PurpurConfig.log(Level.SEVERE, "Invalid block for `tools.axe.waxables`: " + blockId); return; }
+            if (!(obj instanceof Map<?, ?> map)) { PurpurConfig.log(Level.SEVERE, "Invalid yaml for `tools.axe.waxables." + blockId + "`"); return; }
+            String intoId = (String) map.get("into");
+            Block into = Registry.BLOCK.get(new ResourceLocation(intoId));
+            if (into == Blocks.AIR) { PurpurConfig.log(Level.SEVERE, "Invalid block for `tools.axe.waxables." + blockId + ".into`: " + intoId); return; }
+            Object dropsObj = map.get("drops");
+            if (!(dropsObj instanceof Map<?, ?> dropsMap)) { PurpurConfig.log(Level.SEVERE, "Invalid yaml for `tools.axe.waxables." + blockId + ".drops`"); return; }
+            Map<Item, Double> drops = new HashMap<>();
+            dropsMap.forEach((itemId, chance) -> {
+                Item item = Registry.ITEM.get(new ResourceLocation(itemId.toString()));
+                if (item == Items.AIR) { PurpurConfig.log(Level.SEVERE, "Invalid item for `tools.axe.waxables." + blockId + ".drops`: " + itemId); return; }
+                drops.put(item, (double) chance);
+            });
+            axeWaxables.put(block, new Waxable(into, drops));
+        });
+        getMap("tools.axe.weatherables", Map.ofEntries(
+                Map.entry("minecraft:exposed_copper", Map.of("into", "minecraft:copper_block", "drops", new HashMap<String, Double>())),
+                Map.entry("minecraft:weathered_copper", Map.of("into", "minecraft:exposed_copper", "drops", new HashMap<String, Double>())),
+                Map.entry("minecraft:oxidized_copper", Map.of("into", "minecraft:weathered_copper", "drops", new HashMap<String, Double>())),
+                Map.entry("minecraft:exposed_cut_copper", Map.of("into", "minecraft:cut_copper", "drops", new HashMap<String, Double>())),
+                Map.entry("minecraft:weathered_cut_copper", Map.of("into", "minecraft:exposed_cut_copper", "drops", new HashMap<String, Double>())),
+                Map.entry("minecraft:oxidized_cut_copper", Map.of("into", "minecraft:weathered_cut_copper", "drops", new HashMap<String, Double>())),
+                Map.entry("minecraft:exposed_cut_copper_slab", Map.of("into", "minecraft:cut_copper_slab", "drops", new HashMap<String, Double>())),
+                Map.entry("minecraft:weathered_cut_copper_slab", Map.of("into", "minecraft:exposed_cut_copper_slab", "drops", new HashMap<String, Double>())),
+                Map.entry("minecraft:oxidized_cut_copper_slab", Map.of("into", "minecraft:weathered_cut_copper_slab", "drops", new HashMap<String, Double>())),
+                Map.entry("minecraft:exposed_cut_copper_stairs", Map.of("into", "minecraft:cut_copper_stairs", "drops", new HashMap<String, Double>())),
+                Map.entry("minecraft:weathered_cut_copper_stairs", Map.of("into", "minecraft:exposed_cut_copper_stairs", "drops", new HashMap<String, Double>())),
+                Map.entry("minecraft:oxidized_cut_copper_stairs", Map.of("into", "minecraft:weathered_cut_copper_stairs", "drops", new HashMap<String, Double>())))
+        ).forEach((blockId, obj) -> {
+            Block block = Registry.BLOCK.get(new ResourceLocation(blockId));
+            if (block == Blocks.AIR) { PurpurConfig.log(Level.SEVERE, "Invalid block for `tools.axe.weatherables`: " + blockId); return; }
+            if (!(obj instanceof Map<?, ?> map)) { PurpurConfig.log(Level.SEVERE, "Invalid yaml for `tools.axe.weatherables." + blockId + "`"); return; }
+            String intoId = (String) map.get("into");
+            Block into = Registry.BLOCK.get(new ResourceLocation(intoId));
+            if (into == Blocks.AIR) { PurpurConfig.log(Level.SEVERE, "Invalid block for `tools.axe.weatherables." + blockId + ".into`: " + intoId); return; }
+            Object dropsObj = map.get("drops");
+            if (!(dropsObj instanceof Map<?, ?> dropsMap)) { PurpurConfig.log(Level.SEVERE, "Invalid yaml for `tools.axe.weatherables." + blockId + ".drops`"); return; }
+            Map<Item, Double> drops = new HashMap<>();
+            dropsMap.forEach((itemId, chance) -> {
+                Item item = Registry.ITEM.get(new ResourceLocation(itemId.toString()));
+                if (item == Items.AIR) { PurpurConfig.log(Level.SEVERE, "Invalid item for `tools.axe.weatherables." + blockId + ".drops`: " + itemId); return; }
+                drops.put(item, (double) chance);
+            });
+            axeWeatherables.put(block, new Weatherable(into, drops));
+        });
+        getMap("tools.hoe.tillables", Map.ofEntries(
+                Map.entry("minecraft:grass_block", Map.of("condition", "air_above", "into", "minecraft:farmland", "drops", new HashMap<String, Double>())),
+                Map.entry("minecraft:dirt_path", Map.of("condition", "air_above", "into", "minecraft:farmland", "drops", new HashMap<String, Double>())),
+                Map.entry("minecraft:dirt", Map.of("condition", "air_above", "into", "minecraft:farmland", "drops", new HashMap<String, Double>())),
+                Map.entry("minecraft:coarse_dirt", Map.of("condition", "air_above", "into", "minecraft:dirt", "drops", new HashMap<String, Double>())),
+                Map.entry("minecraft:rooted_dirt", Map.of("condition", "always", "into", "minecraft:dirt", "drops", Map.of("minecraft:hanging_roots", 1.0D))))
+        ).forEach((blockId, obj) -> {
+            Block block = Registry.BLOCK.get(new ResourceLocation(blockId));
+            if (block == Blocks.AIR) { PurpurConfig.log(Level.SEVERE, "Invalid block for `tools.hoe.tilling`: " + blockId); return; }
+            if (!(obj instanceof Map<?, ?> map)) { PurpurConfig.log(Level.SEVERE, "Invalid yaml for `tools.hoe.tilling." + blockId + "`"); return; }
+            String conditionId = (String) map.get("condition");
+            Tillable.Condition condition = Tillable.Condition.get(conditionId);
+            if (condition == null) { PurpurConfig.log(Level.SEVERE, "Invalid condition for `tools.hoe.tilling." + blockId + ".condition`: " + conditionId); return; }
+            String intoId = (String) map.get("into");
+            Block into = Registry.BLOCK.get(new ResourceLocation(intoId));
+            if (into == Blocks.AIR) { PurpurConfig.log(Level.SEVERE, "Invalid block for `tools.hoe.tilling." + blockId + ".into`: " + intoId); return; }
+            Object dropsObj = map.get("drops");
+            if (!(dropsObj instanceof Map<?, ?> dropsMap)) { PurpurConfig.log(Level.SEVERE, "Invalid yaml for `tools.hoe.tilling." + blockId + ".drops`"); return; }
+            Map<Item, Double> drops = new HashMap<>();
+            dropsMap.forEach((itemId, chance) -> {
+                Item item = Registry.ITEM.get(new ResourceLocation(itemId.toString()));
+                if (item == Items.AIR) { PurpurConfig.log(Level.SEVERE, "Invalid item for `tools.hoe.tilling." + blockId + ".drops`: " + itemId); return; }
+                drops.put(item, (double) chance);
+            });
+            hoeTillables.put(block, new Tillable(condition, into, drops));
+        });
+    }
+
     public boolean useBetterMending = false;
     public boolean alwaysTameInCreative = false;
     public boolean boatEjectPlayersOnLand = false;
diff --git a/src/main/java/net/pl3x/purpur/tool/Actionable.java b/src/main/java/net/pl3x/purpur/tool/Actionable.java
new file mode 100644
index 0000000000000000000000000000000000000000..9273eb5f5cacf00eedeb5a396616871a3a39a7a0
--- /dev/null
+++ b/src/main/java/net/pl3x/purpur/tool/Actionable.java
@@ -0,0 +1,24 @@
+package net.pl3x.purpur.tool;
+
+import net.minecraft.world.item.Item;
+import net.minecraft.world.level.block.Block;
+
+import java.util.Map;
+
+public abstract class Actionable {
+    private final Block into;
+    private final Map<Item, Double> drops;
+
+    public Actionable(Block into, Map<Item, Double> drops) {
+        this.into = into;
+        this.drops = drops;
+    }
+
+    public Block into() {
+        return into;
+    }
+
+    public Map<Item, Double> drops() {
+        return drops;
+    }
+}
diff --git a/src/main/java/net/pl3x/purpur/tool/Strippable.java b/src/main/java/net/pl3x/purpur/tool/Strippable.java
new file mode 100644
index 0000000000000000000000000000000000000000..c15b623db3331757963a4f565ae842d93471b769
--- /dev/null
+++ b/src/main/java/net/pl3x/purpur/tool/Strippable.java
@@ -0,0 +1,12 @@
+package net.pl3x.purpur.tool;
+
+import net.minecraft.world.item.Item;
+import net.minecraft.world.level.block.Block;
+
+import java.util.Map;
+
+public class Strippable extends Actionable {
+    public Strippable(Block into, Map<Item, Double> drops) {
+        super(into, drops);
+    }
+}
diff --git a/src/main/java/net/pl3x/purpur/tool/Tillable.java b/src/main/java/net/pl3x/purpur/tool/Tillable.java
new file mode 100644
index 0000000000000000000000000000000000000000..ec2e3c4009e7a18592eed677697680926db67247
--- /dev/null
+++ b/src/main/java/net/pl3x/purpur/tool/Tillable.java
@@ -0,0 +1,50 @@
+package net.pl3x.purpur.tool;
+
+import net.minecraft.world.item.HoeItem;
+import net.minecraft.world.item.Item;
+import net.minecraft.world.item.context.UseOnContext;
+import net.minecraft.world.level.block.Block;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.function.Predicate;
+
+public class Tillable extends Actionable {
+    private final Condition condition;
+
+    public Tillable(Condition condition, Block into, Map<Item, Double> drops) {
+        super(into, drops);
+        this.condition = condition;
+    }
+
+    public Condition condition() {
+        return condition;
+    }
+
+    public enum Condition {
+        AIR_ABOVE(HoeItem::onlyIfAirAbove),
+        ALWAYS((useOnContext) -> true);
+
+        private final Predicate<UseOnContext> predicate;
+
+        Condition(Predicate<UseOnContext> predicate) {
+            this.predicate = predicate;
+        }
+
+        public Predicate<UseOnContext> predicate() {
+            return predicate;
+        }
+
+        private static final Map<String, Condition> BY_NAME = new HashMap<>();
+
+        static {
+            for (Condition condition : values()) {
+                BY_NAME.put(condition.name(), condition);
+            }
+        }
+
+        public static Condition get(String name) {
+            return BY_NAME.get(name.toUpperCase());
+        }
+    }
+}
diff --git a/src/main/java/net/pl3x/purpur/tool/Waxable.java b/src/main/java/net/pl3x/purpur/tool/Waxable.java
new file mode 100644
index 0000000000000000000000000000000000000000..45a8007a9aaf3f9f11b9387ed4d980fe4f1ab8e3
--- /dev/null
+++ b/src/main/java/net/pl3x/purpur/tool/Waxable.java
@@ -0,0 +1,12 @@
+package net.pl3x.purpur.tool;
+
+import net.minecraft.world.item.Item;
+import net.minecraft.world.level.block.Block;
+
+import java.util.Map;
+
+public class Waxable extends Actionable {
+    public Waxable(Block into, Map<Item, Double> drops) {
+        super(into, drops);
+    }
+}
diff --git a/src/main/java/net/pl3x/purpur/tool/Weatherable.java b/src/main/java/net/pl3x/purpur/tool/Weatherable.java
new file mode 100644
index 0000000000000000000000000000000000000000..89c6a18580382d011aae000b39e8fede18d7c5ea
--- /dev/null
+++ b/src/main/java/net/pl3x/purpur/tool/Weatherable.java
@@ -0,0 +1,12 @@
+package net.pl3x.purpur.tool;
+
+import net.minecraft.world.item.Item;
+import net.minecraft.world.level.block.Block;
+
+import java.util.Map;
+
+public class Weatherable extends Actionable {
+    public Weatherable(Block into, Map<Item, Double> drops) {
+        super(into, drops);
+    }
+}
