Added shapeless recipe support

This commit is contained in:
Auxilor
2022-03-07 11:32:18 +00:00
parent 9a66e78dcd
commit 595bc76294
7 changed files with 492 additions and 21 deletions

View File

@@ -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<EcoPlugin> 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);
}

View File

@@ -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<EcoPlugin> implements CraftingRecipe {
/**
* Recipe parts.
*/
private final List<TestableItem> 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<TestableItem> 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<TestableItem> items = new ArrayList<>();
if (part instanceof GroupedTestableItems group) {
items.addAll(group.getChildren());
} else {
items.add(part);
}
List<ItemStack> displayedItems = new ArrayList<>();
for (TestableItem testableItem : items) {
if (testableItem instanceof TestableStack) {
ItemStack item = testableItem.getItem().clone();
ItemMeta meta = item.getItemMeta();
assert meta != null;
List<String> 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<TestableItem> 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<TestableItem> 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<TestableItem> 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<TestableItem> match = remaining.stream()
.filter(item -> item.matches(itemStack))
.findFirst();
match.ifPresent(remaining::remove);
return match.orElse(null);
}
}
}

View File

@@ -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),

View File

@@ -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)) {

View File

@@ -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)
}
}

View File

@@ -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()
}
}
}
}

View File

@@ -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 {
}
}
}
}
}