diff --git a/docs/adr/0002-recipe-rewrite.md b/docs/adr/0002-recipe-rewrite.md index 8d2487f54b..4882438a94 100644 --- a/docs/adr/0002-recipe-rewrite.md +++ b/docs/adr/0002-recipe-rewrite.md @@ -1,7 +1,7 @@ # 2. Recipe rewrite Date: 2024-11-03 -Last update: 2024-11-03 +Last update: 2024-11-08 **DO NOT rely on any APIs introduced until we finish the work completely!** @@ -32,9 +32,9 @@ Slimefun, focusing on Slimefun recipes - Performance: Should not blow up any servers -The new system should also be completely backwards compatible with the old. +The new recipe system should also be completely backwards compatible. -## API Changes +## API Additions ### 5 main recipe classes @@ -61,7 +61,7 @@ An `RecipeOutputItem`s controls how an output is generated when the recipe is crafted. It can be a single item (see `RecipeOutputItemStack`, `RecipeOutputSlimefunItem`), or a group of items each with a certain weight of being output (see `RecipeOutputGroup`). -#### Examples +#### Examples (pseudocode) Here are the inputs and outputs of the recipe for a vanilla torch @@ -104,9 +104,9 @@ This is the public interface for the recipe system, there are methods here to ad load, save, and search recipes. It also stores a map of `MatchProcedures` and `RecipeType` by key for conversions from a string -## JSON Serialization +### JSON Serialization -All recipes should be able to be serialized to and deserialized +All recipes are able to be serialized to and deserialized from JSON. The schemas are shown below. Here, `key` is the string representation of a namespaced key @@ -124,7 +124,8 @@ Here, `key` is the string representation of a namespaced key } ``` -The recipe deserializer also needs a `__filename` field, which is inserted when the file is read, so it doesn't (and shouldn't) be in the schema +The recipe deserializer technically needs a `__filename` field, but it is +inserted when the file is read, so it isn't (and shouldn't) be in the schema `RecipeInput` @@ -174,7 +175,7 @@ The recipe deserializer also needs a `__filename` field, which is inserted when } ``` -*In addition to those schemata, items can be in short form: +*In addition to those schemas, items can be in short form: - Single items: `:|` - Tags: `#:|` @@ -183,22 +184,76 @@ The recipe deserializer also needs a `__filename` field, which is inserted when The 5 main recipe classes are all polymorphic, and subclasses can be used in their stead, and should not affect the recipe system (as long as the right methods are -override, see javadocs) +overriden, see javadocs) ### Custom serialization/deserialization The default deserializers recognize subclasses with custom deserializers by -the presence of a `class` field in the json, which is the key of a +the presence of a `class` field in the json, which should be the key of a custom deserializer registered with Slimefun's `RecipeService`. For custom serializers, override the `serialize` method on the subclass, and ensure they also add the `class` field +## Recipe Lifecycle + +### Stage 1a + +When Slimefun is enabled, all recipes in the resources folder will be +moved to `plugins/Slimefun/recipes/` (unless a file with its name already exists). + +Addons should do the same. (We recommend saving to +`plugins/Slimefun/recipes//` but it's not required). + +To prevent unnecessary file operations, Slimefun/addons first send a list of +filenames of recipes present in the resources folder to the recipe service, +which then filters out all the files that already exist. Then each recipe can +be read and copied over. + +### Stage 1b + +Also on enable, recipes defined in code should be registered. These two steps +can be done in no particular order. + +### Stage 2 + +On the first server tick, all recipes in the `plugins/Slimefun/recipes` folder +are read and added to the `RecipeService`, removing all recipes with the +same filename. This is why recipes should ideally be *defined* in JSON, +to prevent unnecessary work. + +When loading JSON recipes, we also need to be able to tell the difference between +a server owner changing a recipe, and a developer changing a recipe. To do this, +we use a system called Recipe Overrides; it allows for updates to recipes from +developers while also preserving custom recipes by server owners + +- Slimefun/addons should tell the recipe service it will apply a recipe + override on enable, **before** any JSON recipes are copied from the resources + folder +- The recipe service checks all recipe overrides that have already run + (in the file `plugins/Slimefun/recipe-overrides`) and if it never received + that override before, it deletes the old files and all recipes inside them. + Then all recipes are loaded as before. + +### Stage 3 + +While the server is running, recipes can be modified in code, saved to disk, or +re-loaded from disk. New recipes can also be added, however not to any existing +file (unless forced, which is not recommended) + +### Stage 4 + +On server shutdown (or `/sf recipe save`), **all** recipes are saved to disk. +This means any changes made while the server is running will be overwritten. +Server owners should run `/sf recipe reload ` to load new recipes +dynamically from disk. + ## Phases Each phase should be a separate PR - Phase 1 - Add the new API -- Phase 2 - Migrate Slimefun toward the new API +- Phase 2 - Migrate Slimefun items/multiblocks/machines toward the new API +- Phase 3 - Update the Slimefun Guide to use the new API The entire process should be seamless for the end users, and backwards compatible with addons that haven't yet migrated diff --git a/pom.xml b/pom.xml index 5bc0d0d651..9122bd28eb 100644 --- a/pom.xml +++ b/pom.xml @@ -317,6 +317,7 @@ biome-maps/*.json languages/**/*.yml + recipes/**/*.json diff --git a/src/main/java/io/github/thebusybiscuit/slimefun4/api/SlimefunAddon.java b/src/main/java/io/github/thebusybiscuit/slimefun4/api/SlimefunAddon.java index 8d8609c8be..32b9b649df 100644 --- a/src/main/java/io/github/thebusybiscuit/slimefun4/api/SlimefunAddon.java +++ b/src/main/java/io/github/thebusybiscuit/slimefun4/api/SlimefunAddon.java @@ -1,6 +1,18 @@ package io.github.thebusybiscuit.slimefun4.api; +import java.io.InputStream; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; +import java.nio.file.FileSystem; +import java.nio.file.FileSystems; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Collections; +import java.util.Set; import java.util.logging.Logger; +import java.util.stream.Collectors; +import java.util.stream.Stream; import javax.annotation.Nonnull; import javax.annotation.Nullable; @@ -11,6 +23,8 @@ import org.bukkit.plugin.java.JavaPlugin; import io.github.thebusybiscuit.slimefun4.api.items.SlimefunItem; +import io.github.thebusybiscuit.slimefun4.core.services.RecipeService; +import io.github.thebusybiscuit.slimefun4.implementation.Slimefun; /** * This is a very basic interface that will be used to identify @@ -97,4 +111,72 @@ default boolean hasDependency(@Nonnull String dependency) { return description.getDepend().contains(dependency) || description.getSoftDepend().contains(dependency); } + /** + * @return A list of all recipes in the resources folder. Addons + * can override this to filter out certain recipes, if desired. + */ + default Set getResourceRecipeFilenames() { + URL resourceDir = getClass().getResource("/recipes"); + if (resourceDir == null) { + return Collections.emptySet(); + } + URI resourceUri; + try { + resourceUri = resourceDir.toURI(); + } catch (URISyntaxException e) { + return Collections.emptySet(); + } + if (!resourceUri.getScheme().equals("jar")) { + return Collections.emptySet(); + } + try (FileSystem fs = FileSystems.newFileSystem(resourceUri, Collections.emptyMap())) { + Path recipeDir = fs.getPath("/recipes"); + try (Stream files = Files.walk(recipeDir)) { + var names = files + .filter(file -> file.toString().endsWith(".json")) + .map(file -> { + String filename = recipeDir.relativize(file).toString(); + return filename.substring(0, filename.length() - 5); + }) + .collect(Collectors.toSet()); + return names; + } catch (Exception e) { + return Collections.emptySet(); + } + } catch (Exception e) { + return Collections.emptySet(); + } + } + + /** + * Copies all recipes in the recipes folder of the jar to + * plugins/Slimefun/recipes/[subdirectory] + * This should be done on enable. If you need to add + * any recipe overrides, those should be done before calling + * this method. + * @param subdirectory The subdirectory to copy files to + */ + default void copyResourceRecipes(String subdirectory) { + Set existingRecipes = Slimefun.getRecipeService().getAllRecipeFilenames(); + Set resourceNames = getResourceRecipeFilenames(); + resourceNames.removeIf(existingRecipes::contains); + for (String name : resourceNames) { + try (InputStream source = getClass().getResourceAsStream("/recipes/" + name + ".json")) { + Files.copy(source, Path.of(RecipeService.SAVED_RECIPE_DIR, subdirectory, name + ".json")); + } catch (Exception e) { + getLogger().warning("Couldn't copy recipes in resource file '" + name + "': " + e.getLocalizedMessage()); + } + } + } + + /** + * Copies all recipes in the recipes folder of the jar to + * plugins/Slimefun/recipes. This should be done on enable. + * If you need to add any recipe overrides, those should + * be done before calling this method. + */ + default void copyResourceRecipes() { + copyResourceRecipes(""); + } + } diff --git a/src/main/java/io/github/thebusybiscuit/slimefun4/api/recipes/Recipe.java b/src/main/java/io/github/thebusybiscuit/slimefun4/api/recipes/Recipe.java index 8e1f6d71cb..0290bb5aa0 100644 --- a/src/main/java/io/github/thebusybiscuit/slimefun4/api/recipes/Recipe.java +++ b/src/main/java/io/github/thebusybiscuit/slimefun4/api/recipes/Recipe.java @@ -66,7 +66,7 @@ public static Recipe fromItemStacks(String id, ItemStack[] inputs, ItemStack[] o public static Recipe fromItemStacks(ItemStack[] inputs, ItemStack[] outputs, RecipeType type, MatchProcedure match) { return new Recipe( Optional.empty(), - "other_recipes", + "other_recipes.json", RecipeInput.fromItemStacks(inputs, match), RecipeOutput.fromItemStacks(outputs), List.of(type), @@ -193,11 +193,14 @@ public String toString() { public JsonElement serialize(JsonSerializationContext context) { JsonObject recipe = new JsonObject(); + if (id.isPresent()) { + recipe.addProperty("id", id.get()); + } if (!input.isEmpty()) { - recipe.add("input", context.serialize(input, AbstractRecipeInput.class)); + recipe.add("input", input.serialize(context)); } if (!output.isEmpty()) { - recipe.add("output", context.serialize(output, AbstractRecipeOutput.class)); + recipe.add("output", output.serialize(context)); } if (types.size() == 1) { recipe.addProperty("type", types.stream().findFirst().get().toString()); diff --git a/src/main/java/io/github/thebusybiscuit/slimefun4/api/recipes/RecipeBuilder.java b/src/main/java/io/github/thebusybiscuit/slimefun4/api/recipes/RecipeBuilder.java index ab24299851..18b31520dc 100644 --- a/src/main/java/io/github/thebusybiscuit/slimefun4/api/recipes/RecipeBuilder.java +++ b/src/main/java/io/github/thebusybiscuit/slimefun4/api/recipes/RecipeBuilder.java @@ -3,9 +3,10 @@ import java.util.ArrayList; import java.util.List; import java.util.Optional; -import java.util.function.BiFunction; import java.util.function.Function; +import javax.annotation.Nonnull; + import org.bukkit.Material; import org.bukkit.Tag; import org.bukkit.inventory.ItemStack; @@ -22,21 +23,27 @@ import io.github.thebusybiscuit.slimefun4.api.recipes.items.RecipeOutputSlimefunItem; import io.github.thebusybiscuit.slimefun4.api.recipes.items.RecipeOutputTag; import io.github.thebusybiscuit.slimefun4.api.recipes.matching.MatchProcedure; -import io.github.thebusybiscuit.slimefun4.core.services.RecipeService; public class RecipeBuilder { + @FunctionalInterface + static interface InputGenerator { + AbstractRecipeInput create(List inputs, MatchProcedure match, int width, int height); + } + private final List inputItems = new ArrayList<>(); private final List outputItems = new ArrayList<>(); private final List types = new ArrayList<>(); private final List permissionNodes = new ArrayList<>(); - private MatchProcedure match = MatchProcedure.DUMMY; - private BiFunction, MatchProcedure, AbstractRecipeInput> inputGenerator = RecipeInput::new; + private MatchProcedure match = null; + private int width = 3; + private int height = 3; + private InputGenerator inputGenerator = RecipeInput::new; private Function, AbstractRecipeOutput> outputGenerator = RecipeOutput::new; private Optional energy = Optional.empty(); private Optional craftingTime = Optional.empty(); private Optional id = Optional.empty(); - private String filename = RecipeService.SAVED_RECIPE_DIR + "other_recipes.json"; + private String filename = null; public RecipeBuilder() {} @@ -44,7 +51,7 @@ public Recipe build() { return new Recipe( id, filename, - inputGenerator.apply(inputItems, match), + inputGenerator.create(inputItems, match, width, height), outputGenerator.apply(outputItems), types, energy, @@ -53,7 +60,7 @@ public Recipe build() { ); } - public RecipeBuilder i(AbstractRecipeInputItem i) { + public RecipeBuilder i(@Nonnull AbstractRecipeInputItem i) { inputItems.add(i); return this; } @@ -62,16 +69,23 @@ public RecipeBuilder i(AbstractRecipeInputItem i) { public RecipeBuilder i(ItemStack item, int amount, int durabilityCost) { return i(RecipeInputItem.fromItemStack(item, amount, durabilityCost)); } public RecipeBuilder i(ItemStack item, int amount) { return i(RecipeInputItem.fromItemStack(item, amount)); } public RecipeBuilder i(ItemStack item) { return i(RecipeInputItem.fromItemStack(item)); } - public RecipeBuilder i(Material mat, int amount, int durabilityCost) { return i(new RecipeInputItemStack(mat, amount, durabilityCost)); } - public RecipeBuilder i(Material mat, int amount) { return i(new RecipeInputItemStack(mat, amount)); } - public RecipeBuilder i(Material mat) { return i(new RecipeInputItemStack(mat)); } - public RecipeBuilder i(String id, int amount, int durabilityCost) { return i(new RecipeInputSlimefunItem(id, amount, durabilityCost)); } - public RecipeBuilder i(String id, int amount) { return i(new RecipeInputSlimefunItem(id, amount)); } - public RecipeBuilder i(String id) { return i(new RecipeInputSlimefunItem(id)); } - public RecipeBuilder i(Tag id, int amount, int durabilityCost) { return i(new RecipeInputTag(id, amount, durabilityCost)); } - public RecipeBuilder i(Tag id, int amount) { return i(new RecipeInputTag(id, amount)); } - public RecipeBuilder i(Tag id) { return i(new RecipeInputTag(id)); } - public RecipeBuilder i(List group) { return i(new RecipeInputGroup(group)); } + public RecipeBuilder i(@Nonnull Material mat, int amount, int durabilityCost) { return i(new RecipeInputItemStack(mat, amount, durabilityCost)); } + public RecipeBuilder i(@Nonnull Material mat, int amount) { return i(new RecipeInputItemStack(mat, amount)); } + public RecipeBuilder i(@Nonnull Material mat) { return i(new RecipeInputItemStack(mat)); } + public RecipeBuilder i(@Nonnull String id, int amount, int durabilityCost) { return i(new RecipeInputSlimefunItem(id, amount, durabilityCost)); } + public RecipeBuilder i(@Nonnull String id, int amount) { return i(new RecipeInputSlimefunItem(id, amount)); } + public RecipeBuilder i(@Nonnull String id) { return i(new RecipeInputSlimefunItem(id)); } + public RecipeBuilder i(@Nonnull Tag id, int amount, int durabilityCost) { return i(new RecipeInputTag(id, amount, durabilityCost)); } + public RecipeBuilder i(@Nonnull Tag id, int amount) { return i(new RecipeInputTag(id, amount)); } + public RecipeBuilder i(@Nonnull Tag id) { return i(new RecipeInputTag(id)); } + public RecipeBuilder i(@Nonnull List group) { return i(new RecipeInputGroup(group)); } + + public RecipeBuilder i(@Nonnull ItemStack[] items) { + for (ItemStack item : items) { + i(item); + } + return this; + } public RecipeBuilder i() { return i(RecipeInputItem.EMPTY); @@ -85,40 +99,54 @@ public RecipeBuilder i(int amount) { } - public RecipeBuilder inputGenerator(BiFunction, MatchProcedure, AbstractRecipeInput> generator) { + public RecipeBuilder inputGenerator(@Nonnull InputGenerator generator) { this.inputGenerator = generator; return this; } - public RecipeBuilder o(AbstractRecipeOutputItem o) { + public RecipeBuilder o(@Nonnull AbstractRecipeOutputItem o) { outputItems.add(o); return this; } public RecipeBuilder o(ItemStack item, int amount) { return o(RecipeOutputItem.fromItemStack(item, amount)); } public RecipeBuilder o(ItemStack item) { return o(RecipeOutputItem.fromItemStack(item)); } - public RecipeBuilder o(Material item, int amount) { return o(new RecipeOutputItemStack(item, amount)); } - public RecipeBuilder o(Material item) { return o(new RecipeOutputItemStack(item)); } - public RecipeBuilder o(String id, int amount) { return o(new RecipeOutputSlimefunItem(id, amount)); } - public RecipeBuilder o(String id) { return o(new RecipeOutputSlimefunItem(id)); } - public RecipeBuilder o(Tag id, int amount) { return o(new RecipeOutputTag(id, amount)); } - public RecipeBuilder o(Tag id) { return o(new RecipeOutputTag(id)); } + public RecipeBuilder o(@Nonnull Material item, int amount) { return o(new RecipeOutputItemStack(item, amount)); } + public RecipeBuilder o(@Nonnull Material item) { return o(new RecipeOutputItemStack(item)); } + public RecipeBuilder o(@Nonnull String id, int amount) { return o(new RecipeOutputSlimefunItem(id, amount)); } + public RecipeBuilder o(@Nonnull String id) { return o(new RecipeOutputSlimefunItem(id)); } + public RecipeBuilder o(@Nonnull Tag id, int amount) { return o(new RecipeOutputTag(id, amount)); } + public RecipeBuilder o(@Nonnull Tag id) { return o(new RecipeOutputTag(id)); } public RecipeBuilder o() { return o(RecipeOutputItem.EMPTY); } - public RecipeBuilder outputGenerator(Function, AbstractRecipeOutput> generator) { + public RecipeBuilder outputGenerator(@Nonnull Function, AbstractRecipeOutput> generator) { this.outputGenerator = generator; return this; } - public RecipeBuilder t(RecipeType t) { + public RecipeBuilder type(@Nonnull RecipeType t) { types.add(t); + if (match == null) { + match = t.getDefaultMatchProcedure(); + } return this; } - public RecipeBuilder permission(String p) { + public RecipeBuilder match(@Nonnull MatchProcedure match) { + this.match = match; + return this; + } + + public RecipeBuilder dim(int width, int height) { + this.width = width; + this.height = height; + return this; + } + + public RecipeBuilder permission(@Nonnull String p) { permissionNodes.add(p); return this; } @@ -133,13 +161,16 @@ public RecipeBuilder craftingTime(int ticks) { return this; } - public RecipeBuilder id(String id) { + public RecipeBuilder id(@Nonnull String id) { this.id = Optional.of(id); + if (filename == null){ + filename = id; + } return this; } - public RecipeBuilder filename(String filename) { - this.filename = RecipeService.SAVED_RECIPE_DIR + filename; + public RecipeBuilder filename(@Nonnull String filename) { + this.filename = filename; return this; } diff --git a/src/main/java/io/github/thebusybiscuit/slimefun4/api/recipes/RecipeInput.java b/src/main/java/io/github/thebusybiscuit/slimefun4/api/recipes/RecipeInput.java index cbbcd533a7..01f225fc97 100644 --- a/src/main/java/io/github/thebusybiscuit/slimefun4/api/recipes/RecipeInput.java +++ b/src/main/java/io/github/thebusybiscuit/slimefun4/api/recipes/RecipeInput.java @@ -229,7 +229,7 @@ public JsonElement serialize(JsonSerializationContext context) { for (Map.Entry entry : keys.entrySet()) { key.add( String.valueOf(RecipeUtils.getKeyCharByNumber(entry.getValue())), - context.serialize(entry.getKey(), AbstractRecipeInputItem.class) + entry.getKey().serialize(context) ); } input.add("items", jsonTemplate); diff --git a/src/main/java/io/github/thebusybiscuit/slimefun4/api/recipes/RecipeOutput.java b/src/main/java/io/github/thebusybiscuit/slimefun4/api/recipes/RecipeOutput.java index cc844e5549..79c1ce4fa5 100644 --- a/src/main/java/io/github/thebusybiscuit/slimefun4/api/recipes/RecipeOutput.java +++ b/src/main/java/io/github/thebusybiscuit/slimefun4/api/recipes/RecipeOutput.java @@ -167,7 +167,7 @@ public JsonElement serialize(JsonSerializationContext context) { JsonObject output = new JsonObject(); JsonArray arr = new JsonArray(); for (AbstractRecipeOutputItem item : items) { - arr.add(context.serialize(item, AbstractRecipeOutputItem.class)); + arr.add(item.serialize(context)); } output.add("items", arr); return output; diff --git a/src/main/java/io/github/thebusybiscuit/slimefun4/api/recipes/items/RecipeInputItem.java b/src/main/java/io/github/thebusybiscuit/slimefun4/api/recipes/items/RecipeInputItem.java index ccca9fc433..b632210b0b 100644 --- a/src/main/java/io/github/thebusybiscuit/slimefun4/api/recipes/items/RecipeInputItem.java +++ b/src/main/java/io/github/thebusybiscuit/slimefun4/api/recipes/items/RecipeInputItem.java @@ -12,8 +12,9 @@ import com.google.gson.JsonPrimitive; import com.google.gson.JsonSerializationContext; -import io.github.thebusybiscuit.slimefun4.api.items.SlimefunItem; +import io.github.thebusybiscuit.slimefun4.api.items.SlimefunItemStack; import io.github.thebusybiscuit.slimefun4.api.recipes.matching.ItemMatchResult; +import io.github.thebusybiscuit.slimefun4.implementation.Slimefun; import io.github.thebusybiscuit.slimefun4.utils.RecipeUtils; public abstract class RecipeInputItem extends AbstractRecipeInputItem { @@ -135,10 +136,15 @@ public static AbstractRecipeInputItem fromItemStack(@Nullable ItemStack item, in return RecipeInputItem.EMPTY; } - SlimefunItem sfItem = SlimefunItem.getByItem(item); + if (item instanceof SlimefunItemStack sfItem) { + return new RecipeInputSlimefunItem(sfItem.getItemId(), amount, durabilityCost); + } + + // The item might not have been registered yet and we only need the id, so no need for `getByItem()` + Optional itemID = Slimefun.getItemDataService().getItemData(item); - if (sfItem != null) { - return new RecipeInputSlimefunItem(sfItem.getId(), amount, durabilityCost); + if (itemID.isPresent()) { + return new RecipeInputSlimefunItem(itemID.get(), amount, durabilityCost); } else { return new RecipeInputItemStack(item, amount, durabilityCost); } diff --git a/src/main/java/io/github/thebusybiscuit/slimefun4/api/recipes/items/RecipeInputItemStack.java b/src/main/java/io/github/thebusybiscuit/slimefun4/api/recipes/items/RecipeInputItemStack.java index 791552c990..67f1ff87cf 100644 --- a/src/main/java/io/github/thebusybiscuit/slimefun4/api/recipes/items/RecipeInputItemStack.java +++ b/src/main/java/io/github/thebusybiscuit/slimefun4/api/recipes/items/RecipeInputItemStack.java @@ -68,13 +68,13 @@ public ItemStack getItemDisplay() { @Override public ItemMatchResult matchItem(ItemStack item, AbstractRecipeInputItem root) { if (item == null || item.getType().isAir()) { - return new ItemMatchResult(isEmpty(), root, item, getAmount()); + return new ItemMatchResult(isEmpty(), root, item, getAmount(), getDurabilityCost()); } else if (item.getAmount() < getAmount()) { - return new ItemMatchResult(false, root, item, getAmount()); + return new ItemMatchResult(false, root, item, getAmount(), getDurabilityCost()); } return new ItemMatchResult( SlimefunUtils.isItemSimilar(item, template, false), - root, item, getAmount() + root, item, getAmount(), getDurabilityCost() ); } diff --git a/src/main/java/io/github/thebusybiscuit/slimefun4/api/recipes/items/RecipeInputSlimefunItem.java b/src/main/java/io/github/thebusybiscuit/slimefun4/api/recipes/items/RecipeInputSlimefunItem.java index d2e135c672..e7efe4e485 100644 --- a/src/main/java/io/github/thebusybiscuit/slimefun4/api/recipes/items/RecipeInputSlimefunItem.java +++ b/src/main/java/io/github/thebusybiscuit/slimefun4/api/recipes/items/RecipeInputSlimefunItem.java @@ -51,13 +51,13 @@ public ItemStack getItemDisplay() { @Override public ItemMatchResult matchItem(ItemStack item, AbstractRecipeInputItem root) { if (item == null || item.getType().isAir()) { - return new ItemMatchResult(isEmpty(), root, item, getAmount()); + return new ItemMatchResult(isEmpty(), root, item, getAmount(), getDurabilityCost()); } else if (item.getAmount() < getAmount()) { - return new ItemMatchResult(false, root, item, getAmount()); + return new ItemMatchResult(false, root, item, getAmount(), getDurabilityCost()); } return new ItemMatchResult( SlimefunUtils.isItemSimilar(item, SlimefunItem.getById(slimefunId).getItem(), false), - root, item, getAmount() + root, item, getAmount(), getDurabilityCost() ); } diff --git a/src/main/java/io/github/thebusybiscuit/slimefun4/api/recipes/items/RecipeInputTag.java b/src/main/java/io/github/thebusybiscuit/slimefun4/api/recipes/items/RecipeInputTag.java index 0f08f78201..93674926e8 100644 --- a/src/main/java/io/github/thebusybiscuit/slimefun4/api/recipes/items/RecipeInputTag.java +++ b/src/main/java/io/github/thebusybiscuit/slimefun4/api/recipes/items/RecipeInputTag.java @@ -43,20 +43,20 @@ public ItemStack getItemDisplay() { @Override public ItemMatchResult matchItem(ItemStack item, AbstractRecipeInputItem root) { if (item == null || item.getType().isAir()) { - return new ItemMatchResult(isEmpty(), root, item, getAmount()); + return new ItemMatchResult(isEmpty(), root, item, getAmount(), getDurabilityCost()); } else if (item.getAmount() < getAmount()) { - return new ItemMatchResult(false, root, item, getAmount()); + return new ItemMatchResult(false, root, item, getAmount(), getDurabilityCost()); } for (Material mat : tag.getValues()) { ItemStack template = new ItemStack(mat); if (SlimefunUtils.isItemSimilar(item, template, true)) { return new ItemMatchResult( SlimefunUtils.isItemSimilar(item, template, false), - root, item, getAmount() + root, item, getAmount(), getDurabilityCost() ); } } - return new ItemMatchResult(false, root, item, getAmount()); + return new ItemMatchResult(false, root, item, getAmount(), getDurabilityCost()); } @Override diff --git a/src/main/java/io/github/thebusybiscuit/slimefun4/api/recipes/items/RecipeOutputItem.java b/src/main/java/io/github/thebusybiscuit/slimefun4/api/recipes/items/RecipeOutputItem.java index d1e88e029c..a9aebb7d95 100644 --- a/src/main/java/io/github/thebusybiscuit/slimefun4/api/recipes/items/RecipeOutputItem.java +++ b/src/main/java/io/github/thebusybiscuit/slimefun4/api/recipes/items/RecipeOutputItem.java @@ -10,8 +10,9 @@ import com.google.gson.JsonPrimitive; import com.google.gson.JsonSerializationContext; -import io.github.thebusybiscuit.slimefun4.api.items.SlimefunItem; +import io.github.thebusybiscuit.slimefun4.api.items.SlimefunItemStack; import io.github.thebusybiscuit.slimefun4.api.recipes.matching.RecipeMatchResult; +import io.github.thebusybiscuit.slimefun4.implementation.Slimefun; import io.github.thebusybiscuit.slimefun4.utils.RecipeUtils; public abstract class RecipeOutputItem extends AbstractRecipeOutputItem { @@ -112,10 +113,15 @@ public static AbstractRecipeOutputItem fromItemStack(ItemStack item, int amount) return RecipeOutputItem.EMPTY; } - SlimefunItem sfItem = SlimefunItem.getByItem(item); + if (item instanceof SlimefunItemStack sfItem) { + return new RecipeOutputSlimefunItem(sfItem.getItemId(), amount); + } - if (sfItem != null) { - return new RecipeOutputSlimefunItem(sfItem.getId(), amount); + // The item might not have been registered yet and we only need the id, so no need for `getByItem()` + Optional itemID = Slimefun.getItemDataService().getItemData(item); + + if (itemID.isPresent()) { + return new RecipeOutputSlimefunItem(itemID.get(), amount); } else { return new RecipeOutputItemStack(item, amount); } diff --git a/src/main/java/io/github/thebusybiscuit/slimefun4/api/recipes/items/RecipeOutputItemStack.java b/src/main/java/io/github/thebusybiscuit/slimefun4/api/recipes/items/RecipeOutputItemStack.java index 592bd09ecd..d9c8da0f27 100644 --- a/src/main/java/io/github/thebusybiscuit/slimefun4/api/recipes/items/RecipeOutputItemStack.java +++ b/src/main/java/io/github/thebusybiscuit/slimefun4/api/recipes/items/RecipeOutputItemStack.java @@ -76,7 +76,7 @@ public JsonElement serialize(JsonSerializationContext context) { } JsonObject item = new JsonObject(); - item.addProperty("id", template.getType().toString()); + item.addProperty("id", template.getType().getKey().toString()); if (getAmount() != 1) { item.addProperty("amount", getAmount()); } diff --git a/src/main/java/io/github/thebusybiscuit/slimefun4/api/recipes/json/RecipeInputItemSerDes.java b/src/main/java/io/github/thebusybiscuit/slimefun4/api/recipes/json/RecipeInputItemSerDes.java index 06afe16a8f..81e6b19666 100644 --- a/src/main/java/io/github/thebusybiscuit/slimefun4/api/recipes/json/RecipeInputItemSerDes.java +++ b/src/main/java/io/github/thebusybiscuit/slimefun4/api/recipes/json/RecipeInputItemSerDes.java @@ -42,6 +42,11 @@ public AbstractRecipeInputItem deserialize(JsonElement el, Type type, AbstractRecipeInputItem aItem = RecipeInputItem.fromString( obj.getAsJsonPrimitive("id").getAsString()); if (aItem instanceof RecipeInputItem item) { + int amount = 1; + if (obj.has("amount")) { + amount = obj.getAsJsonPrimitive("amount").getAsInt(); + } + item.setAmount(amount); item.setAmount(obj.getAsJsonPrimitive("amount").getAsInt()); if (obj.has("durability")) { item.setDurabilityCost(obj.getAsJsonPrimitive("durability").getAsInt()); diff --git a/src/main/java/io/github/thebusybiscuit/slimefun4/api/recipes/matching/ItemMatchResult.java b/src/main/java/io/github/thebusybiscuit/slimefun4/api/recipes/matching/ItemMatchResult.java index 4405be3066..08dd4d7405 100644 --- a/src/main/java/io/github/thebusybiscuit/slimefun4/api/recipes/matching/ItemMatchResult.java +++ b/src/main/java/io/github/thebusybiscuit/slimefun4/api/recipes/matching/ItemMatchResult.java @@ -12,12 +12,18 @@ public class ItemMatchResult { private final AbstractRecipeInputItem recipeItem; private final @Nullable ItemStack matchedItem; private final int consumeAmount; + private final int durabilityConsumeAmount; - public ItemMatchResult(boolean itemsMatch, AbstractRecipeInputItem recipeItem, ItemStack matchedItem, int consumeAmount) { + public ItemMatchResult(boolean itemsMatch, AbstractRecipeInputItem recipeItem, ItemStack matchedItem, int consumeAmount, int durabilityConsumeAmount) { this.itemsMatch = itemsMatch; this.recipeItem = recipeItem; this.matchedItem = matchedItem; this.consumeAmount = consumeAmount; + this.durabilityConsumeAmount = durabilityConsumeAmount; + } + + public ItemMatchResult(boolean itemsMatch, AbstractRecipeInputItem recipeItem, ItemStack matchedItem, int consumeAmount) { + this(itemsMatch, recipeItem, matchedItem, consumeAmount, 0); } /** @@ -39,5 +45,8 @@ public ItemMatchResult(boolean itemsMatch, AbstractRecipeInputItem recipeItem, I public int getConsumeAmount() { return consumeAmount; } + public int getDurabilityConsumeAmount() { + return durabilityConsumeAmount; + } } diff --git a/src/main/java/io/github/thebusybiscuit/slimefun4/core/commands/subcommands/RecipeCommand.java b/src/main/java/io/github/thebusybiscuit/slimefun4/core/commands/subcommands/RecipeCommand.java new file mode 100644 index 0000000000..5cfd731aac --- /dev/null +++ b/src/main/java/io/github/thebusybiscuit/slimefun4/core/commands/subcommands/RecipeCommand.java @@ -0,0 +1,51 @@ +package io.github.thebusybiscuit.slimefun4.core.commands.subcommands; + +import javax.annotation.ParametersAreNonnullByDefault; + +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; + +import io.github.thebusybiscuit.slimefun4.core.commands.SlimefunCommand; +import io.github.thebusybiscuit.slimefun4.core.commands.SubCommand; +import io.github.thebusybiscuit.slimefun4.implementation.Slimefun; + +class RecipeCommand extends SubCommand { + + @ParametersAreNonnullByDefault + RecipeCommand(Slimefun plugin, SlimefunCommand cmd) { + super(plugin, cmd, "recipe", false); + } + + @Override + public void onExecute(CommandSender sender, String[] args) { + if (sender.hasPermission("slimefun.recipe.reload") && sender instanceof Player) { + Slimefun.getLocalization().sendMessage(sender, "messages.no-permission", true); + } + + if (args.length == 1) { + Slimefun.getLocalization().sendMessage(sender, "messages.usage", true, msg -> msg.replace("%usage%", "/sf recipe ")); + } + + switch (args[1]) { + case "reload": + if (args.length == 2) { + Slimefun.getRecipeService().loadAllRecipes(); + } else { + for (int i = 2; i < args.length; i++) { + Slimefun.getRecipeService().loadRecipesFromFile(args[i]); + } + } + break; + case "save": + if (args.length != 2) { + Slimefun.getLocalization().sendMessage(sender, "messages.usage", true, msg -> msg.replace("%usage%", "/sf recipe save")); + break; + } + Slimefun.getRecipeService().saveAllRecipes(); + break; + default: + Slimefun.getLocalization().sendMessage(sender, "messages.usage", true, msg -> msg.replace("%usage%", "/sf recipe ")); + break; + } + } +} diff --git a/src/main/java/io/github/thebusybiscuit/slimefun4/core/commands/subcommands/SlimefunSubCommands.java b/src/main/java/io/github/thebusybiscuit/slimefun4/core/commands/subcommands/SlimefunSubCommands.java index 17d70bce3e..41ffc5b5f1 100644 --- a/src/main/java/io/github/thebusybiscuit/slimefun4/core/commands/subcommands/SlimefunSubCommands.java +++ b/src/main/java/io/github/thebusybiscuit/slimefun4/core/commands/subcommands/SlimefunSubCommands.java @@ -42,6 +42,7 @@ public static Collection getAllCommands(@Nonnull SlimefunCommand cmd commands.add(new BackpackCommand(plugin, cmd)); commands.add(new ChargeCommand(plugin, cmd)); commands.add(new DebugCommand(plugin, cmd)); + commands.add(new RecipeCommand(plugin, cmd)); return commands; } diff --git a/src/main/java/io/github/thebusybiscuit/slimefun4/core/services/RecipeService.java b/src/main/java/io/github/thebusybiscuit/slimefun4/core/services/RecipeService.java index 7c3771a5da..07e23100e4 100644 --- a/src/main/java/io/github/thebusybiscuit/slimefun4/core/services/RecipeService.java +++ b/src/main/java/io/github/thebusybiscuit/slimefun4/core/services/RecipeService.java @@ -1,5 +1,6 @@ package io.github.thebusybiscuit.slimefun4.core.services; +import java.io.BufferedReader; import java.io.File; import java.io.FileReader; import java.io.FileWriter; @@ -7,15 +8,19 @@ import java.io.Writer; import java.nio.file.Files; import java.nio.file.Path; -import java.nio.file.Paths; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; +import java.util.HashSet; import java.util.LinkedHashMap; +import java.util.LinkedHashSet; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.Stream; import javax.annotation.Nonnull; import javax.annotation.Nullable; @@ -48,14 +53,13 @@ import io.github.thebusybiscuit.slimefun4.api.recipes.matching.MatchProcedure; import io.github.thebusybiscuit.slimefun4.api.recipes.matching.RecipeMatchResult; import io.github.thebusybiscuit.slimefun4.api.recipes.matching.RecipeSearchResult; -import io.github.thebusybiscuit.slimefun4.implementation.Slimefun; import io.github.thebusybiscuit.slimefun4.utils.RecipeUtils; public class RecipeService { public static final String SAVED_RECIPE_DIR = "plugins/Slimefun/recipes/"; - private GsonBuilder gsonBuilder; + private Plugin plugin; private Gson gson; private final Map> customRIItemDeserializers = new HashMap<>(); @@ -68,11 +72,21 @@ public class RecipeService { private final Map emptyItems = new HashMap<>(); - private final Map> recipesByType = new HashMap<>(); private final Map recipesById = new HashMap<>(); - private final Map> recipesByFilename = new HashMap<>(); - + // This map allows loading and saving from JSON files + private final Map> recipesByFilename = new HashMap<>(); + // This holds the names of json files read in, it helps differentiates between + // entries in `recipesByFilename` existing because it was read in from a file, + // vs if the recipe was added directly in code. + private final Set filesRead = new HashSet<>(); + // This map facilitates searching through recipe with a certain RecipeType + private final Map> recipesByType = new HashMap<>(); + + private final Set recipeOverrides = new HashSet<>(); + private int maxCacheEntries = 1000; + private boolean allRecipesLoaded = false; + private final Map recipeCache = new LinkedHashMap<>() { protected boolean removeEldestEntry(Map.Entry eldest) { return size() > maxCacheEntries; @@ -80,20 +94,68 @@ protected boolean removeEldestEntry(Map.Entry eldest) { }; public RecipeService(@Nonnull Plugin plugin) { + this.plugin = plugin; + registerMatchProcedure(MatchProcedure.SHAPED); registerMatchProcedure(MatchProcedure.SHAPED_FLIPPABLE); registerMatchProcedure(MatchProcedure.SHAPED_ROTATABLE_45_3X3); registerMatchProcedure(MatchProcedure.SHAPELESS); registerMatchProcedure(MatchProcedure.SUBSET); - this.gsonBuilder = new GsonBuilder() + this.gson = new GsonBuilder() .setPrettyPrinting() .excludeFieldsWithoutExposeAnnotation() .registerTypeAdapter(Recipe.class, new RecipeSerDes()) .registerTypeAdapter(AbstractRecipeInput.class, new RecipeInputSerDes()) .registerTypeAdapter(AbstractRecipeOutput.class, new RecipeOutputSerDes()) .registerTypeAdapter(AbstractRecipeInputItem.class, new RecipeInputItemSerDes()) - .registerTypeAdapter(AbstractRecipeOutputItem.class, new RecipeOutputItemSerDes()); + .registerTypeAdapter(AbstractRecipeOutputItem.class, new RecipeOutputItemSerDes()) + .create(); + + try (BufferedReader reader = new BufferedReader(new FileReader("plugins/Slimefun/recipe-overrides"))) { + String line = reader.readLine(); + while (line != null) { + recipeOverrides.add(line); + line = reader.readLine(); + } + } catch (IOException e) { + plugin.getLogger().warning("Could not load recipe overrides: " + e.getLocalizedMessage()); + } finally { + allRecipesLoaded = true; + } + } + + /** + * Adds a recipe override + * @return If the override was applied or not + */ + public boolean addRecipeOverride(String override, String... filenames) { + if (allRecipesLoaded) { + plugin.getLogger().warning("Recipes were already loaded, so the recipe override '" + override + "' was not processed!"); + return false; + } + if (recipeOverrides.contains(override)) { + return false; + } + for (String filename : filenames) { + File file = new File(SAVED_RECIPE_DIR + filename + ".json"); + if (file.isFile()) { + try { + boolean deleted = file.delete(); + if (!deleted) { + plugin.getLogger().severe("Could not delete file '" + filename + "' for recipe override '" + override + "'"); + return false; + } + } catch (Exception e) { + plugin.getLogger().severe("An error occurred when applying recipe override '" + override + "' to file '" + filename + "': " + e.getLocalizedMessage()); + return false; + }; + } else { + plugin.getLogger().warning("Skipping file '" + filename + "' for recipe override '" + override + "' because it is a directory"); + } + } + recipeOverrides.add(override); + return true; } public void registerMatchProcedure(MatchProcedure m) { @@ -101,16 +163,16 @@ public void registerMatchProcedure(MatchProcedure m) { } @Nonnull - public List getRecipesByType(RecipeType type) { - List list = recipesByType.get(type); - return list == null ? Collections.emptyList() : Collections.unmodifiableList(list); + public Set getRecipesByType(RecipeType type) { + Set set = recipesByType.get(type); + return set == null ? Collections.emptySet() : Collections.unmodifiableSet(set); } /** * You shouldn't call this directly, call recipe.addRecipeType(type) instead */ public void addRecipeToType(Recipe recipe, RecipeType type) { if (!recipesByType.containsKey(type)) { - recipesByType.put(type, new ArrayList<>()); + recipesByType.put(type, new HashSet<>()); } recipesByType.get(type).add(recipe); } @@ -119,25 +181,82 @@ public void addRecipeToType(Recipe recipe, RecipeType type) { public Recipe getRecipe(String id) { return recipesById.get(id); } - public void addRecipe(Recipe recipe) { + + /** + * Registers a recipe in the service. Ideally recipes should be defined + * in a JSON file in the resources directory of your plugin. + * @param recipe Recipe to add + * @param forceId Override the recipe with the same id, if it exists + * @param forceFilename If file was already read, add this recipe to + * the list anyways. + */ + public void addRecipe(Recipe recipe, boolean forceId, boolean forceFilename) { + // Check id conflicts, add to id map if (recipe.getId().isPresent()) { String id = recipe.getId().get(); - if (recipesById.containsKey(id)) { - Slimefun.logger().warning("A recipe with id " + id + " already exists!"); + if (recipesById.containsKey(id) && !forceId) { + plugin.getLogger().warning("A recipe with id " + id + " already exists!"); } else { + if (forceId && recipesById.containsKey(id)) { + Recipe old = recipesById.get(id); + removeRecipeFromFilename(old); + removeRecipeFromTypes(old); + recipeCache.clear(); + } recipesById.put(id, recipe); } } + // Add to file map if (!recipesByFilename.containsKey(recipe.getFilename())) { - recipesByFilename.put(recipe.getFilename(), new ArrayList<>()); + // We want to preserve the order the recipes are + // listed in the file, for consistency. + Set newList = new LinkedHashSet<>(); + newList.add(recipe); + recipesByFilename.put(recipe.getFilename(), newList); + } else if (forceFilename || !filesRead.contains(recipe.getFilename())) { + // If we have already loaded the recipe file with this filename, + // Then we don't want to modify it (or else we get duplicate recipes) + recipesByFilename.get(recipe.getFilename()).add(recipe); } - recipesByFilename.get(recipe.getFilename()).add(recipe); + + // Add to type map recipe.getTypes().forEach(type -> addRecipeToType(recipe, type)); } - public List getRecipesByFilename(String filename) { - List list = recipesByFilename.get(filename); - return list == null ? Collections.emptyList() : Collections.unmodifiableList(list); + /** + * Registers a recipe in the service. + * @param recipe Recipe to register + */ + public void addRecipe(Recipe recipe) { + addRecipe(recipe, false, false); + } + + /** + * Internal utility method for removing old recipes from the id map when a recipe is to be deleted + */ + private void removeRecipeFromId(Recipe recipe) { + if (recipe.getId().isPresent()) { + recipesById.remove(recipe.getId().get()); + } + } + /** + * Internal utility method for removing old recipes from the type map when a recipe is to be deleted + */ + private void removeRecipeFromTypes(Recipe recipe) { + for (RecipeType type : recipe.getTypes()) { + recipesByType.get(type).remove(recipe); + } + } + /** + * Internal utility method for removing old recipes from the file map when a recipe is to be deleted + */ + private void removeRecipeFromFilename(Recipe recipe) { + recipesByFilename.get(recipe.getFilename()).remove(recipe); + } + + public Set getRecipesByFilename(String filename) { + Set set = recipesByFilename.get(filename); + return set == null ? Collections.emptySet() : Collections.unmodifiableSet(set); } @Nullable @@ -155,38 +274,46 @@ public void cacheRecipe(Recipe recipe, int hash) { recipeCache.put(hash, recipe); } - public RecipeSearchResult searchRecipes(RecipeType type, Function recipeIsMatch, Function getHash) { - List recipes = getRecipesByType(type); + public RecipeSearchResult searchRecipes(RecipeType type, Function recipeIsMatch, int hash) { + Recipe cachedRecipe = getCachedRecipe(hash); + // Sanity check + if (cachedRecipe != null && cachedRecipe.getTypes().contains(type)) { + RecipeMatchResult result = recipeIsMatch.apply(cachedRecipe); + if (result.itemsMatch()) { + return new RecipeSearchResult(result); + } + } + Set recipes = getRecipesByType(type); for (Recipe recipe : recipes) { RecipeMatchResult matchResult = recipeIsMatch.apply(recipe); if (matchResult.itemsMatch()) { - cacheRecipe(recipe, getHash.apply(recipe)); + cacheRecipe(recipe, hash); return new RecipeSearchResult(matchResult); } } return new RecipeSearchResult(); } - public RecipeSearchResult searchRecipes(RecipeType type, List givenItems, MatchProcedure matchAs) { - return searchRecipes(type, recipe -> recipe.matchAs(matchAs, givenItems), recipe -> RecipeUtils.hashItemsIgnoreAmount(givenItems)); + return searchRecipes(type, recipe -> recipe.matchAs(matchAs, givenItems), RecipeUtils.hashItemsIgnoreAmount(givenItems)); } - public RecipeSearchResult searchRecipes(RecipeType type, List givenItems) { - return searchRecipes(type, recipe -> recipe.match(givenItems), recipe -> RecipeUtils.hashItemsIgnoreAmount(givenItems)); + return searchRecipes(type, recipe -> recipe.match(givenItems), RecipeUtils.hashItemsIgnoreAmount(givenItems)); } - public RecipeSearchResult searchRecipes(Collection types, Function recipeIsMatch, Function getHash) { + public RecipeSearchResult searchRecipes(Collection types, Function recipeIsMatch, int hash) { for (RecipeType type : types) { - RecipeSearchResult result = searchRecipes(type, recipeIsMatch, getHash); + RecipeSearchResult result = searchRecipes(type, recipeIsMatch, hash); if (result.matchFound()) { return result; } } return new RecipeSearchResult(); } - + public RecipeSearchResult searchRecipes(Collection types, List givenItems, MatchProcedure matchAs) { + return searchRecipes(types, recipe -> recipe.matchAs(matchAs, givenItems), RecipeUtils.hashItemsIgnoreAmount(givenItems)); + } public RecipeSearchResult searchRecipes(Collection types, List givenItems) { - return searchRecipes(types, recipe -> recipe.match(givenItems), recipe -> RecipeUtils.hashItemsIgnoreAmount(givenItems)); + return searchRecipes(types, recipe -> recipe.match(givenItems), RecipeUtils.hashItemsIgnoreAmount(givenItems)); } @Nullable @@ -194,7 +321,9 @@ public MatchProcedure getMatchProcedure(@Nonnull NamespacedKey key) { return matchProcedures.get(key); } /** - * Registers another match procedure if one with key key doesn't already exist. Used when deserializing recipes from json + * Registers another match procedure if one with key key doesn't + * already exist. Used when deserializing recipes from json + * * @return If the procedure was successfully added */ public boolean registerMatchProcedure(NamespacedKey key, MatchProcedure match) { @@ -205,13 +334,28 @@ public boolean registerMatchProcedure(NamespacedKey key, MatchProcedure match) { return true; } - public void addEmptyItem(String id, ItemStack empty) { - emptyItems.put(id, empty); - } public ItemStack getEmptyItem(String id) { return emptyItems.get(id); } + public void addEmptyItem(String id, ItemStack empty) { + emptyItems.put(id, empty); + } + public CustomRecipeDeserializer getRecipeInputItemDeserializer(@Nonnull NamespacedKey key) { + return customRIItemDeserializers.get(key); + } + public CustomRecipeDeserializer getRecipeInputDeserializer(@Nonnull NamespacedKey key) { + return customRInputDeserializers.get(key); + } + public CustomRecipeDeserializer getRecipeOutputItemDeserializer(@Nonnull NamespacedKey key) { + return customROItemDeserializers.get(key); + } + public CustomRecipeDeserializer getRecipeOutputDeserializer(@Nonnull NamespacedKey key) { + return customROutputDeserializers.get(key); + } + public CustomRecipeDeserializer getRecipeDeserializer(@Nonnull NamespacedKey key) { + return customRecipeDeserializers.get(key); + } @ParametersAreNonnullByDefault public void addRecipeInputItemDeserializer(NamespacedKey key, CustomRecipeDeserializer des) { customRIItemDeserializers.put(key, des); @@ -232,52 +376,40 @@ public void addRecipeOutputDeserializer(NamespacedKey key, CustomRecipeDeseriali public void addRecipeDeserializer(NamespacedKey key, CustomRecipeDeserializer des) { customRecipeDeserializers.put(key, des); } - public CustomRecipeDeserializer getRecipeInputItemDeserializer(@Nonnull NamespacedKey key) { - return customRIItemDeserializers.get(key); - } - public CustomRecipeDeserializer getRecipeInputDeserializer(@Nonnull NamespacedKey key) { - return customRInputDeserializers.get(key); - } - public CustomRecipeDeserializer getRecipeOutputItemDeserializer(@Nonnull NamespacedKey key) { - return customROItemDeserializers.get(key); - } - public CustomRecipeDeserializer getRecipeOutputDeserializer(@Nonnull NamespacedKey key) { - return customROutputDeserializers.get(key); - } - public CustomRecipeDeserializer getRecipeDeserializer(@Nonnull NamespacedKey key) { - return customRecipeDeserializers.get(key); + + public Recipe parseRecipeString(String s) { + return gson.fromJson(s, Recipe.class); } /** - * For addons to add custom deserialization for fields in recipe subclasses or recipe component subclasses + * Gets the list of all recipe files in the /plugins/Slimefun/recipes/ + * directory, with the .json removed + * @return */ - @Nonnull - public GsonBuilder getGsonBuilder() { - return gsonBuilder; - } - - private void createGson() { - gson = gsonBuilder.create(); + public Set getAllRecipeFilenames() { + Path dir = Path.of(SAVED_RECIPE_DIR); + try (Stream files = Files.walk(dir)) { + return files + .filter(f -> f.toString().endsWith(".json")) + .map(file -> { + String filename = dir.relativize(file).toString(); + return filename.substring(0, filename.length() - 5); + }) + .collect(Collectors.toSet()); + } catch (Exception e) { + return Collections.emptySet(); + } } public void loadAllRecipes() { - createGson(); - - final String RECIPE_PATH = "plugins/Slimefun/recipes/"; - - try { - Path dir = Files.createDirectories(Paths.get(RECIPE_PATH)); - Files.list(dir).forEach(file -> { - loadRecipesFromFile(file.toString()).forEach(recipe -> addRecipe(recipe)); - }); - } catch (IOException e) { - Slimefun.logger().warning("Could not load recipes: " + e.getMessage()); - } + getAllRecipeFilenames().forEach(this::loadRecipesFromFile); + allRecipesLoaded = true; } /** * Gets a recipe from a json file - * @param filename Filename WITH .json + * + * @param filename Filename WITHOUT .json */ public List loadRecipesFromFile(String filename) { return loadRecipesFromFile(filename, gson); @@ -285,49 +417,68 @@ public List loadRecipesFromFile(String filename) { /** * Gets a recipe from a json file - * @param filename Filename WITH .json - * @param gson The instance of gson to use + * + * @param filename Filename WITHOUT .json + * @param gson The instance of gson to use */ public List loadRecipesFromFile(String filename, Gson gson) { - try { - JsonElement obj = gson.fromJson(new FileReader(new File(filename)), JsonElement.class); + List recipes = new ArrayList<>(); + if (recipesByFilename.containsKey(filename)) { + for (Recipe recipe : recipesByFilename.get(filename)) { + removeRecipeFromId(recipe); + removeRecipeFromTypes(recipe); + recipeCache.clear(); + } + recipesByFilename.get(filename).clear(); + } + try (FileReader fileReader = new FileReader(new File(SAVED_RECIPE_DIR + filename + ".json"))) { + JsonElement obj = gson.fromJson(fileReader, JsonElement.class); if (obj.isJsonArray()) { JsonArray jsonRecipes = obj.getAsJsonArray(); - List recipes = new ArrayList<>(); + recipes = new ArrayList<>(); for (JsonElement jsonRecipe : jsonRecipes) { JsonObject recipe = jsonRecipe.getAsJsonObject(); recipe.addProperty("__filename", filename); recipes.add(gson.fromJson(recipe, Recipe.class)); } - return recipes; } else { JsonObject recipe = obj.getAsJsonObject(); recipe.addProperty("__filename", filename); - return List.of(gson.fromJson(obj, Recipe.class)); + recipes.add(gson.fromJson(obj, Recipe.class)); } + filesRead.add(filename); } catch (IOException e) { - Slimefun.logger().warning("Could not load recipe file '" + filename + "': " + e.getMessage()); + plugin.getLogger().warning("Could not load recipe file '" + filename + "': " + e.getLocalizedMessage()); + recipes = Collections.emptyList(); } catch (NullPointerException e) { - Slimefun.logger().warning("Could not load recipe file '" + filename + "': " + e.getMessage()); + plugin.getLogger().warning("Could not load recipe file '" + filename + "': " + e.getLocalizedMessage()); + recipes = Collections.emptyList(); } - return Collections.emptyList(); + recipes.forEach(r -> addRecipe(r, true, true)); + return recipes; + } + + public boolean areAllRecipesLoaded() { + return allRecipesLoaded; } public void saveAllRecipes() { - for (Map.Entry> entry : recipesByFilename.entrySet()) { + for (Map.Entry> entry : recipesByFilename.entrySet()) { String filename = entry.getKey(); - List recipes = entry.getValue(); - if (recipes.size() == 1) { - try (Writer writer = new FileWriter(filename)) { - JsonWriter jsonWriter = gson.newJsonWriter(writer); - jsonWriter.setIndent(" "); - gson.toJson(recipes.get(0), Recipe.class, jsonWriter); - } catch (IOException e) { - Slimefun.logger().warning("Couldn't save recipe to '" + filename + "': " + e.getMessage()); - } catch (JsonIOException e) { - Slimefun.logger().warning("Couldn't save recipe to '" + filename + "': " + e.getMessage()); + Set recipes = entry.getValue(); + try (Writer writer = new FileWriter(SAVED_RECIPE_DIR + filename + ".json")) { + JsonWriter jsonWriter = gson.newJsonWriter(writer); + jsonWriter.setIndent(" "); + if (recipes.size() == 1) { + gson.toJson(recipes.stream().findFirst().get(), Recipe.class, jsonWriter); + } else { + gson.toJson(recipes, List.class, jsonWriter); } + } catch (IOException e) { + plugin.getLogger().warning("Couldn't save recipe to '" + filename + "': " + e.getLocalizedMessage()); + } catch (JsonIOException e) { + plugin.getLogger().warning("Couldn't save recipe to '" + filename + "': " + e.getLocalizedMessage()); } } } diff --git a/src/main/java/io/github/thebusybiscuit/slimefun4/implementation/Slimefun.java b/src/main/java/io/github/thebusybiscuit/slimefun4/implementation/Slimefun.java index 528fa997b2..0198d475d4 100644 --- a/src/main/java/io/github/thebusybiscuit/slimefun4/implementation/Slimefun.java +++ b/src/main/java/io/github/thebusybiscuit/slimefun4/implementation/Slimefun.java @@ -346,6 +346,10 @@ private void onPluginStart() { logger.log(Level.INFO, "Registering listeners..."); registerListeners(); + // Copy all recipes in the resources dir + logger.log(Level.INFO, "Copying default recipes..."); + copyResourceRecipes(); + // Initiating various Stuff and all items with a slight delay (0ms after the Server finished loading) runSync(new SlimefunStartupTask(this, () -> { textureService.register(registry.getAllSlimefunItems(), true); @@ -593,7 +597,7 @@ private boolean isVersionUnsupported() { */ private void createDirectories() { String[] storageFolders = { "Players", "blocks", "stored-blocks", "stored-inventories", "stored-chunks", "universal-inventories", "waypoints", "block-backups" }; - String[] pluginFolders = { "scripts", "error-reports", "cache/github", "world-settings" }; + String[] pluginFolders = { "scripts", "error-reports", "cache/github", "world-settings", "recipes" }; for (String folder : storageFolders) { File file = new File("data-storage/Slimefun", folder); diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml index 5e5a3adbe5..5e20cb0bc6 100644 --- a/src/main/resources/plugin.yml +++ b/src/main/resources/plugin.yml @@ -83,3 +83,6 @@ permissions: slimefun.debugging: description: Allows you to use the debugging tool from Slimefun default: op + slimefun.recipe.reload: + description: Allows you to reload slimefun recipes + default: op diff --git a/src/main/resources/recipes/test-recipe-01.json b/src/main/resources/recipes/test-recipe-01.json new file mode 100644 index 0000000000..9fcc50de88 --- /dev/null +++ b/src/main/resources/recipes/test-recipe-01.json @@ -0,0 +1,8 @@ +{ + "output": { + "items": [ + "slimefun:iron_dust|19" + ] + }, + "type": "slimefun:enhanced_crafting_table" +} \ No newline at end of file