diff --git a/README.md b/README.md index de5d4a5..c8c9ac6 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,9 +92,19 @@ 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 ItemRecipe recipe4 = new RecipeBuilder() .setType(RecipeType.SMELTING) @@ -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,6 +189,37 @@ The API supports several types of ingredients: - **Tag**: Minecraft tags (e.g., planks, logs, wool) - **Plugin Items**: ItemsAdder and Oraxen custom items +<<<<<<< HEAD +### 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 +``` + ### 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 @@ -291,6 +335,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,6 +346,62 @@ For `CRAFTING_SHAPED` recipes, the pattern is validated: - Empty rows are not allowed ### Ingredient Types in YAML + +#### 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 - `item: MATERIAL_NAME` - Simple material - `item: material:MATERIAL_NAME` - Explicit material - `item: tag:TAG_NAME` - Minecraft tag @@ -329,13 +430,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/build.gradle b/build.gradle index 6fc9877..aeffb6a 100644 --- a/build.gradle +++ b/build.gradle @@ -24,7 +24,7 @@ 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' @@ -44,6 +44,8 @@ tasks.register('generateVersionProperties') { processResources.dependsOn generateVersionProperties java { + sourceCompatibility = JavaVersion.VERSION_21 + targetCompatibility = JavaVersion.VERSION_21 withSourcesJar() withJavadocJar() } 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/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 new file mode 100644 index 0000000..fd07ed8 --- /dev/null +++ b/src/main/java/fr/traqueur/recipes/api/Util.java @@ -0,0 +1,148 @@ +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; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.ObjectInputStream; +import java.util.Base64; +import java.util.zip.GZIPInputStream; + +/** + * This class is used to provide utility methods for recipes. + */ +public class Util { + + /** + * Private constructor to prevent instantiation of this utility class. + */ + private Util() { + throw new UnsupportedOperationException("This is a utility class and cannot be instantiated"); + } + + /** + * 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()); + } + } + + /** + * This method is used to convert an itemstack to a base64 string. + * @param itemStack the item stack. + * @return the base64 string. + */ + 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."); + } + } + + /** + * 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); + }; + } + } + + /** + * 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 4d5b77d..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, 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, 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 a199b18..811e2d1 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,9 @@ 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 +53,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 a53c229..d5b7ca3 100644 --- a/src/main/java/fr/traqueur/recipes/impl/PrepareCraftListener.java +++ b/src/main/java/fr/traqueur/recipes/impl/PrepareCraftListener.java @@ -5,9 +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.*; @@ -61,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(); @@ -80,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)); } }); } @@ -130,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())); } } @@ -147,7 +153,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; @@ -160,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); } } } @@ -176,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); @@ -211,6 +217,9 @@ private void checkGoodShapedRecipe(ItemRecipe itemRecipe, PrepareItemCraftEvent return; } } + + this.api.debug("The shaped recipe %s is good.", itemRecipe.getKey()); + event.getInventory().setResult(itemRecipe.toBukkitItemStack(player)); } /** @@ -218,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(); @@ -247,5 +256,6 @@ private void checkGoodShapelessRecipe(ItemRecipe itemRecipe, PrepareItemCraftEve } this.api.debug("The shapeless recipe %s is good.", itemRecipe.getKey()); + 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..2227a71 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 @@ -19,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, ItemStack result, int amount, Ingredient[] ingredients, - String[] pattern, int cookingTime, float experience) { +public record ItemRecipe(String recipeName, String group, String category, RecipeType recipeType, String result, int amount, Ingredient[] ingredients, + String[] pattern, int cookingTime, float experience, int priority) { /** * Convert the recipe to a bukkit recipe @@ -114,12 +126,38 @@ 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); } + /** + * Convert the result to a bukkit item stack + * @param player The player to get the item stack for (can be null) + * @return The bukkit item stack + */ + public ItemStack toBukkitItemStack(Player player) { + ItemStack result; + String[] resultParts = this.result.split(":"); + if(resultParts.length == 1) { + result = new ItemStack(Util.getMaterial(resultParts[0])); + } else { + result = switch (resultParts[0]) { + case "material" -> new ItemStack(Util.getMaterial(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])) + .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..9ef8cdd 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; @@ -15,6 +16,12 @@ */ public class RecipeBuilder implements Recipe { + /** + * Default constructor. + */ + public RecipeBuilder() { + } + /** * The list of ingredients. */ @@ -28,7 +35,7 @@ public class RecipeBuilder implements Recipe { /** * The result of the recipe. */ - private ItemStack result; + private String result; /** * The amount of the result. @@ -60,6 +67,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. */ @@ -85,7 +97,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; } @@ -227,6 +239,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} */ @@ -252,6 +277,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 49df875..9d97e9a 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,29 +1,17 @@ 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. @@ -43,7 +31,7 @@ public class RecipeConfiguration implements Recipe { /** * The result of the recipe. */ - private final ItemStack result; + private final String resultStr; /** * The amount of the result. @@ -75,6 +63,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. */ @@ -125,31 +118,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(this.getMaterial(data[0]), sign)); - } else { - Ingredient ingred = switch (data[0]) { - case "material" -> new MaterialIngredient(this.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 ItemStackIngredient(this.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")) { @@ -159,35 +131,13 @@ 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); 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); } /** @@ -198,46 +148,6 @@ private boolean isStrict(Map ingredient) { return ingredient.containsKey("strict") && (boolean) ingredient.get("strict"); } - /** - * This method is used to get the itemstack from base64 string - * @param base64itemstack the base64 item stack. - * @return the item stack. - */ - 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."); - } - - 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()); - } - } - - /** - * 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."); - } - } - /** * This method is used to check if the category is valid. * @param category the group to check. @@ -402,11 +312,19 @@ public RecipeType getType() { throw new UnsupportedOperationException("Not supported yet."); } + /** + * Get the priority of the recipe. + * @return the priority of the recipe. + */ + public int getPriority() { + return priority; + } + /** * {@inheritDoc} */ @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, priority); } } 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/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