From b9195fa2cc933aec754af4da94402c169603eeae Mon Sep 17 00:00:00 2001 From: Max Date: Sat, 12 Oct 2024 19:53:45 +0200 Subject: [PATCH] First attempt at reworking recipe event (#909) * First attempt at reworking recipe event * Code cleanup * Improve logging of failed recipe * Bump version * DataResult cleanup * Fix handleError for null errors * Fix server data not being sent if recipe event is null --- gradle.properties | 2 +- .../kubejs/core/mixin/RecipeManagerMixin.java | 79 ++++- .../mods/kubejs/plugin/KubeJSPlugin.java | 3 + .../mods/kubejs/recipe/KubeRecipe.java | 60 ++-- .../mods/kubejs/recipe/RecipeHelper.java | 69 ---- .../kubejs/recipe/RecipeTypeFunction.java | 2 +- .../mods/kubejs/recipe/RecipesKubeEvent.java | 328 +++++++----------- .../recipe/component/RecipeComponent.java | 12 +- .../latvian/mods/kubejs/script/ConsoleJS.java | 16 +- .../kubejs/server/ServerScriptManager.java | 37 -- 10 files changed, 259 insertions(+), 349 deletions(-) delete mode 100644 src/main/java/dev/latvian/mods/kubejs/recipe/RecipeHelper.java diff --git a/gradle.properties b/gradle.properties index 188d03e2e..dda048ab1 100644 --- a/gradle.properties +++ b/gradle.properties @@ -11,7 +11,7 @@ modrinth_id=umyGl7zF minecraft_version=1.21.1 supported_versions=1.21.1, 1.21 -mod_version=2101.7.0 +mod_version=2101.7.1 neoforge_version=21.1.42 parchment_mc_version=1.21 diff --git a/src/main/java/dev/latvian/mods/kubejs/core/mixin/RecipeManagerMixin.java b/src/main/java/dev/latvian/mods/kubejs/core/mixin/RecipeManagerMixin.java index c9dac9e50..390ab5d0d 100644 --- a/src/main/java/dev/latvian/mods/kubejs/core/mixin/RecipeManagerMixin.java +++ b/src/main/java/dev/latvian/mods/kubejs/core/mixin/RecipeManagerMixin.java @@ -3,8 +3,19 @@ import com.google.common.collect.ImmutableMultimap; import com.google.common.collect.Multimap; import com.google.gson.JsonElement; +import com.llamalad7.mixinextras.sugar.Local; +import dev.latvian.mods.kubejs.bindings.event.ServerEvents; import dev.latvian.mods.kubejs.core.RecipeManagerKJS; import dev.latvian.mods.kubejs.core.ReloadableServerResourcesKJS; +import dev.latvian.mods.kubejs.net.KubeServerData; +import dev.latvian.mods.kubejs.net.SyncServerDataPayload; +import dev.latvian.mods.kubejs.plugin.KubeJSPlugins; +import dev.latvian.mods.kubejs.recipe.CompostableRecipesKubeEvent; +import dev.latvian.mods.kubejs.recipe.RecipesKubeEvent; +import dev.latvian.mods.kubejs.recipe.special.SpecialRecipeSerializerManager; +import dev.latvian.mods.kubejs.script.ConsoleJS; +import dev.latvian.mods.kubejs.script.ScriptType; +import dev.latvian.mods.kubejs.util.Cast; import net.minecraft.resources.ResourceLocation; import net.minecraft.server.packs.resources.ResourceManager; import net.minecraft.util.profiling.ProfilerFiller; @@ -18,6 +29,7 @@ import org.spongepowered.asm.mixin.injection.Inject; import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; +import java.util.HashMap; import java.util.Map; @Mixin(value = RecipeManager.class, priority = 1100) @@ -31,11 +43,72 @@ public abstract class RecipeManagerMixin implements RecipeManagerKJS { @Unique private ReloadableServerResourcesKJS kjs$resources; - @Inject(method = "apply(Ljava/util/Map;Lnet/minecraft/server/packs/resources/ResourceManager;Lnet/minecraft/util/profiling/ProfilerFiller;)V", at = @At("HEAD"), cancellable = true) + @Unique + private RecipesKubeEvent kjs$event; + + @Inject( + method = "apply(Ljava/util/Map;Lnet/minecraft/server/packs/resources/ResourceManager;Lnet/minecraft/util/profiling/ProfilerFiller;)V", + at = @At("HEAD") + ) private void customRecipesHead(Map map, ResourceManager resourceManager, ProfilerFiller profiler, CallbackInfo ci) { - if (kjs$resources.kjs$getServerScriptManager().recipes(this, resourceManager, map)) { - ci.cancel(); + var manager = kjs$resources.kjs$getServerScriptManager(); + + // FIXME: data maps! + if (ServerEvents.COMPOSTABLE_RECIPES.hasListeners()) { + ServerEvents.COMPOSTABLE_RECIPES.post(ScriptType.SERVER, new CompostableRecipesKubeEvent()); + } + + for (var entry : manager.getRegistries().cachedRegistryTags.values()) { + if (entry.registry() == null || entry.lookup() == null) { + continue; + } + + entry.registry().bindTags(Cast.to(entry.lookup().bindingMap())); } + + manager.recipeSchemaStorage.fireEvents(manager.getRegistries(), resourceManager); + + SpecialRecipeSerializerManager.INSTANCE.reset(); + ServerEvents.SPECIAL_RECIPES.post(ScriptType.SERVER, SpecialRecipeSerializerManager.INSTANCE); + + if (ServerEvents.RECIPES.hasListeners()) { + ConsoleJS.SERVER.info("Processing recipes..."); + kjs$event = new RecipesKubeEvent(manager, resourceManager); + kjs$event.post(this, map); + } + } + + @Inject( + method = "apply(Ljava/util/Map;Lnet/minecraft/server/packs/resources/ResourceManager;Lnet/minecraft/util/profiling/ProfilerFiller;)V", + at = @At(value = "INVOKE", target = "Lorg/slf4j/Logger;error(Ljava/lang/String;Ljava/lang/Object;Ljava/lang/Object;)V") + ) + private void catchFailingRecipes(CallbackInfo ci, @Local Map.Entry entry, @Local RuntimeException ex) { + if (kjs$event != null) { + kjs$event.handleFailedRecipe(entry.getKey(), entry.getValue(), ex); + } + } + + @Inject( + method = "apply(Ljava/util/Map;Lnet/minecraft/server/packs/resources/ResourceManager;Lnet/minecraft/util/profiling/ProfilerFiller;)V", + at = @At(value = "TAIL") + ) + @SuppressWarnings("removal") + private void addServerData(CallbackInfo ci) { + if (kjs$event != null) { + // FIXME: please remove this soon! massive performance implications! + var recipesByName = new HashMap<>(byName); + + KubeJSPlugins.forEachPlugin(p -> p.injectRuntimeRecipes(kjs$event, this, recipesByName)); + + kjs$event.finishEvent(); + + // make sure byType is also set correctly + kjs$replaceRecipes(recipesByName); + } + + kjs$event = null; + + kjs$getResources().kjs$getServerScriptManager().serverData = new SyncServerDataPayload(KubeServerData.collect()); } @Override diff --git a/src/main/java/dev/latvian/mods/kubejs/plugin/KubeJSPlugin.java b/src/main/java/dev/latvian/mods/kubejs/plugin/KubeJSPlugin.java index df5e2916b..daae8aceb 100644 --- a/src/main/java/dev/latvian/mods/kubejs/plugin/KubeJSPlugin.java +++ b/src/main/java/dev/latvian/mods/kubejs/plugin/KubeJSPlugin.java @@ -126,7 +126,10 @@ default void beforeRecipeLoading(RecipesKubeEvent event, RecipeManagerKJS manage /** * Only use this method if your mod adds runtime recipes and is conflicting with KubeJS recipe manager. Disable your other hook if "kubejs" mod is loaded! + * + * @deprecated This method should no longer be necessary, as KubeJS no longer interferes with dynamic recipes added through RecipeManager. */ + @Deprecated(forRemoval = true) default void injectRuntimeRecipes(RecipesKubeEvent event, RecipeManagerKJS manager, Map> recipesByName) { } diff --git a/src/main/java/dev/latvian/mods/kubejs/recipe/KubeRecipe.java b/src/main/java/dev/latvian/mods/kubejs/recipe/KubeRecipe.java index a6d56750b..2b958c678 100644 --- a/src/main/java/dev/latvian/mods/kubejs/recipe/KubeRecipe.java +++ b/src/main/java/dev/latvian/mods/kubejs/recipe/KubeRecipe.java @@ -2,6 +2,7 @@ import com.google.gson.JsonObject; import com.google.gson.JsonPrimitive; +import com.mojang.serialization.DataResult; import dev.latvian.mods.kubejs.CommonProperties; import dev.latvian.mods.kubejs.DevProperties; import dev.latvian.mods.kubejs.core.RecipeLikeKJS; @@ -35,15 +36,18 @@ import net.minecraft.world.item.ItemStack; import net.minecraft.world.item.crafting.Ingredient; import net.minecraft.world.item.crafting.Recipe; -import net.minecraft.world.item.crafting.RecipeHolder; +import net.minecraft.world.item.crafting.RecipeManager; import net.minecraft.world.item.crafting.RecipeSerializer; import org.apache.commons.lang3.mutable.MutableObject; import org.jetbrains.annotations.Nullable; import java.util.ArrayList; import java.util.List; +import java.util.Optional; public class KubeRecipe implements RecipeLikeKJS, CustomJavaToJsWrapper { + public static final String CHANGED_MARKER = "_kubejs_changed_marker"; + public ResourceLocation id; public RecipeTypeFunction type; public boolean newRecipe; @@ -459,18 +463,13 @@ public KubeRecipe stage(String s) { } /** - * Only used by {@link KubeRecipe#getOrCreateId()} and {@link KubeRecipe#createRecipe()} in rare case that a recipe can be another recipe type than itself (e.g. kubejs:shaped -> minecraft:crafting_shaped) + * Only used by {@link KubeRecipe#getOrCreateId()} and {@link KubeRecipe#serializeChanges()} in rare case that a recipe can be another recipe type than itself (e.g. kubejs:shaped -> minecraft:crafting_shaped) */ public RecipeTypeFunction getSerializationTypeFunction() { return type; } - @Nullable - public RecipeHolder createRecipe() { - if (removed) { - return null; - } - + public KubeRecipe serializeChanges() { if (newRecipe || hasChanged()) { serialize(); @@ -490,25 +489,17 @@ public RecipeHolder createRecipe() { json.addProperty("type", getSerializationTypeFunction().idString); } - var id = getOrCreateId(); - if (type.event.stageSerializer != null && json.has(KubeJSCraftingRecipe.STAGE_KEY) && !type.idString.equals("recipestages:stage")) { - var o = new JsonObject(); - o.addProperty("stage", json.get(KubeJSCraftingRecipe.STAGE_KEY).getAsString()); - o.add("recipe", json); - - try { - var recipe = type.event.registries.decodeJson(type.event.stageSerializer.codec(), o); - return recipe == null ? null : new RecipeHolder<>(id, recipe); - } catch (Throwable ex) { - ConsoleJS.SERVER.error("Failed to decode " + id + " from json " + o, sourceLine, ex, RecipesKubeEvent.CREATE_RECIPE_SKIP_ERROR); - } + var staged = new JsonObject(); + staged.addProperty("stage", json.get(KubeJSCraftingRecipe.STAGE_KEY).getAsString()); + staged.add("recipe", json); + json = staged; } - } else if (originalRecipe != null && originalRecipe.getValue() != null) { - return new RecipeHolder<>(getOrCreateId(), originalRecipe.getValue()); + + json.addProperty(CHANGED_MARKER, true); } - return RecipeHelper.fromJson(type.event.jsonOps, getSerializationTypeFunction().schemaType.getSerializer(), getOrCreateId(), json, DevProperties.get().logErroringParsedRecipes); + return this; } @Nullable @@ -516,11 +507,24 @@ public Recipe getOriginalRecipe() { if (originalRecipe == null) { originalRecipe = new MutableObject<>(); try { - var holder = RecipeHelper.fromJson(type.event.jsonOps, type.schemaType.getSerializer(), getOrCreateId(), originalJson, DevProperties.get().logErroringParsedRecipes); - - if (holder != null) { - originalRecipe.setValue(holder.value()); - } + var serializer = type.schemaType.getSerializer(); + var ops = type.event.jsonOps; + + // people apparently violate the contract here?! + //noinspection OptionalOfNullableMisuse + Optional.ofNullable(serializer.codec()) + .map(DataResult::success) + .orElseGet(() -> DataResult.error(() -> "Codec for " + serializer.getClass().getName() + " is null!")) + .flatMap(codec -> ops.getMap(json).flatMap(map -> codec.decode(ops, map))) + .mapError(err -> "Error parsing recipe " + id + ": " + err) + .ifSuccess(originalRecipe::setValue) + .ifError(err -> { + if (DevProperties.get().logErroringParsedRecipes) { + ConsoleJS.SERVER.error(err.message()); + } else { + RecipeManager.LOGGER.error(err.message()); + } + }); } catch (Throwable e) { ConsoleJS.SERVER.error("Could not create recipe from json for " + this, e); } diff --git a/src/main/java/dev/latvian/mods/kubejs/recipe/RecipeHelper.java b/src/main/java/dev/latvian/mods/kubejs/recipe/RecipeHelper.java deleted file mode 100644 index 6a56ac9ed..000000000 --- a/src/main/java/dev/latvian/mods/kubejs/recipe/RecipeHelper.java +++ /dev/null @@ -1,69 +0,0 @@ -package dev.latvian.mods.kubejs.recipe; - -import com.google.gson.JsonElement; -import com.google.gson.JsonObject; -import com.mojang.serialization.DynamicOps; -import dev.latvian.mods.kubejs.script.ConsoleJS; -import net.minecraft.resources.ResourceLocation; -import net.minecraft.world.item.crafting.RecipeHolder; -import net.minecraft.world.item.crafting.RecipeManager; -import net.minecraft.world.item.crafting.RecipeSerializer; -import org.jetbrains.annotations.Nullable; - -public interface RecipeHelper { - @Nullable - static RecipeHolder fromJson(DynamicOps ops, RecipeSerializer serializer, ResourceLocation id, JsonObject json, boolean errors) { - var codec = serializer.codec(); - - if (codec == null) { - if (errors) { - ConsoleJS.SERVER.error("Error parsing recipe " + id + ": Codec not found in " + serializer.getClass().getName()); - } else { - RecipeManager.LOGGER.error("Error parsing recipe " + id + ": Codec not found in " + serializer.getClass().getName()); - } - - return null; - } - - var map = ops.getMap(json).result(); - - if (map.isEmpty()) { - if (errors) { - ConsoleJS.SERVER.error("Error parsing recipe " + id + ": Couldn't convert " + json + " to a map"); - } else { - RecipeManager.LOGGER.error("Error parsing recipe " + id + ": Couldn't convert " + json + " to a map"); - } - - return null; - } - - try { - var recipe = codec.decode(ops, map.get()); - - if (recipe.isSuccess()) { - var r = recipe.getOrThrow(); - return r == null ? null : new RecipeHolder<>(id, r); - } else if (recipe.error().isPresent()) { - if (errors) { - ConsoleJS.SERVER.error("Error parsing recipe " + id + ": " + recipe.error().get().message()); - } else { - RecipeManager.LOGGER.error("Error parsing recipe " + id + ": " + recipe.error().get().message()); - } - } else { - if (errors) { - ConsoleJS.SERVER.error("Error parsing recipe " + id + ": Unknown"); - } else { - RecipeManager.LOGGER.error("Error parsing recipe " + id + ": Unknown"); - } - } - } catch (Exception e) { - if (errors) { - ConsoleJS.SERVER.error("Error parsing recipe " + id + " from " + map.get(), e, RecipesKubeEvent.CREATE_RECIPE_SKIP_ERROR); - } else { - RecipeManager.LOGGER.error("Error parsing recipe " + id + " from " + map.get(), e); - } - } - - return null; - } -} diff --git a/src/main/java/dev/latvian/mods/kubejs/recipe/RecipeTypeFunction.java b/src/main/java/dev/latvian/mods/kubejs/recipe/RecipeTypeFunction.java index 76a780a99..e35a749fd 100644 --- a/src/main/java/dev/latvian/mods/kubejs/recipe/RecipeTypeFunction.java +++ b/src/main/java/dev/latvian/mods/kubejs/recipe/RecipeTypeFunction.java @@ -43,7 +43,7 @@ public KubeRecipe call(Context cx, Scriptable scope, Scriptable thisObj, Object[ } catch (Throwable cause) { var r = schemaType.schema.recipeFactory.create(this, sourceLine, true); r.creationError = true; - event.failedCount.incrementAndGet(); + event.failedCount++; ConsoleJS.SERVER.error("Failed to create a '" + idString + "' recipe from args " + Arrays.toString(args0), sourceLine, cause, SKIP_ERROR); r.json = new JsonObject(); r.json.addProperty("type", idString); diff --git a/src/main/java/dev/latvian/mods/kubejs/recipe/RecipesKubeEvent.java b/src/main/java/dev/latvian/mods/kubejs/recipe/RecipesKubeEvent.java index 59888fd19..da31f7fc5 100644 --- a/src/main/java/dev/latvian/mods/kubejs/recipe/RecipesKubeEvent.java +++ b/src/main/java/dev/latvian/mods/kubejs/recipe/RecipesKubeEvent.java @@ -5,6 +5,7 @@ import com.google.gson.JsonObject; import com.google.gson.JsonParseException; import com.mojang.serialization.Codec; +import com.mojang.serialization.DataResult; import dev.latvian.mods.kubejs.CommonProperties; import dev.latvian.mods.kubejs.DevProperties; import dev.latvian.mods.kubejs.bindings.event.ServerEvents; @@ -12,7 +13,6 @@ import dev.latvian.mods.kubejs.error.InvalidRecipeComponentException; import dev.latvian.mods.kubejs.error.KubeRuntimeException; import dev.latvian.mods.kubejs.error.UnknownRecipeTypeException; -import dev.latvian.mods.kubejs.event.EventExceptionHandler; import dev.latvian.mods.kubejs.event.KubeEvent; import dev.latvian.mods.kubejs.plugin.KubeJSPlugins; import dev.latvian.mods.kubejs.recipe.filter.ConstantFilter; @@ -46,9 +46,6 @@ import net.minecraft.resources.ResourceLocation; import net.minecraft.server.packs.resources.ResourceManager; import net.minecraft.util.GsonHelper; -import net.minecraft.world.item.ItemStack; -import net.minecraft.world.item.crafting.Recipe; -import net.minecraft.world.item.crafting.RecipeHolder; import net.minecraft.world.item.crafting.RecipeManager; import net.minecraft.world.item.crafting.RecipeSerializer; import net.neoforged.neoforge.common.conditions.ConditionalOps; @@ -59,15 +56,11 @@ import java.util.Collections; import java.util.HashMap; import java.util.HashSet; -import java.util.LinkedHashMap; import java.util.Map; -import java.util.Objects; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.function.BinaryOperator; import java.util.function.Consumer; import java.util.function.Function; import java.util.function.Predicate; @@ -79,52 +72,9 @@ public class RecipesKubeEvent implements KubeEvent { public static final Pattern POST_SKIP_ERROR = Pattern.compile("dev\\.latvian\\.mods\\.kubejs\\.recipe\\.RecipesKubeEvent\\.post"); public static final Pattern CREATE_RECIPE_SKIP_ERROR = Pattern.compile("dev\\.latvian\\.mods\\.kubejs\\.recipe\\.RecipesKubeEvent\\.createRecipe"); private static final Predicate RECIPE_NOT_REMOVED = r -> r != null && !r.removed; - private static final EventExceptionHandler RECIPE_EXCEPTION_HANDLER = (event, handler, ex) -> { - // skip the current handler on a recipe or JSON exception, but let other handlers run - if (ex instanceof KubeRuntimeException || ex instanceof JsonParseException) { - ConsoleJS.SERVER.error("Error while processing recipe event handler: " + handler, ex); - return null; - } else { - return ex; // rethrow - } - }; - - private String recipeToString(Recipe recipe) { - var map = new LinkedHashMap(); - map.put("type", BuiltInRegistries.RECIPE_SERIALIZER.getKey(recipe.getSerializer())); - - try { - var in = new ArrayList<>(); - - for (var ingredient : recipe.getIngredients()) { - var list = new ArrayList(); + private static final Predicate RECIPE_IS_SYNTHETIC = r -> !r.newRecipe; - for (var item : ingredient.getItems()) { - list.add(item.kjs$toItemString0(registries.nbt())); - } - - in.add(list); - } - - map.put("in", in); - } catch (Exception ex) { - map.put("in_error", ex.toString()); - } - - try { - var result = recipe.getResultItem(registries.access()); - //noinspection ConstantValue - map.put("out", (result == null ? ItemStack.EMPTY : result).kjs$toItemString0(registries.nbt())); - } catch (Exception ex) { - map.put("out_error", ex.toString()); - } - - return map.toString(); - } - - private static final Function, ResourceLocation> RECIPE_ID = RecipeHolder::id; - private static final Predicate> RECIPE_NON_NULL = Objects::nonNull; - private static final Function, RecipeHolder> RECIPE_IDENTITY = Function.identity(); + private final Stopwatch overallTimer; public final RecipeSchemaStorage recipeSchemaStorage; public final RegistryAccessContainer registries; @@ -132,10 +82,11 @@ private String recipeToString(Recipe recipe) { public final RegistryOps jsonOps; public final Map originalRecipes; public final Collection addedRecipes; - private final BinaryOperator> mergeOriginal, mergeAdded; + public final Collection removedRecipes; - public final AtomicInteger failedCount; - public final Map takenIds; + int modifiedCount, failedCount; + + private final Map takenIds; private final Map recipeFunctions; public final transient RecipeTypeFunction vanillaShaped; @@ -154,30 +105,21 @@ private String recipeToString(Recipe recipe) { public RecipesKubeEvent(ServerScriptManager manager, ResourceManager resourceManager) { ConsoleJS.SERVER.info("Initializing recipe event..."); + this.overallTimer = Stopwatch.createStarted(); + this.recipeSchemaStorage = manager.recipeSchemaStorage; this.registries = manager.getRegistries(); this.resourceManager = resourceManager; this.jsonOps = new ConditionalOps<>(registries.json(), registries); this.originalRecipes = new HashMap<>(); this.addedRecipes = new ConcurrentLinkedQueue<>(); + this.removedRecipes = new ConcurrentLinkedQueue<>(); this.recipeFunctions = new HashMap<>(); this.takenIds = new ConcurrentHashMap<>(); // var itemTags = manager.getLoadedTags(Registries.ITEM); // System.out.println(itemTags); - this.mergeOriginal = (a, b) -> { - ConsoleJS.SERVER.warn("Duplicate original recipe for id " + a.id() + "!\nRecipe A: " + recipeToString(a.value()) + "\nRecipe B: " + recipeToString(b.value()) + "\nUsing last one encountered."); - return b; - }; - - this.mergeAdded = (a, b) -> { - ConsoleJS.SERVER.error("Duplicate added recipe for id " + a.id() + "!\nRecipe A: " + recipeToString(a.value()) + "\nRecipe B: " + recipeToString(b.value()) + "\nUsing last one encountered."); - return b; - }; - - this.failedCount = new AtomicInteger(0); - for (var namespace : recipeSchemaStorage.namespaces.values()) { var nsMap = new HashMap(); recipeFunctions.put(namespace.name, new NamespaceFunction(namespace, nsMap)); @@ -234,8 +176,13 @@ public RecipesKubeEvent(ServerScriptManager manager, ResourceManager resourceMan @HideFromJS public void post(RecipeManagerKJS recipeManager, Map datapackRecipeMap) { - ConsoleJS.SERVER.info("Processing recipes..."); + discoverRecipes(recipeManager, datapackRecipeMap); + postEvent(); + applyChanges(datapackRecipeMap); + } + @HideFromJS + public void discoverRecipes(RecipeManagerKJS recipeManager, Map datapackRecipeMap) { var timer = Stopwatch.createStarted(); KubeJSPlugins.forEachPlugin(p -> p.beforeRecipeLoading(this, recipeManager, datapackRecipeMap)); @@ -245,118 +192,112 @@ public void post(RecipeManagerKJS recipeManager, Map { + if (jsonResult.isEmpty()) { + infoSkip("Skipping recipe %s, conditions not met".formatted(recipeId)); + } else { + parseOriginalRecipe(jsonResult.get(), recipeId); + } } - - continue; + case DataResult.Error error -> errorSkip("Skipping recipe %s, error parsing conditions: %s".formatted(recipeId, error.message())); } + } - var result = jsonResult.result().get(); + takenIds.putAll(originalRecipes); + ConsoleJS.SERVER.info("Found %d recipes in %s".formatted(originalRecipes.size(), timer.stop())); + } - if (result.isEmpty()) { - if (DevProperties.get().logSkippedRecipes) { - ConsoleJS.SERVER.info("Skipping recipe %s, conditions not met".formatted(recipeId)); - } else { - RecipeManager.LOGGER.info("Skipping recipe %s, conditions not met".formatted(recipeId)); - } + private void parseOriginalRecipe(JsonObject json, ResourceLocation recipeId) { + var typeStr = GsonHelper.getAsString(json, "type"); + var recipeIdAndType = recipeId + "[" + typeStr + "]"; + var type = getRecipeFunction(typeStr); - continue; - } + if (type == null) { + warnSkip("Skipping recipe %s, unknown type: %s".formatted(recipeId, typeStr)); + return; + } + + try { + var recipe = type.schemaType.schema.deserialize(SourceLine.UNKNOWN, type, recipeId, json); + recipe.afterLoaded(); + originalRecipes.put(recipeId, recipe); - var json = result.get(); - var typeStr = GsonHelper.getAsString(json, "type"); - var recipeIdAndType = recipeId + "[" + typeStr + "]"; - var type = getRecipeFunction(typeStr); + if (ConsoleJS.SERVER.shouldPrintDebug()) { + var original = recipe.getOriginalRecipe(); - if (type == null) { - if (DevProperties.get().logSkippedRecipes) { - ConsoleJS.SERVER.warn("Skipping recipe " + recipeId + ", unknown type: " + typeStr); + if (original == null || SpecialRecipeSerializerManager.INSTANCE.isSpecial(original)) { + ConsoleJS.SERVER.debug("Loaded recipe " + recipeIdAndType + ": "); } else { - RecipeManager.LOGGER.warn("Skipping recipe " + recipeId + ", unknown type: " + typeStr); + ConsoleJS.SERVER.debug("Loaded recipe " + recipeIdAndType + ": " + recipe.getFromToString()); } - - continue; + } + } catch (InvalidRecipeComponentException ignore) { + } catch (Throwable ex) { + if (DevProperties.get().logErroringRecipes) { + ConsoleJS.SERVER.warn("Failed to parse recipe '" + recipeIdAndType + "'! Falling back to vanilla", ex, POST_SKIP_ERROR); } try { - var recipe = type.schemaType.schema.deserialize(SourceLine.UNKNOWN, type, recipeId, json); - recipe.afterLoaded(); - originalRecipes.put(recipeId, recipe); - - if (ConsoleJS.SERVER.shouldPrintDebug()) { - var original = recipe.getOriginalRecipe(); - - if (original == null || SpecialRecipeSerializerManager.INSTANCE.isSpecial(original)) { - ConsoleJS.SERVER.debug("Loaded recipe " + recipeIdAndType + ": "); - } else { - ConsoleJS.SERVER.debug("Loaded recipe " + recipeIdAndType + ": " + recipe.getFromToString()); - } - } - } catch (InvalidRecipeComponentException ignore) { - } catch (Throwable ex) { + originalRecipes.put(recipeId, UnknownRecipeSchema.SCHEMA.deserialize(SourceLine.UNKNOWN, type, recipeId, json)); + } catch (NullPointerException | IllegalArgumentException | JsonParseException ex2) { if (DevProperties.get().logErroringRecipes) { - ConsoleJS.SERVER.warn("Failed to parse recipe '" + recipeIdAndType + "'! Falling back to vanilla", ex, POST_SKIP_ERROR); - } - - try { - originalRecipes.put(recipeId, UnknownRecipeSchema.SCHEMA.deserialize(SourceLine.UNKNOWN, type, recipeId, json)); - } catch (NullPointerException | IllegalArgumentException | JsonParseException ex2) { - if (DevProperties.get().logErroringRecipes) { - ConsoleJS.SERVER.warn("Failed to parse recipe " + recipeIdAndType, ex2, POST_SKIP_ERROR); - } - } catch (Exception ex3) { - ConsoleJS.SERVER.warn("Failed to parse recipe " + recipeIdAndType, ex3, POST_SKIP_ERROR); + ConsoleJS.SERVER.warn("Failed to parse recipe " + recipeIdAndType, ex2, POST_SKIP_ERROR); } + } catch (Exception ex3) { + ConsoleJS.SERVER.warn("Failed to parse recipe " + recipeIdAndType, ex3, POST_SKIP_ERROR); } } + } - takenIds.putAll(originalRecipes); - ConsoleJS.SERVER.info("Found " + originalRecipes.size() + " recipes in " + timer.stop()); + private void infoSkip(String s) { + if (DevProperties.get().logSkippedRecipes) { + ConsoleJS.SERVER.info(s); + } else { + RecipeManager.LOGGER.info(s); + } + } - timer.reset().start(); - ServerEvents.RECIPES.post(ScriptType.SERVER, this); + private void warnSkip(String s) { + if (DevProperties.get().logSkippedRecipes) { + ConsoleJS.SERVER.warn(s); + } else { + RecipeManager.LOGGER.warn(s); + } + } - int modifiedCount = 0; - var removedRecipes = new ConcurrentLinkedQueue(); + private void errorSkip(String s) { + if (DevProperties.get().logSkippedRecipes) { + ConsoleJS.SERVER.error(s); + } else { + RecipeManager.LOGGER.error(s); + } + } + + @HideFromJS + public void postEvent() { + var timer = Stopwatch.createStarted(); + + ServerEvents.RECIPES.post(ScriptType.SERVER, this); for (var r : originalRecipes.values()) { if (r.removed) { @@ -367,40 +308,40 @@ public void post(RecipeManagerKJS recipeManager, Map>(originalRecipes.size() + addedRecipes.size()); - - try { - recipesByName.putAll(originalRecipes.values().parallelStream() - .filter(RECIPE_NOT_REMOVED) - .map(this::createRecipe) - .filter(RECIPE_NON_NULL) - .collect(Collectors.toConcurrentMap(RECIPE_ID, RECIPE_IDENTITY, mergeOriginal))); - } catch (Throwable ex) { - ConsoleJS.SERVER.error("Error creating datapack recipes", ex, POST_SKIP_ERROR); - } - - try { - recipesByName.putAll(addedRecipes.parallelStream() - .map(this::createRecipe) - .filter(RECIPE_NON_NULL) - .collect(Collectors.toConcurrentMap(RECIPE_ID, RECIPE_IDENTITY, mergeAdded))); - } catch (Throwable ex) { - ConsoleJS.SERVER.error("Error creating script recipes", ex, POST_SKIP_ERROR); - } - - KubeJSPlugins.forEachPlugin(p -> p.injectRuntimeRecipes(this, recipeManager, recipesByName)); + @HideFromJS + public void applyChanges(Map map) { + var timer = Stopwatch.createStarted(); + addedRecipes.removeIf(RECIPE_IS_SYNTHETIC); + + map.clear(); + map.putAll(originalRecipes.values().parallelStream() + .filter(RECIPE_NOT_REMOVED) + .map(KubeRecipe::serializeChanges) + .peek(this::addToExport) + .collect(Collectors.toConcurrentMap(KubeRecipe::getOrCreateId, recipe -> recipe.json, (a, b) -> b))); + + map.putAll(addedRecipes.parallelStream() + .filter(RECIPE_NOT_REMOVED) + .map(KubeRecipe::serializeChanges) + .peek(this::addToExport) + .collect(Collectors.toConcurrentMap(KubeRecipe::getOrCreateId, recipe -> recipe.json, (a, b) -> { + ConsoleJS.SERVER.warn("KubeJS has found two recipes with the same ID in your custom recipes! Picking the last one encountered!"); + return b; + }))); + + ConsoleJS.SERVER.info("KubeJS modifications to recipe manager finished in %s".formatted(timer.stop())); + } - recipeManager.kjs$replaceRecipes(recipesByName); + @HideFromJS + public void finishEvent() { ChangesForChat.recipesAdded = addedRecipes.size(); ChangesForChat.recipesModified = modifiedCount; ChangesForChat.recipesRemoved = removedRecipes.size(); - ChangesForChat.recipesMs = timer.stop().elapsed(TimeUnit.MILLISECONDS); + ChangesForChat.recipesMs = overallTimer.stop().elapsed(TimeUnit.MILLISECONDS); - ConsoleJS.SERVER.info("Added " + addedRecipes.size() + " recipes, removed " + removedRecipes.size() + " recipes, modified " + modifiedCount + " recipes, with " + failedCount.get() + " failed recipes in " + TimeJS.msToString(ChangesForChat.recipesMs)); + ConsoleJS.SERVER.info("Added %d recipes, removed %d recipes, modified %d recipes, with %d failed recipes taking %s in total".formatted(addedRecipes.size(), removedRecipes.size(), modifiedCount, failedCount, TimeJS.msToString(ChangesForChat.recipesMs))); if (DataExport.export != null) { for (var r : removedRecipes) { @@ -433,36 +374,25 @@ public void post(RecipeManagerKJS recipeManager, Map createRecipe(KubeRecipe r) { - try { - var rec = r.createRecipe(); - var path = r.kjs$getMod() + "/" + r.getPath(); - - if (!r.removed && DataExport.export != null) { - DataExport.export.addJson("recipes/%s.json".formatted(path), r.json); - - if (r.newRecipe) { - DataExport.export.addJson("added_recipes/%s.json".formatted(path), r.json); - } - } + private void addToExport(KubeRecipe r) { + var path = r.kjs$getMod() + "/" + r.getPath(); + if (DataExport.export != null) { + DataExport.export.addJson("recipes/%s.json".formatted(path), r.json); - //noinspection ConstantValue - if (rec == null || rec.value() == null) { - return null; + if (r.newRecipe) { + DataExport.export.addJson("added_recipes/%s.json".formatted(path), r.json); } - - return rec; - } catch (Throwable ex) { - ConsoleJS.SERVER.warn("Error parsing recipe " + r + ": " + r.json, ex, POST_SKIP_ERROR); - failedCount.incrementAndGet(); - return null; } } - private static boolean addedRecipeRemoveCheck(KubeRecipe r) { - // r.getOrCreateId(); // Generate ID synchronously? - return !r.newRecipe; + @HideFromJS + public void handleFailedRecipe(ResourceLocation id, JsonElement json, Throwable ex) { + // only handle recipes that failed because of kubejs interfering + if (json.isJsonObject() && json.getAsJsonObject().has(KubeRecipe.CHANGED_MARKER)) { + json.getAsJsonObject().remove(KubeRecipe.CHANGED_MARKER); // cleanup for logging + ConsoleJS.SERVER.warn("Error parsing recipe %s: %s".formatted(id, json), ex); + failedCount++; + } } public Map getRecipes() { diff --git a/src/main/java/dev/latvian/mods/kubejs/recipe/component/RecipeComponent.java b/src/main/java/dev/latvian/mods/kubejs/recipe/component/RecipeComponent.java index 9f7b60f67..437985746 100644 --- a/src/main/java/dev/latvian/mods/kubejs/recipe/component/RecipeComponent.java +++ b/src/main/java/dev/latvian/mods/kubejs/recipe/component/RecipeComponent.java @@ -1,7 +1,9 @@ package dev.latvian.mods.kubejs.recipe.component; +import com.google.gson.JsonElement; import com.google.gson.JsonObject; import com.mojang.serialization.Codec; +import com.mojang.serialization.DataResult; import dev.latvian.mods.kubejs.error.EmptyRecipeComponentValueException; import dev.latvian.mods.kubejs.recipe.KubeRecipe; import dev.latvian.mods.kubejs.recipe.RecipeKey; @@ -114,13 +116,9 @@ default void writeToJson(KubeRecipe recipe, RecipeComponentValue cv, JsonObje } } - var encoded = cv.key.codec.encodeStart(recipe.type.event.registries.json(), cv.value); - - if (encoded.error().isPresent()) { - ConsoleJS.SERVER.error("Failed to encode " + cv.key.name + " for " + recipe.id + " from " + cv.value + ": " + encoded.error().get().message(), recipe.sourceLine, null, RecipesKubeEvent.POST_SKIP_ERROR); - } else if (encoded.isSuccess()) { - var e = encoded.getOrThrow(); - json.add(cv.key.name, e); + switch (cv.key.codec.encodeStart(recipe.type.event.registries.json(), cv.value)) { + case DataResult.Success(var value, var lifecycle) -> json.add(cv.key.name, value); + case DataResult.Error error -> ConsoleJS.SERVER.error("Failed to encode " + cv.key.name + " for recipe " + recipe.id + " from value" + cv.value + ": " + error.message(), recipe.sourceLine, null, RecipesKubeEvent.POST_SKIP_ERROR); } } diff --git a/src/main/java/dev/latvian/mods/kubejs/script/ConsoleJS.java b/src/main/java/dev/latvian/mods/kubejs/script/ConsoleJS.java index b78fc1c69..2cabdaa71 100644 --- a/src/main/java/dev/latvian/mods/kubejs/script/ConsoleJS.java +++ b/src/main/java/dev/latvian/mods/kubejs/script/ConsoleJS.java @@ -335,7 +335,7 @@ public ConsoleLine warn(String message, Throwable error, @Nullable Pattern exitP public ConsoleLine warn(String message, SourceLine sourceLine, Throwable error, @Nullable Pattern exitPattern) { if (shouldPrint()) { - var l = log(LogType.WARN, sourceLine, error, message.isEmpty() ? error.toString() : (message + ": " + error.toString())); + var l = log(LogType.WARN, sourceLine, error, messageForPrint(message, error)); handleError(l, error, exitPattern, !capturingErrors); return l; } @@ -343,6 +343,14 @@ public ConsoleLine warn(String message, SourceLine sourceLine, Throwable error, return null; } + private static String messageForPrint(String message, @Nullable Throwable error) { + if (message.isEmpty()) { + return Objects.requireNonNull(error, "Both message and error are empty!").toString(); + } else { + return error == null ? message : (message + ": " + error); + } + } + public ConsoleLine warn(String message, Throwable error) { return warn(message, error, null); } @@ -361,7 +369,7 @@ public ConsoleLine error(String message, Throwable error, @Nullable Pattern exit public ConsoleLine error(String message, SourceLine sourceLine, Throwable error, @Nullable Pattern exitPattern) { if (shouldPrint()) { - var l = log(LogType.ERROR, sourceLine, error, message.isEmpty() ? error.toString() : (message + ": " + error.toString())); + var l = log(LogType.ERROR, sourceLine, error, messageForPrint(message, error)); handleError(l, error, exitPattern, true); return l; } @@ -535,12 +543,12 @@ public void printObject(@Nullable Object o) { printObject(o, false); } - public void handleError(ConsoleLine line, Throwable error, @Nullable Pattern exitPattern, boolean print) { + public void handleError(ConsoleLine line, @Nullable Throwable error, @Nullable Pattern exitPattern, boolean print) { while (error instanceof WrappedException ex) { error = ex.getWrappedException(); } - if (error instanceof EcmaError) { + if (error == null || error instanceof EcmaError) { return; } diff --git a/src/main/java/dev/latvian/mods/kubejs/server/ServerScriptManager.java b/src/main/java/dev/latvian/mods/kubejs/server/ServerScriptManager.java index aab7c8118..96a84f2ae 100644 --- a/src/main/java/dev/latvian/mods/kubejs/server/ServerScriptManager.java +++ b/src/main/java/dev/latvian/mods/kubejs/server/ServerScriptManager.java @@ -1,23 +1,17 @@ package dev.latvian.mods.kubejs.server; -import com.google.gson.JsonElement; import com.google.gson.JsonObject; import com.mojang.serialization.Codec; import dev.latvian.mods.kubejs.KubeJS; import dev.latvian.mods.kubejs.KubeJSPaths; import dev.latvian.mods.kubejs.bindings.event.ServerEvents; -import dev.latvian.mods.kubejs.core.RecipeManagerKJS; import dev.latvian.mods.kubejs.error.KubeRuntimeException; import dev.latvian.mods.kubejs.item.ItemBuilder; import dev.latvian.mods.kubejs.item.ItemModificationKubeEvent; -import dev.latvian.mods.kubejs.net.KubeServerData; import dev.latvian.mods.kubejs.net.SyncServerDataPayload; import dev.latvian.mods.kubejs.plugin.KubeJSPlugin; import dev.latvian.mods.kubejs.plugin.KubeJSPlugins; -import dev.latvian.mods.kubejs.recipe.CompostableRecipesKubeEvent; -import dev.latvian.mods.kubejs.recipe.RecipesKubeEvent; import dev.latvian.mods.kubejs.recipe.schema.RecipeSchemaStorage; -import dev.latvian.mods.kubejs.recipe.special.SpecialRecipeSerializerManager; import dev.latvian.mods.kubejs.registry.AdditionalObjectRegistry; import dev.latvian.mods.kubejs.registry.BuilderBase; import dev.latvian.mods.kubejs.registry.RegistryObjectStorage; @@ -38,14 +32,12 @@ import net.minecraft.resources.ResourceLocation; import net.minecraft.server.packs.PackResources; import net.minecraft.server.packs.PackType; -import net.minecraft.server.packs.resources.ResourceManager; import net.neoforged.fml.loading.FMLLoader; import net.neoforged.neoforge.registries.DataPackRegistriesHooks; import net.neoforged.neoforge.server.ServerLifecycleHooks; import java.nio.file.Files; import java.util.ArrayList; -import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; @@ -247,35 +239,6 @@ public void reload() { } } - public boolean recipes(RecipeManagerKJS recipeManager, ResourceManager resourceManager, Map map) { - if (ServerEvents.COMPOSTABLE_RECIPES.hasListeners()) { - ServerEvents.COMPOSTABLE_RECIPES.post(ScriptType.SERVER, new CompostableRecipesKubeEvent()); - } - - boolean result = false; - - for (var entry : getRegistries().cachedRegistryTags.values()) { - if (entry.registry() == null || entry.lookup() == null) { - continue; - } - - entry.registry().bindTags((Map) entry.lookup().bindingMap()); - } - - recipeSchemaStorage.fireEvents(getRegistries(), resourceManager); - - SpecialRecipeSerializerManager.INSTANCE.reset(); - ServerEvents.SPECIAL_RECIPES.post(ScriptType.SERVER, SpecialRecipeSerializerManager.INSTANCE); - - if (ServerEvents.RECIPES.hasListeners()) { - new RecipesKubeEvent(this, resourceManager).post(recipeManager, new HashMap<>(map)); - result = true; - } - - serverData = new SyncServerDataPayload(KubeServerData.collect()); - return result; - } - @Override protected void fullReload() { var server = ServerLifecycleHooks.getCurrentServer();