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..4950f0d 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -1 +1 @@
-version=2.0.2
\ No newline at end of file
+version=3.0.0
diff --git a/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>2) );
- }
- byte val1 = ((b1 & SIGN)==0)?(byte)(b1>>2):(byte)((b1)>>2^0xc0);
- encodedData[encodedIndex++] = lookUpBase64Alphabet[ val1 ];
- encodedData[encodedIndex++] = lookUpBase64Alphabet[ k<<4 ];
- encodedData[encodedIndex++] = PAD;
- encodedData[encodedIndex++] = PAD;
- } else if (fewerThan24bits == SIXTEENBIT) {
- b1 = binaryData[dataIndex];
- b2 = binaryData[dataIndex +1 ];
- l = ( byte ) ( b2 &0x0f );
- k = ( byte ) ( b1 &0x03 );
-
- byte val1 = ((b1 & SIGN)==0)?(byte)(b1>>2):(byte)((b1)>>2^0xc0);
- byte val2 = ((b2 & SIGN)==0)?(byte)(b2>>4):(byte)((b2)>>4^0xf0);
-
- encodedData[encodedIndex++] = lookUpBase64Alphabet[ val1 ];
- encodedData[encodedIndex++] = lookUpBase64Alphabet[ val2 | ( k<<4 )];
- encodedData[encodedIndex++] = lookUpBase64Alphabet[ l<<2 ];
- encodedData[encodedIndex++] = PAD;
- }
-
- return new String(encodedData);
- }
-
- /**
- * Decodes Base64 data into octects
- *
- * @param encoded string containing Base64 data
- * @return Array containind decoded data.
- */
- public static byte[] decode(String encoded) {
-
- if (encoded == null)
- return null;
-
- char[] base64Data = encoded.toCharArray();
- // remove white spaces
- int len = removeWhiteSpace(base64Data);
-
- if (len%FOURBYTE != 0) {
- return null;//should be divisible by four
- }
-
- int numberQuadruple = (len/FOURBYTE );
-
- if (numberQuadruple == 0)
- return new byte[0];
-
- byte decodedData[] = null;
- byte b1=0,b2=0,b3=0,b4=0;
- char d1=0,d2=0,d3=0,d4=0;
-
- int i = 0;
- int encodedIndex = 0;
- int dataIndex = 0;
- decodedData = new byte[ (numberQuadruple)*3];
-
- for (; 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 62dd6f7..a53c229 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;
}
@@ -202,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);