diff --git a/CHANGELOG.md b/CHANGELOG.md index 973bb13..51baa37 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,24 @@ # Changelog All notable changes to this project will be documented in this file. +## [5.3.0] + +### Added + +- A new requirements system that has a lot more customizable + - You can now require any item to be held [#24](https://github.com/nanite/SquatGrow/issues/24) + - You can now require equipment to be worn + - You can now require the required equipment or held item to be enchanted [#16](https://github.com/nanite/SquatGrow/issues/16) + +### Fixed + +- Config system not reloading as it should when you do `/reload` +- Chance config not being computed properly leading to the chance always being 100% [#27](https://github.com/nanite/SquatGrow/issues/27) + +### Deprecated + +- The old `requireHoe` and `hoeTakesDamage` config options have been deprecated in favor of the new requirements system + ## [5.2.0] ### Added diff --git a/common/src/main/java/dev/wuffs/squatgrow/SquatAction.java b/common/src/main/java/dev/wuffs/squatgrow/SquatAction.java index a2113a7..5cc1369 100644 --- a/common/src/main/java/dev/wuffs/squatgrow/SquatAction.java +++ b/common/src/main/java/dev/wuffs/squatgrow/SquatAction.java @@ -7,16 +7,18 @@ import net.minecraft.core.BlockPos; import net.minecraft.core.Direction; import net.minecraft.core.particles.ParticleTypes; -import net.minecraft.resources.ResourceLocation; import net.minecraft.server.level.ServerLevel; import net.minecraft.server.level.ServerPlayer; import net.minecraft.sounds.SoundEvents; import net.minecraft.sounds.SoundSource; import net.minecraft.tags.ItemTags; +import net.minecraft.tags.TagKey; import net.minecraft.util.Mth; import net.minecraft.world.entity.EquipmentSlot; import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.Item; import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.enchantment.EnchantmentHelper; import net.minecraft.world.level.GameType; import net.minecraft.world.level.Level; import net.minecraft.world.level.block.Blocks; @@ -25,7 +27,7 @@ import java.util.*; -import static dev.wuffs.squatgrow.SquatGrow.config; +import static dev.wuffs.squatgrow.SquatGrow.*; public class SquatAction { public static void performAction(Level level, Player player) { @@ -44,15 +46,10 @@ public static Pair> passesRequirements(Player player) { List itemsThatHandleDamage = new ArrayList<>(); // Legacy support, if this is enabled, the requirements system is disabled. if (config.requireHoe) { - ItemStack mainHand = player.getMainHandItem(); - if (mainHand.is(ItemTags.HOES)) { - itemsThatHandleDamage.add(mainHand); - return Pair.of(true, itemsThatHandleDamage); - } - - ItemStack offHand = player.getOffhandItem(); - if (offHand.is(ItemTags.HOES)) { - itemsThatHandleDamage.add(offHand); + // Meh, lists aren't free but emptylist is a constant so it's fine + var matchedItem = getMatchingHeldItem(player, Collections.emptyList(), List.of(ItemTags.HOES)); + if (!matchedItem.isEmpty()) { + itemsThatHandleDamage.add(matchedItem); return Pair.of(true, itemsThatHandleDamage); } @@ -60,29 +57,20 @@ public static Pair> passesRequirements(Player player) { } SquatGrowConfig.Requirements requirements = config.requirements; - if (requirements.enabled) { + if (requirements.enabled && SquatGrow.computedRequirements != null) { + // Easier to compare the originals than it is to compare the computed ones if (requirements.heldItemRequirement.isEmpty() && requirements.equipmentRequirement.isEmpty()) { return Pair.of(true, itemsThatHandleDamage); } // Let's check the correct things. First, the lighter of the two checks boolean passesEquipment = false; - if (!requirements.heldItemRequirement.isEmpty()) { - BitSet foundItems = new BitSet(); - for (Map.Entry entry : requirements.equipmentRequirement.entrySet()) { - ItemStack stack = player.getItemBySlot(entry.getKey()); - if (stack.isEmpty()) { - continue; - } + if (!requirements.equipmentRequirement.isEmpty()) { + var matchingEquipment = matchingEquipmentItem(player, computedRequirements.equipmentRequirementStacks(), computedRequirements.equipmentRequirementTags()); - // Now compare the item in the slot to the requirement - if (stack.getItem().arch$registryName().toString().equals(entry.getValue())) { - foundItems.set(entry.getKey().getIndex()); - itemsThatHandleDamage.add(stack); - } - } - - if (foundItems.cardinality() == requirements.heldItemRequirement.size()) { + // This is safe to do as it will only increment if the equipment is found, and you can only have one item per slot + if (matchingEquipment.size() == requirements.equipmentRequirement.size()) { + itemsThatHandleDamage.addAll(matchingEquipment); passesEquipment = true; } } @@ -93,20 +81,22 @@ public static Pair> passesRequirements(Player player) { // Now, the heavier of the two checks // We can only have gotten here if heldItemRequirement is not empty so no need to check again - BitSet foundItems = new BitSet(); - for (String item : requirements.heldItemRequirement) { - ResourceLocation itemLocation = new ResourceLocation(item); - if (player.getMainHandItem().getItem().arch$registryName().equals(itemLocation) || player.getOffhandItem().getItem().arch$registryName().equals(itemLocation)) { - foundItems.set(itemLocation.hashCode()); - itemsThatHandleDamage.add(player.getMainHandItem()); - } + boolean passedHeldItem = false; + var matchingHeldItem = getMatchingHeldItem(player, computedRequirements.heldItemRequirementStacks(), computedRequirements.heldItemRequirementTags()); + if (!matchingHeldItem.isEmpty()) { + itemsThatHandleDamage.add(matchingHeldItem); + passedHeldItem = true; + } + + if (!requirements.heldItemRequirement.isEmpty() && !passedHeldItem) { + return Pair.of(false, itemsThatHandleDamage); // If the held item check is required and failed, we can return false } - var passes = foundItems.cardinality() == requirements.heldItemRequirement.size(); - return Pair.of(passes, itemsThatHandleDamage); + return Pair.of(true, itemsThatHandleDamage); // If we got here, we passed both checks } - return Pair.of(true, itemsThatHandleDamage); + // Nothing is required, so we can return true + return Pair.of(true, Collections.emptyList()); } public static void grow(Level level, ServerPlayer player, List itemsToDamage) { @@ -213,4 +203,70 @@ private static void addGrowthParticles(ServerLevel level, BlockPos pos, ServerPl level.playSound(null, immutablePos, SoundEvents.BONE_MEAL_USE, SoundSource.MASTER, 0.5F, 1.0F); } } + + private static ItemStack getMatchingHeldItem(Player player, List itemStacks, List> itemTags) { + var mainHand = player.getMainHandItem(); + var offHand = player.getOffhandItem(); + + // Check the main hand first + var matchingItem = compareItemToLists(mainHand, itemStacks, itemTags); + if (!matchingItem.isEmpty()) { + return matchingItem; + } + + // Check the offhand next + return compareItemToLists(offHand, itemStacks, itemTags); + } + + private static ItemStack compareItemToLists(ItemStack stack, List itemStacks, List> itemTags) { + for (ItemStack item : itemStacks) { + if (itemStackMatches(stack, item)) { + return stack; + } + } + + for (TagKey tag : itemTags) { + if (itemStackMatches(stack, tag)) { + return stack; + } + } + + return ItemStack.EMPTY; + } + + private static List matchingEquipmentItem(Player player, Map equipmentStacks, Map> equipmentTags) { + List matchedItems = new ArrayList<>(); + + for (Map.Entry entry : equipmentStacks.entrySet()) { + ItemStack itemBySlot = player.getItemBySlot(entry.getKey()); + if (itemStackMatches(itemBySlot, entry.getValue())) { + matchedItems.add(itemBySlot); + } + } + + for (Map.Entry> entry : equipmentTags.entrySet()) { + ItemStack itemBySlot = player.getItemBySlot(entry.getKey()); + if (itemStackMatches(itemBySlot, entry.getValue())) { + matchedItems.add(itemBySlot); + } + } + + return matchedItems; + } + + private static boolean itemStackMatches(ItemStack stack, TagKey tag) { + if (computedEnchantment != null && stack.isEnchantable()) { + return stack.is(tag) && EnchantmentHelper.getEnchantments(stack).containsKey(computedEnchantment); + } + + return stack.is(tag); + } + + private static boolean itemStackMatches(ItemStack stack, ItemStack item) { + if (computedEnchantment != null && stack.isEnchantable()) { + return stack.is(item.getItem()) && EnchantmentHelper.getEnchantments(stack).containsKey(computedEnchantment); + } + + return stack.is(item.getItem()); + } } diff --git a/common/src/main/java/dev/wuffs/squatgrow/SquatGrow.java b/common/src/main/java/dev/wuffs/squatgrow/SquatGrow.java index c2116c0..b59fb31 100644 --- a/common/src/main/java/dev/wuffs/squatgrow/SquatGrow.java +++ b/common/src/main/java/dev/wuffs/squatgrow/SquatGrow.java @@ -3,10 +3,12 @@ import dev.architectury.event.events.common.LifecycleEvent; import dev.architectury.registry.ReloadListenerRegistry; import dev.wuffs.squatgrow.actions.Actions; +import dev.wuffs.squatgrow.config.ComputedRequirements; import dev.wuffs.squatgrow.config.SquatGrowConfig; import me.shedaniel.autoconfig.AutoConfig; import me.shedaniel.autoconfig.ConfigHolder; import me.shedaniel.autoconfig.serializer.YamlConfigSerializer; +import net.minecraft.core.registries.BuiltInRegistries; import net.minecraft.core.registries.Registries; import net.minecraft.resources.ResourceLocation; import net.minecraft.server.packs.PackType; @@ -14,12 +16,21 @@ import net.minecraft.server.packs.resources.ResourceManagerReloadListener; import net.minecraft.tags.TagKey; import net.minecraft.world.InteractionResult; +import net.minecraft.world.entity.EquipmentSlot; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.enchantment.Enchantment; +import net.minecraft.world.item.enchantment.Enchantments; import net.minecraft.world.level.block.Block; import net.minecraft.world.level.block.state.BlockState; +import org.apache.commons.lang3.tuple.Pair; +import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.HashSet; +import java.util.List; +import java.util.Map; import java.util.Set; import java.util.stream.Collectors; @@ -33,6 +44,8 @@ public class SquatGrow { public static final Set> tagCache = new HashSet<>(); public static final Set wildcardCache = new HashSet<>(); + public static ComputedRequirements computedRequirements = null; + public static Enchantment computedEnchantment = null; public static void init() { configHolder = AutoConfig.register(SquatGrowConfig.class, YamlConfigSerializer::new); @@ -66,8 +79,6 @@ private static InteractionResult onConfigChanged(ConfigHolder h tagCache.clear(); wildcardCache.clear(); - LOGGER.info("Config loading"); - tagCache.addAll(newConfig.ignoreList.stream() .filter(e -> e.contains("#")) .map(e -> TagKey.create(Registries.BLOCK, new ResourceLocation(e.replace("#", "")))) @@ -76,8 +87,39 @@ private static InteractionResult onConfigChanged(ConfigHolder h wildcardCache.addAll(newConfig.ignoreList.stream().filter(e -> e.contains("*")).map(e -> e.split(":")[0]) .collect(Collectors.toSet())); - LOGGER.info("Tags: " + tagCache); - LOGGER.info("Wildcards: " + wildcardCache); + List heldItemRequirement = newConfig.requirements.heldItemRequirement; + Map equipmentRequirement = newConfig.requirements.equipmentRequirement; + + Pair, List>> computedHeldEntries = computeItemsAndTagsFromStringList(heldItemRequirement); + + // This is kinda gross, but it does work so /shrug + Map equipmentRequirementStacks = equipmentRequirement.entrySet().stream() + .filter(e -> !e.getValue().contains("#")) + .collect(Collectors.toMap(Map.Entry::getKey, e -> new ItemStack(BuiltInRegistries.ITEM.get(new ResourceLocation(e.getValue()))))); + + Map> equipmentRequirementTags = equipmentRequirement.entrySet().stream() + .filter(e -> e.getValue().contains("#")) + .collect(Collectors.toMap(Map.Entry::getKey, e -> TagKey.create(Registries.ITEM, new ResourceLocation(e.getValue().replace("#", ""))))); + + computedRequirements = new ComputedRequirements( + computedHeldEntries.getLeft(), + computedHeldEntries.getRight(), + equipmentRequirementStacks, + equipmentRequirementTags + ); + + // This makes me want to puke, defaulted registries suck + if (!newConfig.requirements.requiredEnchantment.isEmpty()) { + ResourceLocation enchantmentRl = new ResourceLocation(newConfig.requirements.requiredEnchantment); + Enchantment enchantment = BuiltInRegistries.ENCHANTMENT.get(enchantmentRl); + // Default for the registry is fortune, we need to make that if we get fortune it matches the enchantmentRl + if (enchantment != Enchantments.BLOCK_FORTUNE || enchantmentRl.equals(new ResourceLocation("minecraft:fortune"))) { + // If the enchantment is not fortune or it is but we want fortune, set it + computedEnchantment = enchantment; + } + } else { + computedEnchantment = null; + } return InteractionResult.SUCCESS; } @@ -98,4 +140,18 @@ private static boolean isBlockInIgnoreList(BlockState state) { return tagCache.stream().anyMatch(state::is); } + + private static Pair, List>> computeItemsAndTagsFromStringList(List list) { + List stacks = list.stream() + .filter(e -> !e.contains("#")) + .map(e -> new ItemStack(BuiltInRegistries.ITEM.get(new ResourceLocation(e)))) + .toList(); + + List> tags = list.stream() + .filter(e -> e.contains("#")) + .map(e -> TagKey.create(Registries.ITEM, new ResourceLocation(e.replace("#", "")))) + .toList(); + + return Pair.of(stacks, tags); + } } diff --git a/common/src/main/java/dev/wuffs/squatgrow/config/ComputedRequirements.java b/common/src/main/java/dev/wuffs/squatgrow/config/ComputedRequirements.java new file mode 100644 index 0000000..50947ad --- /dev/null +++ b/common/src/main/java/dev/wuffs/squatgrow/config/ComputedRequirements.java @@ -0,0 +1,16 @@ +package dev.wuffs.squatgrow.config; + +import net.minecraft.tags.TagKey; +import net.minecraft.world.entity.EquipmentSlot; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; + +import java.util.List; +import java.util.Map; + +public record ComputedRequirements( + List heldItemRequirementStacks, + List> heldItemRequirementTags, + Map equipmentRequirementStacks, + Map> equipmentRequirementTags +) {} diff --git a/common/src/main/java/dev/wuffs/squatgrow/config/SquatGrowConfig.java b/common/src/main/java/dev/wuffs/squatgrow/config/SquatGrowConfig.java index 1ca0aba..e0e2ccd 100644 --- a/common/src/main/java/dev/wuffs/squatgrow/config/SquatGrowConfig.java +++ b/common/src/main/java/dev/wuffs/squatgrow/config/SquatGrowConfig.java @@ -5,8 +5,12 @@ import me.shedaniel.autoconfig.annotation.Config; import me.shedaniel.autoconfig.annotation.ConfigEntry; import me.shedaniel.cloth.clothconfig.shadowed.blue.endless.jankson.Comment; +import net.minecraft.tags.TagKey; import net.minecraft.world.entity.EquipmentSlot; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import java.beans.Transient; import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -86,5 +90,10 @@ public static class Requirements { @Comment("Amount of damage to take when used to grow") public int durabilityDamage = 1; + + @Comment( + "Enchantment required to grow, leave empty to disable\n" + ) + public String requiredEnchantment = ""; } } diff --git a/gradle.properties b/gradle.properties index 13be800..b2a0af4 100644 --- a/gradle.properties +++ b/gradle.properties @@ -4,7 +4,7 @@ minecraft_version=1.20.1 enabled_platforms=fabric,forge archives_base_name=squatgrow -mod_version=5.2.0 +mod_version=5.3.0 maven_group=dev.wuffs architectury_version=9.1.12