From e136859b8531059d60ca9c61744f9963f841acd7 Mon Sep 17 00:00:00 2001 From: XiaoMoMi Date: Wed, 12 Feb 2025 03:42:51 +0800 Subject: [PATCH] Optimize add recipe --- .../item/recipe/BukkitRecipeManager.java | 163 +++++++++++++----- .../craftengine/bukkit/util/Reflections.java | 50 +++++- .../craftengine/core/plugin/CraftEngine.java | 2 +- 3 files changed, 164 insertions(+), 51 deletions(-) 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 fdf4b43e3..2d57a2cb1 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 @@ -20,6 +20,7 @@ import net.momirealms.craftengine.core.item.recipe.vanilla.impl.VanillaRecipeRea import net.momirealms.craftengine.core.item.recipe.vanilla.impl.VanillaRecipeReader1_20_5; import net.momirealms.craftengine.core.item.recipe.vanilla.impl.VanillaRecipeReader1_21_2; import net.momirealms.craftengine.core.pack.Pack; +import net.momirealms.craftengine.core.plugin.CraftEngine; import net.momirealms.craftengine.core.plugin.config.ConfigManager; import net.momirealms.craftengine.core.registry.BuiltInRegistries; import net.momirealms.craftengine.core.registry.Holder; @@ -39,11 +40,12 @@ import org.jetbrains.annotations.Nullable; import java.io.Reader; import java.nio.file.Path; import java.util.*; +import java.util.concurrent.CompletableFuture; import java.util.function.BiFunction; import java.util.function.Consumer; public class BukkitRecipeManager implements RecipeManager { - private static final Map, org.bukkit.inventory.Recipe>> BUKKIT_RECIPE_CONVERTORS = new HashMap<>(); + private static final Map, Object>> BUKKIT_RECIPE_CONVERTORS = new HashMap<>(); static { BUKKIT_RECIPE_CONVERTORS.put(RecipeTypes.SHAPED, (key, recipe) -> { @@ -59,7 +61,12 @@ public class BukkitRecipeManager implements RecipeManager { for (Map.Entry> entry : ceRecipe.pattern().ingredients().entrySet()) { shapedRecipe.setIngredient(entry.getKey(), new RecipeChoice.MaterialChoice(ingredientToBukkitMaterials(entry.getValue()))); } - return shapedRecipe; + try { + return Reflections.method$CraftShapedRecipe$fromBukkitRecipe.invoke(null, shapedRecipe); + } catch (Exception e) { + CraftEngine.instance().logger().warn("Failed to convert shaped recipe", e); + return null; + } }); BUKKIT_RECIPE_CONVERTORS.put(RecipeTypes.SHAPELESS, (key, recipe) -> { CustomShapelessRecipe ceRecipe = (CustomShapelessRecipe) recipe; @@ -73,7 +80,12 @@ public class BukkitRecipeManager implements RecipeManager { for (Ingredient ingredient : ceRecipe.ingredients()) { shapelessRecipe.addIngredient(new RecipeChoice.MaterialChoice(ingredientToBukkitMaterials(ingredient))); } - return shapelessRecipe; + try { + return Reflections.method$CraftShapelessRecipe$fromBukkitRecipe.invoke(null, shapelessRecipe); + } catch (Exception e) { + CraftEngine.instance().logger().warn("Failed to convert shapeless recipe", e); + return null; + } }); } @@ -89,6 +101,9 @@ public class BukkitRecipeManager implements RecipeManager { // data pack recipe resource locations [minecraft:xxx] 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()); @@ -104,6 +119,11 @@ public class BukkitRecipeManager implements RecipeManager { } else { this.recipeReader = new VanillaRecipeReader1_20(); } + try { + this.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); + } } @Override @@ -120,6 +140,14 @@ public class BukkitRecipeManager implements RecipeManager { public void load() { if (!ConfigManager.enableRecipeSystem()) return; Bukkit.getPluginManager().registerEvents(this.recipeEventListener, 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); + } catch (ReflectiveOperationException e) { + this.plugin.logger().warn("Failed to steal featureflagset", e); + } + } } @Override @@ -130,15 +158,14 @@ public class BukkitRecipeManager implements RecipeManager { this.customRecipes.clear(); if (VersionHelper.isVersionNewerThan1_21_2()) { try { - Object recipeManager = Reflections.method$MinecraftServer$getRecipeManager.invoke(Reflections.method$MinecraftServer$getServer.invoke(null)); - Object recipeMap = Reflections.field$RecipeManager$recipes.get(recipeManager); + Object recipeMap = Reflections.field$RecipeManager$recipes.get(this.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(recipeManager); + Reflections.method$RecipeManager$finalizeRecipeLoading.invoke(this.minecraftRecipeManager); } catch (ReflectiveOperationException e) { plugin.logger().warn("Failed to unload custom recipes", e); } @@ -162,11 +189,17 @@ public class BukkitRecipeManager implements RecipeManager { return; } Recipe recipe = RecipeTypes.fromMap(section); - this.recipes.computeIfAbsent(recipe.type(), k -> new ArrayList<>()).add(recipe); NamespacedKey key = NamespacedKey.fromString(id.toString()); - Bukkit.addRecipe(BUKKIT_RECIPE_CONVERTORS.get(recipe.type()).apply(key, recipe)); - this.registeredCustomRecipes.add(key); - this.customRecipes.add(id); + Object craftRecipe = BUKKIT_RECIPE_CONVERTORS.get(recipe.type()).apply(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); + } catch (Exception e) { + plugin.logger().warn("Failed to add custom recipe " + id, e); + } } @Override @@ -194,56 +227,92 @@ public class BukkitRecipeManager implements RecipeManager { @Override public void delayedLoad() { - processVanillaRecipes(); + this.processVanillaRecipes().thenRun(() -> { + if (VersionHelper.isVersionNewerThan1_21_2() && this.stolenFeatureFlagSet != null) { + try { + Reflections.field$RecipeManager$featureflagset.set(this.minecraftRecipeManager, this.stolenFeatureFlagSet); + this.stolenFeatureFlagSet = false; + } catch (ReflectiveOperationException e) { + this.plugin.logger().warn("Failed to give featureflagset back", e); + } + } + }); } @SuppressWarnings("unchecked") - private void processVanillaRecipes() { + private CompletableFuture processVanillaRecipes() { + CompletableFuture future = new CompletableFuture<>(); try { - Object fileToIdConverter = Reflections.method$FileToIdConverter$json.invoke(null, VersionHelper.isVersionNewerThan1_21() ? "recipe" : "recipes"); - Object minecraftServer = Reflections.method$MinecraftServer$getServer.invoke(null); - Object packRepository = Reflections.method$MinecraftServer$getPackRepository.invoke(minecraftServer); - List selected = (List) Reflections.field$PackRepository$selected.get(packRepository); - List packResources = new ArrayList<>(); - for (Object pack : selected) { - packResources.add(Reflections.method$Pack$open.invoke(pack)); - } - List bukkitRecipesToRegister = new ArrayList<>(); - try (AutoCloseable resourceManager = (AutoCloseable) Reflections.constructor$MultiPackResourceManager.newInstance(Reflections.instance$PackType$SERVER_DATA, packResources)) { - Map scannedResources = (Map) Reflections.method$FileToIdConverter$listMatchingResources.invoke(fileToIdConverter, resourceManager); - for (Map.Entry entry : scannedResources.entrySet()) { - Key id = extractKeyFromResourceLocation(entry.getKey().toString()); - this.dataPackRecipes.add(id); - // Maybe it's unregistered by other plugins - if (Bukkit.getRecipe(new NamespacedKey(id.namespace(), id.value())) == null) { - continue; + List bukkitRecipesToRegister = new ArrayList<>(); + plugin.scheduler().async().execute(() -> { + try { + Object fileToIdConverter = Reflections.method$FileToIdConverter$json.invoke(null, VersionHelper.isVersionNewerThan1_21() ? "recipe" : "recipes"); + Object minecraftServer = Reflections.method$MinecraftServer$getServer.invoke(null); + Object packRepository = Reflections.method$MinecraftServer$getPackRepository.invoke(minecraftServer); + List selected = (List) Reflections.field$PackRepository$selected.get(packRepository); + List packResources = new ArrayList<>(); + for (Object pack : selected) { + packResources.add(Reflections.method$Pack$open.invoke(pack)); } - Reader reader = (Reader) Reflections.method$Resource$openAsReader.invoke(entry.getValue()); - JsonObject jsonObject = JsonParser.parseReader(reader).getAsJsonObject(); - String type = jsonObject.get("type").getAsString(); - switch (type) { - case "minecraft:crafting_shaped" -> { - VanillaShapedRecipe recipe = this.recipeReader.readShaped(jsonObject); - handleDataPackShapedRecipe(id, recipe, bukkitRecipesToRegister::add); - } - case "minecraft:crafting_shapeless" -> { - VanillaShapelessRecipe recipe = this.recipeReader.readShapeless(jsonObject); - handleDataPackShapelessRecipe(id, recipe, bukkitRecipesToRegister::add); + try (AutoCloseable resourceManager = (AutoCloseable) Reflections.constructor$MultiPackResourceManager.newInstance(Reflections.instance$PackType$SERVER_DATA, packResources)) { + Map scannedResources = (Map) Reflections.method$FileToIdConverter$listMatchingResources.invoke(fileToIdConverter, resourceManager); + for (Map.Entry entry : scannedResources.entrySet()) { + Key id = extractKeyFromResourceLocation(entry.getKey().toString()); + this.dataPackRecipes.add(id); + // Maybe it's unregistered by other plugins + if (Bukkit.getRecipe(new NamespacedKey(id.namespace(), id.value())) == null) { + continue; + } + Reader reader = (Reader) Reflections.method$Resource$openAsReader.invoke(entry.getValue()); + JsonObject jsonObject = JsonParser.parseReader(reader).getAsJsonObject(); + String type = jsonObject.get("type").getAsString(); + 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); + } + })); + } + 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); + } + })); + } + } } } - } - } - plugin.scheduler().sync().run(() -> { - for (org.bukkit.inventory.Recipe recipe : bukkitRecipesToRegister) { - Bukkit.addRecipe(recipe); + } catch (Exception e) { + plugin.logger().warn("Failed to read data pack recipes", e); + } finally { + plugin.scheduler().sync().run(() -> { + try { + for (Object recipe : bukkitRecipesToRegister) { + Reflections.method$CraftRecipe$addToCraftingManager.invoke(recipe); + } + } catch (Exception e) { + CraftEngine.instance().logger().warn("Failed to register recipes", e); + } finally { + future.complete(null); + } + }); } }); } catch (Exception e) { this.plugin.logger().warn("Failed to inject vanilla recipes", e); } + 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); @@ -291,7 +360,7 @@ public class BukkitRecipeManager implements RecipeManager { 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); 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 04a8951df..09e4c9847 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 @@ -21,6 +21,8 @@ import org.bukkit.event.block.BlockPhysicsEvent; import org.bukkit.event.block.BlockPlaceEvent; import org.bukkit.event.entity.CreatureSpawnEvent; import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.ShapedRecipe; +import org.bukkit.inventory.ShapelessRecipe; import sun.misc.Unsafe; import java.io.BufferedReader; @@ -3829,12 +3831,54 @@ public class Reflections { .map(it -> ReflectionUtils.getMethod(it, boolean.class, clazz$ResourceKey)) .orElse(null); - public static final Class clazz$CraftRecipe = + public static final Class clazz$CraftRecipe = requireNonNull( ReflectionUtils.getClazz( BukkitReflectionUtils.assembleCBClass("inventory.CraftRecipe") - ); + ) + ); - public static final Method method$CraftRecipe$toMinecraft = Optional.ofNullable(clazz$CraftRecipe) + public static final Method method$CraftRecipe$addToCraftingManager = requireNonNull( + ReflectionUtils.getMethod( + clazz$CraftRecipe, new String[]{"addToCraftingManager"} + ) + ); + + public static final Method method$CraftRecipe$toMinecraft = Optional.of(clazz$CraftRecipe) .map(it -> ReflectionUtils.getStaticMethod(it, clazz$ResourceKey, NamespacedKey.class)) .orElse(null); + + public static final Class clazz$CraftShapedRecipe = requireNonNull( + ReflectionUtils.getClazz( + BukkitReflectionUtils.assembleCBClass("inventory.CraftShapedRecipe") + ) + ); + + public static final Method method$CraftShapedRecipe$fromBukkitRecipe = requireNonNull( + ReflectionUtils.getStaticMethod( + clazz$CraftShapedRecipe, clazz$CraftShapedRecipe, ShapedRecipe.class + ) + ); + + public static final Class clazz$CraftShapelessRecipe = requireNonNull( + ReflectionUtils.getClazz( + BukkitReflectionUtils.assembleCBClass("inventory.CraftShapelessRecipe") + ) + ); + + public static final Method method$CraftShapelessRecipe$fromBukkitRecipe = requireNonNull( + ReflectionUtils.getStaticMethod( + clazz$CraftShapelessRecipe, clazz$CraftShapelessRecipe, ShapelessRecipe.class + ) + ); + + public static final Class clazz$FeatureFlagSet = requireNonNull( + ReflectionUtils.getClazz( + BukkitReflectionUtils.assembleMCClass("world.flag.FeatureFlagSet") + ) + ); + + public static final Field field$RecipeManager$featureflagset = + ReflectionUtils.getDeclaredField( + clazz$RecipeManager, clazz$FeatureFlagSet, 0 + ); } 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 d1f832aba..03e310dac 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,9 +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(); this.packManager.generateResourcePack(); }); }