From e0025e5ac53b769e00fdf3e37b3206479cd7a719 Mon Sep 17 00:00:00 2001 From: Traqueur <54551467+Traqueur-dev@users.noreply.github.com> Date: Fri, 25 Jul 2025 17:00:59 +0200 Subject: [PATCH 1/7] Feat/improve (#25) --- gradle.properties | 2 +- .../fr/traqueur/recipes/api/RecipesAPI.java | 7 ++++++ .../recipes/impl/PrepareCraftListener.java | 18 +++++++++++++-- .../ingredients/ItemStackIngredient.java | 7 ++++++ .../ingredients/MaterialIngredient.java | 5 ++++ .../StrictItemStackIngredient.java | 7 ++++++ .../domains/ingredients/TagIngredient.java | 5 ++++ .../fr/traqueur/recipes/impl/hook/Hooks.java | 7 +++--- .../impl/hook/hooks/ItemsAdderIngredient.java | 23 +++++++++++-------- .../impl/hook/hooks/OraxenIngredient.java | 5 ++++ src/main/resources/version.properties | 2 +- 11 files changed, 72 insertions(+), 16 deletions(-) diff --git a/gradle.properties b/gradle.properties index 516314f..fb7cb53 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1 +1 @@ -version=2.0.1 \ No newline at end of file +version=2.0.2 \ No newline at end of file diff --git a/src/main/java/fr/traqueur/recipes/api/RecipesAPI.java b/src/main/java/fr/traqueur/recipes/api/RecipesAPI.java index 317a6c9..7c746da 100644 --- a/src/main/java/fr/traqueur/recipes/api/RecipesAPI.java +++ b/src/main/java/fr/traqueur/recipes/api/RecipesAPI.java @@ -201,4 +201,11 @@ public JavaPlugin getPlugin() { public boolean isDebug() { return debug; } + + public void debug(String message, Object... args) { + String formattedMessage = String.format(message, args); + if (debug) { + this.plugin.getLogger().info(formattedMessage); + } + } } \ No newline at end of file diff --git a/src/main/java/fr/traqueur/recipes/impl/PrepareCraftListener.java b/src/main/java/fr/traqueur/recipes/impl/PrepareCraftListener.java index 903f406..62dd6f7 100644 --- a/src/main/java/fr/traqueur/recipes/impl/PrepareCraftListener.java +++ b/src/main/java/fr/traqueur/recipes/impl/PrepareCraftListener.java @@ -76,7 +76,10 @@ public void onSmelt(BlockCookEvent event) { .findFirst() .ifPresent(recipe -> { if(!isSimilar(item, itemRecipe.ingredients()[0])) { + this.api.debug("The smelting recipe %s is not good.", itemRecipe.getKey()); event.setCancelled(true); + } else { + this.api.debug("The smelting recipe %s is good.", itemRecipe.getKey()); } }); } @@ -112,6 +115,7 @@ public void onSmithingTransform(PrepareSmithingEvent event) { if (!itemRecipe.getKey() .equals(recipe.getKey())) continue; + this.api.debug("The recipe %s is a smithing recipe.", itemRecipe.getKey()); Ingredient templateIngredient = itemRecipe.ingredients()[0]; Ingredient baseIngredient = itemRecipe.ingredients()[1]; Ingredient additionIngredient = itemRecipe.ingredients()[2]; @@ -121,9 +125,11 @@ && isSimilar(base, baseIngredient) && isSimilar(addition, additionIngredient); if(!isSimilar) { + this.api.debug("The smithing recipe %s is not good.", itemRecipe.getKey()); event.setResult(new ItemStack(Material.AIR)); return; } + this.api.debug("The smithing recipe %s is good.", itemRecipe.getKey()); } } @@ -153,11 +159,13 @@ public void onPrepareCraft(PrepareItemCraftEvent event) { for (ItemRecipe itemRecipe : itemRecipes) { if(recipe instanceof ShapedRecipe shapedRecipe && itemRecipe.recipeType() == RecipeType.CRAFTING_SHAPED) { if (!shapedRecipe.getKey().equals(itemRecipe.getKey())) continue; + this.api.debug("The recipe %s is a shaped recipe.", itemRecipe.getKey()); this.checkGoodShapedRecipe(itemRecipe, event); } if(recipe instanceof ShapelessRecipe shapelessRecipe && itemRecipe.recipeType() == RecipeType.CRAFTING_SHAPELESS) { if(!shapelessRecipe.getKey().equals(itemRecipe.getKey())) continue; + this.api.debug("The recipe %s is a shapeless recipe.", itemRecipe.getKey()); this.checkGoodShapelessRecipe(itemRecipe, event); } } @@ -169,18 +177,19 @@ public void onPrepareCraft(PrepareItemCraftEvent event) { * @param event the event */ private void checkGoodShapedRecipe(ItemRecipe itemRecipe, PrepareItemCraftEvent event) { - AtomicBoolean isSimilar = new AtomicBoolean(true); ItemStack[] matrix = event.getInventory().getMatrix(); matrix = Arrays.stream(matrix).filter(stack -> stack != null && stack.getType() != Material.AIR).toArray(ItemStack[]::new); String[] pattern = Arrays.stream(itemRecipe.pattern()).map(s -> s.split("")).flatMap(Arrays::stream).toArray(String[]::new); for (int i = 0; i < matrix.length; i++) { + AtomicBoolean isSimilar = new AtomicBoolean(true); ItemStack stack = matrix[i]; char sign = pattern[i].charAt(0); Arrays.stream(itemRecipe.ingredients()).filter(ingredient -> ingredient.sign() == sign).findFirst().ifPresent(ingredient -> { isSimilar.set(ingredient.isSimilar(stack)); }); if(!isSimilar.get()) { + this.api.debug("The shaped recipe %s is not good.", itemRecipe.getKey()); event.getInventory().setResult(new ItemStack(Material.AIR)); return; } @@ -203,13 +212,18 @@ private void checkGoodShapelessRecipe(ItemRecipe itemRecipe, PrepareItemCraftEve return ingredient.isSimilar(stack); }); if (!found) { + this.api.debug("Ingredient %s not found in the matrix.", ingredient.toString()); isSimilar.set(false); break; } + this.api.debug("Ingredient %s found in the matrix.", ingredient.toString()); } - if (!isSimilar.get()) { + if (!isSimilar.get() || matrix.size() != itemIngredients.length) { + this.api.debug("The shapeless recipe %s is not good.", itemRecipe.getKey()); event.getInventory().setResult(new ItemStack(Material.AIR)); + return; } + this.api.debug("The shapeless recipe %s is good.", itemRecipe.getKey()); } } diff --git a/src/main/java/fr/traqueur/recipes/impl/domains/ingredients/ItemStackIngredient.java b/src/main/java/fr/traqueur/recipes/impl/domains/ingredients/ItemStackIngredient.java index 7d4ad39..3d62181 100644 --- a/src/main/java/fr/traqueur/recipes/impl/domains/ingredients/ItemStackIngredient.java +++ b/src/main/java/fr/traqueur/recipes/impl/domains/ingredients/ItemStackIngredient.java @@ -79,4 +79,11 @@ private boolean similarMeta(ItemMeta sourceMeta, ItemMeta ingredientMeta) { public RecipeChoice choice() { return new RecipeChoice.MaterialChoice(this.item.getType()); } + + @Override + public String toString() { + return "ItemStackIngredient{" + + "item=" + item + + '}'; + } } diff --git a/src/main/java/fr/traqueur/recipes/impl/domains/ingredients/MaterialIngredient.java b/src/main/java/fr/traqueur/recipes/impl/domains/ingredients/MaterialIngredient.java index 2cb9244..b6b1362 100644 --- a/src/main/java/fr/traqueur/recipes/impl/domains/ingredients/MaterialIngredient.java +++ b/src/main/java/fr/traqueur/recipes/impl/domains/ingredients/MaterialIngredient.java @@ -48,4 +48,9 @@ public boolean isSimilar(ItemStack item) { public RecipeChoice choice() { return new RecipeChoice.MaterialChoice(this.material); } + + @Override + public String toString() { + return this.material.toString(); + } } diff --git a/src/main/java/fr/traqueur/recipes/impl/domains/ingredients/StrictItemStackIngredient.java b/src/main/java/fr/traqueur/recipes/impl/domains/ingredients/StrictItemStackIngredient.java index 55aa955..de925c0 100644 --- a/src/main/java/fr/traqueur/recipes/impl/domains/ingredients/StrictItemStackIngredient.java +++ b/src/main/java/fr/traqueur/recipes/impl/domains/ingredients/StrictItemStackIngredient.java @@ -40,4 +40,11 @@ public boolean isSimilar(ItemStack item) { public RecipeChoice choice() { return new RecipeChoice.ExactChoice(this.item); } + + @Override + public String toString() { + return "StrictItemStackIngredient{" + + "item=" + item + + '}'; + } } diff --git a/src/main/java/fr/traqueur/recipes/impl/domains/ingredients/TagIngredient.java b/src/main/java/fr/traqueur/recipes/impl/domains/ingredients/TagIngredient.java index 7a8f352..cce6e36 100644 --- a/src/main/java/fr/traqueur/recipes/impl/domains/ingredients/TagIngredient.java +++ b/src/main/java/fr/traqueur/recipes/impl/domains/ingredients/TagIngredient.java @@ -49,4 +49,9 @@ public boolean isSimilar(ItemStack item) { public RecipeChoice choice() { return new RecipeChoice.MaterialChoice(this.tag); } + + @Override + public String toString() { + return this.tag.getKey().toString(); + } } diff --git a/src/main/java/fr/traqueur/recipes/impl/hook/Hooks.java b/src/main/java/fr/traqueur/recipes/impl/hook/Hooks.java index 9b42086..081fb70 100644 --- a/src/main/java/fr/traqueur/recipes/impl/hook/Hooks.java +++ b/src/main/java/fr/traqueur/recipes/impl/hook/Hooks.java @@ -23,10 +23,11 @@ public Ingredient getIngredient(String data, Character sign) { @Override public ItemStack getItemStack(String data) { - if(!CustomStack.isInRegistry(data)) { - throw new IllegalArgumentException("The item " + data + " is not registered in ItemsAdder."); + CustomStack stack = CustomStack.getInstance(data); + if (stack == null) { + throw new IllegalArgumentException("ItemsAdder item with id " + data + " not found"); } - return CustomStack.getInstance(data).getItemStack(); + return stack.getItemStack(); } }, /** diff --git a/src/main/java/fr/traqueur/recipes/impl/hook/hooks/ItemsAdderIngredient.java b/src/main/java/fr/traqueur/recipes/impl/hook/hooks/ItemsAdderIngredient.java index e92fe02..b7ec83d 100644 --- a/src/main/java/fr/traqueur/recipes/impl/hook/hooks/ItemsAdderIngredient.java +++ b/src/main/java/fr/traqueur/recipes/impl/hook/hooks/ItemsAdderIngredient.java @@ -4,6 +4,7 @@ import fr.traqueur.recipes.api.domains.Ingredient; import org.bukkit.inventory.ItemStack; import org.bukkit.inventory.RecipeChoice; +import org.checkerframework.checker.units.qual.C; /** * This class is an implementation of the BaseIngredient class. @@ -14,8 +15,8 @@ public class ItemsAdderIngredient extends Ingredient { /** * The CustomStack object that represents the item from ItemsAdder. */ - private final CustomStack customStack; + private final String data; /** * Constructor of the class. * @param data The id of the item from ItemsAdder. @@ -23,10 +24,7 @@ public class ItemsAdderIngredient extends Ingredient { */ public ItemsAdderIngredient(String data, Character sign) { super(sign); - this.customStack = CustomStack.getInstance(data); - if(this.customStack == null) { - throw new IllegalArgumentException("The item " + data + " is not registered in ItemsAdder."); - } + this.data = data; } /** @@ -44,8 +42,7 @@ public ItemsAdderIngredient(String data) { public boolean isSimilar(ItemStack ingredient) { CustomStack item = CustomStack.byItemStack(ingredient); if (item == null) return false; - if (!item.getNamespacedID().equals(this.customStack.getNamespacedID())) return false; - return true; + return item.getNamespacedID().equals(this.getCustomStack().getNamespacedID()); } /** @@ -53,11 +50,19 @@ public boolean isSimilar(ItemStack ingredient) { */ @Override public RecipeChoice choice() { - return new RecipeChoice.MaterialChoice(this.customStack.getItemStack().getType()); + return new RecipeChoice.MaterialChoice(this.getCustomStack().getItemStack().getType()); + } + + private CustomStack getCustomStack() { + CustomStack customStack = CustomStack.getInstance(data); + if(customStack == null) { + throw new IllegalArgumentException("The item " + data + " is not registered in ItemsAdder."); + } + return customStack; } @Override public String toString() { - return this.customStack.getNamespacedID(); + return this.getCustomStack().getNamespacedID(); } } diff --git a/src/main/java/fr/traqueur/recipes/impl/hook/hooks/OraxenIngredient.java b/src/main/java/fr/traqueur/recipes/impl/hook/hooks/OraxenIngredient.java index 287e7b1..c63f601 100644 --- a/src/main/java/fr/traqueur/recipes/impl/hook/hooks/OraxenIngredient.java +++ b/src/main/java/fr/traqueur/recipes/impl/hook/hooks/OraxenIngredient.java @@ -74,4 +74,9 @@ public boolean isSimilar(ItemStack item) { public RecipeChoice choice() { return new RecipeChoice.MaterialChoice(material); } + + @Override + public String toString() { + return this.id; + } } diff --git a/src/main/resources/version.properties b/src/main/resources/version.properties index 516314f..fb7cb53 100644 --- a/src/main/resources/version.properties +++ b/src/main/resources/version.properties @@ -1 +1 @@ -version=2.0.1 \ No newline at end of file +version=2.0.2 \ No newline at end of file From a485e947c6230649f1bd629db1c263dab935a34a Mon Sep 17 00:00:00 2001 From: Traqueur <54551467+Traqueur-dev@users.noreply.github.com> Date: Sat, 26 Jul 2025 09:34:57 +0200 Subject: [PATCH 2/7] Feat/improve (#27) * fix: ia hook (#23) * feat: improve * feat: remove some useless usage of variable --- .../java/fr/traqueur/recipes/api/RecipeType.java | 5 +++-- .../java/fr/traqueur/recipes/api/RecipesAPI.java | 13 +++++++------ .../java/fr/traqueur/recipes/api/hook/Hook.java | 11 ++++++++--- .../impl/domains/recipes/RecipeConfiguration.java | 12 +++++------- 4 files changed, 23 insertions(+), 18 deletions(-) diff --git a/src/main/java/fr/traqueur/recipes/api/RecipeType.java b/src/main/java/fr/traqueur/recipes/api/RecipeType.java index 5cfe8ed..bbe6186 100644 --- a/src/main/java/fr/traqueur/recipes/api/RecipeType.java +++ b/src/main/java/fr/traqueur/recipes/api/RecipeType.java @@ -1,6 +1,7 @@ package fr.traqueur.recipes.api; import org.bukkit.NamespacedKey; +import org.bukkit.plugin.Plugin; import org.bukkit.plugin.java.JavaPlugin; import java.util.List; @@ -47,7 +48,7 @@ public enum RecipeType { /** * The plugin that registered this enum. */ - private static JavaPlugin plugin; + private static Plugin plugin; /** * The maximum number of ingredients that can be used in this recipe. @@ -83,7 +84,7 @@ public NamespacedKey getNamespacedKey(String key) { * Registers the plugin that is using this enum. * @param plugin the plugin */ - public static void registerPlugin(JavaPlugin plugin) { + public static void registerPlugin(Plugin plugin) { RecipeType.plugin = plugin; } diff --git a/src/main/java/fr/traqueur/recipes/api/RecipesAPI.java b/src/main/java/fr/traqueur/recipes/api/RecipesAPI.java index 7c746da..46161ce 100644 --- a/src/main/java/fr/traqueur/recipes/api/RecipesAPI.java +++ b/src/main/java/fr/traqueur/recipes/api/RecipesAPI.java @@ -6,6 +6,7 @@ import fr.traqueur.recipes.impl.domains.recipes.RecipeConfiguration; import fr.traqueur.recipes.impl.updater.Updater; import org.bukkit.configuration.file.YamlConfiguration; +import org.bukkit.plugin.Plugin; import org.bukkit.plugin.java.JavaPlugin; import java.io.File; @@ -29,7 +30,7 @@ public final class RecipesAPI { /** * The plugin instance */ - private final JavaPlugin plugin; + private final Plugin plugin; /** * If the debug mode is enabled @@ -46,7 +47,7 @@ public final class RecipesAPI { * @param plugin The plugin instance * @param debug If the debug mode is enabled */ - public RecipesAPI(JavaPlugin plugin, boolean debug) { + public RecipesAPI(Plugin plugin, boolean debug) { this(plugin, debug, true); } @@ -56,7 +57,7 @@ public RecipesAPI(JavaPlugin plugin, boolean debug) { * @param debug If the debug mode is enabled * @param enableYmlSupport If the yml support is enabled */ - public RecipesAPI(JavaPlugin plugin, boolean debug, boolean enableYmlSupport) { + public RecipesAPI(Plugin plugin, boolean debug, boolean enableYmlSupport) { this.debug = debug; this.plugin = plugin; this.recipes = new ArrayList<>(); @@ -77,7 +78,7 @@ public RecipesAPI(JavaPlugin plugin, boolean debug, boolean enableYmlSupport) { if(this.debug) { Hook.HOOKS.stream() - .filter(hook -> hook.isEnable(plugin)) + .filter(Hook::isEnable) .forEach(hook -> this.plugin.getLogger().info("Hook enabled: " + hook.getPluginName())); Updater.update("RecipesAPI"); @@ -134,7 +135,7 @@ private void addConfiguredRecipes(File recipeFolder) { */ private void loadRecipe(File file) { YamlConfiguration configuration = YamlConfiguration.loadConfiguration(file); - var recipe = new RecipeConfiguration(this.plugin, file.getName().replace(".yml", ""), configuration) + var recipe = new RecipeConfiguration(file.getName().replace(".yml", ""), configuration) .build(); this.addRecipe(recipe); } @@ -190,7 +191,7 @@ public List getRecipes() { * Get the plugin instance * @return The plugin instance */ - public JavaPlugin getPlugin() { + public Plugin getPlugin() { return plugin; } diff --git a/src/main/java/fr/traqueur/recipes/api/hook/Hook.java b/src/main/java/fr/traqueur/recipes/api/hook/Hook.java index 921e35a..a199b18 100644 --- a/src/main/java/fr/traqueur/recipes/api/hook/Hook.java +++ b/src/main/java/fr/traqueur/recipes/api/hook/Hook.java @@ -2,6 +2,7 @@ import fr.traqueur.recipes.api.domains.Ingredient; import fr.traqueur.recipes.impl.hook.Hooks; +import org.bukkit.Bukkit; import org.bukkit.inventory.ItemStack; import org.bukkit.plugin.java.JavaPlugin; @@ -42,12 +43,16 @@ static void addHook(Hook hook) { /** * Check if the plugin is enabled - * @param plugin The plugin which use the API * @return If the plugin is enabled */ - default boolean isEnable(JavaPlugin plugin) { - return plugin.getServer().getPluginManager().getPlugin(getPluginName()) != null; + default boolean isEnable() { + return Bukkit.getPluginManager().getPlugin(getPluginName()) != null; } + /** + * Get the ItemStack from a result part + * @param resultPart The result part to get the ItemStack from + * @return The ItemStack from the result part + */ ItemStack getItemStack(String resultPart); } diff --git a/src/main/java/fr/traqueur/recipes/impl/domains/recipes/RecipeConfiguration.java b/src/main/java/fr/traqueur/recipes/impl/domains/recipes/RecipeConfiguration.java index 3554323..b399bc8 100644 --- a/src/main/java/fr/traqueur/recipes/impl/domains/recipes/RecipeConfiguration.java +++ b/src/main/java/fr/traqueur/recipes/impl/domains/recipes/RecipeConfiguration.java @@ -80,22 +80,20 @@ public class RecipeConfiguration implements Recipe { /** * The constructor of the recipe. - * @param plugin the plugin of the recipe. * @param name the name of the recipe. * @param configuration the configuration of the recipe. */ - public RecipeConfiguration(JavaPlugin plugin, String name, YamlConfiguration configuration) { - this(plugin, name, "", configuration); + public RecipeConfiguration(String name, YamlConfiguration configuration) { + this(name, "", configuration); } /** * The constructor of the recipe. - * @param plugin the plugin of the recipe. * @param name the name of the recipe. * @param path the path of the recipe. * @param configuration the configuration of the recipe. */ - public RecipeConfiguration(JavaPlugin plugin, String name, String path, YamlConfiguration configuration) { + public RecipeConfiguration(String name, String path, YamlConfiguration configuration) { this.name = name.replace(".yml", ""); if(!path.endsWith(".") && !path.isEmpty()) { path += "."; @@ -140,7 +138,7 @@ public RecipeConfiguration(JavaPlugin plugin, String name, String path, YamlConf yield new ItemStackIngredient(this.getItemStack(data[1]), sign); } default -> Hook.HOOKS.stream() - .filter(hook -> hook.isEnable(plugin)) + .filter(Hook::isEnable) .filter(hook -> hook.getPluginName().equalsIgnoreCase(data[0])) .findFirst() .orElseThrow(() -> new IllegalArgumentException("The data " + data[0] + " isn't valid.")) @@ -163,7 +161,7 @@ public RecipeConfiguration(JavaPlugin plugin, String name, String path, YamlConf case "material" -> new ItemStack(this.getMaterial(resultParts[1])); case "item", "base64" -> this.getItemStack(resultParts[1]); default -> Hook.HOOKS.stream() - .filter(hook -> hook.isEnable(plugin)) + .filter(Hook::isEnable) .filter(hook -> hook.getPluginName().equalsIgnoreCase(resultParts[0])) .findFirst() .orElseThrow(() -> new IllegalArgumentException("The result " + strItem + " isn't valid.")) From a016d67b3da95c7dd5967871c38bfc66a9c35688 Mon Sep 17 00:00:00 2001 From: Traqueur <54551467+Traqueur-dev@users.noreply.github.com> Date: Wed, 1 Oct 2025 19:43:59 +0200 Subject: [PATCH 3/7] Hotfix/air recipe (#30) * fix: ia hook (#23) * Feat/improve (#25) * Feat/improve (#27) * fix: ia hook (#23) * feat: improve * feat: remove some useless usage of variable * fix: air in recipe doesn't work --- .../recipes/impl/PrepareCraftListener.java | 25 ++++++++++++++++--- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/src/main/java/fr/traqueur/recipes/impl/PrepareCraftListener.java b/src/main/java/fr/traqueur/recipes/impl/PrepareCraftListener.java index 62dd6f7..493bfa4 100644 --- a/src/main/java/fr/traqueur/recipes/impl/PrepareCraftListener.java +++ b/src/main/java/fr/traqueur/recipes/impl/PrepareCraftListener.java @@ -178,18 +178,35 @@ public void onPrepareCraft(PrepareItemCraftEvent event) { */ private void checkGoodShapedRecipe(ItemRecipe itemRecipe, PrepareItemCraftEvent event) { ItemStack[] matrix = event.getInventory().getMatrix(); - matrix = Arrays.stream(matrix).filter(stack -> stack != null && stack.getType() != Material.AIR).toArray(ItemStack[]::new); String[] pattern = Arrays.stream(itemRecipe.pattern()).map(s -> s.split("")).flatMap(Arrays::stream).toArray(String[]::new); - for (int i = 0; i < matrix.length; i++) { - AtomicBoolean isSimilar = new AtomicBoolean(true); + for (int i = 0; i < matrix.length && i < pattern.length; i++) { ItemStack stack = matrix[i]; char sign = pattern[i].charAt(0); + + // Si le pattern indique un espace (air), vérifier que l'item est null ou AIR + if (sign == ' ') { + if (stack != null && stack.getType() != Material.AIR) { + this.api.debug("The shaped recipe %s is not good - expected air at position %d.", itemRecipe.getKey(), i); + event.getInventory().setResult(new ItemStack(Material.AIR)); + return; + } + continue; + } + + // Si l'item est null ou AIR mais que le pattern attend un ingrédient + if (stack == null || stack.getType() == Material.AIR) { + this.api.debug("The shaped recipe %s is not good - missing ingredient at position %d.", itemRecipe.getKey(), i); + event.getInventory().setResult(new ItemStack(Material.AIR)); + return; + } + + AtomicBoolean isSimilar = new AtomicBoolean(false); Arrays.stream(itemRecipe.ingredients()).filter(ingredient -> ingredient.sign() == sign).findFirst().ifPresent(ingredient -> { isSimilar.set(ingredient.isSimilar(stack)); }); if(!isSimilar.get()) { - this.api.debug("The shaped recipe %s is not good.", itemRecipe.getKey()); + this.api.debug("The shaped recipe %s is not good - ingredient mismatch at position %d.", itemRecipe.getKey(), i); event.getInventory().setResult(new ItemStack(Material.AIR)); return; } From 839b5e4a6478c308b4d5a92bbee49fbcefa7b025 Mon Sep 17 00:00:00 2001 From: Traqueur <54551467+Traqueur-dev@users.noreply.github.com> Date: Fri, 24 Oct 2025 09:11:11 +0200 Subject: [PATCH 4/7] Fix/result not appear (#33) * fix: ia hook (#23) * Feat/improve (#25) * Feat/improve (#27) * fix: ia hook (#23) * feat: improve * feat: remove some useless usage of variable * version 3.0.0 (#32) * Feat/improve (#25) * Feat/improve (#27) * fix: ia hook (#23) * feat: improve * feat: remove some useless usage of variable * Hotfix/air recipe (#30) * fix: ia hook (#23) * Feat/improve (#25) * Feat/improve (#27) * fix: ia hook (#23) * feat: improve * feat: remove some useless usage of variable * fix: air in recipe doesn't work * Feat/modernize (#31) * feat: modernize * feat: version * fix: shapeless craft * fix: set handler highest and add result manually * fix: remove useless properties * feat: java version * feat: improve jitpack --- README.md | 286 ++++++++++++-- build.gradle | 11 +- gradle.properties | 2 +- jitpack.yml | 4 +- .../java/fr/traqueur/recipes/api/Base64.java | 348 ------------------ .../fr/traqueur/recipes/api/RecipeLoader.java | 224 +++++++++++ .../fr/traqueur/recipes/api/RecipesAPI.java | 101 +---- .../traqueur/recipes/api/domains/Recipe.java | 2 +- .../recipes/impl/PrepareCraftListener.java | 40 +- .../ingredients/ItemStackIngredient.java | 26 +- .../domains/recipes/RecipeConfiguration.java | 115 +++++- .../recipes/impl/updater/Updater.java | 4 +- src/main/resources/recipeapi.properties | 1 + src/main/resources/version.properties | 2 +- 14 files changed, 644 insertions(+), 522 deletions(-) delete mode 100644 src/main/java/fr/traqueur/recipes/api/Base64.java create mode 100644 src/main/java/fr/traqueur/recipes/api/RecipeLoader.java create mode 100644 src/main/resources/recipeapi.properties diff --git a/README.md b/README.md index d758bc7..de5d4a5 100644 --- a/README.md +++ b/README.md @@ -5,11 +5,11 @@ **RecipesAPI** is a lightweight and easy-to-use API that allows you to create custom recipes for your Spigot server. Whether you want to add custom shaped or shapeless crafting recipes, furnace smelting recipes, or other custom item interactions, this API makes it simple to do so. ## Features -- **Create Custom Recipes**: Add shaped, shapeless, and furnace, and other type recipes with ease. -- **Advanced Recipe Handling**: Support for custom ingredients with meta data (e.g., items with custom names). +- **Create Custom Recipes**: Add shaped, shapeless, furnace, and other types of recipes with ease. +- **Advanced Recipe Handling**: Support for custom ingredients with metadata (lore, custom model data, persistent data container). - **Easy Integration**: Simple API to integrate into any Spigot plugin. -- **Hooks**: Support ItemsAdder, Oraxen items. You can create your own hook with your customs items systems. -- **Version Compatibility**: Works with recent Spigot versions and allows you to create recipes dynamically. Folia compatibility if you use FoliaLib. +- **Plugin Hooks**: Built-in support for ItemsAdder and Oraxen items. You can create your own hook with your custom item systems. +- **Version Compatibility**: Works with recent Spigot versions and allows you to create recipes dynamically. - **Lightweight**: No need to include large libraries or dependencies. - **Open Source**: Available under the MIT License. - **Javadoc**: Comprehensive documentation for easy reference. @@ -42,10 +42,12 @@ shadowJar { ## Usage Example -Below is an example of how to use **RecipesAPI** in your Spigot plugin. -This example demonstrates adding four types of recipes: a simple shapeless crafting recipe, a shaped crafting recipe, a custom ingredient shapeless recipe, and a furnace recipe. +Below is an example of how to use **RecipesAPI** in your Spigot plugin. +This example demonstrates adding multiple types of recipes including shapeless, shaped, custom ingredients, and furnace recipes. You can see how easy it is to create and register recipes with the API. -The exemple plugin is available in the `test-plugin` directory. +The example plugin is available in the `test-plugin` directory. + +### Programmatic Recipe Creation ```java public final class TestPlugin extends JavaPlugin { @@ -54,11 +56,11 @@ public final class TestPlugin extends JavaPlugin { @Override public void onEnable() { - // Initialize RecipesAPI + // Initialize RecipesAPI (plugin, debug mode enabled) recipesAPI = new RecipesAPI(this, true); - // Create a simple shapeless crafting recipe (DIRT -> 64 DIAMOND) - ItemRecipe recipe = new RecipeBuilder() + // 1. Simple shapeless crafting recipe (DIRT -> 64 DIAMOND) + ItemRecipe recipe1 = new RecipeBuilder() .setType(RecipeType.CRAFTING_SHAPELESS) .setName("example-simple") .setResult(new ItemStack(Material.DIAMOND)) @@ -66,7 +68,7 @@ public final class TestPlugin extends JavaPlugin { .addIngredient(Material.DIRT) .build(); - // Create a shaped crafting recipe (DIRT and DIAMOND -> 64 DIAMOND) + // 2. Shaped crafting recipe (8 DIRT around 1 DIAMOND -> 64 DIAMOND) ItemRecipe recipe2 = new RecipeBuilder() .setType(RecipeType.CRAFTING_SHAPED) .setName("example-shaped") @@ -77,32 +79,33 @@ public final class TestPlugin extends JavaPlugin { .addIngredient(Material.DIAMOND, 'I') .build(); - // Create a shapeless recipe with a custom ingredient (named PAPER) - ItemStack ingredient = new ItemStack(Material.PAPER); - ItemMeta meta = ingredient.getItemMeta(); - meta.setDisplayName("Dirt Magic"); - ingredient.setItemMeta(meta); + // 3. Custom ingredient with lore (only lore is checked, displayName can be changed by player) + ItemStack magicPaper = new ItemStack(Material.PAPER); + ItemMeta meta = magicPaper.getItemMeta(); + meta.setLore(List.of("§6Magic Paper", "§7Used for special crafting")); + magicPaper.setItemMeta(meta); ItemRecipe recipe3 = new RecipeBuilder() .setType(RecipeType.CRAFTING_SHAPELESS) - .setName("example-complex") + .setName("example-custom-ingredient") .setResult(new ItemStack(Material.DIAMOND)) .setAmount(64) - .addIngredient(ingredient) + .addIngredient(magicPaper) .build(); - // Create a furnace smelting recipe (PAPER -> 64 DIAMOND) + // 4. Furnace smelting recipe with cooking time and experience ItemRecipe recipe4 = new RecipeBuilder() .setType(RecipeType.SMELTING) .setName("example-furnace") .setResult(new ItemStack(Material.DIAMOND)) .setAmount(64) - .addIngredient(ingredient) - .setCookingTime(10) + .addIngredient(Material.COAL) + .setCookingTime(200) // in ticks (200 ticks = 10 seconds) + .setExperience(10.0f) .build(); - // Add the recipes to the API - recipesAPI.addRecipe(recipe); + // Add all recipes to the API + recipesAPI.addRecipe(recipe1); recipesAPI.addRecipe(recipe2); recipesAPI.addRecipe(recipe3); recipesAPI.addRecipe(recipe4); @@ -110,23 +113,234 @@ public final class TestPlugin extends JavaPlugin { } ``` -## How to Use +### Loading Recipes from YAML Files + +RecipesAPI provides a flexible `RecipeLoader` for loading recipes from YAML files: + +```java +public final class TestPlugin extends JavaPlugin { + + private RecipesAPI recipesAPI; + private RecipeLoader recipeLoader; -- **Shapeless Recipe**: Add items to crafting in any arrangement. -- **Shaped Recipe**: Define specific patterns for crafting items. -- **Custom Ingredients**: Use items with custom names or metadata in recipes. -- **Furnace Recipes**: Create custom smelting recipes with adjustable cooking time. + @Override + public void onEnable() { + // Initialize RecipesAPI + recipesAPI = new RecipesAPI(this, true); + + // Create a RecipeLoader and configure it + recipeLoader = recipesAPI.createLoader() + .addFolder("recipes/") // Load all .yml files from recipes/ folder + .addFolder("recipes/custom/") // Load from additional folders + .addFile("special/unique.yml"); // Load a specific file + + // Load all configured recipes + recipeLoader.load(); + } + + // Reload recipes at runtime + public void reloadRecipes() { + recipeLoader.reload(); + } +} +``` + +**How RecipeLoader works:** +- All paths are relative to the plugin's data folder +- `addFolder()` loads recipes recursively from the specified folder +- If a folder doesn't exist, it automatically extracts default recipes from your plugin JAR +- `addFile()` loads a single recipe file +- `load()` loads all configured recipes +- `reload()` unregisters all recipes and reloads them + +## Recipe Types + +RecipesAPI supports all vanilla Minecraft recipe types: + +- **`CRAFTING_SHAPELESS`** - Shapeless crafting recipes (items in any arrangement) +- **`CRAFTING_SHAPED`** - Shaped crafting recipes (specific pattern required) +- **`SMELTING`** - Furnace smelting recipes +- **`BLASTING`** - Blast furnace recipes +- **`SMOKING`** - Smoker recipes +- **`CAMPFIRE_COOKING`** - Campfire cooking recipes +- **`STONE_CUTTING`** - Stonecutter recipes +- **`SMITHING_TRANSFORM`** - Smithing table transformation recipes + +## Custom Ingredients + +The API supports several types of ingredients: + +- **Material**: Simple material type (e.g., `Material.DIAMOND`) +- **ItemStack**: Items with custom metadata (lore, custom model data, PDC) +- **Strict ItemStack**: Exact item match including all metadata +- **Tag**: Minecraft tags (e.g., planks, logs, wool) +- **Plugin Items**: ItemsAdder and Oraxen custom items + +### Important Notes +- **Display Name**: Player can rename items - only lore, custom model data, and PDC are checked +- **Strict Mode**: Use `.addIngredient(item, sign, true)` to require exact match including display name ## API Documentation The API is simple and intuitive to use. You can easily: -- **Define crafting types**: `RecipeType.CRAFTING_SHAPELESS`, `RecipeType.CRAFTING_SHAPED`, -`RecipeType.SMELTING`, etc. -- **Add ingredients**: Either regular materials or custom items with `ItemMeta`. -- **Set crafting patterns**: For shaped recipes, you can define the crafting grid with `.setPattern()`. -- **Control output**: Set the resulting item and amount. - -You can check javadoc here : [Javadoc](https://jitpack.io/com/github/Traqueur-dev/RecipesAPI/latest/javadoc/) -You can check the wiki here : [Wiki](https://github.com/Traqueur-dev/RecipesAPI/wiki) +- **Define crafting types**: All vanilla recipe types supported +- **Add ingredients**: Regular materials, custom items with `ItemMeta`, or plugin items +- **Set crafting patterns**: For shaped recipes, define the crafting grid with `.setPattern()` +- **Control output**: Set the resulting item and amount +- **Configure cooking**: Set cooking time and experience for smelting recipes + +## Plugin Hooks + +RecipesAPI provides built-in support for popular custom item plugins: + +### Using ItemsAdder Items + +```java +// In your YAML recipe file +ingredients: + - item: itemsadder:custom_item_id + +# Or in code +ItemRecipe recipe = new RecipeBuilder() + .setType(RecipeType.CRAFTING_SHAPELESS) + .setName("itemsadder-recipe") + .setResult(itemsAdderItem) // Get from ItemsAdder API + .addIngredient(/* ItemsAdder ingredient */) + .build(); +``` + +### Using Oraxen Items + +```java +// In your YAML recipe file +ingredients: + - item: oraxen:custom_item_id + +# Or in code +ItemRecipe recipe = new RecipeBuilder() + .setType(RecipeType.CRAFTING_SHAPELESS) + .setName("oraxen-recipe") + .setResult(oraxenItem) // Get from Oraxen API + .addIngredient(/* Oraxen ingredient */) + .build(); +``` + +### Creating Custom Hooks + +You can create your own hooks for any custom item plugin: + +```java +public class MyCustomItemHook implements Hook { + + @Override + public String getPluginName() { + return "MyCustomPlugin"; + } + + @Override + public Ingredient getIngredient(String data, Character sign) { + // Create your custom ingredient implementation + return new MyCustomIngredient(data, sign); + } + + @Override + public ItemStack getItemStack(String data) { + // Return the ItemStack for your custom item + return MyCustomPlugin.getItem(data); + } +} + +// Register your hook +Hook.addHook(new MyCustomItemHook()); +``` + +## YAML Configuration + +RecipesAPI supports loading recipes from YAML files. Simply place `.yml` files in your plugin's `recipes/` folder (or any folder you configure with `RecipeLoader`). + +### Recipe File Format + +```yaml +type: CRAFTING_SHAPED +pattern: + - "DDD" + - "DID" + - "DDD" +ingredients: + - item: DIRT + sign: D + - item: DIAMOND + sign: I +result: + item: DIAMOND + amount: 64 +group: "custom_recipes" +category: "MISC" +``` + +### YAML Recipe Fields + +#### Required Fields +- `type` - Recipe type (see Recipe Types section) +- `ingredients` - List of ingredients (see Ingredient Types below) +- `result.item` - The resulting item + +#### Optional Fields +- `result.amount` - Output amount (default: 1) +- `pattern` - Pattern for shaped recipes (max 3 rows, max 3 chars per row) +- `group` - Recipe group for the recipe book +- `category` - Recipe category (BUILDING, REDSTONE, EQUIPMENT, MISC for crafting; FOOD, BLOCKS, MISC for cooking) +- `cooking-time` - Cooking time in ticks for smelting recipes (default: 0) +- `experience` - Experience reward for smelting recipes (default: 0.0) + +### Pattern Validation + +For `CRAFTING_SHAPED` recipes, the pattern is validated: +- Maximum 3 rows +- Maximum 3 characters per row +- All pattern characters must have corresponding ingredients with matching signs +- Empty rows are not allowed + +### Ingredient Types in YAML +- `item: MATERIAL_NAME` - Simple material +- `item: material:MATERIAL_NAME` - Explicit material +- `item: tag:TAG_NAME` - Minecraft tag +- `item: item:BASE64_STRING` or `item: base64:BASE64_STRING` - Custom item from Base64 +- `item: itemsadder:ITEM_ID` - ItemsAdder item +- `item: oraxen:ITEM_ID` - Oraxen item +- `sign: X` - Character used in shaped recipe patterns (required for shaped recipes) +- `strict: true` - Require exact item match including display name (optional, default: false) + +### Example: Smelting Recipe + +```yaml +type: SMELTING +ingredients: + - item: COAL +result: + item: DIAMOND + amount: 64 +cooking-time: 200 +experience: 10.0 +category: MISC +``` + +### Example: Shapeless Recipe with Custom Item + +```yaml +type: CRAFTING_SHAPELESS +ingredients: + - item: item:BASE64_ENCODED_ITEM_HERE + strict: true +result: + item: DIAMOND + amount: 1 +``` + +## Resources + +- **Javadoc**: [API Documentation](https://jitpack.io/com/github/Traqueur-dev/RecipesAPI/latest/javadoc/) +- **Wiki**: [GitHub Wiki](https://github.com/Traqueur-dev/RecipesAPI/wiki) +- **Issues**: [Report bugs or request features](https://github.com/Traqueur-dev/RecipesAPI/issues) ## License This project is licensed under the MIT License. diff --git a/build.gradle b/build.gradle index 161c51d..aeffb6a 100644 --- a/build.gradle +++ b/build.gradle @@ -16,8 +16,7 @@ repositories { url = "https://oss.sonatype.org/content/groups/public/" } maven { - name = "jitpack" - url = "https://jitpack.io" + url "https://maven.devs.beer/" } maven { url "https://repo.oraxen.com/releases" @@ -25,16 +24,16 @@ repositories { } dependencies { - compileOnly "org.spigotmc:spigot-api:1.21.3-R0.1-SNAPSHOT" + compileOnly "org.spigotmc:spigot-api:1.21.4-R0.1-SNAPSHOT" // Hooks compileOnly 'io.th0rgal:oraxen:1.181.0' - compileOnly 'com.github.LoneDev6:API-ItemsAdder:3.6.1' + compileOnly 'dev.lone:api-itemsadder:4.0.10' } tasks.register('generateVersionProperties') { doLast { - def file = new File("$projectDir/src/main/resources/version.properties") + def file = new File("$projectDir/src/main/resources/recipeapi.properties") if (!file.parentFile.exists()) { file.parentFile.mkdirs() } @@ -45,6 +44,8 @@ tasks.register('generateVersionProperties') { processResources.dependsOn generateVersionProperties java { + sourceCompatibility = JavaVersion.VERSION_21 + targetCompatibility = JavaVersion.VERSION_21 withSourcesJar() withJavadocJar() } diff --git a/gradle.properties b/gradle.properties index fb7cb53..4950f0d 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1 +1 @@ -version=2.0.2 \ No newline at end of file +version=3.0.0 diff --git a/jitpack.yml b/jitpack.yml index a202792..3547ff7 100644 --- a/jitpack.yml +++ b/jitpack.yml @@ -1,2 +1,4 @@ jdk: - - openjdk21 \ No newline at end of file + - openjdk21 +install: + - ./gradlew clean publishToMavenLocal -xtest --no-daemon --console=plain \ No newline at end of file diff --git a/src/main/java/fr/traqueur/recipes/api/Base64.java b/src/main/java/fr/traqueur/recipes/api/Base64.java deleted file mode 100644 index 66afc2d..0000000 --- a/src/main/java/fr/traqueur/recipes/api/Base64.java +++ /dev/null @@ -1,348 +0,0 @@ -package fr.traqueur.recipes.api; - -import org.bukkit.inventory.ItemStack; -import org.bukkit.util.io.BukkitObjectInputStream; -import org.bukkit.util.io.BukkitObjectOutputStream; - -import java.io.*; -import java.util.zip.GZIPInputStream; -import java.util.zip.GZIPOutputStream; - -/** - * A class that provides Base64 encoding and decoding as defined by RFC 2045 - * This class is used to encode and decode to and from Base64 in a way - * This class from zEssentials project coded by Maxlego08 - * Link of his project - */ -public final class Base64 { - - static private final int BASELENGTH = 128; - static private final int LOOKUPLENGTH = 64; - static private final int TWENTYFOURBITGROUP = 24; - static private final int EIGHTBIT = 8; - static private final int SIXTEENBIT = 16; - static private final int FOURBYTE = 4; - static private final int SIGN = -128; - static private final char PAD = '='; - static private final boolean fDebug = false; - static final private byte [] base64Alphabet = new byte[BASELENGTH]; - static final private char [] lookUpBase64Alphabet = new char[LOOKUPLENGTH]; - - static { - - for (int i = 0; i < BASELENGTH; ++i) { - base64Alphabet[i] = -1; - } - for (int i = 'Z'; i >= 'A'; i--) { - base64Alphabet[i] = (byte) (i-'A'); - } - for (int i = 'z'; i>= 'a'; i--) { - base64Alphabet[i] = (byte) ( i-'a' + 26); - } - - for (int i = '9'; i >= '0'; i--) { - base64Alphabet[i] = (byte) (i-'0' + 52); - } - - base64Alphabet['+'] = 62; - base64Alphabet['/'] = 63; - - for (int i = 0; i<=25; i++) - lookUpBase64Alphabet[i] = (char)('A'+i); - - for (int i = 26, j = 0; i<=51; i++, j++) - lookUpBase64Alphabet[i] = (char)('a'+ j); - - for (int i = 52, j = 0; i<=61; i++, j++) - lookUpBase64Alphabet[i] = (char)('0' + j); - lookUpBase64Alphabet[62] = (char)'+'; - lookUpBase64Alphabet[63] = (char)'/'; - - } - - /** - * Encodes a itemstack into Base64 - * - * @param item String to encode - * @return Encoded Base64 string - */ - public static String encodeItem(ItemStack item) { - try { - ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); - GZIPOutputStream gzipOutputStream = new GZIPOutputStream(byteArrayOutputStream); - ObjectOutputStream objectOutputStream = new BukkitObjectOutputStream(gzipOutputStream); - objectOutputStream.writeObject(item); - objectOutputStream.close(); - return Base64.encode(byteArrayOutputStream.toByteArray()); - } catch (IOException exception) { - exception.printStackTrace(); - return null; - } - } - - /** - * Decodes a Base64 string into a itemstack - * - * @param data String to decode - * @return Decoded itemstack - */ - public static ItemStack decodeItem(String data) { - try { - ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(Base64.decode(data)); - GZIPInputStream gzipInputStream = new GZIPInputStream(byteArrayInputStream); - ObjectInputStream objectInputStream = new BukkitObjectInputStream(gzipInputStream); - ItemStack item = (ItemStack) objectInputStream.readObject(); - objectInputStream.close(); - return item; - } catch (IOException | ClassNotFoundException exception) { - exception.printStackTrace(); - return null; - } - } - - - /** - * Check if octect is whitespace - * - * @param octect The octet to check - * @return True if the octect is whitespace, false otherwise - */ - protected static boolean isWhiteSpace(char octect) { - return (octect == 0x20 || octect == 0xd || octect == 0xa || octect == 0x9); - } - - /** - * Check if octect is a pad - * - * @param octect The octet to check - * @return True if the octect is a pad, false otherwise - */ - protected static boolean isPad(char octect) { - return (octect == PAD); - } - - /** - * Check if octect is data - * - * @param octect The octet to check - * @return True if the octect is data, false otherwise - */ - protected static boolean isData(char octect) { - return (octect < BASELENGTH && base64Alphabet[octect] != -1); - } - - /** - * Check if octect is base64 - * - * @param octect The octet to check - * @return True if the octect is base64, false otherwise - */ - protected static boolean isBase64(char octect) { - return (isWhiteSpace(octect) || isPad(octect) || isData(octect)); - } - - /** - * Encodes hex octects into Base64 - * - * @param binaryData Array containing binaryData - * @return Encoded Base64 array - */ - public static String encode(byte[] binaryData) { - - if (binaryData == null) - return null; - - int lengthDataBits = binaryData.length*EIGHTBIT; - if (lengthDataBits == 0) { - return ""; - } - - int fewerThan24bits = lengthDataBits%TWENTYFOURBITGROUP; - int numberTriplets = lengthDataBits/TWENTYFOURBITGROUP; - int numberQuartet = fewerThan24bits != 0 ? numberTriplets+1 : numberTriplets; - char encodedData[] = null; - - encodedData = new char[numberQuartet*4]; - - byte k=0, l=0, b1=0,b2=0,b3=0; - - int encodedIndex = 0; - int dataIndex = 0; - if (fDebug) { - System.out.println("number of triplets = " + numberTriplets ); - } - - for (int i=0; i>4 ) ; - decodedData[encodedIndex++] = (byte)(((b2 & 0xf)<<4 ) |( (b3>>2) & 0xf) ); - decodedData[encodedIndex++] = (byte)( b3<<6 | b4 ); - } - - if (!isData( (d1 = base64Data[dataIndex++]) ) || - !isData( (d2 = base64Data[dataIndex++]) )) { - return null;//if found "no data" just return null - } - - b1 = base64Alphabet[d1]; - b2 = base64Alphabet[d2]; - - d3 = base64Data[dataIndex++]; - d4 = base64Data[dataIndex++]; - if (!isData( (d3 ) ) || - !isData( (d4 ) )) {//Check if they are PAD characters - if (isPad( d3 ) && isPad( d4)) { //Two PAD e.g. 3c[Pad][Pad] - if ((b2 & 0xf) != 0)//last 4 bits should be zero - return null; - byte[] tmp = new byte[ i*3 + 1 ]; - System.arraycopy( decodedData, 0, tmp, 0, i*3 ); - tmp[encodedIndex] = (byte)( b1 <<2 | b2>>4 ) ; - return tmp; - } else if (!isPad( d3) && isPad(d4)) { //One PAD e.g. 3cQ[Pad] - b3 = base64Alphabet[ d3 ]; - if ((b3 & 0x3 ) != 0)//last 2 bits should be zero - return null; - byte[] tmp = new byte[ i*3 + 2 ]; - System.arraycopy( decodedData, 0, tmp, 0, i*3 ); - tmp[encodedIndex++] = (byte)( b1 <<2 | b2>>4 ); - tmp[encodedIndex] = (byte)(((b2 & 0xf)<<4 ) |( (b3>>2) & 0xf) ); - return tmp; - } else { - return null;//an error like "3c[Pad]r", "3cdX", "3cXd", "3cXX" where X is non data - } - } else { //No PAD e.g 3cQl - b3 = base64Alphabet[ d3 ]; - b4 = base64Alphabet[ d4 ]; - decodedData[encodedIndex++] = (byte)( b1 <<2 | b2>>4 ) ; - decodedData[encodedIndex++] = (byte)(((b2 & 0xf)<<4 ) |( (b3>>2) & 0xf) ); - decodedData[encodedIndex++] = (byte)( b3<<6 | b4 ); - - } - - return decodedData; - } - - /** - * remove WhiteSpace from MIME containing encoded Base64 data. - * - * @param data the byte array of base64 data (with WS) - * @return the new length - */ - protected static int removeWhiteSpace(char[] data) { - if (data == null) - return 0; - - // count characters that's not whitespace - int newSize = 0; - int len = data.length; - for (int i = 0; i < len; i++) { - if (!isWhiteSpace(data[i])) - data[newSize++] = data[i]; - } - return newSize; - } -} \ No newline at end of file diff --git a/src/main/java/fr/traqueur/recipes/api/RecipeLoader.java b/src/main/java/fr/traqueur/recipes/api/RecipeLoader.java new file mode 100644 index 0000000..7811c82 --- /dev/null +++ b/src/main/java/fr/traqueur/recipes/api/RecipeLoader.java @@ -0,0 +1,224 @@ +package fr.traqueur.recipes.api; + +import fr.traqueur.recipes.impl.domains.ItemRecipe; +import fr.traqueur.recipes.impl.domains.recipes.RecipeConfiguration; +import org.bukkit.configuration.file.YamlConfiguration; +import org.bukkit.plugin.Plugin; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.nio.file.Files; +import java.nio.file.Path; +import java.security.CodeSource; +import java.util.ArrayList; +import java.util.List; +import java.util.jar.JarEntry; +import java.util.jar.JarInputStream; +import java.util.stream.Stream; + +/** + * RecipeLoader allows you to load recipes from multiple sources + * using a fluent API. + */ +public class RecipeLoader { + + /** + * The plugin instance + */ + private final Plugin plugin; + + /** + * The API instance to register recipes + */ + private final RecipesAPI api; + + /** + * List of folders to load recipes from + */ + private final List folders = new ArrayList<>(); + + /** + * List of individual files to load + */ + private final List files = new ArrayList<>(); + + /** + * Create a new RecipeLoader + * Can be instantiated via RecipesAPI.createLoader() + * @param plugin The plugin instance + * @param api The RecipesAPI instance + */ + protected RecipeLoader(Plugin plugin, RecipesAPI api) { + this.plugin = plugin; + this.api = api; + } + + /** + * Add a folder to load recipes from (recursive) + * The path is relative to the plugin's data folder + * If the folder doesn't exist, it will automatically extract default recipes from the JAR + * @param path The path to the folder + * @return This RecipeLoader instance for chaining + */ + public RecipeLoader addFolder(String path) { + File folder = new File(plugin.getDataFolder(), path); + + // If folder doesn't exist, extract defaults from JAR + if (!folder.exists()) { + // Create folder if extraction didn't create it + if (!folder.mkdirs()) { + plugin.getLogger().warning("Could not create folder: " + path); + return this; + } + extractDefaultsFromJar(path); + } + + if (!folder.isDirectory()) { + plugin.getLogger().warning("Path is not a folder: " + path); + return this; + } + this.folders.add(folder); + return this; + } + + /** + * Add a file to load a recipe from + * The path is relative to the plugin's data folder + * @param path The path to the file + * @return This RecipeLoader instance for chaining + */ + public RecipeLoader addFile(String path) { + File file = new File(plugin.getDataFolder(), path); + if (!file.exists()) { + plugin.getLogger().warning("File does not exist: " + path); + return this; + } + if (!file.isFile()) { + plugin.getLogger().warning("Path is not a file: " + path); + return this; + } + if (!file.getName().endsWith(".yml")) { + plugin.getLogger().warning("File is not a YAML file: " + path); + return this; + } + this.files.add(file); + return this; + } + + /** + * Extract default recipes from the JAR to the data folder + * This will scan for .yml files in the specified JAR path and extract them + * if they don't already exist in the data folder + * @param jarPath The path inside the JAR to scan for recipes (e.g., "recipes/") + */ + private void extractDefaultsFromJar(String jarPath) { + if (!jarPath.endsWith("/")) { + jarPath += "/"; + } + + try { + CodeSource src = plugin.getClass().getProtectionDomain().getCodeSource(); + if (src != null) { + URL jar = src.getLocation(); + try (JarInputStream jarStream = new JarInputStream(jar.openStream())) { + JarEntry entry; + while ((entry = jarStream.getNextJarEntry()) != null) { + if (entry.getName().startsWith(jarPath) && entry.getName().endsWith(".yml")) { + File outFile = new File(plugin.getDataFolder(), entry.getName()); + File parentDir = outFile.getParentFile(); + if (parentDir != null && !parentDir.exists() && !parentDir.mkdirs()) { + plugin.getLogger().warning("Could not create directory: " + parentDir.getAbsolutePath()); + continue; + } + if (!outFile.exists()) { + plugin.saveResource(entry.getName(), false); + } + } + } + } + } + } catch (IOException e) { + plugin.getLogger().severe("Could not extract default recipes from JAR: " + e.getMessage()); + } + } + + /** + * Load all recipes from the configured folders and files + * @return The number of recipes loaded + */ + public int load() { + int count = 0; + + // Load from folders + for (File folder : folders) { + count += loadFromFolder(folder); + } + + // Load from individual files + for (File file : files) { + if (loadRecipe(file)) { + count++; + } + } + + plugin.getLogger().info("Loaded " + count + " recipes via RecipeLoader."); + return count; + } + + /** + * Reload all recipes from the configured folders and files + * This will unregister all existing recipes and reload them + * @return The number of recipes loaded + */ + public int reload() { + api.unregisterRecipes(); + return load(); + } + + /** + * Load all recipes from a folder (recursive) + * @param folder The folder to load recipes from + * @return The number of recipes loaded + */ + private int loadFromFolder(File folder) { + int count = 0; + try (Stream stream = Files.walk(folder.toPath())) { + List ymlFiles = stream.map(Path::toFile) + .filter(File::isFile) + .filter(f -> f.getName().endsWith(".yml")) + .toList(); + + for (File file : ymlFiles) { + if (loadRecipe(file)) { + count++; + } + } + } catch (IOException exception) { + plugin.getLogger().severe("Could not load recipes from folder " + folder.getAbsolutePath() + ": " + exception.getMessage()); + } + return count; + } + + /** + * Load a recipe from a file + * @param file The file to load the recipe from + * @return true if the recipe was loaded successfully, false otherwise + */ + private boolean loadRecipe(File file) { + try { + YamlConfiguration configuration = YamlConfiguration.loadConfiguration(file); + ItemRecipe recipe = new RecipeConfiguration(file.getName().replace(".yml", ""), configuration) + .build(); + api.addRecipe(recipe); + return true; + } catch (Exception e) { + plugin.getLogger().severe("Could not load recipe from file " + file.getAbsolutePath() + ": " + e.getMessage()); + if (api.isDebug()) { + e.printStackTrace(); + } + return false; + } + } +} \ No newline at end of file diff --git a/src/main/java/fr/traqueur/recipes/api/RecipesAPI.java b/src/main/java/fr/traqueur/recipes/api/RecipesAPI.java index 46161ce..4f22c8a 100644 --- a/src/main/java/fr/traqueur/recipes/api/RecipesAPI.java +++ b/src/main/java/fr/traqueur/recipes/api/RecipesAPI.java @@ -3,23 +3,14 @@ import fr.traqueur.recipes.api.hook.Hook; import fr.traqueur.recipes.impl.PrepareCraftListener; import fr.traqueur.recipes.impl.domains.ItemRecipe; -import fr.traqueur.recipes.impl.domains.recipes.RecipeConfiguration; import fr.traqueur.recipes.impl.updater.Updater; +import org.bukkit.plugin.Plugin; import org.bukkit.configuration.file.YamlConfiguration; import org.bukkit.plugin.Plugin; import org.bukkit.plugin.java.JavaPlugin; -import java.io.File; -import java.io.IOException; -import java.net.URL; -import java.nio.file.Files; -import java.nio.file.Path; -import java.security.CodeSource; import java.util.ArrayList; import java.util.List; -import java.util.jar.JarEntry; -import java.util.jar.JarInputStream; -import java.util.stream.Stream; /** * RecipesAPI is the main class of the API @@ -42,22 +33,12 @@ public final class RecipesAPI { */ private final List recipes; - /** - * Create a new instance of RecipesAPI with yml support enabled - * @param plugin The plugin instance - * @param debug If the debug mode is enabled - */ - public RecipesAPI(Plugin plugin, boolean debug) { - this(plugin, debug, true); - } - /** * Create a new instance of RecipesAPI * @param plugin The plugin instance * @param debug If the debug mode is enabled - * @param enableYmlSupport If the yml support is enabled */ - public RecipesAPI(Plugin plugin, boolean debug, boolean enableYmlSupport) { + public RecipesAPI(Plugin plugin, boolean debug) { this.debug = debug; this.plugin = plugin; this.recipes = new ArrayList<>(); @@ -66,16 +47,6 @@ public RecipesAPI(Plugin plugin, boolean debug, boolean enableYmlSupport) { plugin.getServer().getPluginManager().registerEvents(new PrepareCraftListener(this), plugin); - if(enableYmlSupport) { - var recipeFolder = new File(plugin.getDataFolder(), "recipes/"); - if (!recipeFolder.exists() && !recipeFolder.mkdirs()) { - plugin.getLogger().warning("Could not create recipes folder."); - return; - } - this.loadDefaultRecipes(); - this.addConfiguredRecipes(recipeFolder); - } - if(this.debug) { Hook.HOOKS.stream() .filter(Hook::isEnable) @@ -85,61 +56,6 @@ public RecipesAPI(Plugin plugin, boolean debug, boolean enableYmlSupport) { } } - /** - * Load the default recipes from the jar - */ - private void loadDefaultRecipes() { - try { - CodeSource src = getClass().getProtectionDomain().getCodeSource(); - if (src != null) { - URL jar = src.getLocation(); - try (JarInputStream jarStream = new JarInputStream(jar.openStream())) { - JarEntry entry; - while ((entry = jarStream.getNextJarEntry()) != null) { - if (entry.getName().startsWith("recipes/") && entry.getName().endsWith(".yml")) { - File outFile = new File(plugin.getDataFolder(), entry.getName()); - if (!outFile.exists()) { - plugin.saveResource(entry.getName(), false); - } - } - } - } - } - } catch (IOException e) { - plugin.getLogger().warning("Could not load default recipes."); - plugin.getServer().getPluginManager().disablePlugin(plugin); - } - } - - /** - * Add all the recipes in the recipe folder to the list of recipes - * @param recipeFolder The folder containing the recipes - */ - private void addConfiguredRecipes(File recipeFolder) { - - try (Stream stream = Files.walk(recipeFolder.toPath())) { - stream.skip(1) - .map(Path::toFile) - .filter(File::isFile) - .filter(e -> e.getName().endsWith(".yml")) - .forEach(this::loadRecipe); - } catch (IOException exception) { - plugin.getLogger().warning("Could not load recipes."); - plugin.getServer().getPluginManager().disablePlugin(plugin); - } - } - - /** - * Load a recipe from a file - * @param file The file to load the recipe from - */ - private void loadRecipe(File file) { - YamlConfiguration configuration = YamlConfiguration.loadConfiguration(file); - var recipe = new RecipeConfiguration(file.getName().replace(".yml", ""), configuration) - .build(); - this.addRecipe(recipe); - } - /** * Unregister all the recipes in the list of recipes from the server */ @@ -203,10 +119,23 @@ public boolean isDebug() { return debug; } + /** + * Log a debug message + * @param message The message to log + * @param args The arguments to format the message + */ public void debug(String message, Object... args) { String formattedMessage = String.format(message, args); if (debug) { this.plugin.getLogger().info(formattedMessage); } } + + /** + * Create a new RecipeLoader instance for custom recipe loading + * @return A new RecipeLoader instance + */ + public RecipeLoader createLoader() { + return new RecipeLoader(plugin, this); + } } \ No newline at end of file diff --git a/src/main/java/fr/traqueur/recipes/api/domains/Recipe.java b/src/main/java/fr/traqueur/recipes/api/domains/Recipe.java index cd5b93e..4d5b77d 100644 --- a/src/main/java/fr/traqueur/recipes/api/domains/Recipe.java +++ b/src/main/java/fr/traqueur/recipes/api/domains/Recipe.java @@ -84,7 +84,7 @@ default Recipe addIngredient(Tag tag, Character sign) { */ default Recipe addIngredient(ItemStack item) { if(this.getType() == RecipeType.CRAFTING_SHAPED) { - throw new UnsupportedOperationException("You can't add an ingredient withou sign to a shaped recipe"); + throw new UnsupportedOperationException("You can't add an ingredient without sign to a shaped recipe"); } return addIngredient(item, null); } diff --git a/src/main/java/fr/traqueur/recipes/impl/PrepareCraftListener.java b/src/main/java/fr/traqueur/recipes/impl/PrepareCraftListener.java index 493bfa4..2bcfe65 100644 --- a/src/main/java/fr/traqueur/recipes/impl/PrepareCraftListener.java +++ b/src/main/java/fr/traqueur/recipes/impl/PrepareCraftListener.java @@ -6,6 +6,7 @@ import fr.traqueur.recipes.impl.domains.ItemRecipe; import org.bukkit.Material; import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; import org.bukkit.event.Listener; import org.bukkit.event.block.BlockCookEvent; import org.bukkit.event.inventory.PrepareItemCraftEvent; @@ -147,7 +148,7 @@ private boolean isSimilar(ItemStack item, Ingredient itemIngredient) { * This method is called when an item is prepared to be crafted. * @param event the event */ - @EventHandler + @EventHandler(priority = EventPriority.HIGHEST) public void onPrepareCraft(PrepareItemCraftEvent event) { Recipe recipe = event.getRecipe(); if (recipe == null) return; @@ -211,6 +212,9 @@ private void checkGoodShapedRecipe(ItemRecipe itemRecipe, PrepareItemCraftEvent return; } } + + this.api.debug("The shaped recipe %s is good.", itemRecipe.getKey()); + event.getInventory().setResult(itemRecipe.result()); } /** @@ -219,28 +223,34 @@ private void checkGoodShapedRecipe(ItemRecipe itemRecipe, PrepareItemCraftEvent * @param event the event */ private void checkGoodShapelessRecipe(ItemRecipe itemRecipe, PrepareItemCraftEvent event) { - List matrix = Arrays.stream(event.getInventory().getMatrix()).filter(Objects::nonNull).filter(it -> it.getType() != Material.AIR).toList(); + List matrix = new ArrayList<>(Arrays.stream(event.getInventory().getMatrix()).filter(Objects::nonNull).filter(it -> it.getType() != Material.AIR).toList()); Ingredient[] itemIngredients = itemRecipe.ingredients(); - AtomicBoolean isSimilar = new AtomicBoolean(true); + if (matrix.size() != itemIngredients.length) { + this.api.debug("The shapeless recipe %s is not good - wrong number of items.", itemRecipe.getKey()); + event.getInventory().setResult(new ItemStack(Material.AIR)); + return; + } + for (Ingredient ingredient : itemIngredients) { - boolean found = matrix.stream().anyMatch(stack -> { - if (stack == null || stack.getType() == Material.AIR) return false; - return ingredient.isSimilar(stack); - }); + boolean found = false; + for (int i = 0; i < matrix.size(); i++) { + ItemStack stack = matrix.get(i); + if (stack != null && stack.getType() != Material.AIR && ingredient.isSimilar(stack)) { + this.api.debug("Ingredient %s found in the matrix.", ingredient.toString()); + matrix.remove(i); + found = true; + break; + } + } if (!found) { this.api.debug("Ingredient %s not found in the matrix.", ingredient.toString()); - isSimilar.set(false); - break; + event.getInventory().setResult(new ItemStack(Material.AIR)); + return; } - this.api.debug("Ingredient %s found in the matrix.", ingredient.toString()); } - if (!isSimilar.get() || matrix.size() != itemIngredients.length) { - this.api.debug("The shapeless recipe %s is not good.", itemRecipe.getKey()); - event.getInventory().setResult(new ItemStack(Material.AIR)); - return; - } this.api.debug("The shapeless recipe %s is good.", itemRecipe.getKey()); + event.getInventory().setResult(itemRecipe.result()); } } diff --git a/src/main/java/fr/traqueur/recipes/impl/domains/ingredients/ItemStackIngredient.java b/src/main/java/fr/traqueur/recipes/impl/domains/ingredients/ItemStackIngredient.java index 3d62181..50338ba 100644 --- a/src/main/java/fr/traqueur/recipes/impl/domains/ingredients/ItemStackIngredient.java +++ b/src/main/java/fr/traqueur/recipes/impl/domains/ingredients/ItemStackIngredient.java @@ -41,6 +41,9 @@ public ItemStackIngredient(ItemStack item) { */ @Override public boolean isSimilar(ItemStack item) { + if (item == null || this.item == null) { + return false; + } return item.getType() == this.item.getType() && item.getAmount() >= this.item.getAmount() @@ -55,21 +58,26 @@ public boolean isSimilar(ItemStack item) { * @return True if the meta of the two items are similar */ private boolean similarMeta(ItemMeta sourceMeta, ItemMeta ingredientMeta) { - for (NamespacedKey key : sourceMeta.getPersistentDataContainer().getKeys()) { - if (!ingredientMeta.getPersistentDataContainer().has(key)) { - System.out.println("Key " + key + " not found in ingredient meta"); + // Check if all required PDC keys from ingredient are present in source + for (NamespacedKey key : ingredientMeta.getPersistentDataContainer().getKeys()) { + if (!sourceMeta.getPersistentDataContainer().has(key)) { return false; } } - boolean lore = sourceMeta.hasLore() == ingredientMeta.hasLore() && (!sourceMeta.hasLore() - || Objects.equals(sourceMeta.getLore(), ingredientMeta.getLore())); + // Check lore (only if ingredient has lore) + if (ingredientMeta.hasLore()) { + if (!sourceMeta.hasLore() || !Objects.equals(sourceMeta.getLore(), ingredientMeta.getLore())) { + return false; + } + } - boolean customData = sourceMeta.hasCustomModelData() == ingredientMeta.hasCustomModelData() - && (!sourceMeta.hasCustomModelData() - || sourceMeta.getCustomModelData() == ingredientMeta.getCustomModelData()); + // Check custom model data (only if ingredient has custom model data) + if (ingredientMeta.hasCustomModelData()) { + return sourceMeta.hasCustomModelData() && sourceMeta.getCustomModelData() == ingredientMeta.getCustomModelData(); + } - return lore && customData; + return true; } /** diff --git a/src/main/java/fr/traqueur/recipes/impl/domains/recipes/RecipeConfiguration.java b/src/main/java/fr/traqueur/recipes/impl/domains/recipes/RecipeConfiguration.java index b399bc8..49df875 100644 --- a/src/main/java/fr/traqueur/recipes/impl/domains/recipes/RecipeConfiguration.java +++ b/src/main/java/fr/traqueur/recipes/impl/domains/recipes/RecipeConfiguration.java @@ -1,6 +1,5 @@ package fr.traqueur.recipes.impl.domains.recipes; -import fr.traqueur.recipes.api.Base64; import fr.traqueur.recipes.api.RecipeType; import fr.traqueur.recipes.api.TagRegistry; import fr.traqueur.recipes.api.domains.Ingredient; @@ -17,11 +16,14 @@ import org.bukkit.inventory.ItemStack; import org.bukkit.inventory.recipe.CookingBookCategory; import org.bukkit.inventory.recipe.CraftingBookCategory; -import org.bukkit.plugin.java.JavaPlugin; +import org.bukkit.util.io.BukkitObjectInputStream; +import org.jetbrains.annotations.NotNull; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.ObjectInputStream; +import java.util.*; +import java.util.zip.GZIPInputStream; /** * This class is used to build recipes via yaml configuration. @@ -106,12 +108,13 @@ public RecipeConfiguration(String name, String path, YamlConfiguration configura } this.category = configuration.getString(path + "category", ""); this.group = configuration.getString(path + "group", ""); - if(!this.checkGategory(this.category)) { + if(!this.checkCategory(this.category)) { throw new IllegalArgumentException("The category " + this.category + " isn't valid."); } if(configuration.contains(path + "pattern")) { this.pattern = configuration.getStringList(path+"pattern").toArray(new String[0]); + this.validatePattern(); } if(!configuration.contains(path + "ingredients")) { @@ -153,6 +156,9 @@ public RecipeConfiguration(String name, String path, YamlConfiguration configura throw new IllegalArgumentException("The recipe " + name + " doesn't have a result."); } String strItem = configuration.getString(path + "result.item"); + if (strItem == null) { + throw new IllegalArgumentException("The recipe " + name + " doesn't have a result."); + } String[] resultParts = strItem.split(":"); if(resultParts.length == 1) { this.result = this.getItemStack(resultParts[0]); @@ -198,7 +204,25 @@ private boolean isStrict(Map ingredient) { * @return the item stack. */ private ItemStack getItemStack(String base64itemstack) { - return Base64.decodeItem(base64itemstack); + try { + ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(Base64.getDecoder().decode(base64itemstack)); + GZIPInputStream gzipInputStream = new GZIPInputStream(byteArrayInputStream); + ObjectInputStream objectInputStream = new BukkitObjectInputStream(gzipInputStream); + Object deserialized = objectInputStream.readObject(); + objectInputStream.close(); + + if (!(deserialized instanceof ItemStack)) { + throw new IllegalArgumentException("The deserialized object is not an ItemStack."); + } + + return (ItemStack) deserialized; + } catch (IOException exception) { + throw new IllegalArgumentException("The itemstack " + base64itemstack + " is not a valid base64 or corrupted: " + exception.getMessage()); + } catch (ClassNotFoundException exception) { + throw new IllegalArgumentException("The itemstack " + base64itemstack + " contains an unknown class: " + exception.getMessage()); + } catch (IllegalArgumentException exception) { + throw new IllegalArgumentException("The itemstack " + base64itemstack + " is not valid: " + exception.getMessage()); + } } /** @@ -219,18 +243,75 @@ private Material getMaterial(String material) { * @param category the group to check. * @return true if the category is valid. */ - private boolean checkGategory(String category) { - category = category.toUpperCase(); - try { - CookingBookCategory.valueOf(category); - } catch (IllegalArgumentException ignored) { - try { - CraftingBookCategory.valueOf(category); - } catch (IllegalArgumentException ignored_2) { - return false; + private boolean checkCategory(@NotNull String category) { + if(category.isEmpty()) { + return true; + } + + String upperCategory = category.toUpperCase(); + + for(CookingBookCategory cookingCategory : CookingBookCategory.values()) { + if(cookingCategory.name().equals(upperCategory)) { + return true; + } + } + + for(CraftingBookCategory craftingCategory : CraftingBookCategory.values()) { + if(craftingCategory.name().equals(upperCategory)) { + return true; + } + } + + return false; + } + + /** + * This method is used to validate the pattern. + * It checks if the pattern is valid for a shaped recipe. + */ + private void validatePattern() { + if (this.pattern == null || this.pattern.length == 0) { + throw new IllegalArgumentException("The recipe " + name + " has an empty pattern."); + } + + // Validate pattern size (max 3 rows) + if (this.pattern.length > 3) { + throw new IllegalArgumentException("The recipe " + name + " has a pattern with more than 3 rows."); + } + + // Validate each row length (max 3 characters) and collect all characters + Set patternChars = new HashSet<>(); + for (int i = 0; i < this.pattern.length; i++) { + String row = this.pattern[i]; + if (row.length() > 3) { + throw new IllegalArgumentException("The recipe " + name + " has a pattern row '" + row + "' with more than 3 characters."); + } + if (row.isEmpty()) { + throw new IllegalArgumentException("The recipe " + name + " has an empty pattern row at index " + i + "."); + } + // Collect all non-space characters + for (char c : row.toCharArray()) { + if (c != ' ') { + patternChars.add(c); + } + } + } + + // Validate that all pattern characters will have corresponding ingredients + if (!patternChars.isEmpty()) { + Set ingredientSigns = new HashSet<>(); + for (Ingredient ingredient : ingredientList) { + if (ingredient.sign() != null) { + ingredientSigns.add(ingredient.sign()); + } + } + + for (Character patternChar : patternChars) { + if (!ingredientSigns.contains(patternChar)) { + throw new IllegalArgumentException("The recipe " + name + " has a pattern character '" + patternChar + "' that doesn't match any ingredient sign."); + } } } - return true; } /** diff --git a/src/main/java/fr/traqueur/recipes/impl/updater/Updater.java b/src/main/java/fr/traqueur/recipes/impl/updater/Updater.java index 508283f..859cd56 100644 --- a/src/main/java/fr/traqueur/recipes/impl/updater/Updater.java +++ b/src/main/java/fr/traqueur/recipes/impl/updater/Updater.java @@ -44,7 +44,7 @@ private Updater(String name) { private void checkUpdates() { if(!this.isUpToDate()) { Logger.getLogger(name) - .warning("The framework is not up to date, " + + .warning("The API is not up to date, " + "the latest version is " + this.fetchLatestVersion()); } } @@ -56,7 +56,7 @@ private void checkUpdates() { private String getVersion() { Properties prop = new Properties(); try { - prop.load(Updater.class.getClassLoader().getResourceAsStream("version.properties")); + prop.load(Updater.class.getClassLoader().getResourceAsStream("recipeapi.properties")); return prop.getProperty("version"); } catch (IOException e) { throw new RuntimeException(e); diff --git a/src/main/resources/recipeapi.properties b/src/main/resources/recipeapi.properties new file mode 100644 index 0000000..317fe99 --- /dev/null +++ b/src/main/resources/recipeapi.properties @@ -0,0 +1 @@ +version=3.0.0 \ No newline at end of file diff --git a/src/main/resources/version.properties b/src/main/resources/version.properties index fb7cb53..516314f 100644 --- a/src/main/resources/version.properties +++ b/src/main/resources/version.properties @@ -1 +1 @@ -version=2.0.2 \ No newline at end of file +version=2.0.1 \ No newline at end of file From 97b37b978e27e680b7a5ce5fed05e64486b823dc Mon Sep 17 00:00:00 2001 From: Traqueur <54551467+Traqueur-dev@users.noreply.github.com> Date: Fri, 24 Oct 2025 09:16:50 +0200 Subject: [PATCH 5/7] Feat/player dynamic (#34) * fix: ia hook (#23) * Feat/improve (#25) * Feat/improve (#27) * fix: ia hook (#23) * feat: improve * feat: remove some useless usage of variable * version 3.0.0 (#32) * Feat/improve (#25) * Feat/improve (#27) * fix: ia hook (#23) * feat: improve * feat: remove some useless usage of variable * Hotfix/air recipe (#30) * fix: ia hook (#23) * Feat/improve (#25) * Feat/improve (#27) * fix: ia hook (#23) * feat: improve * feat: remove some useless usage of variable * fix: air in recipe doesn't work * Feat/modernize (#31) * feat: modernize * feat: version * fix: shapeless craft * fix: set handler highest and add result manually * fix: remove useless properties * feat: java version * feat: improve jitpack * feat: add dynamic player when it's possible during a craft * fix: compilation error --- .../java/fr/traqueur/recipes/api/Util.java | 68 +++++++++++++++ .../traqueur/recipes/api/domains/Recipe.java | 2 +- .../fr/traqueur/recipes/api/hook/Hook.java | 7 +- .../recipes/impl/PrepareCraftListener.java | 17 ++-- .../recipes/impl/domains/ItemRecipe.java | 37 +++++++- .../impl/domains/recipes/RecipeBuilder.java | 5 +- .../domains/recipes/RecipeConfiguration.java | 85 +++++++------------ .../fr/traqueur/recipes/impl/hook/Hooks.java | 5 +- src/main/resources/version.properties | 1 - 9 files changed, 155 insertions(+), 72 deletions(-) create mode 100644 src/main/java/fr/traqueur/recipes/api/Util.java delete mode 100644 src/main/resources/version.properties diff --git a/src/main/java/fr/traqueur/recipes/api/Util.java b/src/main/java/fr/traqueur/recipes/api/Util.java new file mode 100644 index 0000000..79b958e --- /dev/null +++ b/src/main/java/fr/traqueur/recipes/api/Util.java @@ -0,0 +1,68 @@ +package fr.traqueur.recipes.api; + +import org.bukkit.Material; +import org.bukkit.inventory.ItemStack; +import org.bukkit.util.io.BukkitObjectInputStream; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.ObjectInputStream; +import java.util.Base64; +import java.util.zip.GZIPInputStream; + +public class Util { + + /** + * This method is used to get the itemstack from base64 string + * @param base64itemstack the base64 item stack. + * @return the item stack. + */ + public static ItemStack getItemStack(String base64itemstack) { + try { + ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(Base64.getDecoder().decode(base64itemstack)); + GZIPInputStream gzipInputStream = new GZIPInputStream(byteArrayInputStream); + ObjectInputStream objectInputStream = new BukkitObjectInputStream(gzipInputStream); + Object deserialized = objectInputStream.readObject(); + objectInputStream.close(); + + if (!(deserialized instanceof ItemStack)) { + throw new IllegalArgumentException("The deserialized object is not an ItemStack."); + } + + return (ItemStack) deserialized; + } catch (IOException exception) { + throw new IllegalArgumentException("The itemstack " + base64itemstack + " is not a valid base64 or corrupted: " + exception.getMessage()); + } catch (ClassNotFoundException exception) { + throw new IllegalArgumentException("The itemstack " + base64itemstack + " contains an unknown class: " + exception.getMessage()); + } catch (IllegalArgumentException exception) { + throw new IllegalArgumentException("The itemstack " + base64itemstack + " is not valid: " + exception.getMessage()); + } + } + + public static String fromItemStack(ItemStack itemStack) { + try { + java.io.ByteArrayOutputStream byteArrayOutputStream = new java.io.ByteArrayOutputStream(); + java.util.zip.GZIPOutputStream gzipOutputStream = new java.util.zip.GZIPOutputStream(byteArrayOutputStream); + org.bukkit.util.io.BukkitObjectOutputStream bukkitObjectOutputStream = new org.bukkit.util.io.BukkitObjectOutputStream(gzipOutputStream); + bukkitObjectOutputStream.writeObject(itemStack); + bukkitObjectOutputStream.close(); + return Base64.getEncoder().encodeToString(byteArrayOutputStream.toByteArray()); + } catch (IOException e) { + throw new IllegalArgumentException("The itemstack " + itemStack + " cannot be serialized: " + e.getMessage()); + } + } + + /** + * This method is used to get the material from the string. + * @param material the material string. + * @return the material. + */ + public static Material getMaterial(String material) { + try { + return Material.valueOf(material.toUpperCase()); + } catch (IllegalArgumentException e) { + throw new IllegalArgumentException("The material " + material + " isn't valid."); + } + } + +} diff --git a/src/main/java/fr/traqueur/recipes/api/domains/Recipe.java b/src/main/java/fr/traqueur/recipes/api/domains/Recipe.java index 4d5b77d..e8316d6 100644 --- a/src/main/java/fr/traqueur/recipes/api/domains/Recipe.java +++ b/src/main/java/fr/traqueur/recipes/api/domains/Recipe.java @@ -211,7 +211,7 @@ default Recipe addIngredient(Material material, Character sign) { * @param experience The experience of the recipe. * @return The item recipe. */ - default ItemRecipe getItemRecipe(List ingredientList, RecipeType type, String[] pattern, int cookingTime, String name, String group, String category, ItemStack result, int amount, float experience) { + default ItemRecipe getItemRecipe(List ingredientList, RecipeType type, String[] pattern, int cookingTime, String name, String group, String category, String result, int amount, float experience) { if (ingredientList.isEmpty()) { throw new IllegalArgumentException("Ingredients are not set"); } diff --git a/src/main/java/fr/traqueur/recipes/api/hook/Hook.java b/src/main/java/fr/traqueur/recipes/api/hook/Hook.java index a199b18..fa38740 100644 --- a/src/main/java/fr/traqueur/recipes/api/hook/Hook.java +++ b/src/main/java/fr/traqueur/recipes/api/hook/Hook.java @@ -3,8 +3,10 @@ import fr.traqueur.recipes.api.domains.Ingredient; import fr.traqueur.recipes.impl.hook.Hooks; import org.bukkit.Bukkit; +import org.bukkit.entity.Player; import org.bukkit.inventory.ItemStack; import org.bukkit.plugin.java.JavaPlugin; +import org.jetbrains.annotations.Nullable; import java.util.ArrayList; import java.util.List; @@ -52,7 +54,10 @@ default boolean isEnable() { /** * Get the ItemStack from a result part * @param resultPart The result part to get the ItemStack from + * @param player The player (can be null) if needed for dynamic items generation + * This method are called when a recipe is register so, player is null at this moment + * but when crafting, player are provided to get the correct item (not in Furnace) * @return The ItemStack from the result part */ - ItemStack getItemStack(String resultPart); + ItemStack getItemStack(@Nullable Player player, String resultPart); } diff --git a/src/main/java/fr/traqueur/recipes/impl/PrepareCraftListener.java b/src/main/java/fr/traqueur/recipes/impl/PrepareCraftListener.java index 2bcfe65..d5b7ca3 100644 --- a/src/main/java/fr/traqueur/recipes/impl/PrepareCraftListener.java +++ b/src/main/java/fr/traqueur/recipes/impl/PrepareCraftListener.java @@ -5,10 +5,12 @@ import fr.traqueur.recipes.api.domains.Ingredient; import fr.traqueur.recipes.impl.domains.ItemRecipe; import org.bukkit.Material; +import org.bukkit.entity.Player; import org.bukkit.event.EventHandler; import org.bukkit.event.EventPriority; import org.bukkit.event.Listener; import org.bukkit.event.block.BlockCookEvent; +import org.bukkit.event.inventory.FurnaceSmeltEvent; import org.bukkit.event.inventory.PrepareItemCraftEvent; import org.bukkit.event.inventory.PrepareSmithingEvent; import org.bukkit.inventory.*; @@ -62,6 +64,7 @@ public void onSmelt(BlockCookEvent event) { if(event.isCancelled()) { return; } + ItemStack item = event.getSource(); if (item == null || item.getType() == Material.AIR) return; ItemStack result = event.getResult(); @@ -81,6 +84,7 @@ public void onSmelt(BlockCookEvent event) { event.setCancelled(true); } else { this.api.debug("The smelting recipe %s is good.", itemRecipe.getKey()); + event.setResult(itemRecipe.toBukkitItemStack(null)); } }); } @@ -131,6 +135,7 @@ && isSimilar(base, baseIngredient) return; } this.api.debug("The smithing recipe %s is good.", itemRecipe.getKey()); + event.setResult(itemRecipe.toBukkitItemStack((Player) event.getViewers().getFirst())); } } @@ -161,13 +166,13 @@ public void onPrepareCraft(PrepareItemCraftEvent event) { if(recipe instanceof ShapedRecipe shapedRecipe && itemRecipe.recipeType() == RecipeType.CRAFTING_SHAPED) { if (!shapedRecipe.getKey().equals(itemRecipe.getKey())) continue; this.api.debug("The recipe %s is a shaped recipe.", itemRecipe.getKey()); - this.checkGoodShapedRecipe(itemRecipe, event); + this.checkGoodShapedRecipe((Player) event.getViewers().getFirst(), itemRecipe, event); } if(recipe instanceof ShapelessRecipe shapelessRecipe && itemRecipe.recipeType() == RecipeType.CRAFTING_SHAPELESS) { if(!shapelessRecipe.getKey().equals(itemRecipe.getKey())) continue; this.api.debug("The recipe %s is a shapeless recipe.", itemRecipe.getKey()); - this.checkGoodShapelessRecipe(itemRecipe, event); + this.checkGoodShapelessRecipe((Player) event.getViewers().getFirst(), itemRecipe, event); } } } @@ -177,7 +182,7 @@ public void onPrepareCraft(PrepareItemCraftEvent event) { * @param itemRecipe the item recipe * @param event the event */ - private void checkGoodShapedRecipe(ItemRecipe itemRecipe, PrepareItemCraftEvent event) { + private void checkGoodShapedRecipe(Player player, ItemRecipe itemRecipe, PrepareItemCraftEvent event) { ItemStack[] matrix = event.getInventory().getMatrix(); String[] pattern = Arrays.stream(itemRecipe.pattern()).map(s -> s.split("")).flatMap(Arrays::stream).toArray(String[]::new); @@ -214,7 +219,7 @@ private void checkGoodShapedRecipe(ItemRecipe itemRecipe, PrepareItemCraftEvent } this.api.debug("The shaped recipe %s is good.", itemRecipe.getKey()); - event.getInventory().setResult(itemRecipe.result()); + event.getInventory().setResult(itemRecipe.toBukkitItemStack(player)); } /** @@ -222,7 +227,7 @@ private void checkGoodShapedRecipe(ItemRecipe itemRecipe, PrepareItemCraftEvent * @param itemRecipe the item recipe * @param event the event */ - private void checkGoodShapelessRecipe(ItemRecipe itemRecipe, PrepareItemCraftEvent event) { + private void checkGoodShapelessRecipe(Player player, ItemRecipe itemRecipe, PrepareItemCraftEvent event) { List matrix = new ArrayList<>(Arrays.stream(event.getInventory().getMatrix()).filter(Objects::nonNull).filter(it -> it.getType() != Material.AIR).toList()); Ingredient[] itemIngredients = itemRecipe.ingredients(); @@ -251,6 +256,6 @@ private void checkGoodShapelessRecipe(ItemRecipe itemRecipe, PrepareItemCraftEve } this.api.debug("The shapeless recipe %s is good.", itemRecipe.getKey()); - event.getInventory().setResult(itemRecipe.result()); + event.getInventory().setResult(itemRecipe.toBukkitItemStack(player)); } } diff --git a/src/main/java/fr/traqueur/recipes/impl/domains/ItemRecipe.java b/src/main/java/fr/traqueur/recipes/impl/domains/ItemRecipe.java index 257e976..7624a8b 100644 --- a/src/main/java/fr/traqueur/recipes/impl/domains/ItemRecipe.java +++ b/src/main/java/fr/traqueur/recipes/impl/domains/ItemRecipe.java @@ -1,11 +1,22 @@ package fr.traqueur.recipes.impl.domains; import fr.traqueur.recipes.api.RecipeType; +import fr.traqueur.recipes.api.Util; import fr.traqueur.recipes.api.domains.Ingredient; +import fr.traqueur.recipes.api.hook.Hook; +import org.bukkit.Material; import org.bukkit.NamespacedKey; +import org.bukkit.entity.Player; import org.bukkit.inventory.*; import org.bukkit.inventory.recipe.CookingBookCategory; import org.bukkit.inventory.recipe.CraftingBookCategory; +import org.bukkit.util.io.BukkitObjectInputStream; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.ObjectInputStream; +import java.util.Base64; +import java.util.zip.GZIPInputStream; /** * This class represents a recipe for an item @@ -20,7 +31,7 @@ * @param cookingTime The cooking time of the recipe * @param experience The experience of the recipe */ -public record ItemRecipe(String recipeName, String group, String category, RecipeType recipeType, ItemStack result, int amount, Ingredient[] ingredients, +public record ItemRecipe(String recipeName, String group, String category, RecipeType recipeType, String result, int amount, Ingredient[] ingredients, String[] pattern, int cookingTime, float experience) { /** @@ -114,12 +125,32 @@ public Recipe toBukkitRecipe(NamespacedKey key, ItemStack result) { * @return The bukkit recipe */ public Recipe toBukkitRecipe() { - ItemStack result = new ItemStack(this.result()); - result.setAmount(this.amount()); + ItemStack result = this.toBukkitItemStack(null); NamespacedKey key = this.getKey(); return this.toBukkitRecipe(key, result); } + public ItemStack toBukkitItemStack(Player player) { + ItemStack result; + String[] resultParts = this.result.split(":"); + if(resultParts.length == 1) { + result = Util.getItemStack(resultParts[0]); + } else { + result = switch (resultParts[0]) { + case "material" -> new ItemStack(Util.getMaterial(resultParts[1])); + case "item", "base64" -> Util.getItemStack(resultParts[1]); + default -> Hook.HOOKS.stream() + .filter(Hook::isEnable) + .filter(hook -> hook.getPluginName().equalsIgnoreCase(resultParts[0])) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException("The result " + this.result + " isn't valid.")) + .getItemStack(player, resultParts[1]); + }; + } + result.setAmount(this.amount()); + return result; + } + /** * Get the key of the recipe * @return The key of the recipe diff --git a/src/main/java/fr/traqueur/recipes/impl/domains/recipes/RecipeBuilder.java b/src/main/java/fr/traqueur/recipes/impl/domains/recipes/RecipeBuilder.java index adf9f08..55c7adf 100644 --- a/src/main/java/fr/traqueur/recipes/impl/domains/recipes/RecipeBuilder.java +++ b/src/main/java/fr/traqueur/recipes/impl/domains/recipes/RecipeBuilder.java @@ -1,6 +1,7 @@ package fr.traqueur.recipes.impl.domains.recipes; import fr.traqueur.recipes.api.RecipeType; +import fr.traqueur.recipes.api.Util; import fr.traqueur.recipes.api.domains.Ingredient; import fr.traqueur.recipes.api.domains.Recipe; import fr.traqueur.recipes.impl.domains.ItemRecipe; @@ -28,7 +29,7 @@ public class RecipeBuilder implements Recipe { /** * The result of the recipe. */ - private ItemStack result; + private String result; /** * The amount of the result. @@ -85,7 +86,7 @@ public Recipe setResult(ItemStack result) { if(type == null) { throw new IllegalArgumentException("Recipe type is not set"); } - this.result = result; + this.result = Util.fromItemStack(result); return this; } diff --git a/src/main/java/fr/traqueur/recipes/impl/domains/recipes/RecipeConfiguration.java b/src/main/java/fr/traqueur/recipes/impl/domains/recipes/RecipeConfiguration.java index 49df875..316f17c 100644 --- a/src/main/java/fr/traqueur/recipes/impl/domains/recipes/RecipeConfiguration.java +++ b/src/main/java/fr/traqueur/recipes/impl/domains/recipes/RecipeConfiguration.java @@ -2,6 +2,7 @@ import fr.traqueur.recipes.api.RecipeType; import fr.traqueur.recipes.api.TagRegistry; +import fr.traqueur.recipes.api.Util; import fr.traqueur.recipes.api.domains.Ingredient; import fr.traqueur.recipes.api.domains.Recipe; import fr.traqueur.recipes.api.hook.Hook; @@ -43,7 +44,7 @@ public class RecipeConfiguration implements Recipe { /** * The result of the recipe. */ - private final ItemStack result; + private final String resultStr; /** * The amount of the result. @@ -128,17 +129,17 @@ public RecipeConfiguration(String name, String path, YamlConfiguration configura String[] data = material.split(":"); if(data.length == 1) { - this.ingredientList.add(new MaterialIngredient(this.getMaterial(data[0]), sign)); + this.ingredientList.add(new MaterialIngredient(Util.getMaterial(data[0]), sign)); } else { Ingredient ingred = switch (data[0]) { - case "material" -> new MaterialIngredient(this.getMaterial(data[1]), sign); + case "material" -> new MaterialIngredient(Util.getMaterial(data[1]), sign); case "tag" -> new TagIngredient(this.getTag(data[1]), sign); case "item" -> { boolean strict = this.isStrict(ingredient); if(strict) { - yield new StrictItemStackIngredient(this.getItemStack(data[1]), sign); + yield new StrictItemStackIngredient(Util.getItemStack(data[1]), sign); } - yield new ItemStackIngredient(this.getItemStack(data[1]), sign); + yield new ItemStackIngredient(Util.getItemStack(data[1]), sign); } default -> Hook.HOOKS.stream() .filter(Hook::isEnable) @@ -159,21 +160,7 @@ public RecipeConfiguration(String name, String path, YamlConfiguration configura if (strItem == null) { throw new IllegalArgumentException("The recipe " + name + " doesn't have a result."); } - String[] resultParts = strItem.split(":"); - if(resultParts.length == 1) { - this.result = this.getItemStack(resultParts[0]); - } else { - this.result = switch (resultParts[0]) { - case "material" -> new ItemStack(this.getMaterial(resultParts[1])); - case "item", "base64" -> this.getItemStack(resultParts[1]); - default -> Hook.HOOKS.stream() - .filter(Hook::isEnable) - .filter(hook -> hook.getPluginName().equalsIgnoreCase(resultParts[0])) - .findFirst() - .orElseThrow(() -> new IllegalArgumentException("The result " + strItem + " isn't valid.")) - .getItemStack(resultParts[1]); - }; - } + this.resultStr = strItem; this.amount = configuration.getInt(path + "result.amount", 1); @@ -199,49 +186,35 @@ private boolean isStrict(Map ingredient) { } /** - * This method is used to get the itemstack from base64 string - * @param base64itemstack the base64 item stack. - * @return the item stack. + * This method is used to check if the category is valid. + * @param category the group to check. + * @return true if the category is valid. */ - private ItemStack getItemStack(String base64itemstack) { - try { - ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(Base64.getDecoder().decode(base64itemstack)); - GZIPInputStream gzipInputStream = new GZIPInputStream(byteArrayInputStream); - ObjectInputStream objectInputStream = new BukkitObjectInputStream(gzipInputStream); - Object deserialized = objectInputStream.readObject(); - objectInputStream.close(); - - if (!(deserialized instanceof ItemStack)) { - throw new IllegalArgumentException("The deserialized object is not an ItemStack."); - } + private boolean checkCategory(@NotNull String category) { + if(category.isEmpty()) { + return true; + } + + String upperCategory = category.toUpperCase(); - return (ItemStack) deserialized; - } catch (IOException exception) { - throw new IllegalArgumentException("The itemstack " + base64itemstack + " is not a valid base64 or corrupted: " + exception.getMessage()); - } catch (ClassNotFoundException exception) { - throw new IllegalArgumentException("The itemstack " + base64itemstack + " contains an unknown class: " + exception.getMessage()); - } catch (IllegalArgumentException exception) { - throw new IllegalArgumentException("The itemstack " + base64itemstack + " is not valid: " + exception.getMessage()); + for(CookingBookCategory cookingCategory : CookingBookCategory.values()) { + if(cookingCategory.name().equals(upperCategory)) { + return true; + } } - } - /** - * This method is used to get the material from the string. - * @param material the material string. - * @return the material. - */ - private Material getMaterial(String material) { - try { - return Material.valueOf(material.toUpperCase()); - } catch (IllegalArgumentException e) { - throw new IllegalArgumentException("The material " + material + " isn't valid."); + for(CraftingBookCategory craftingCategory : CraftingBookCategory.values()) { + if(craftingCategory.name().equals(upperCategory)) { + return true; + } } + + return false; } /** - * This method is used to check if the category is valid. - * @param category the group to check. - * @return true if the category is valid. + * This method is used to validate the pattern. + * It checks if the pattern is valid for a shaped recipe. */ private boolean checkCategory(@NotNull String category) { if(category.isEmpty()) { @@ -407,6 +380,6 @@ public RecipeType getType() { */ @Override public ItemRecipe build() { - return this.getItemRecipe(ingredientList, type, pattern, cookingTime, name, group, category, result, amount, experience); + return this.getItemRecipe(ingredientList, type, pattern, cookingTime, name, group, category, resultStr, amount, experience); } } diff --git a/src/main/java/fr/traqueur/recipes/impl/hook/Hooks.java b/src/main/java/fr/traqueur/recipes/impl/hook/Hooks.java index 081fb70..be2b93d 100644 --- a/src/main/java/fr/traqueur/recipes/impl/hook/Hooks.java +++ b/src/main/java/fr/traqueur/recipes/impl/hook/Hooks.java @@ -5,6 +5,7 @@ import fr.traqueur.recipes.api.hook.Hook; import fr.traqueur.recipes.impl.hook.hooks.ItemsAdderIngredient; import fr.traqueur.recipes.impl.hook.hooks.OraxenIngredient; +import org.bukkit.entity.Player; import org.bukkit.inventory.ItemStack; /** @@ -22,7 +23,7 @@ public Ingredient getIngredient(String data, Character sign) { } @Override - public ItemStack getItemStack(String data) { + public ItemStack getItemStack(Player player, String data) { CustomStack stack = CustomStack.getInstance(data); if (stack == null) { throw new IllegalArgumentException("ItemsAdder item with id " + data + " not found"); @@ -40,7 +41,7 @@ public Ingredient getIngredient(String data, Character sign) { } @Override - public ItemStack getItemStack(String data) { + public ItemStack getItemStack(Player player, String data) { var builder = io.th0rgal.oraxen.api.OraxenItems.getItemById(data); if(builder == null) { throw new IllegalArgumentException("Oraxen item with id " + data + " not found"); diff --git a/src/main/resources/version.properties b/src/main/resources/version.properties deleted file mode 100644 index 516314f..0000000 --- a/src/main/resources/version.properties +++ /dev/null @@ -1 +0,0 @@ -version=2.0.1 \ No newline at end of file From 880c06fdd4509c8d4d53d0ab7f94b15e678b5fa9 Mon Sep 17 00:00:00 2001 From: Traqueur_ Date: Fri, 24 Oct 2025 10:04:51 +0200 Subject: [PATCH 6/7] feat: develop --- README.md | 237 +++++++++++++++++- .../fr/traqueur/recipes/api/RecipeLoader.java | 38 +-- .../fr/traqueur/recipes/api/TagRegistry.java | 53 ---- .../java/fr/traqueur/recipes/api/Util.java | 84 +++++++ .../traqueur/recipes/api/domains/Recipe.java | 5 +- .../fr/traqueur/recipes/api/hook/Hook.java | 1 - .../recipes/impl/domains/ItemRecipe.java | 8 +- .../impl/domains/recipes/RecipeBuilder.java | 20 +- .../domains/recipes/RecipeConfiguration.java | 93 ++----- 9 files changed, 372 insertions(+), 167 deletions(-) delete mode 100644 src/main/java/fr/traqueur/recipes/api/TagRegistry.java diff --git a/README.md b/README.md index de5d4a5..7ff0cf8 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,9 @@ ## Features - **Create Custom Recipes**: Add shaped, shapeless, furnace, and other types of recipes with ease. +- **Recipe Priority System**: Control recipe registration order to handle conflicting recipes (higher priority = registered first). - **Advanced Recipe Handling**: Support for custom ingredients with metadata (lore, custom model data, persistent data container). +- **Strict Ingredient Matching**: Option to require exact item matches including all metadata for precise recipe control. - **Easy Integration**: Simple API to integrate into any Spigot plugin. - **Plugin Hooks**: Built-in support for ItemsAdder and Oraxen items. You can create your own hook with your custom item systems. - **Version Compatibility**: Works with recent Spigot versions and allows you to create recipes dynamically. @@ -90,7 +92,17 @@ public final class TestPlugin extends JavaPlugin { .setName("example-custom-ingredient") .setResult(new ItemStack(Material.DIAMOND)) .setAmount(64) - .addIngredient(magicPaper) + .addIngredient(magicPaper, false) // false = normal matching (lore must match) + .build(); + + // 3b. Same recipe but with strict matching (exact item required) + ItemRecipe recipe3strict = new RecipeBuilder() + .setType(RecipeType.CRAFTING_SHAPELESS) + .setName("example-custom-ingredient-strict") + .setResult(new ItemStack(Material.EMERALD)) + .setAmount(32) + .addIngredient(magicPaper, true) // true = strict matching (all metadata must match) + .setPriority(10) // Higher priority than normal recipes .build(); // 4. Furnace smelting recipe with cooking time and experience @@ -108,6 +120,7 @@ public final class TestPlugin extends JavaPlugin { recipesAPI.addRecipe(recipe1); recipesAPI.addRecipe(recipe2); recipesAPI.addRecipe(recipe3); + recipesAPI.addRecipe(recipe3strict); recipesAPI.addRecipe(recipe4); } } @@ -176,9 +189,35 @@ The API supports several types of ingredients: - **Tag**: Minecraft tags (e.g., planks, logs, wool) - **Plugin Items**: ItemsAdder and Oraxen custom items -### Important Notes -- **Display Name**: Player can rename items - only lore, custom model data, and PDC are checked -- **Strict Mode**: Use `.addIngredient(item, sign, true)` to require exact match including display name +### Ingredient Matching Modes + +**Normal Mode** (default): +- Only checks metadata that is present in the recipe ingredient +- Player can add extra metadata without breaking the recipe +- Display name is NOT checked (players can rename items) +- Lore, Custom Model Data, and PDC keys present in ingredient must match + +**Strict Mode** (`strict: true`): +- Requires exact match using Bukkit's `ItemStack.isSimilar()` +- All metadata must match exactly +- Use this when you need precise ingredient control + +Example in code: +```java +// Normal mode - flexible matching +.addIngredient(customItem, 'C', false) + +// Strict mode - exact matching +.addIngredient(customItem, 'C', true) +``` + +Example in YAML: +```yaml +ingredients: + - item: item:COBBLESTONE + sign: 'C' + strict: true # Requires exact match +``` ## API Documentation The API is simple and intuitive to use. You can easily: @@ -291,6 +330,7 @@ category: "MISC" - `category` - Recipe category (BUILDING, REDSTONE, EQUIPMENT, MISC for crafting; FOOD, BLOCKS, MISC for cooking) - `cooking-time` - Cooking time in ticks for smelting recipes (default: 0) - `experience` - Experience reward for smelting recipes (default: 0.0) +- `priority` - Recipe registration priority (default: 0, higher = registered first) ### Pattern Validation @@ -301,14 +341,64 @@ For `CRAFTING_SHAPED` recipes, the pattern is validated: - Empty rows are not allowed ### Ingredient Types in YAML -- `item: MATERIAL_NAME` - Simple material -- `item: material:MATERIAL_NAME` - Explicit material -- `item: tag:TAG_NAME` - Minecraft tag -- `item: item:BASE64_STRING` or `item: base64:BASE64_STRING` - Custom item from Base64 -- `item: itemsadder:ITEM_ID` - ItemsAdder item -- `item: oraxen:ITEM_ID` - Oraxen item -- `sign: X` - Character used in shaped recipe patterns (required for shaped recipes) -- `strict: true` - Require exact item match including display name (optional, default: false) + +#### Basic Format +```yaml +ingredients: + - item: : + sign: 'X' # Optional: Required for shaped recipes + strict: true # Optional: Enable strict matching (default: false) +``` + +#### Supported Types + +- **Simple Material** (auto-detected): + ```yaml + - item: DIAMOND # No prefix = Material + ``` + +- **Explicit Material** (same as above): + ```yaml + - item: material:DIAMOND + ``` + +- **ItemStack from Material** (with metadata support): + ```yaml + - item: item:COBBLESTONE + strict: true # Recommended for items with metadata + ``` + Creates an ItemStack that can have metadata (lore, custom model data, PDC) + +- **ItemStack from Base64**: + ```yaml + - item: base64:BASE64_ENCODED_ITEM_STRING + strict: true # Recommended for custom items + ``` + Load a custom item from a serialized Base64 string + +- **Minecraft Tag**: + ```yaml + - item: tag:planks # Accepts any plank type + ``` + +- **ItemsAdder Item**: + ```yaml + - item: itemsadder:custom_item_id + ``` + +- **Oraxen Item**: + ```yaml + - item: oraxen:custom_item_id + ``` + +- **Custom Plugin Hook**: + ```yaml + - item: yourplugin:custom_item_id + ``` + +#### Field Details +- `sign` - Character used in shaped recipe patterns (required for `CRAFTING_SHAPED`, ignored for shapeless) +- `strict` - When `true`, requires exact item match (only applies to `item:` and `base64:` types) ### Example: Smelting Recipe @@ -329,13 +419,134 @@ category: MISC ```yaml type: CRAFTING_SHAPELESS ingredients: - - item: item:BASE64_ENCODED_ITEM_HERE + - item: base64:BASE64_ENCODED_ITEM_HERE strict: true result: item: DIAMOND amount: 1 ``` +## Recipe Priority System + +When multiple recipes have similar ingredients, the **priority** field determines which recipe is checked first. This is crucial for handling conflicting recipes. + +### How Priority Works +- Recipes are sorted by priority before registration (higher priority = registered first) +- Default priority is `0` +- Higher priority recipes are checked before lower priority ones +- Useful for specific recipes that should take precedence over generic ones + +### Example Use Case: Compressed Cobblestone + +```yaml +# cobblestone_to_compressed_x1.yml +type: CRAFTING_SHAPED +priority: 0 # Lower priority (default) +pattern: + - "CCC" + - "CCC" + - "CCC" +ingredients: + - item: material:COBBLESTONE + sign: 'C' +result: + item: yourplugin:compressed_cobblestone_x1 + amount: 1 +``` + +```yaml +# compressed_x1_to_x2.yml +type: CRAFTING_SHAPED +priority: 10 # Higher priority - checked first! +pattern: + - "CCC" + - "CCC" + - "CCC" +ingredients: + - item: item:COMPRESSED_COBBLESTONE_X1 # Custom item with metadata + sign: 'C' + strict: true # Important: ensures exact match +result: + item: yourplugin:compressed_cobblestone_x2 + amount: 1 +``` + +In this example, the x1→x2 recipe will be checked first because it has `priority: 10`. The `strict: true` flag ensures that only compressed x1 cobblestone (not regular cobblestone) triggers this recipe. + +### Priority in Code + +```java +ItemRecipe highPriorityRecipe = new RecipeBuilder() + .setType(RecipeType.CRAFTING_SHAPED) + .setName("specific-recipe") + .setPriority(10) // Higher priority + .setPattern("AAA", "AAA", "AAA") + .addIngredient(specificItem, 'A', true) // Strict matching + .setResult(new ItemStack(Material.DIAMOND)) + .build(); + +ItemRecipe lowPriorityRecipe = new RecipeBuilder() + .setType(RecipeType.CRAFTING_SHAPED) + .setName("generic-recipe") + .setPriority(0) // Default priority + .setPattern("AAA", "AAA", "AAA") + .addIngredient(Material.STONE, 'A') + .setResult(new ItemStack(Material.COAL)) + .build(); +``` + +## Parsing Ingredients Programmatically + +RecipesAPI provides a public utility method to parse ingredients from strings, useful for loading recipes from custom sources or configuration files. + +### Using Util.parseIngredient() + +```java +import fr.traqueur.recipes.api.Util; +import fr.traqueur.recipes.api.domains.Ingredient; + +// Parse a simple material +Ingredient diamond = Util.parseIngredient("DIAMOND"); + +// Parse with a sign for shaped recipes +Ingredient stone = Util.parseIngredient("item:STONE", 'S'); + +// Parse with strict mode enabled +Ingredient customItem = Util.parseIngredient("item:COBBLESTONE", 'C', true); + +// Parse from base64 +Ingredient base64Item = Util.parseIngredient("base64:YOUR_BASE64_STRING", null, true); + +// Parse from tags +Ingredient planks = Util.parseIngredient("tag:planks", 'P'); + +// Parse from plugin items +Ingredient iaItem = Util.parseIngredient("itemsadder:custom_item", 'I'); +Ingredient oraxenItem = Util.parseIngredient("oraxen:custom_sword", 'S'); +``` + +### Method Signatures + +```java +// Full control +public static Ingredient parseIngredient(String itemString, Character sign, boolean strict) + +// Without strict mode (default: false) +public static Ingredient parseIngredient(String itemString, Character sign) + +// Shapeless recipe (no sign) +public static Ingredient parseIngredient(String itemString) +``` + +### Supported Formats +All formats supported in YAML are also supported here: +- `"MATERIAL_NAME"` → MaterialIngredient +- `"material:MATERIAL_NAME"` → MaterialIngredient +- `"item:MATERIAL_NAME"` → ItemStackIngredient (supports metadata) +- `"base64:BASE64_STRING"` → ItemStackIngredient from serialized item +- `"tag:TAG_NAME"` → TagIngredient +- `"pluginname:item_id"` → Custom plugin hook + ## Resources - **Javadoc**: [API Documentation](https://jitpack.io/com/github/Traqueur-dev/RecipesAPI/latest/javadoc/) diff --git a/src/main/java/fr/traqueur/recipes/api/RecipeLoader.java b/src/main/java/fr/traqueur/recipes/api/RecipeLoader.java index 7811c82..734bd77 100644 --- a/src/main/java/fr/traqueur/recipes/api/RecipeLoader.java +++ b/src/main/java/fr/traqueur/recipes/api/RecipeLoader.java @@ -149,22 +149,28 @@ private void extractDefaultsFromJar(String jarPath) { * @return The number of recipes loaded */ public int load() { - int count = 0; + List recipes = new ArrayList<>(); // Load from folders for (File folder : folders) { - count += loadFromFolder(folder); + loadFromFolder(folder, recipes); } // Load from individual files for (File file : files) { - if (loadRecipe(file)) { - count++; - } + loadRecipe(file, recipes); + } + + // Sort recipes by priority (higher priority first) + recipes.sort((r1, r2) -> Integer.compare(r2.priority(), r1.priority())); + + // Register sorted recipes + for (ItemRecipe recipe : recipes) { + api.addRecipe(recipe); } - plugin.getLogger().info("Loaded " + count + " recipes via RecipeLoader."); - return count; + plugin.getLogger().info("Loaded " + recipes.size() + " recipes via RecipeLoader."); + return recipes.size(); } /** @@ -180,10 +186,9 @@ public int reload() { /** * Load all recipes from a folder (recursive) * @param folder The folder to load recipes from - * @return The number of recipes loaded + * @param recipes The list to add loaded recipes to */ - private int loadFromFolder(File folder) { - int count = 0; + private void loadFromFolder(File folder, List recipes) { try (Stream stream = Files.walk(folder.toPath())) { List ymlFiles = stream.map(Path::toFile) .filter(File::isFile) @@ -191,34 +196,29 @@ private int loadFromFolder(File folder) { .toList(); for (File file : ymlFiles) { - if (loadRecipe(file)) { - count++; - } + loadRecipe(file, recipes); } } catch (IOException exception) { plugin.getLogger().severe("Could not load recipes from folder " + folder.getAbsolutePath() + ": " + exception.getMessage()); } - return count; } /** * Load a recipe from a file * @param file The file to load the recipe from - * @return true if the recipe was loaded successfully, false otherwise + * @param recipes The list to add the loaded recipe to */ - private boolean loadRecipe(File file) { + private void loadRecipe(File file, List recipes) { try { YamlConfiguration configuration = YamlConfiguration.loadConfiguration(file); ItemRecipe recipe = new RecipeConfiguration(file.getName().replace(".yml", ""), configuration) .build(); - api.addRecipe(recipe); - return true; + recipes.add(recipe); } catch (Exception e) { plugin.getLogger().severe("Could not load recipe from file " + file.getAbsolutePath() + ": " + e.getMessage()); if (api.isDebug()) { e.printStackTrace(); } - return false; } } } \ No newline at end of file diff --git a/src/main/java/fr/traqueur/recipes/api/TagRegistry.java b/src/main/java/fr/traqueur/recipes/api/TagRegistry.java deleted file mode 100644 index 1565270..0000000 --- a/src/main/java/fr/traqueur/recipes/api/TagRegistry.java +++ /dev/null @@ -1,53 +0,0 @@ -package fr.traqueur.recipes.api; - -import org.bukkit.Material; -import org.bukkit.Tag; - -import java.lang.reflect.Field; -import java.util.HashMap; -import java.util.Map; -import java.util.Optional; - -/** - * This class is used to register tags. - */ -public class TagRegistry { - - /** - * The map of tags. - */ - private static final Map> tagMap = new HashMap<>(); - - static { - for (Field field : Tag.class.getDeclaredFields()) { - if (Tag.class.isAssignableFrom(field.getType())) { - try { - Class genericType = (Class) ((java.lang.reflect.ParameterizedType) field.getGenericType()).getActualTypeArguments()[0]; - if (Material.class.isAssignableFrom(genericType)) { - register(field.getName(), (Tag) field.get(null)); - } - } catch (Exception exception) { - throw new RuntimeException("Failed to register tag: " + field.getName(), exception); - } - } - } - } - - /** - * Register a tag. - * @param key the key of the tag. - * @param tag the tag. - */ - private static void register(String key, Tag tag) { - tagMap.put(key, tag); - } - - /** - * Get a tag by its key. - * @param key the key of the tag. - * @return the tag. - */ - public static Optional> getTag(String key) { - return Optional.ofNullable(tagMap.get(key.toUpperCase())); - } -} \ No newline at end of file diff --git a/src/main/java/fr/traqueur/recipes/api/Util.java b/src/main/java/fr/traqueur/recipes/api/Util.java index 79b958e..c898f72 100644 --- a/src/main/java/fr/traqueur/recipes/api/Util.java +++ b/src/main/java/fr/traqueur/recipes/api/Util.java @@ -1,6 +1,15 @@ package fr.traqueur.recipes.api; +import fr.traqueur.recipes.api.domains.Ingredient; +import fr.traqueur.recipes.api.hook.Hook; +import fr.traqueur.recipes.impl.domains.ingredients.ItemStackIngredient; +import fr.traqueur.recipes.impl.domains.ingredients.MaterialIngredient; +import fr.traqueur.recipes.impl.domains.ingredients.StrictItemStackIngredient; +import fr.traqueur.recipes.impl.domains.ingredients.TagIngredient; +import org.bukkit.Bukkit; import org.bukkit.Material; +import org.bukkit.NamespacedKey; +import org.bukkit.Tag; import org.bukkit.inventory.ItemStack; import org.bukkit.util.io.BukkitObjectInputStream; @@ -65,4 +74,79 @@ public static Material getMaterial(String material) { } } + /** + * Parse an ingredient from a string. + * @param itemString The string representation of the ingredient (e.g., "COBBLESTONE", "material:STONE", "item:DIAMOND", "base64:xxx", "tag:planks", "plugin:custom_item") + * @param sign The sign of the ingredient (can be null for shapeless recipes) + * @param strict Whether the ingredient should use strict matching (only applies to item: and base64: types) + * @return The parsed ingredient + */ + public static Ingredient parseIngredient(String itemString, Character sign, boolean strict) { + String[] data = itemString.split(":", 2); + if(data.length == 1) { + return new MaterialIngredient(getMaterial(data[0]), sign); + } else { + return switch (data[0]) { + case "material" -> new MaterialIngredient(getMaterial(data[1]), sign); + case "tag" -> new TagIngredient(getTag(data[1]), sign); + case "item" -> { + // Create ItemStack from Material for ItemStackIngredient + ItemStack stack = new ItemStack(getMaterial(data[1])); + if(strict) { + yield new StrictItemStackIngredient(stack, sign); + } + yield new ItemStackIngredient(stack, sign); + } + case "base64" -> { + if(strict) { + yield new StrictItemStackIngredient(getItemStack(data[1]), sign); + } + yield new ItemStackIngredient(getItemStack(data[1]), sign); + } + default -> Hook.HOOKS.stream() + .filter(Hook::isEnable) + .filter(hook -> hook.getPluginName().equalsIgnoreCase(data[0])) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException("The data " + data[0] + " isn't valid.")) + .getIngredient(data[1], sign); + }; + } + } + + /** + * Parse an ingredient from a string without strict mode. + * @param itemString The string representation of the ingredient + * @param sign The sign of the ingredient (can be null for shapeless recipes) + * @return The parsed ingredient + */ + public static Ingredient parseIngredient(String itemString, Character sign) { + return parseIngredient(itemString, sign, false); + } + + /** + * Parse an ingredient from a string without sign and strict mode. + * @param itemString The string representation of the ingredient + * @return The parsed ingredient + */ + public static Ingredient parseIngredient(String itemString) { + return parseIngredient(itemString, null, false); + } + + /** + * This method is used to get Tag from the string. + * @param data the data to get the tag. + * @return the tag. + */ + private static Tag getTag(String data) { + Tag blockTag = Bukkit.getTag(Tag.REGISTRY_BLOCKS, NamespacedKey.minecraft(data), Material.class); + if (blockTag != null) { + return blockTag; + } + Tag itemTag = Bukkit.getTag(Tag.REGISTRY_ITEMS, NamespacedKey.minecraft(data), Material.class); + if (itemTag != null) { + return itemTag; + } + throw new IllegalArgumentException("The tag " + data + " isn't valid."); + } + } diff --git a/src/main/java/fr/traqueur/recipes/api/domains/Recipe.java b/src/main/java/fr/traqueur/recipes/api/domains/Recipe.java index e8316d6..56493ee 100644 --- a/src/main/java/fr/traqueur/recipes/api/domains/Recipe.java +++ b/src/main/java/fr/traqueur/recipes/api/domains/Recipe.java @@ -209,9 +209,10 @@ default Recipe addIngredient(Material material, Character sign) { * @param result The result of the recipe. * @param amount The amount of the result. * @param experience The experience of the recipe. + * @param priority The priority of the recipe. * @return The item recipe. */ - default ItemRecipe getItemRecipe(List ingredientList, RecipeType type, String[] pattern, int cookingTime, String name, String group, String category, String result, int amount, float experience) { + default ItemRecipe getItemRecipe(List ingredientList, RecipeType type, String[] pattern, int cookingTime, String name, String group, String category, String result, int amount, float experience, int priority) { if (ingredientList.isEmpty()) { throw new IllegalArgumentException("Ingredients are not set"); } @@ -228,7 +229,7 @@ default ItemRecipe getItemRecipe(List ingredientList, RecipeType typ throw new IllegalArgumentException("Cooking time is not set"); } - return new ItemRecipe(name, group, category, type, result, amount, ingredientList.toArray(new Ingredient[0]), pattern, cookingTime, experience); + return new ItemRecipe(name, group, category, type, result, amount, ingredientList.toArray(new Ingredient[0]), pattern, cookingTime, experience, priority); } } diff --git a/src/main/java/fr/traqueur/recipes/api/hook/Hook.java b/src/main/java/fr/traqueur/recipes/api/hook/Hook.java index fa38740..811e2d1 100644 --- a/src/main/java/fr/traqueur/recipes/api/hook/Hook.java +++ b/src/main/java/fr/traqueur/recipes/api/hook/Hook.java @@ -5,7 +5,6 @@ import org.bukkit.Bukkit; import org.bukkit.entity.Player; import org.bukkit.inventory.ItemStack; -import org.bukkit.plugin.java.JavaPlugin; import org.jetbrains.annotations.Nullable; import java.util.ArrayList; diff --git a/src/main/java/fr/traqueur/recipes/impl/domains/ItemRecipe.java b/src/main/java/fr/traqueur/recipes/impl/domains/ItemRecipe.java index 7624a8b..af250ca 100644 --- a/src/main/java/fr/traqueur/recipes/impl/domains/ItemRecipe.java +++ b/src/main/java/fr/traqueur/recipes/impl/domains/ItemRecipe.java @@ -30,9 +30,10 @@ * @param pattern The pattern of the recipe * @param cookingTime The cooking time of the recipe * @param experience The experience of the recipe + * @param priority The priority of the recipe (higher = registered first) */ public record ItemRecipe(String recipeName, String group, String category, RecipeType recipeType, String result, int amount, Ingredient[] ingredients, - String[] pattern, int cookingTime, float experience) { + String[] pattern, int cookingTime, float experience, int priority) { /** * Convert the recipe to a bukkit recipe @@ -134,11 +135,12 @@ public ItemStack toBukkitItemStack(Player player) { ItemStack result; String[] resultParts = this.result.split(":"); if(resultParts.length == 1) { - result = Util.getItemStack(resultParts[0]); + result = new ItemStack(Util.getMaterial(resultParts[0])); } else { result = switch (resultParts[0]) { case "material" -> new ItemStack(Util.getMaterial(resultParts[1])); - case "item", "base64" -> Util.getItemStack(resultParts[1]); + case "item" -> new ItemStack(Util.getMaterial(resultParts[1])); + case "base64" -> Util.getItemStack(resultParts[1]); default -> Hook.HOOKS.stream() .filter(Hook::isEnable) .filter(hook -> hook.getPluginName().equalsIgnoreCase(resultParts[0])) diff --git a/src/main/java/fr/traqueur/recipes/impl/domains/recipes/RecipeBuilder.java b/src/main/java/fr/traqueur/recipes/impl/domains/recipes/RecipeBuilder.java index 55c7adf..13bfacc 100644 --- a/src/main/java/fr/traqueur/recipes/impl/domains/recipes/RecipeBuilder.java +++ b/src/main/java/fr/traqueur/recipes/impl/domains/recipes/RecipeBuilder.java @@ -61,6 +61,11 @@ public class RecipeBuilder implements Recipe { */ private float experience = 0; + /** + * The priority of the recipe (higher = registered first). + */ + private int priority = 0; + /** * The pattern of the recipe. */ @@ -228,6 +233,19 @@ public Recipe setExperience(float experience) { return this; } + /** + * Set the priority of the recipe (higher = registered first). + * @param priority The priority of the recipe. + * @return The recipe. + */ + public Recipe setPriority(int priority) { + if(type == null) { + throw new IllegalArgumentException("Recipe type is not set"); + } + this.priority = priority; + return this; + } + /** * {@inheritDoc} */ @@ -253,6 +271,6 @@ public ItemRecipe build() { throw new IllegalArgumentException("Type is not set"); } - return this.getItemRecipe(ingredientList, type, pattern, cookingTime, name, group, category, result, amount, experience); + return this.getItemRecipe(ingredientList, type, pattern, cookingTime, name, group, category, result, amount, experience, priority); } } diff --git a/src/main/java/fr/traqueur/recipes/impl/domains/recipes/RecipeConfiguration.java b/src/main/java/fr/traqueur/recipes/impl/domains/recipes/RecipeConfiguration.java index 316f17c..a152e5d 100644 --- a/src/main/java/fr/traqueur/recipes/impl/domains/recipes/RecipeConfiguration.java +++ b/src/main/java/fr/traqueur/recipes/impl/domains/recipes/RecipeConfiguration.java @@ -1,30 +1,16 @@ package fr.traqueur.recipes.impl.domains.recipes; import fr.traqueur.recipes.api.RecipeType; -import fr.traqueur.recipes.api.TagRegistry; import fr.traqueur.recipes.api.Util; import fr.traqueur.recipes.api.domains.Ingredient; import fr.traqueur.recipes.api.domains.Recipe; -import fr.traqueur.recipes.api.hook.Hook; import fr.traqueur.recipes.impl.domains.ItemRecipe; -import fr.traqueur.recipes.impl.domains.ingredients.ItemStackIngredient; -import fr.traqueur.recipes.impl.domains.ingredients.MaterialIngredient; -import fr.traqueur.recipes.impl.domains.ingredients.StrictItemStackIngredient; -import fr.traqueur.recipes.impl.domains.ingredients.TagIngredient; -import org.bukkit.Material; -import org.bukkit.Tag; import org.bukkit.configuration.file.YamlConfiguration; -import org.bukkit.inventory.ItemStack; import org.bukkit.inventory.recipe.CookingBookCategory; import org.bukkit.inventory.recipe.CraftingBookCategory; -import org.bukkit.util.io.BukkitObjectInputStream; import org.jetbrains.annotations.NotNull; -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.io.ObjectInputStream; import java.util.*; -import java.util.zip.GZIPInputStream; /** * This class is used to build recipes via yaml configuration. @@ -76,6 +62,11 @@ public class RecipeConfiguration implements Recipe { */ private final float experience; + /** + * The priority of the recipe (higher = registered first). + */ + private final int priority; + /** * The pattern of the recipe. */ @@ -126,31 +117,10 @@ public RecipeConfiguration(String name, String path, YamlConfiguration configura String material = (String) ingredient.get("item"); var objSign = ingredient.getOrDefault("sign", null); Character sign = objSign == null ? null : objSign.toString().charAt(0); + boolean strict = this.isStrict(ingredient); - String[] data = material.split(":"); - if(data.length == 1) { - this.ingredientList.add(new MaterialIngredient(Util.getMaterial(data[0]), sign)); - } else { - Ingredient ingred = switch (data[0]) { - case "material" -> new MaterialIngredient(Util.getMaterial(data[1]), sign); - case "tag" -> new TagIngredient(this.getTag(data[1]), sign); - case "item" -> { - boolean strict = this.isStrict(ingredient); - if(strict) { - yield new StrictItemStackIngredient(Util.getItemStack(data[1]), sign); - } - yield new ItemStackIngredient(Util.getItemStack(data[1]), sign); - } - default -> Hook.HOOKS.stream() - .filter(Hook::isEnable) - .filter(hook -> hook.getPluginName().equalsIgnoreCase(data[0])) - .findFirst() - .orElseThrow(() -> new IllegalArgumentException("The data " + data[0] + " isn't valid.")) - .getIngredient(data[1], sign); - }; - this.ingredientList.add(ingred); - } - + Ingredient ingred = Util.parseIngredient(material, sign, strict); + this.ingredientList.add(ingred); } if(!configuration.contains(path + "result.item")) { @@ -166,15 +136,7 @@ public RecipeConfiguration(String name, String path, YamlConfiguration configura this.cookingTime = configuration.getInt(path + "cooking-time", 0); this.experience = (float) configuration.getDouble(path + "experience", 0d); - } - - /** - * This method is used to get Tag from the string. - * @param data the data to get the tag. - * @return the tag. - */ - private Tag getTag(String data) { - return TagRegistry.getTag(data).orElseThrow(() -> new IllegalArgumentException("The tag " + data + " isn't valid.")); + this.priority = configuration.getInt(path + "priority", 0); } /** @@ -185,33 +147,6 @@ private boolean isStrict(Map ingredient) { return ingredient.containsKey("strict") && (boolean) ingredient.get("strict"); } - /** - * This method is used to check if the category is valid. - * @param category the group to check. - * @return true if the category is valid. - */ - private boolean checkCategory(@NotNull String category) { - if(category.isEmpty()) { - return true; - } - - String upperCategory = category.toUpperCase(); - - for(CookingBookCategory cookingCategory : CookingBookCategory.values()) { - if(cookingCategory.name().equals(upperCategory)) { - return true; - } - } - - for(CraftingBookCategory craftingCategory : CraftingBookCategory.values()) { - if(craftingCategory.name().equals(upperCategory)) { - return true; - } - } - - return false; - } - /** * This method is used to validate the pattern. * It checks if the pattern is valid for a shaped recipe. @@ -380,6 +315,14 @@ public RecipeType getType() { */ @Override public ItemRecipe build() { - return this.getItemRecipe(ingredientList, type, pattern, cookingTime, name, group, category, resultStr, amount, experience); + return this.getItemRecipe(ingredientList, type, pattern, cookingTime, name, group, category, resultStr, amount, experience, priority); + } + + /** + * Get the priority of the recipe. + * @return the priority of the recipe. + */ + public int getPriority() { + return priority; } } From 1b557a4701aa0594bda97673d35c5caff8d654dc Mon Sep 17 00:00:00 2001 From: Traqueur_ Date: Fri, 24 Oct 2025 10:05:43 +0200 Subject: [PATCH 7/7] fix: import --- .../recipes/impl/domains/recipes/RecipeConfiguration.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/fr/traqueur/recipes/impl/domains/recipes/RecipeConfiguration.java b/src/main/java/fr/traqueur/recipes/impl/domains/recipes/RecipeConfiguration.java index a152e5d..147ba25 100644 --- a/src/main/java/fr/traqueur/recipes/impl/domains/recipes/RecipeConfiguration.java +++ b/src/main/java/fr/traqueur/recipes/impl/domains/recipes/RecipeConfiguration.java @@ -6,6 +6,7 @@ import fr.traqueur.recipes.api.domains.Recipe; import fr.traqueur.recipes.impl.domains.ItemRecipe; import org.bukkit.configuration.file.YamlConfiguration; +import org.bukkit.inventory.ItemStack; import org.bukkit.inventory.recipe.CookingBookCategory; import org.bukkit.inventory.recipe.CraftingBookCategory; import org.jetbrains.annotations.NotNull;