From 595bc762942ebcaecd108a0267dd0e552877d559 Mon Sep 17 00:00:00 2001 From: Auxilor Date: Mon, 7 Mar 2022 11:32:18 +0000 Subject: [PATCH] Added shapeless recipe support --- .../recipe/recipes/ShapedCraftingRecipe.java | 7 - .../recipes/ShapelessCraftingRecipe.java | 341 ++++++++++++++++++ .../eco/internal/spigot/EcoSpigotPlugin.kt | 15 +- ...eListener.kt => CraftingRecipeListener.kt} | 2 +- .../recipes/ShapelessStackedRecipeListener.kt | 136 +++++++ .../recipes/listeners/ComplexInComplex.kt | 6 +- .../recipes/listeners/ComplexInVanilla.kt | 6 +- 7 files changed, 492 insertions(+), 21 deletions(-) create mode 100644 eco-api/src/main/java/com/willfp/eco/core/recipe/recipes/ShapelessCraftingRecipe.java rename eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/recipes/{ShapedRecipeListener.kt => CraftingRecipeListener.kt} (97%) create mode 100644 eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/recipes/ShapelessStackedRecipeListener.kt diff --git a/eco-api/src/main/java/com/willfp/eco/core/recipe/recipes/ShapedCraftingRecipe.java b/eco-api/src/main/java/com/willfp/eco/core/recipe/recipes/ShapedCraftingRecipe.java index 54a02286..2c54ab11 100644 --- a/eco-api/src/main/java/com/willfp/eco/core/recipe/recipes/ShapedCraftingRecipe.java +++ b/eco-api/src/main/java/com/willfp/eco/core/recipe/recipes/ShapedCraftingRecipe.java @@ -3,7 +3,6 @@ package com.willfp.eco.core.recipe.recipes; import com.willfp.eco.core.Eco; import com.willfp.eco.core.EcoPlugin; import com.willfp.eco.core.PluginDependent; -import com.willfp.eco.core.Prerequisite; import com.willfp.eco.core.items.TestableItem; import com.willfp.eco.core.recipe.Recipes; import com.willfp.eco.core.recipe.parts.EmptyTestableItem; @@ -139,12 +138,6 @@ public final class ShapedCraftingRecipe extends PluginDependent imple displayedRecipe.setIngredient(character, new RecipeChoice.ExactChoice(displayedItems)); } - if (Prerequisite.HAS_1_18.isMet() && !Prerequisite.HAS_PAPER.isMet()) { - if (Bukkit.getServer().getRecipe(this.getKey()) != null) { - return; - } - } - Bukkit.getServer().addRecipe(shapedRecipe); Bukkit.getServer().addRecipe(displayedRecipe); } diff --git a/eco-api/src/main/java/com/willfp/eco/core/recipe/recipes/ShapelessCraftingRecipe.java b/eco-api/src/main/java/com/willfp/eco/core/recipe/recipes/ShapelessCraftingRecipe.java new file mode 100644 index 00000000..22450a22 --- /dev/null +++ b/eco-api/src/main/java/com/willfp/eco/core/recipe/recipes/ShapelessCraftingRecipe.java @@ -0,0 +1,341 @@ +package com.willfp.eco.core.recipe.recipes; + +import com.willfp.eco.core.Eco; +import com.willfp.eco.core.EcoPlugin; +import com.willfp.eco.core.PluginDependent; +import com.willfp.eco.core.items.TestableItem; +import com.willfp.eco.core.recipe.Recipes; +import com.willfp.eco.core.recipe.parts.EmptyTestableItem; +import com.willfp.eco.core.recipe.parts.GroupedTestableItems; +import com.willfp.eco.core.recipe.parts.TestableStack; +import org.bukkit.Bukkit; +import org.bukkit.NamespacedKey; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.RecipeChoice; +import org.bukkit.inventory.ShapelessRecipe; +import org.bukkit.inventory.meta.ItemMeta; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +/** + * Shapeless crafting recipe. + */ +public final class ShapelessCraftingRecipe extends PluginDependent implements CraftingRecipe { + /** + * Recipe parts. + */ + private final List parts; + + /** + * The key of the recipe. + */ + private final NamespacedKey key; + + /** + * The key of the displayed recipe. + */ + private final NamespacedKey displayedKey; + + /** + * The recipe's output. + */ + private final ItemStack output; + + /** + * The permission. + */ + private final String permission; + + private ShapelessCraftingRecipe(@NotNull final EcoPlugin plugin, + @NotNull final String key, + @NotNull final List parts, + @NotNull final ItemStack output, + @Nullable final String permission) { + super(plugin); + + this.parts = parts; + this.key = plugin.getNamespacedKeyFactory().create(key); + this.displayedKey = plugin.getNamespacedKeyFactory().create(key + "_displayed"); + this.output = output; + this.permission = permission; + } + + /** + * Make a new test. + * + * @return The test. + */ + @NotNull + public RecipeTest newTest() { + return new RecipeTest(this); + } + + @Override + public boolean test(@NotNull final ItemStack[] matrix) { + RecipeTest test = newTest(); + + for (ItemStack stack : matrix) { + if (test.matchAndRemove(stack) == null) { + return false; + } + } + + return true; + } + + @Override + public void register() { + Recipes.register(this); + + Bukkit.getServer().removeRecipe(this.getKey()); + Bukkit.getServer().removeRecipe(this.getDisplayedKey()); + + ShapelessRecipe shapelessRecipe = new ShapelessRecipe(this.getKey(), this.getOutput()); + for (TestableItem part : parts) { + shapelessRecipe.addIngredient(part.getItem().getType()); + } + + ShapelessRecipe displayedRecipe = new ShapelessRecipe(this.getDisplayedKey(), this.getOutput()); + for (TestableItem part : parts) { + List items = new ArrayList<>(); + if (part instanceof GroupedTestableItems group) { + items.addAll(group.getChildren()); + } else { + items.add(part); + } + + List displayedItems = new ArrayList<>(); + + for (TestableItem testableItem : items) { + if (testableItem instanceof TestableStack) { + ItemStack item = testableItem.getItem().clone(); + ItemMeta meta = item.getItemMeta(); + assert meta != null; + + List lore = meta.hasLore() ? meta.getLore() : new ArrayList<>(); + assert lore != null; + lore.add(""); + String add = Eco.getHandler().getEcoPlugin().getLangYml().getFormattedString("multiple-in-craft"); + add = add.replace("%amount%", String.valueOf(item.getAmount())); + lore.add(add); + meta.setLore(lore); + item.setItemMeta(meta); + + displayedItems.add(item); + } else { + displayedItems.add(testableItem.getItem()); + } + } + + displayedRecipe.addIngredient(new RecipeChoice.ExactChoice(displayedItems)); + } + + Bukkit.getServer().addRecipe(shapelessRecipe); + Bukkit.getServer().addRecipe(displayedRecipe); + } + + /** + * Create a new recipe builder. + * + * @param plugin The plugin that owns the recipe. + * @param key The recipe key. + * @return A new builder. + */ + public static Builder builder(@NotNull final EcoPlugin plugin, + @NotNull final String key) { + return new Builder(plugin, key); + } + + /** + * Get the parts. + * + * @return The parts. + */ + @NotNull + @Override + public List getParts() { + return this.parts; + } + + /** + * Get the key. + * + * @return The key. + */ + @NotNull + @Override + public NamespacedKey getKey() { + return this.key; + } + + /** + * Get the displayed key. + * + * @return The displayed key. + */ + @NotNull + @Override + public NamespacedKey getDisplayedKey() { + return this.displayedKey; + } + + /** + * Get the output. + * + * @return The output. + */ + @NotNull + @Override + public ItemStack getOutput() { + return this.output; + } + + /** + * Get the permission. + * + * @return The permission. + */ + @Nullable + @Override + public String getPermission() { + return permission; + } + + /** + * Builder for recipes. + */ + public static final class Builder { + /** + * The recipe parts. + */ + private final List recipeParts = new ArrayList<>(); + + /** + * The output of the recipe. + */ + private ItemStack output = null; + + /** + * The permission for the recipe. + */ + private String permission = null; + + /** + * The key of the recipe. + */ + private final String key; + + /** + * The plugin that created the recipe. + */ + private final EcoPlugin plugin; + + /** + * Create a new recipe builder. + * + * @param plugin The plugin that owns the recipe. + * @param key The recipe key. + */ + private Builder(@NotNull final EcoPlugin plugin, + @NotNull final String key) { + this.key = key; + this.plugin = plugin; + } + + /** + * Add a recipe part. + * + * @param part The part of the recipe. + * @return The builder. + */ + public Builder addRecipePart(@NotNull final TestableItem part) { + recipeParts.add(part); + return this; + } + + /** + * Set the output of the recipe. + * + * @param output The output. + * @return The builder. + */ + public Builder setOutput(@NotNull final ItemStack output) { + this.output = output; + return this; + } + + /** + * Set the permission required to craft the recipe. + * + * @param permission The permission. + * @return The builder. + */ + public Builder setPermission(@Nullable final String permission) { + this.permission = permission; + return this; + } + + /** + * Check if recipe parts are all air. + * + * @return If recipe parts are all air. + */ + public boolean isAir() { + for (TestableItem recipePart : this.recipeParts) { + if (recipePart != null && !(recipePart instanceof EmptyTestableItem)) { + return false; + } + } + return true; + } + + /** + * Build the recipe. + * + * @return The built recipe. + */ + public ShapelessCraftingRecipe build() { + return new ShapelessCraftingRecipe(plugin, key.toLowerCase(), recipeParts, output, permission); + } + } + + /** + * Test for shapeless recipes. + */ + public static final class RecipeTest { + /** + * The remaining items left to be found. + */ + private final List remaining; + + private RecipeTest(@NotNull final ShapelessCraftingRecipe recipe) { + this.remaining = new ArrayList<>(recipe.getParts()); + } + + /** + * If the item is in the recipe, remove it from the remaining items to test and + * return the matching item. + * + * @param itemStack The item. + * @return The matching item, or null if no match was found. + */ + @Nullable + public TestableItem matchAndRemove(@NotNull final ItemStack itemStack) { + if (remaining.isEmpty() && !(new EmptyTestableItem().matches(itemStack))) { + return null; + } + + Optional match = remaining.stream() + .filter(item -> item.matches(itemStack)) + .findFirst(); + + match.ifPresent(remaining::remove); + + return match.orElse(null); + } + } +} diff --git a/eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/EcoSpigotPlugin.kt b/eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/EcoSpigotPlugin.kt index ec2162d6..11b637d1 100644 --- a/eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/EcoSpigotPlugin.kt +++ b/eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/EcoSpigotPlugin.kt @@ -110,7 +110,8 @@ import com.willfp.eco.internal.spigot.math.evaluateExpression import com.willfp.eco.internal.spigot.proxy.FastItemStackFactoryProxy import com.willfp.eco.internal.spigot.proxy.SkullProxy import com.willfp.eco.internal.spigot.proxy.TPSProxy -import com.willfp.eco.internal.spigot.recipes.ShapedRecipeListener +import com.willfp.eco.internal.spigot.recipes.CraftingRecipeListener +import com.willfp.eco.internal.spigot.recipes.ShapelessStackedRecipeListener import com.willfp.eco.internal.spigot.recipes.StackedRecipeListener import com.willfp.eco.internal.spigot.recipes.listeners.ComplexInComplex import com.willfp.eco.internal.spigot.recipes.listeners.ComplexInVanilla @@ -156,8 +157,8 @@ abstract class EcoSpigotPlugin : EcoPlugin() { Entities.registerArgParser(EntityArgParserSilent()) Entities.registerArgParser(EntityArgParserEquipment()) - ShapedRecipeListener.registerListener(ComplexInComplex()) - ShapedRecipeListener.registerListener(ComplexInVanilla()) + CraftingRecipeListener.registerListener(ComplexInComplex()) + CraftingRecipeListener.registerListener(ComplexInVanilla()) SegmentParserGroup().register() SegmentParserUseIfPresent().register() @@ -263,9 +264,8 @@ abstract class EcoSpigotPlugin : EcoPlugin() { IntegrationLoader("HeadDatabase") { CustomItemsManager.register(CustomItemsHeadDatabase(this)) }, IntegrationLoader("ExecutableItems") { CustomItemsManager.register(CustomItemsExecutableItems()) }, IntegrationLoader("CustomCrafting") { - CustomItemsManager.register(CustomItemsCustomCrafting()); ShapedRecipeListener.registerValidator( - CustomRecipeCustomCrafting() - ) + CustomItemsManager.register(CustomItemsCustomCrafting()) + CraftingRecipeListener.registerValidator(CustomRecipeCustomCrafting()) }, IntegrationLoader("MythicMobs") { CustomItemsManager.register(CustomItemsMythicMobs(this)) }, @@ -317,8 +317,9 @@ abstract class EcoSpigotPlugin : EcoPlugin() { NaturalExpGainListeners(), ArmorListener(), EntityDeathByEntityListeners(this), - ShapedRecipeListener(), + CraftingRecipeListener(), StackedRecipeListener(this), + ShapelessStackedRecipeListener(this), GUIListener(this), ArrowDataListener(this), ArmorChangeEventListeners(this), diff --git a/eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/recipes/ShapedRecipeListener.kt b/eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/recipes/CraftingRecipeListener.kt similarity index 97% rename from eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/recipes/ShapedRecipeListener.kt rename to eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/recipes/CraftingRecipeListener.kt index 18c83085..b26634b0 100644 --- a/eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/recipes/ShapedRecipeListener.kt +++ b/eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/recipes/CraftingRecipeListener.kt @@ -9,7 +9,7 @@ import org.bukkit.event.inventory.CraftItemEvent import org.bukkit.event.inventory.PrepareItemCraftEvent import org.bukkit.event.player.PlayerRecipeDiscoverEvent -class ShapedRecipeListener : Listener { +class CraftingRecipeListener : Listener { @EventHandler fun preventLearningDisplayedRecipes(event: PlayerRecipeDiscoverEvent) { if (!EcoPlugin.getPluginNames().contains(event.recipe.namespace)) { diff --git a/eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/recipes/ShapelessStackedRecipeListener.kt b/eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/recipes/ShapelessStackedRecipeListener.kt new file mode 100644 index 00000000..7c60e2e3 --- /dev/null +++ b/eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/recipes/ShapelessStackedRecipeListener.kt @@ -0,0 +1,136 @@ +package com.willfp.eco.internal.spigot.recipes + +import com.willfp.eco.core.EcoPlugin +import com.willfp.eco.core.recipe.Recipes +import com.willfp.eco.core.recipe.parts.EmptyTestableItem +import com.willfp.eco.core.recipe.parts.GroupedTestableItems +import com.willfp.eco.core.recipe.parts.TestableStack +import com.willfp.eco.core.recipe.recipes.ShapelessCraftingRecipe +import org.bukkit.Material +import org.bukkit.event.EventHandler +import org.bukkit.event.EventPriority +import org.bukkit.event.Listener +import org.bukkit.event.inventory.InventoryClickEvent +import org.bukkit.inventory.CraftingInventory +import kotlin.math.max +import kotlin.math.min + +class ShapelessStackedRecipeListener( + private val plugin: EcoPlugin +) : Listener { + /* + If you think you can fix this code, you're wrong. + Or, pray to whatever god you have that you can figure it out. + Best of luck, you're going to need it. + */ + @EventHandler(priority = EventPriority.HIGH, ignoreCancelled = true) + fun handleStacks(event: InventoryClickEvent) { + val inventory = event.clickedInventory as? CraftingInventory ?: return + if (event.slot != 0) { + return + } + + // Just in case + if (EmptyTestableItem().matches(inventory.getItem(event.slot))) { + return + } + + val matrix = inventory.matrix + + val recipe = Recipes.getMatch(matrix) as? ShapelessCraftingRecipe ?: return + + var isStackedRecipe = false + var maxCraftable = Int.MAX_VALUE + + val test = recipe.newTest() + + // Start by calculating the maximum number of items to craft + for (i in 0..8) { + val item = inventory.matrix.getOrNull(i) ?: continue + val part = test.matchAndRemove(item).let { + if (it is GroupedTestableItems) { + it.getMatchingChild(item) + } else it + } ?: continue + + if (part is TestableStack) { + isStackedRecipe = true + } + + maxCraftable = min(maxCraftable, Math.floorDiv(item.amount, part.item.amount)) + } + + if (!isStackedRecipe) { + return + } + + // Don't allow crafting above the max stack size of the output + maxCraftable = min(maxCraftable, Math.floorDiv(recipe.output.maxStackSize, recipe.output.amount)) + + // Run this first before the deduction or shift-clicking breaks + val existingResult = inventory.result + + val test2 = recipe.newTest(); + + // Deduct the correct number of items from the inventory + for (i in 0..8) { + val item = inventory.matrix.getOrNull(i) ?: continue + val part = test2.matchAndRemove(item).let { + if (it is GroupedTestableItems) { + it.getMatchingChild(item) + } else it + } ?: continue + + val amount = max( + if (event.isShiftClick) { + item.amount - (part.item.amount * maxCraftable) + } else { + item.amount - part.item.amount + }, 0 + ) + + // Anti-Underflow + if (amount == 0) { + item.type = Material.AIR + } + item.amount = amount + + /* + Everything below this point is unreadable garbage + If you want to modify the behaviour of stacked recipes, then + change the code above. The code after this just sets the items + in the inventory, despite spigot trying to stop me. + */ + + // Do it twice because spigot hates me + // Everything has to be cloned because the inventory changes the item + inventory.matrix[i] = item.clone() // Use un-cloned version first + // This isn't even funny anymore + runTwice { + val newItem = item.clone() + // Just use every method possible to set the item + inventory.matrix[i] = newItem + inventory.setItem(i + 1, newItem) + // Just to be safe, modify the instance (safe check) Using ?. causes a warning. + @Suppress("SENSELESS_COMPARISON") // I hate compiler warnings + if (inventory.matrix[i] != null) { + inventory.matrix[i].amount = amount + } + } + } + + // Multiply the result by the amount to craft if shift-clicking + existingResult ?: return + + // Modify the item and then set it + if (event.isShiftClick) { + existingResult.amount *= maxCraftable + } + inventory.result = existingResult + } + + private fun runTwice(block: () -> Unit) { + block() + plugin.scheduler.run(block) + } +} diff --git a/eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/recipes/listeners/ComplexInComplex.kt b/eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/recipes/listeners/ComplexInComplex.kt index c9d67826..ee7e2471 100644 --- a/eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/recipes/listeners/ComplexInComplex.kt +++ b/eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/recipes/listeners/ComplexInComplex.kt @@ -2,9 +2,9 @@ package com.willfp.eco.internal.spigot.recipes.listeners import com.willfp.eco.core.EcoPlugin import com.willfp.eco.core.recipe.Recipes +import com.willfp.eco.internal.spigot.recipes.CraftingRecipeListener import com.willfp.eco.internal.spigot.recipes.GenericCraftEvent import com.willfp.eco.internal.spigot.recipes.RecipeListener -import com.willfp.eco.internal.spigot.recipes.ShapedRecipeListener import org.bukkit.entity.Player class ComplexInComplex : RecipeListener { @@ -19,7 +19,7 @@ class ComplexInComplex : RecipeListener { val matrix = event.inventory.matrix - if (ShapedRecipeListener.validators.any { it.validate(event) }) { + if (CraftingRecipeListener.validators.any { it.validate(event) }) { return } @@ -44,4 +44,4 @@ class ComplexInComplex : RecipeListener { event.deny() } } -} \ No newline at end of file +} diff --git a/eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/recipes/listeners/ComplexInVanilla.kt b/eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/recipes/listeners/ComplexInVanilla.kt index c55ed64e..00849e61 100644 --- a/eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/recipes/listeners/ComplexInVanilla.kt +++ b/eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/recipes/listeners/ComplexInVanilla.kt @@ -2,9 +2,9 @@ package com.willfp.eco.internal.spigot.recipes.listeners import com.willfp.eco.core.EcoPlugin import com.willfp.eco.core.items.Items +import com.willfp.eco.internal.spigot.recipes.CraftingRecipeListener import com.willfp.eco.internal.spigot.recipes.GenericCraftEvent import com.willfp.eco.internal.spigot.recipes.RecipeListener -import com.willfp.eco.internal.spigot.recipes.ShapedRecipeListener class ComplexInVanilla : RecipeListener { override fun handle(event: GenericCraftEvent) { @@ -12,7 +12,7 @@ class ComplexInVanilla : RecipeListener { return } - if (ShapedRecipeListener.validators.any { it.validate(event) }) { + if (CraftingRecipeListener.validators.any { it.validate(event) }) { return } @@ -22,4 +22,4 @@ class ComplexInVanilla : RecipeListener { } } } -} \ No newline at end of file +}