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/4] 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/4] 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/4] 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 1a965ffaeefdc3f1500039d945b7ba29644078b5 Mon Sep 17 00:00:00 2001 From: Traqueur <54551467+Traqueur-dev@users.noreply.github.com> Date: Wed, 1 Oct 2025 21:52:47 +0200 Subject: [PATCH 4/4] Feat/modernize (#31) * feat: modernize * feat: version * fix: shapeless craft --- README.md | 286 ++++++++++++-- build.gradle | 7 +- gradle.properties | 2 +- .../java/fr/traqueur/recipes/api/Base64.java | 348 ------------------ .../fr/traqueur/recipes/api/RecipeLoader.java | 224 +++++++++++ .../fr/traqueur/recipes/api/RecipesAPI.java | 104 +----- .../traqueur/recipes/api/domains/Recipe.java | 2 +- .../recipes/impl/PrepareCraftListener.java | 33 +- .../ingredients/ItemStackIngredient.java | 26 +- .../domains/recipes/RecipeConfiguration.java | 115 +++++- .../recipes/impl/updater/Updater.java | 4 +- 11 files changed, 630 insertions(+), 521 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 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..6fc9877 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" @@ -29,12 +28,12 @@ dependencies { // 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() } diff --git a/gradle.properties b/gradle.properties index fb7cb53..317fe99 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1 +1 @@ -version=2.0.2 \ No newline at end of file +version=3.0.0 \ 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..f8724b8 100644 --- a/src/main/java/fr/traqueur/recipes/api/RecipesAPI.java +++ b/src/main/java/fr/traqueur/recipes/api/RecipesAPI.java @@ -3,23 +3,11 @@ 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.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 +30,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 +44,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 +53,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 +116,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..a53c229 100644 --- a/src/main/java/fr/traqueur/recipes/impl/PrepareCraftListener.java +++ b/src/main/java/fr/traqueur/recipes/impl/PrepareCraftListener.java @@ -219,28 +219,33 @@ 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()); } } 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);