From 7cf9a22916a49b32ac72773786eb42ec86bbc851 Mon Sep 17 00:00:00 2001 From: XiaoMoMi Date: Thu, 13 Feb 2025 03:15:13 +0800 Subject: [PATCH] Improve Recipe system --- .../item/recipe/BukkitRecipeManager.java | 222 +++++++++++++----- .../item/recipe/CrafterEventListener.java | 112 +++++++++ .../craftengine/bukkit/util/RecipeUtils.java | 49 ++++ .../craftengine/bukkit/util/Reflections.java | 132 +++++++++++ .../core/item/recipe/CustomShapedRecipe.java | 8 + .../core/item/recipe/OptimizedIDItem.java | 7 + .../core/item/recipe/RecipeManager.java | 3 +- .../craftengine/core/plugin/CraftEngine.java | 6 +- 8 files changed, 479 insertions(+), 60 deletions(-) create mode 100644 bukkit/src/main/java/net/momirealms/craftengine/bukkit/item/recipe/CrafterEventListener.java create mode 100644 bukkit/src/main/java/net/momirealms/craftengine/bukkit/util/RecipeUtils.java diff --git a/bukkit/src/main/java/net/momirealms/craftengine/bukkit/item/recipe/BukkitRecipeManager.java b/bukkit/src/main/java/net/momirealms/craftengine/bukkit/item/recipe/BukkitRecipeManager.java index 5813abac8..e27a3811b 100644 --- a/bukkit/src/main/java/net/momirealms/craftengine/bukkit/item/recipe/BukkitRecipeManager.java +++ b/bukkit/src/main/java/net/momirealms/craftengine/bukkit/item/recipe/BukkitRecipeManager.java @@ -8,6 +8,7 @@ import net.momirealms.craftengine.bukkit.item.BukkitItemManager; import net.momirealms.craftengine.bukkit.item.CloneableConstantItem; import net.momirealms.craftengine.bukkit.plugin.BukkitCraftEngine; import net.momirealms.craftengine.bukkit.util.MaterialUtils; +import net.momirealms.craftengine.bukkit.util.RecipeUtils; import net.momirealms.craftengine.bukkit.util.Reflections; import net.momirealms.craftengine.core.item.CustomItem; import net.momirealms.craftengine.core.item.recipe.*; @@ -38,19 +39,22 @@ import org.bukkit.inventory.recipe.CraftingBookCategory; import org.jetbrains.annotations.Nullable; import java.io.Reader; +import java.lang.reflect.Array; import java.nio.file.Path; import java.util.*; import java.util.concurrent.CompletableFuture; -import java.util.function.BiFunction; +import java.util.function.BiConsumer; import java.util.function.Consumer; public class BukkitRecipeManager implements RecipeManager { - private static final Map, Object>> BUKKIT_RECIPE_CONVERTORS = new HashMap<>(); + private static final Map>> BUKKIT_RECIPE_REGISTER = new HashMap<>(); + private static Object minecraftRecipeManager; + private static final List injectedIngredients = new ArrayList<>(); static { - BUKKIT_RECIPE_CONVERTORS.put(RecipeTypes.SHAPED, (key, recipe) -> { + BUKKIT_RECIPE_REGISTER.put(RecipeTypes.SHAPED, (key, recipe) -> { CustomShapedRecipe ceRecipe = (CustomShapedRecipe) recipe; - ShapedRecipe shapedRecipe = new ShapedRecipe(key, new ItemStack(Material.STONE)); + ShapedRecipe shapedRecipe = new ShapedRecipe(key, ceRecipe.getResult(null)); if (ceRecipe.group() != null) { shapedRecipe.setGroup(Objects.requireNonNull(ceRecipe.group())); } @@ -62,15 +66,16 @@ public class BukkitRecipeManager implements RecipeManager { shapedRecipe.setIngredient(entry.getKey(), new RecipeChoice.MaterialChoice(ingredientToBukkitMaterials(entry.getValue()))); } try { - return Reflections.method$CraftShapedRecipe$fromBukkitRecipe.invoke(null, shapedRecipe); + Object craftRecipe = Reflections.method$CraftShapedRecipe$fromBukkitRecipe.invoke(null, shapedRecipe); + Reflections.method$CraftRecipe$addToCraftingManager.invoke(craftRecipe); + injectShapedRecipe(new Key(key.namespace(), key.value()), recipe); } catch (Exception e) { CraftEngine.instance().logger().warn("Failed to convert shaped recipe", e); - return null; } }); - BUKKIT_RECIPE_CONVERTORS.put(RecipeTypes.SHAPELESS, (key, recipe) -> { + BUKKIT_RECIPE_REGISTER.put(RecipeTypes.SHAPELESS, (key, recipe) -> { CustomShapelessRecipe ceRecipe = (CustomShapelessRecipe) recipe; - ShapelessRecipe shapelessRecipe = new ShapelessRecipe(key, new ItemStack(Material.STONE)); + ShapelessRecipe shapelessRecipe = new ShapelessRecipe(key, ceRecipe.getResult(null)); if (ceRecipe.group() != null) { shapelessRecipe.setGroup(Objects.requireNonNull(ceRecipe.group())); } @@ -81,16 +86,18 @@ public class BukkitRecipeManager implements RecipeManager { shapelessRecipe.addIngredient(new RecipeChoice.MaterialChoice(ingredientToBukkitMaterials(ingredient))); } try { - return Reflections.method$CraftShapelessRecipe$fromBukkitRecipe.invoke(null, shapelessRecipe); + Object craftRecipe = Reflections.method$CraftShapelessRecipe$fromBukkitRecipe.invoke(null, shapelessRecipe); + Reflections.method$CraftRecipe$addToCraftingManager.invoke(craftRecipe); + injectShapelessRecipe(new Key(key.namespace(), key.value()), recipe); } catch (Exception e) { CraftEngine.instance().logger().warn("Failed to convert shapeless recipe", e); - return null; } }); } private final BukkitCraftEngine plugin; private final RecipeEventListener recipeEventListener; + private final CrafterEventListener crafterEventListener; private final Map>> recipes; private final VanillaRecipeReader recipeReader; private final List injectedDataPackRecipes; @@ -102,16 +109,20 @@ public class BukkitRecipeManager implements RecipeManager { private final Set dataPackRecipes; private Object stolenFeatureFlagSet; - private Object minecraftRecipeManager; public BukkitRecipeManager(BukkitCraftEngine plugin) { this.plugin = plugin; - this.recipeEventListener = new RecipeEventListener(plugin, this, plugin.itemManager()); this.recipes = new HashMap<>(); this.injectedDataPackRecipes = new ArrayList<>(); this.registeredCustomRecipes = new ArrayList<>(); this.dataPackRecipes = new HashSet<>(); this.customRecipes = new HashSet<>(); + this.recipeEventListener = new RecipeEventListener(plugin, this, plugin.itemManager()); + if (VersionHelper.isVersionNewerThan1_21()) { + this.crafterEventListener = new CrafterEventListener(plugin, this, plugin.itemManager()); + } else { + this.crafterEventListener = null; + } if (VersionHelper.isVersionNewerThan1_21_2()) { this.recipeReader = new VanillaRecipeReader1_21_2(); } else if (VersionHelper.isVersionNewerThan1_20_5()) { @@ -120,7 +131,7 @@ public class BukkitRecipeManager implements RecipeManager { this.recipeReader = new VanillaRecipeReader1_20(); } try { - this.minecraftRecipeManager = Reflections.method$MinecraftServer$getRecipeManager.invoke(Reflections.method$MinecraftServer$getServer.invoke(null)); + minecraftRecipeManager = Reflections.method$MinecraftServer$getRecipeManager.invoke(Reflections.method$MinecraftServer$getServer.invoke(null)); } catch (ReflectiveOperationException e) { plugin.logger().warn("Failed to get minecraft recipe manager", e); } @@ -140,10 +151,13 @@ public class BukkitRecipeManager implements RecipeManager { public void load() { if (!ConfigManager.enableRecipeSystem()) return; Bukkit.getPluginManager().registerEvents(this.recipeEventListener, this.plugin.bootstrap()); + if (this.crafterEventListener != null) { + Bukkit.getPluginManager().registerEvents(this.crafterEventListener, this.plugin.bootstrap()); + } if (VersionHelper.isVersionNewerThan1_21_2()) { try { - this.stolenFeatureFlagSet = Reflections.field$RecipeManager$featureflagset.get(this.minecraftRecipeManager); - Reflections.field$RecipeManager$featureflagset.set(this.minecraftRecipeManager, null); + this.stolenFeatureFlagSet = Reflections.field$RecipeManager$featureflagset.get(minecraftRecipeManager); + Reflections.field$RecipeManager$featureflagset.set(minecraftRecipeManager, null); } catch (ReflectiveOperationException e) { this.plugin.logger().warn("Failed to steal featureflagset", e); } @@ -153,19 +167,22 @@ public class BukkitRecipeManager implements RecipeManager { @Override public void unload() { HandlerList.unregisterAll(this.recipeEventListener); + if (this.crafterEventListener != null) { + HandlerList.unregisterAll(this.crafterEventListener); + } this.recipes.clear(); this.dataPackRecipes.clear(); this.customRecipes.clear(); if (VersionHelper.isVersionNewerThan1_21_2()) { try { - Object recipeMap = Reflections.field$RecipeManager$recipes.get(this.minecraftRecipeManager); + Object recipeMap = Reflections.field$RecipeManager$recipes.get(minecraftRecipeManager); for (NamespacedKey key : this.injectedDataPackRecipes) { Reflections.method$RecipeMap$removeRecipe.invoke(recipeMap, Reflections.method$CraftRecipe$toMinecraft.invoke(null, key)); } for (NamespacedKey key : this.registeredCustomRecipes) { Reflections.method$RecipeMap$removeRecipe.invoke(recipeMap, Reflections.method$CraftRecipe$toMinecraft.invoke(null, key)); } - Reflections.method$RecipeManager$finalizeRecipeLoading.invoke(this.minecraftRecipeManager); + Reflections.method$RecipeManager$finalizeRecipeLoading.invoke(minecraftRecipeManager); } catch (ReflectiveOperationException e) { plugin.logger().warn("Failed to unload custom recipes", e); } @@ -190,10 +207,8 @@ public class BukkitRecipeManager implements RecipeManager { } Recipe recipe = RecipeTypes.fromMap(section); NamespacedKey key = NamespacedKey.fromString(id.toString()); - Object craftRecipe = BUKKIT_RECIPE_CONVERTORS.get(recipe.type()).apply(key, recipe); + BUKKIT_RECIPE_REGISTER.get(recipe.type()).accept(key, recipe); try { - // to bypass paper's "resend" - Reflections.method$CraftRecipe$addToCraftingManager.invoke(craftRecipe); this.registeredCustomRecipes.add(key); this.customRecipes.add(id); this.recipes.computeIfAbsent(recipe.type(), k -> new ArrayList<>()).add(recipe); @@ -226,16 +241,39 @@ public class BukkitRecipeManager implements RecipeManager { } @Override - public void delayedLoad() { - this.processVanillaRecipes().thenRun(() -> { - if (VersionHelper.isVersionNewerThan1_21_2() && this.stolenFeatureFlagSet != null) { - try { - Reflections.field$RecipeManager$featureflagset.set(this.minecraftRecipeManager, this.stolenFeatureFlagSet); + public CompletableFuture delayedLoad() { + if (!ConfigManager.enableRecipeSystem()) return CompletableFuture.completedFuture(null); + return this.processVanillaRecipes().thenRun(() -> { + try { + // give flags back on 1.21.2+ + if (VersionHelper.isVersionNewerThan1_21_2() && this.stolenFeatureFlagSet != null) { + Reflections.field$RecipeManager$featureflagset.set(minecraftRecipeManager, this.stolenFeatureFlagSet); this.stolenFeatureFlagSet = false; - Reflections.method$RecipeManager$finalizeRecipeLoading.invoke(this.minecraftRecipeManager); - } catch (ReflectiveOperationException e) { - this.plugin.logger().warn("Failed to give featureflagset back", e); } + + // refresh recipes + if (VersionHelper.isVersionNewerThan1_21_2()) { + Reflections.method$RecipeManager$finalizeRecipeLoading.invoke(minecraftRecipeManager); + } + + // send to players + + + // now we need to remove the fake `exact` + if (VersionHelper.isVersionNewerThan1_21_4()) { + for (Object ingredient : injectedIngredients) { + Reflections.field$Ingredient$itemStacks1_21_4.set(ingredient, null); + } + } else if (VersionHelper.isVersionNewerThan1_21_2()) { + for (Object ingredient : injectedIngredients) { + Reflections.field$Ingredient$itemStacks1_21_2.set(ingredient, null); + } + } + + // clear cache + injectedIngredients.clear(); + } catch (Exception e) { + this.plugin.logger().warn("Failed to run delayed recipe tasks", e); } }); } @@ -244,7 +282,7 @@ public class BukkitRecipeManager implements RecipeManager { private CompletableFuture processVanillaRecipes() { CompletableFuture future = new CompletableFuture<>(); try { - List bukkitRecipesToRegister = new ArrayList<>(); + List injectLogics = new ArrayList<>(); plugin.scheduler().async().execute(() -> { try { Object fileToIdConverter = Reflections.method$FileToIdConverter$json.invoke(null, VersionHelper.isVersionNewerThan1_21() ? "recipe" : "recipes"); @@ -270,23 +308,11 @@ public class BukkitRecipeManager implements RecipeManager { switch (type) { case "minecraft:crafting_shaped" -> { VanillaShapedRecipe recipe = this.recipeReader.readShaped(jsonObject); - handleDataPackShapedRecipe(id, recipe, (shapedRecipe -> { - try { - bukkitRecipesToRegister.add(Reflections.method$CraftShapedRecipe$fromBukkitRecipe.invoke(null, shapedRecipe)); - } catch (Exception e) { - CraftEngine.instance().logger().warn("Failed to convert shaped recipe", e); - } - })); + handleDataPackShapedRecipe(id, recipe, (injectLogics::add)); } case "minecraft:crafting_shapeless" -> { VanillaShapelessRecipe recipe = this.recipeReader.readShapeless(jsonObject); - handleDataPackShapelessRecipe(id, recipe, (shapelessRecipe -> { - try { - bukkitRecipesToRegister.add(Reflections.method$CraftShapelessRecipe$fromBukkitRecipe.invoke(null, shapelessRecipe)); - } catch (Exception e) { - CraftEngine.instance().logger().warn("Failed to convert shapeless recipe", e); - } - })); + handleDataPackShapelessRecipe(id, recipe, (injectLogics::add)); } } } @@ -296,8 +322,8 @@ public class BukkitRecipeManager implements RecipeManager { } finally { plugin.scheduler().sync().run(() -> { try { - for (Object recipe : bukkitRecipesToRegister) { - Reflections.method$CraftRecipe$addToCraftingManager.invoke(recipe); + for (Runnable runnable : injectLogics) { + runnable.run(); } } catch (Exception e) { CraftEngine.instance().logger().warn("Failed to register recipes", e); @@ -313,7 +339,7 @@ public class BukkitRecipeManager implements RecipeManager { return future; } - private void handleDataPackShapelessRecipe(Key id, VanillaShapelessRecipe recipe, Consumer callback) { + private void handleDataPackShapelessRecipe(Key id, VanillaShapelessRecipe recipe, Consumer callback) { NamespacedKey key = new NamespacedKey("internal", id.value()); ItemStack result = createResultStack(recipe.result()); ShapelessRecipe shapelessRecipe = new ShapelessRecipe(key, result); @@ -348,20 +374,27 @@ public class BukkitRecipeManager implements RecipeManager { ingredientList.add(Ingredient.of(holders)); } - if (hasCustomItemInTag) { - callback.accept(shapelessRecipe); - this.injectedDataPackRecipes.add(key); - } CustomShapelessRecipe ceRecipe = new CustomShapelessRecipe<>( recipe.category(), recipe.group(), ingredientList, new CustomRecipeResult<>(new CloneableConstantItem(result), recipe.result().count()) ); + if (hasCustomItemInTag) { + callback.accept(() -> { + try { + Reflections.method$CraftRecipe$addToCraftingManager.invoke(Reflections.method$CraftShapelessRecipe$fromBukkitRecipe.invoke(null, shapelessRecipe)); + injectShapelessRecipe(id, ceRecipe); + } catch (Exception e) { + CraftEngine.instance().logger().warn("Failed to convert shapeless recipe", e); + } + }); + this.injectedDataPackRecipes.add(key); + } this.addVanillaInternalRecipe(Key.of("internal", id.value()), ceRecipe); } - private void handleDataPackShapedRecipe(Key id, VanillaShapedRecipe recipe, Consumer callback) { + private void handleDataPackShapedRecipe(Key id, VanillaShapedRecipe recipe, Consumer callback) { NamespacedKey key = new NamespacedKey("internal", id.value()); ItemStack result = createResultStack(recipe.result()); ShapedRecipe shapedRecipe = new ShapedRecipe(key, result); @@ -397,16 +430,23 @@ public class BukkitRecipeManager implements RecipeManager { ingredients.put(entry.getKey(), Ingredient.of(holders)); } - if (hasCustomItemInTag) { - callback.accept(shapedRecipe); - this.injectedDataPackRecipes.add(key); - } CustomShapedRecipe ceRecipe = new CustomShapedRecipe<>( recipe.category(), recipe.group(), new CustomShapedRecipe.Pattern<>(recipe.pattern(), ingredients), new CustomRecipeResult<>(new CloneableConstantItem(result), recipe.result().count()) ); + if (hasCustomItemInTag) { + callback.accept(() -> { + try { + Reflections.method$CraftRecipe$addToCraftingManager.invoke(Reflections.method$CraftShapedRecipe$fromBukkitRecipe.invoke(null, shapedRecipe)); + injectShapedRecipe(id, ceRecipe); + } catch (Exception e) { + CraftEngine.instance().logger().warn("Failed to convert shaped recipe", e); + } + }); + this.injectedDataPackRecipes.add(key); + } this.addVanillaInternalRecipe(Key.of("internal", id.value()), ceRecipe); } @@ -475,4 +515,76 @@ public class BukkitRecipeManager implements RecipeManager { Optional> optionalItem = BukkitItemManager.instance().getCustomItem(key); return optionalItem.map(itemStackCustomItem -> MaterialUtils.getMaterial(itemStackCustomItem.material())).orElse(null); } + + private static List getIngredientLooks(List> holders) throws ReflectiveOperationException { + List itemStacks = new ArrayList<>(); + for (Holder holder : holders) { + ItemStack itemStack = BukkitItemManager.instance().getBuildableItem(holder.value()).get().buildItemStack(null, 1); + Object nmsStack = Reflections.method$CraftItemStack$asNMSMirror.invoke(null, itemStack); + itemStacks.add(nmsStack); + } + return itemStacks; + } + + private static void injectShapedRecipe(Key id, Recipe recipe) throws ReflectiveOperationException { + List> actualIngredients = ((CustomShapedRecipe) recipe).parsedPattern().ingredients() + .stream() + .filter(Optional::isPresent) + .map(Optional::get) + .toList(); + + List ingredients = RecipeUtils.getIngredientsFromShapedRecipe(getNMSRecipe(id)); + injectIngredients(ingredients, actualIngredients); + } + + @SuppressWarnings("unchecked") + private static void injectShapelessRecipe(Key id, Recipe recipe) throws ReflectiveOperationException { + List> actualIngredients = ((CustomShapelessRecipe) recipe).ingredients(); + Object shapelessRecipe = getNMSRecipe(id); + List ingredients = (List) Reflections.field$ShapelessRecipe$ingredients.get(shapelessRecipe); + injectIngredients(ingredients, actualIngredients); + } + + private static Object getNMSRecipe(Key id) throws ReflectiveOperationException { + if (VersionHelper.isVersionNewerThan1_21_2()) { + Object resourceKey = Reflections.method$CraftRecipe$toMinecraft.invoke(null, new NamespacedKey(id.namespace(), id.value())); + @SuppressWarnings("unchecked") + Optional optional = (Optional) Reflections.method$RecipeManager$byKey.invoke(minecraftRecipeManager, resourceKey); + if (optional.isEmpty()) { + throw new IllegalArgumentException("Recipe " + id + " not found"); + } + return Reflections.field$RecipeHolder$recipe.get(optional.get()); + } else { + Object resourceLocation = Reflections.method$ResourceLocation$fromNamespaceAndPath.invoke(null, id.namespace(), id.value()); + @SuppressWarnings("unchecked") + Optional optional = (Optional) Reflections.method$RecipeManager$byKey.invoke(minecraftRecipeManager, resourceLocation); + if (optional.isEmpty()) { + throw new IllegalArgumentException("Recipe " + id + " not found"); + } + return VersionHelper.isVersionNewerThan1_20_2() ? Reflections.field$RecipeHolder$recipe.get(optional.get()) : optional.get(); + } + } + + private static void injectIngredients(List ingredients, List> actualIngredients) throws ReflectiveOperationException { + if (ingredients.size() != actualIngredients.size()) { + throw new IllegalArgumentException("Ingredient count mismatch"); + } + for (int i = 0; i < ingredients.size(); i++) { + Object ingredient = ingredients.get(i); + Ingredient actualIngredient = actualIngredients.get(i); + List items = getIngredientLooks(actualIngredient.items()); + if (VersionHelper.isVersionNewerThan1_21_4()) { + Reflections.field$Ingredient$itemStacks1_21_4.set(ingredient, new HashSet<>(items)); + } else if (VersionHelper.isVersionNewerThan1_21_2()) { + Reflections.field$Ingredient$itemStacks1_21_2.set(ingredient, items); + } else { + Object itemStackArray = Array.newInstance(Reflections.clazz$ItemStack, items.size()); + for (int j = 0; j < items.size(); j++) { + Array.set(itemStackArray, j, items.get(j)); + } + Reflections.field$Ingredient$itemStacks1_20_1.set(ingredient, itemStackArray); + } + injectedIngredients.add(ingredient); + } + } } diff --git a/bukkit/src/main/java/net/momirealms/craftengine/bukkit/item/recipe/CrafterEventListener.java b/bukkit/src/main/java/net/momirealms/craftengine/bukkit/item/recipe/CrafterEventListener.java new file mode 100644 index 000000000..1f623efe1 --- /dev/null +++ b/bukkit/src/main/java/net/momirealms/craftengine/bukkit/item/recipe/CrafterEventListener.java @@ -0,0 +1,112 @@ +package net.momirealms.craftengine.bukkit.item.recipe; + +import net.momirealms.craftengine.bukkit.plugin.BukkitCraftEngine; +import net.momirealms.craftengine.bukkit.util.ItemUtils; +import net.momirealms.craftengine.core.item.Item; +import net.momirealms.craftengine.core.item.ItemManager; +import net.momirealms.craftengine.core.item.recipe.OptimizedIDItem; +import net.momirealms.craftengine.core.item.recipe.Recipe; +import net.momirealms.craftengine.core.item.recipe.RecipeTypes; +import net.momirealms.craftengine.core.item.recipe.input.CraftingInput; +import net.momirealms.craftengine.core.registry.BuiltInRegistries; +import net.momirealms.craftengine.core.registry.Holder; +import net.momirealms.craftengine.core.util.Key; +import org.bukkit.block.Crafter; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.block.CrafterCraftEvent; +import org.bukkit.inventory.CraftingRecipe; +import org.bukkit.inventory.Inventory; +import org.bukkit.inventory.ItemStack; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +public class CrafterEventListener implements Listener { + private static final OptimizedIDItem EMPTY = new OptimizedIDItem<>(null, null); + private final ItemManager itemManager; + private final BukkitRecipeManager recipeManager; + private final BukkitCraftEngine plugin; + + public CrafterEventListener(BukkitCraftEngine plugin, BukkitRecipeManager recipeManager, ItemManager itemManager) { + this.itemManager = itemManager; + this.recipeManager = recipeManager; + this.plugin = plugin; + } + + @EventHandler + public void onCrafting(CrafterCraftEvent event) { + CraftingRecipe recipe = event.getRecipe(); + if (!(event.getBlock().getState() instanceof Crafter crafter)) { + return; + } + + Inventory inventory = crafter.getInventory(); + ItemStack[] ingredients = inventory.getStorageContents(); + + Key recipeId = Key.of(recipe.getKey().namespace(), recipe.getKey().value()); + // if the recipe is a vanilla one, custom items should never be ingredients + if (this.recipeManager.isDataPackRecipe(recipeId)) { + if (hasCustomItem(ingredients)) { + event.setCancelled(true); + } + return; + } + + // Maybe it's recipe from other plugins, then we ignore it + if (!this.recipeManager.isCustomRecipe(recipeId)) { + return; + } + + List> optimizedIDItems = new ArrayList<>(); + for (ItemStack itemStack : ingredients) { + if (ItemUtils.isEmpty(itemStack)) { + optimizedIDItems.add(EMPTY); + } else { + Item wrappedItem = this.itemManager.wrap(itemStack); + Optional> idHolder = BuiltInRegistries.OPTIMIZED_ITEM_ID.get(wrappedItem.id()); + if (idHolder.isEmpty()) { + // an invalid item is used in recipe, we disallow it + event.setCancelled(true); + return; + } else { + optimizedIDItems.add(new OptimizedIDItem<>(idHolder.get(), itemStack)); + } + } + } + + CraftingInput input; + if (ingredients.length == 9) { + input = CraftingInput.of(3, 3, optimizedIDItems); + } else if (ingredients.length == 4) { + input = CraftingInput.of(2, 2, optimizedIDItems); + } else { + return; + } + + Recipe ceRecipe = this.recipeManager.getRecipe(RecipeTypes.SHAPELESS, input); + if (ceRecipe != null) { + event.setResult(ceRecipe.getResult(null)); + return; + } + ceRecipe = this.recipeManager.getRecipe(RecipeTypes.SHAPED, input); + if (ceRecipe != null) { + event.setResult(ceRecipe.getResult(null)); + return; + } + // clear result if not met + event.setCancelled(true); + } + + private boolean hasCustomItem(ItemStack[] stack) { + for (ItemStack itemStack : stack) { + if (!ItemUtils.isEmpty(itemStack)) { + if (this.itemManager.wrap(itemStack).customId().isPresent()) { + return true; + } + } + } + return false; + } +} diff --git a/bukkit/src/main/java/net/momirealms/craftengine/bukkit/util/RecipeUtils.java b/bukkit/src/main/java/net/momirealms/craftengine/bukkit/util/RecipeUtils.java new file mode 100644 index 000000000..dfff3fd0a --- /dev/null +++ b/bukkit/src/main/java/net/momirealms/craftengine/bukkit/util/RecipeUtils.java @@ -0,0 +1,49 @@ +package net.momirealms.craftengine.bukkit.util; + +import net.momirealms.craftengine.core.plugin.CraftEngine; +import net.momirealms.craftengine.core.util.VersionHelper; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +public class RecipeUtils { + + private RecipeUtils() {} + + @SuppressWarnings("unchecked") + public static List getIngredientsFromShapedRecipe(Object recipe) { + List ingredients = new ArrayList<>(); + try { + if (VersionHelper.isVersionNewerThan1_20_3()) { + Object pattern = Reflections.field$1_20_3$ShapedRecipe$pattern.get(recipe); + if (VersionHelper.isVersionNewerThan1_21_2()) { + List> optionals = (List>) Reflections.field$ShapedRecipePattern$ingredients1_21_2.get(pattern); + for (Optional optional : optionals) { + optional.ifPresent(ingredients::add); + } + } else { + List objectList = (List) Reflections.field$ShapedRecipePattern$ingredients1_20_3.get(pattern); + for (Object object : objectList) { + Object[] values = (Object[]) Reflections.field$Ingredient$values.get(object); + // is empty or not + if (values.length != 0) { + ingredients.add(object); + } + } + } + } else { + List objectList = (List) Reflections.field$1_20_1$ShapedRecipe$recipeItems.get(recipe); + for (Object object : objectList) { + Object[] values = (Object[]) Reflections.field$Ingredient$values.get(object); + if (values.length != 0) { + ingredients.add(object); + } + } + } + } catch (ReflectiveOperationException e) { + CraftEngine.instance().logger().warn("Failed to get ingredients from shaped recipe", e); + } + return ingredients; + } +} diff --git a/bukkit/src/main/java/net/momirealms/craftengine/bukkit/util/Reflections.java b/bukkit/src/main/java/net/momirealms/craftengine/bukkit/util/Reflections.java index 09b18499a..ac2e30ce7 100644 --- a/bukkit/src/main/java/net/momirealms/craftengine/bukkit/util/Reflections.java +++ b/bukkit/src/main/java/net/momirealms/craftengine/bukkit/util/Reflections.java @@ -3918,4 +3918,136 @@ public class Reflections { clazz$NonNullList, Object.class, int.class, Object.class ) ); + + public static final Class clazz$Ingredient = requireNonNull( + ReflectionUtils.getClazz( + BukkitReflectionUtils.assembleMCClass("world.item.crafting.Ingredient"), + BukkitReflectionUtils.assembleMCClass("world.item.crafting.RecipeItemStack") + ) + ); + + // 1.20.1-1.21.1 + public static final Field field$Ingredient$itemStacks1_20_1 = + ReflectionUtils.getDeclaredField( + clazz$Ingredient, clazz$ItemStack.arrayType(), 0 + ); + + // 1.21.2-1.21.3 + public static final Field field$Ingredient$itemStacks1_21_2 = + ReflectionUtils.getDeclaredField( + clazz$Ingredient, List.class, 1 + ); + + // 1.21.4 paper + public static final Field field$Ingredient$itemStacks1_21_4 = + ReflectionUtils.getDeclaredField( + clazz$Ingredient, Set.class, 0 + ); + + // Since 1.21.2, exact has been removed + public static final Field field$Ingredient$exact = + ReflectionUtils.getDeclaredField( + clazz$Ingredient, boolean.class, 0 + ); + + public static final Class clazz$ShapedRecipe = requireNonNull( + ReflectionUtils.getClazz( + BukkitReflectionUtils.assembleMCClass("world.item.crafting.ShapedRecipe"), + BukkitReflectionUtils.assembleMCClass("world.item.crafting.ShapedRecipes") + ) + ); + + // 1.20.3+ + public static final Class clazz$ShapedRecipePattern = + ReflectionUtils.getClazz( + BukkitReflectionUtils.assembleMCClass("world.item.crafting.ShapedRecipePattern") + ); + + // 1.20.1-1.20.2 + public static final Field field$1_20_1$ShapedRecipe$recipeItems= + ReflectionUtils.getDeclaredField( + clazz$ShapedRecipe, clazz$NonNullList, 0 + ); + + // 1.20.3+ + public static final Field field$1_20_3$ShapedRecipe$pattern= + ReflectionUtils.getDeclaredField( + clazz$ShapedRecipe, clazz$ShapedRecipePattern, 0 + ); + + // 1.20.3-1.21.1 + public static final Field field$ShapedRecipePattern$ingredients1_20_3 = Optional.ofNullable(clazz$ShapedRecipePattern) + .map(it -> ReflectionUtils.getDeclaredField(it, clazz$NonNullList, 0)) + .orElse(null); + + // 1.21.2+ + public static final Field field$ShapedRecipePattern$ingredients1_21_2 = Optional.ofNullable(clazz$ShapedRecipePattern) + .map(it -> ReflectionUtils.getDeclaredField(it, List.class, 0)) + .orElse(null); + + // 1.20.1-1.21.1 + public static final Field field$Ingredient$values = ReflectionUtils.getInstanceDeclaredField( + clazz$Ingredient, 0 + ); + + // 1.20.2+ + public static final Class clazz$RecipeHolder = ReflectionUtils.getClazz( + BukkitReflectionUtils.assembleMCClass("world.item.crafting.RecipeHolder") + ); + + // 1.20.2+ + public static final Field field$RecipeHolder$recipe = Optional.ofNullable(clazz$RecipeHolder) + .map(it -> ReflectionUtils.getDeclaredField(it, 1)) + .orElse(null); + + public static final Class clazz$ShapelessRecipe = requireNonNull( + ReflectionUtils.getClazz( + BukkitReflectionUtils.assembleMCClass("world.item.crafting.ShapelessRecipe"), + BukkitReflectionUtils.assembleMCClass("world.item.crafting.ShapelessRecipes") + ) + ); + + public static final Field field$ShapelessRecipe$ingredients = + Optional.ofNullable(ReflectionUtils.getDeclaredField(clazz$ShapelessRecipe, List.class, 0)) + .orElse(ReflectionUtils.getDeclaredField(clazz$ShapelessRecipe, clazz$NonNullList, 0)); + + // require ResourceLocation for 1.20.1-1.21.1 + // require ResourceKey for 1.21.2+ + public static final Method method$RecipeManager$byKey; + + static { + Method method$RecipeManager$byKey0 = null; + if (VersionHelper.isVersionNewerThan1_21_2()) { + for (Method method : clazz$RecipeManager.getMethods()) { + if (method.getParameterCount() == 1 && method.getParameterTypes()[0] == clazz$ResourceKey) { + if (method.getReturnType() == Optional.class && method.getGenericReturnType() instanceof ParameterizedType type) { + Type[] actualTypeArguments = type.getActualTypeArguments(); + if (actualTypeArguments.length == 1) { + method$RecipeManager$byKey0 = method; + } + } + } + } + } else if (VersionHelper.isVersionNewerThan1_20_2()) { + for (Method method : clazz$RecipeManager.getMethods()) { + if (method.getParameterCount() == 1 && method.getParameterTypes()[0] == clazz$ResourceLocation) { + if (method.getReturnType() == Optional.class && method.getGenericReturnType() instanceof ParameterizedType type) { + Type[] actualTypeArguments = type.getActualTypeArguments(); + if (actualTypeArguments.length == 1) { + method$RecipeManager$byKey0 = method; + } + } + } + } + } else { + for (Method method : clazz$RecipeManager.getMethods()) { + if (method.getParameterCount() == 1 && method.getParameterTypes()[0] == clazz$ResourceLocation) { + if (method.getReturnType() == Optional.class) { + method$RecipeManager$byKey0 = method; + } + } + } + } + method$RecipeManager$byKey = requireNonNull(method$RecipeManager$byKey0); + } } diff --git a/core/src/main/java/net/momirealms/craftengine/core/item/recipe/CustomShapedRecipe.java b/core/src/main/java/net/momirealms/craftengine/core/item/recipe/CustomShapedRecipe.java index f75cc0f2a..e69d99caa 100644 --- a/core/src/main/java/net/momirealms/craftengine/core/item/recipe/CustomShapedRecipe.java +++ b/core/src/main/java/net/momirealms/craftengine/core/item/recipe/CustomShapedRecipe.java @@ -25,6 +25,10 @@ public class CustomShapedRecipe extends CraftingTableRecipe { this.result = result; } + public ParsedPattern parsedPattern() { + return parsedPattern; + } + @SuppressWarnings("unchecked") @Override public boolean matches(RecipeInput input) { @@ -72,6 +76,10 @@ public class CustomShapedRecipe extends CraftingTableRecipe { this.ingredients = ingredients; } + public List>> ingredients() { + return ingredients; + } + public int width() { return width; } diff --git a/core/src/main/java/net/momirealms/craftengine/core/item/recipe/OptimizedIDItem.java b/core/src/main/java/net/momirealms/craftengine/core/item/recipe/OptimizedIDItem.java index 7e7750f00..fc899ff76 100644 --- a/core/src/main/java/net/momirealms/craftengine/core/item/recipe/OptimizedIDItem.java +++ b/core/src/main/java/net/momirealms/craftengine/core/item/recipe/OptimizedIDItem.java @@ -27,4 +27,11 @@ public class OptimizedIDItem { public boolean isEmpty() { return idHolder == null; } + + @Override + public String toString() { + return "OptimizedIDItem{" + + "idHolder=" + idHolder + + '}'; + } } diff --git a/core/src/main/java/net/momirealms/craftengine/core/item/recipe/RecipeManager.java b/core/src/main/java/net/momirealms/craftengine/core/item/recipe/RecipeManager.java index 44909544d..f58cd23d2 100644 --- a/core/src/main/java/net/momirealms/craftengine/core/item/recipe/RecipeManager.java +++ b/core/src/main/java/net/momirealms/craftengine/core/item/recipe/RecipeManager.java @@ -8,6 +8,7 @@ import net.momirealms.craftengine.core.util.Key; import org.jetbrains.annotations.Nullable; import java.util.List; +import java.util.concurrent.CompletableFuture; public interface RecipeManager extends Reloadable, ConfigSectionParser { String CONFIG_SECTION_NAME = "recipes"; @@ -25,7 +26,7 @@ public interface RecipeManager extends Reloadable, ConfigSectionParser { @Nullable Recipe getRecipe(Key type, RecipeInput input); - void delayedLoad(); + CompletableFuture delayedLoad(); default int loadingSequence() { return LoadingSequence.RECIPE; diff --git a/core/src/main/java/net/momirealms/craftengine/core/plugin/CraftEngine.java b/core/src/main/java/net/momirealms/craftengine/core/plugin/CraftEngine.java index 03e310dac..2a19dc656 100644 --- a/core/src/main/java/net/momirealms/craftengine/core/plugin/CraftEngine.java +++ b/core/src/main/java/net/momirealms/craftengine/core/plugin/CraftEngine.java @@ -80,11 +80,9 @@ public abstract class CraftEngine implements Plugin { this.worldManager.reload(); this.packManager.reload(); this.blockManager.delayedLoad(); - this.recipeManager.delayedLoad(); - - this.scheduler.async().execute(() -> { + this.recipeManager.delayedLoad().thenRunAsync(() -> { this.packManager.generateResourcePack(); - }); + }, this.scheduler.async()); } @Override