diff --git a/patches/minecraft/net/minecraft/advancements/Advancement.java.patch b/patches/minecraft/net/minecraft/advancements/Advancement.java.patch index 7a25438646..2edaf1bc74 100644 --- a/patches/minecraft/net/minecraft/advancements/Advancement.java.patch +++ b/patches/minecraft/net/minecraft/advancements/Advancement.java.patch @@ -8,10 +8,10 @@ + @Deprecated public static Advancement fromJson(JsonObject p_300691_, DeserializationContext p_300053_) { + return fromJson(p_300691_, p_300053_, net.minecraftforge.common.crafting.conditions.ICondition.IContext.EMPTY); -+ } ++ } + -+ public static Advancement fromJson(JsonObject p_300691_, DeserializationContext p_300053_, net.minecraftforge.common.crafting.conditions.ICondition.IContext context) { -+ //if ((p_300691_ = net.minecraftforge.common.crafting.ConditionalAdvancement.processConditional(p_300691_, context)) == null) return null; ++ public static Advancement fromJson(JsonObject p_300691_, DeserializationContext p_300053_, net.minecraftforge.common.crafting.conditions.ICondition.IContext context) { ++ if ((p_300691_ = net.minecraftforge.common.ForgeHooks.readConditionalAdvancement(context, p_300691_)) == null) return null; Optional optional = p_300691_.has("parent") ? Optional.of(new ResourceLocation(GsonHelper.getAsString(p_300691_, "parent"))) : Optional.empty(); Optional optional1 = p_300691_.has("display") ? Optional.of(DisplayInfo.fromJson(GsonHelper.getAsJsonObject(p_300691_, "display"))) : Optional.empty(); AdvancementRewards advancementrewards = p_300691_.has("rewards") ? AdvancementRewards.deserialize(GsonHelper.getAsJsonObject(p_300691_, "rewards")) : AdvancementRewards.EMPTY; diff --git a/patches/minecraft/net/minecraft/data/recipes/CraftingRecipeBuilder.java.patch b/patches/minecraft/net/minecraft/data/recipes/CraftingRecipeBuilder.java.patch new file mode 100644 index 0000000000..a931eee169 --- /dev/null +++ b/patches/minecraft/net/minecraft/data/recipes/CraftingRecipeBuilder.java.patch @@ -0,0 +1,18 @@ +--- a/net/minecraft/data/recipes/CraftingRecipeBuilder.java ++++ b/net/minecraft/data/recipes/CraftingRecipeBuilder.java +@@ -31,8 +_,15 @@ + this.category = p_250313_; + } + ++ private net.minecraftforge.common.crafting.conditions.ICondition condition; ++ public R withCondition(net.minecraftforge.common.crafting.conditions.ICondition condition) { ++ this.condition = condition; ++ return (R)this; ++ } ++ + public void serializeRecipeData(JsonObject p_250456_) { + p_250456_.addProperty("category", this.category.getSerializedName()); ++ net.minecraftforge.common.ForgeHooks.writeCondition(this.condition, p_250456_); + } + } + } diff --git a/patches/minecraft/net/minecraft/data/recipes/FinishedRecipe.java.patch b/patches/minecraft/net/minecraft/data/recipes/FinishedRecipe.java.patch new file mode 100644 index 0000000000..b697fc4933 --- /dev/null +++ b/patches/minecraft/net/minecraft/data/recipes/FinishedRecipe.java.patch @@ -0,0 +1,15 @@ +--- a/net/minecraft/data/recipes/FinishedRecipe.java ++++ b/net/minecraft/data/recipes/FinishedRecipe.java +@@ -23,4 +_,12 @@ + + @Nullable + AdvancementHolder advancement(); ++ ++ @Nullable ++ default AdvancementData advancementData() { ++ var adv = this.advancement(); ++ return adv == null ? null : new AdvancementData(adv.id(), adv.value().serializeToJson()); ++ } ++ ++ record AdvancementData(ResourceLocation id, JsonObject data) {} + } diff --git a/patches/minecraft/net/minecraft/data/recipes/RecipeProvider.java.patch b/patches/minecraft/net/minecraft/data/recipes/RecipeProvider.java.patch index 8ae361308b..60f6ab2f05 100644 --- a/patches/minecraft/net/minecraft/data/recipes/RecipeProvider.java.patch +++ b/patches/minecraft/net/minecraft/data/recipes/RecipeProvider.java.patch @@ -1,11 +1,16 @@ --- a/net/minecraft/data/recipes/RecipeProvider.java +++ b/net/minecraft/data/recipes/RecipeProvider.java -@@ -93,7 +_,9 @@ - AdvancementHolder advancementholder = p_300066_.advancement(); - if (advancementholder != null) { - JsonObject jsonobject = advancementholder.value().serializeToJson(); +@@ -90,10 +_,11 @@ + throw new IllegalStateException("Duplicate recipe " + p_300066_.id()); + } else { + list.add(DataProvider.saveStable(p_254020_, p_300066_.serializeRecipe(), RecipeProvider.this.recipePathProvider.json(p_300066_.id()))); +- AdvancementHolder advancementholder = p_300066_.advancement(); +- if (advancementholder != null) { +- JsonObject jsonobject = advancementholder.value().serializeToJson(); - list.add(DataProvider.saveStable(p_254020_, jsonobject, RecipeProvider.this.advancementPathProvider.json(advancementholder.id()))); -+ var saveAdvancementFuture = saveAdvancement(p_254020_, jsonobject, advancementholder, p_300066_); ++ var advancementdata = p_300066_.advancementData(); ++ if (advancementdata != null) { ++ var saveAdvancementFuture = saveAdvancement(p_254020_, advancementdata.id(), advancementdata.data(), p_300066_); + if (saveAdvancementFuture != null) + list.add(saveAdvancementFuture); } @@ -23,8 +28,8 @@ + * @return A completable future that saves the advancement to disk, or null to cancel saving the advancement. + */ + @Nullable -+ protected CompletableFuture saveAdvancement(CachedOutput output, JsonObject advancementJson, AdvancementHolder holder, FinishedRecipe finishedRecipe) { -+ return DataProvider.saveStable(output, advancementJson, this.advancementPathProvider.json(holder.id())); ++ protected CompletableFuture saveAdvancement(CachedOutput output, ResourceLocation advancementId, JsonObject advancementJson, FinishedRecipe finishedRecipe) { ++ return DataProvider.saveStable(output, advancementJson, this.advancementPathProvider.json(advancementId)); } protected CompletableFuture buildAdvancement(CachedOutput p_253674_, AdvancementHolder p_297687_) { diff --git a/patches/minecraft/net/minecraft/data/recipes/ShapedRecipeBuilder.java.patch b/patches/minecraft/net/minecraft/data/recipes/ShapedRecipeBuilder.java.patch new file mode 100644 index 0000000000..0a908e840d --- /dev/null +++ b/patches/minecraft/net/minecraft/data/recipes/ShapedRecipeBuilder.java.patch @@ -0,0 +1,18 @@ +--- a/net/minecraft/data/recipes/ShapedRecipeBuilder.java ++++ b/net/minecraft/data/recipes/ShapedRecipeBuilder.java +@@ -101,7 +_,14 @@ + this.ensureValid(p_126142_); + Advancement.Builder advancement$builder = p_298334_.advancement().addCriterion("has_the_recipe", RecipeUnlockedTrigger.unlocked(p_126142_)).rewards(AdvancementRewards.Builder.recipe(p_126142_)).requirements(AdvancementRequirements.Strategy.OR); + this.criteria.forEach(advancement$builder::addCriterion); +- p_298334_.accept(new ShapedRecipeBuilder.Result(p_126142_, this.result, this.count, this.group == null ? "" : this.group, determineBookCategory(this.category), this.rows, this.key, advancement$builder.build(p_126142_.withPrefix("recipes/" + this.category.getFolderName() + "/")), this.showNotification)); ++ p_298334_.accept(new ShapedRecipeBuilder.Result(p_126142_, this.result, this.count, this.group == null ? "" : this.group, determineBookCategory(this.category), this.rows, this.key, advancement$builder.build(p_126142_.withPrefix("recipes/" + this.category.getFolderName() + "/")), this.showNotification).withCondition(this.condition)); ++ } ++ ++ @Nullable ++ private net.minecraftforge.common.crafting.conditions.ICondition condition; ++ public ShapedRecipeBuilder condition(net.minecraftforge.common.crafting.conditions.ICondition condition) { ++ this.condition = condition; ++ return this; + } + + private void ensureValid(ResourceLocation p_126144_) { diff --git a/patches/minecraft/net/minecraft/data/recipes/ShapelessRecipeBuilder.java.patch b/patches/minecraft/net/minecraft/data/recipes/ShapelessRecipeBuilder.java.patch new file mode 100644 index 0000000000..d3779d933d --- /dev/null +++ b/patches/minecraft/net/minecraft/data/recipes/ShapelessRecipeBuilder.java.patch @@ -0,0 +1,18 @@ +--- a/net/minecraft/data/recipes/ShapelessRecipeBuilder.java ++++ b/net/minecraft/data/recipes/ShapelessRecipeBuilder.java +@@ -91,7 +_,14 @@ + this.ensureValid(p_126206_); + Advancement.Builder advancement$builder = p_300117_.advancement().addCriterion("has_the_recipe", RecipeUnlockedTrigger.unlocked(p_126206_)).rewards(AdvancementRewards.Builder.recipe(p_126206_)).requirements(AdvancementRequirements.Strategy.OR); + this.criteria.forEach(advancement$builder::addCriterion); +- p_300117_.accept(new ShapelessRecipeBuilder.Result(p_126206_, this.result, this.count, this.group == null ? "" : this.group, determineBookCategory(this.category), this.ingredients, advancement$builder.build(p_126206_.withPrefix("recipes/" + this.category.getFolderName() + "/")))); ++ p_300117_.accept(new ShapelessRecipeBuilder.Result(p_126206_, this.result, this.count, this.group == null ? "" : this.group, determineBookCategory(this.category), this.ingredients, advancement$builder.build(p_126206_.withPrefix("recipes/" + this.category.getFolderName() + "/"))).withCondition(this.condition)); ++ } ++ ++ @Nullable ++ private net.minecraftforge.common.crafting.conditions.ICondition condition; ++ public ShapelessRecipeBuilder condition(net.minecraftforge.common.crafting.conditions.ICondition condition) { ++ this.condition = condition; ++ return this; + } + + private void ensureValid(ResourceLocation p_126208_) { diff --git a/patches/minecraft/net/minecraft/data/recipes/SimpleCookingRecipeBuilder.java.patch b/patches/minecraft/net/minecraft/data/recipes/SimpleCookingRecipeBuilder.java.patch new file mode 100644 index 0000000000..97ac313e99 --- /dev/null +++ b/patches/minecraft/net/minecraft/data/recipes/SimpleCookingRecipeBuilder.java.patch @@ -0,0 +1,33 @@ +--- a/net/minecraft/data/recipes/SimpleCookingRecipeBuilder.java ++++ b/net/minecraft/data/recipes/SimpleCookingRecipeBuilder.java +@@ -80,7 +_,14 @@ + this.ensureValid(p_126264_); + Advancement.Builder advancement$builder = p_299448_.advancement().addCriterion("has_the_recipe", RecipeUnlockedTrigger.unlocked(p_126264_)).rewards(AdvancementRewards.Builder.recipe(p_126264_)).requirements(AdvancementRequirements.Strategy.OR); + this.criteria.forEach(advancement$builder::addCriterion); +- p_299448_.accept(new SimpleCookingRecipeBuilder.Result(p_126264_, this.group == null ? "" : this.group, this.bookCategory, this.ingredient, this.result, this.experience, this.cookingTime, advancement$builder.build(p_126264_.withPrefix("recipes/" + this.category.getFolderName() + "/")), this.serializer)); ++ p_299448_.accept(new SimpleCookingRecipeBuilder.Result(p_126264_, this.group == null ? "" : this.group, this.bookCategory, this.ingredient, this.result, this.experience, this.cookingTime, advancement$builder.build(p_126264_.withPrefix("recipes/" + this.category.getFolderName() + "/")), this.serializer, this.condition)); ++ } ++ ++ @Nullable ++ private net.minecraftforge.common.crafting.conditions.ICondition condition; ++ public SimpleCookingRecipeBuilder condition(net.minecraftforge.common.crafting.conditions.ICondition condition) { ++ this.condition = condition; ++ return this; + } + + private static CookingBookCategory determineSmeltingRecipeCategory(ItemLike p_251938_) { +@@ -113,8 +_,13 @@ + } + } + +- static record Result(ResourceLocation id, String group, CookingBookCategory category, Ingredient ingredient, Item result, float experience, int cookingTime, AdvancementHolder advancement, RecipeSerializer type) implements FinishedRecipe { ++ static record Result(ResourceLocation id, String group, CookingBookCategory category, Ingredient ingredient, Item result, float experience, int cookingTime, AdvancementHolder advancement, RecipeSerializer type, net.minecraftforge.common.crafting.conditions.ICondition condition) implements FinishedRecipe { ++ public Result(ResourceLocation id, String group, CookingBookCategory category, Ingredient ingredient, Item result, float experience, int cookingTime, AdvancementHolder advancement, RecipeSerializer type) { ++ this(id, group, category, ingredient, result, experience, cookingTime, advancement, type, null); ++ } ++ + public void serializeRecipeData(JsonObject p_126297_) { ++ net.minecraftforge.common.ForgeHooks.writeCondition(this.condition, p_126297_); + if (!this.group.isEmpty()) { + p_126297_.addProperty("group", this.group); + } diff --git a/patches/minecraft/net/minecraft/data/recipes/SingleItemRecipeBuilder.java.patch b/patches/minecraft/net/minecraft/data/recipes/SingleItemRecipeBuilder.java.patch new file mode 100644 index 0000000000..238ab41fc2 --- /dev/null +++ b/patches/minecraft/net/minecraft/data/recipes/SingleItemRecipeBuilder.java.patch @@ -0,0 +1,33 @@ +--- a/net/minecraft/data/recipes/SingleItemRecipeBuilder.java ++++ b/net/minecraft/data/recipes/SingleItemRecipeBuilder.java +@@ -61,7 +_,14 @@ + this.ensureValid(p_126328_); + Advancement.Builder advancement$builder = p_298439_.advancement().addCriterion("has_the_recipe", RecipeUnlockedTrigger.unlocked(p_126328_)).rewards(AdvancementRewards.Builder.recipe(p_126328_)).requirements(AdvancementRequirements.Strategy.OR); + this.criteria.forEach(advancement$builder::addCriterion); +- p_298439_.accept(new SingleItemRecipeBuilder.Result(p_126328_, this.type, this.group == null ? "" : this.group, this.ingredient, this.result, this.count, advancement$builder.build(p_126328_.withPrefix("recipes/" + this.category.getFolderName() + "/")))); ++ p_298439_.accept(new SingleItemRecipeBuilder.Result(p_126328_, this.type, this.group == null ? "" : this.group, this.ingredient, this.result, this.count, advancement$builder.build(p_126328_.withPrefix("recipes/" + this.category.getFolderName() + "/")), condition)); ++ } ++ ++ @Nullable ++ private net.minecraftforge.common.crafting.conditions.ICondition condition; ++ public SingleItemRecipeBuilder condition(net.minecraftforge.common.crafting.conditions.ICondition condition) { ++ this.condition = condition; ++ return this; + } + + private void ensureValid(ResourceLocation p_126330_) { +@@ -70,8 +_,13 @@ + } + } + +- public static record Result(ResourceLocation id, RecipeSerializer type, String group, Ingredient ingredient, Item result, int count, AdvancementHolder advancement) implements FinishedRecipe { ++ public static record Result(ResourceLocation id, RecipeSerializer type, String group, Ingredient ingredient, Item result, int count, AdvancementHolder advancement, net.minecraftforge.common.crafting.conditions.ICondition condition) implements FinishedRecipe { ++ Result(ResourceLocation id, RecipeSerializer type, String group, Ingredient ingredient, Item result, int count, AdvancementHolder advancement) { ++ this(id, type, group, ingredient, result, count, advancement, null); ++ } ++ + public void serializeRecipeData(JsonObject p_126349_) { ++ net.minecraftforge.common.ForgeHooks.writeCondition(this.condition, p_126349_); + if (!this.group.isEmpty()) { + p_126349_.addProperty("group", this.group); + } diff --git a/patches/minecraft/net/minecraft/world/item/crafting/RecipeManager.java.patch b/patches/minecraft/net/minecraft/world/item/crafting/RecipeManager.java.patch index 9533b3ca67..0480f06fa8 100644 --- a/patches/minecraft/net/minecraft/world/item/crafting/RecipeManager.java.patch +++ b/patches/minecraft/net/minecraft/world/item/crafting/RecipeManager.java.patch @@ -18,22 +18,17 @@ } protected void apply(Map p_44037_, ResourceManager p_44038_, ProfilerFiller p_44039_) { -@@ -52,9 +_,18 @@ +@@ -52,8 +_,13 @@ for(Map.Entry entry : p_44037_.entrySet()) { ResourceLocation resourcelocation = entry.getKey(); + if (resourcelocation.getPath().startsWith("_")) continue; //Forge: filter anything beginning with "_" as it's used for metadata. try { -+ if (entry.getValue().isJsonObject() && !net.minecraftforge.common.crafting.CraftingHelper.processConditions(entry.getValue().getAsJsonObject(), "conditions", this.context)) { ++ if (entry.getValue().isJsonObject() && !net.minecraftforge.common.ForgeHooks.readAndTestCondition(this.context, entry.getValue().getAsJsonObject())) { + LOGGER.debug("Skipping loading recipe {} as it's conditions were not met", resourcelocation); + continue; + } RecipeHolder recipeholder = fromJson(resourcelocation, GsonHelper.convertToJsonObject(entry.getValue(), "top element")); -+ if (recipeholder == null) { -+ LOGGER.info("Skipping loading recipe {} as it's serializer returned null", resourcelocation); -+ continue; -+ } map.computeIfAbsent(recipeholder.value().getType(), (p_44075_) -> { return ImmutableMap.builder(); - }).put(resourcelocation, recipeholder); diff --git a/src/generated_test/resources/conditional_recipe/data/conditional_recipe/advancements/recipes/conditional_doesnt_load_empty.json b/src/generated_test/resources/conditional_recipe/data/conditional_recipe/advancements/recipes/conditional_doesnt_load_empty.json new file mode 100644 index 0000000000..714ca55a04 --- /dev/null +++ b/src/generated_test/resources/conditional_recipe/data/conditional_recipe/advancements/recipes/conditional_doesnt_load_empty.json @@ -0,0 +1,43 @@ +{ + "advancements": [ + { + "parent": "minecraft:recipes/root", + "criteria": { + "has_dirt": { + "conditions": { + "items": [ + { + "items": [ + "minecraft:dirt" + ] + } + ] + }, + "trigger": "minecraft:inventory_changed" + }, + "has_the_recipe": { + "conditions": { + "recipe": "minecraft:diamond_block" + }, + "trigger": "minecraft:recipe_unlocked" + } + }, + "forge:condition": { + "type": "forge:false", + "value": {} + }, + "requirements": [ + [ + "has_the_recipe", + "has_dirt" + ] + ], + "rewards": { + "recipes": [ + "minecraft:diamond_block" + ] + }, + "sends_telemetry_event": false + } + ] +} \ No newline at end of file diff --git a/src/generated_test/resources/conditional_recipe/data/conditional_recipe/advancements/recipes/conditional_recipe_choice.json b/src/generated_test/resources/conditional_recipe/data/conditional_recipe/advancements/recipes/conditional_recipe_choice.json new file mode 100644 index 0000000000..28800a22e9 --- /dev/null +++ b/src/generated_test/resources/conditional_recipe/data/conditional_recipe/advancements/recipes/conditional_recipe_choice.json @@ -0,0 +1,82 @@ +{ + "advancements": [ + { + "parent": "minecraft:recipes/root", + "criteria": { + "has_dirt": { + "conditions": { + "items": [ + { + "items": [ + "minecraft:dirt" + ] + } + ] + }, + "trigger": "minecraft:inventory_changed" + }, + "has_the_recipe": { + "conditions": { + "recipe": "minecraft:diamond_block" + }, + "trigger": "minecraft:recipe_unlocked" + } + }, + "forge:condition": { + "type": "forge:false", + "value": {} + }, + "requirements": [ + [ + "has_the_recipe", + "has_dirt" + ] + ], + "rewards": { + "recipes": [ + "minecraft:diamond_block" + ] + }, + "sends_telemetry_event": false + }, + { + "parent": "minecraft:recipes/root", + "criteria": { + "has_oak_log": { + "conditions": { + "items": [ + { + "items": [ + "minecraft:oak_log" + ] + } + ] + }, + "trigger": "minecraft:inventory_changed" + }, + "has_the_recipe": { + "conditions": { + "recipe": "minecraft:diamond_block" + }, + "trigger": "minecraft:recipe_unlocked" + } + }, + "forge:condition": { + "type": "forge:true", + "value": {} + }, + "requirements": [ + [ + "has_the_recipe", + "has_oak_log" + ] + ], + "rewards": { + "recipes": [ + "minecraft:diamond_block" + ] + }, + "sends_telemetry_event": false + } + ] +} \ No newline at end of file diff --git a/src/generated_test/resources/conditional_recipe/data/conditional_recipe/advancements/recipes/misc/cooking_false_conditions.json b/src/generated_test/resources/conditional_recipe/data/conditional_recipe/advancements/recipes/misc/cooking_false_conditions.json new file mode 100644 index 0000000000..c4d41a0a22 --- /dev/null +++ b/src/generated_test/resources/conditional_recipe/data/conditional_recipe/advancements/recipes/misc/cooking_false_conditions.json @@ -0,0 +1,35 @@ +{ + "parent": "minecraft:recipes/root", + "criteria": { + "has_dirt": { + "conditions": { + "items": [ + { + "items": [ + "minecraft:dirt" + ] + } + ] + }, + "trigger": "minecraft:inventory_changed" + }, + "has_the_recipe": { + "conditions": { + "recipe": "conditional_recipe:cooking_false_conditions" + }, + "trigger": "minecraft:recipe_unlocked" + } + }, + "requirements": [ + [ + "has_the_recipe", + "has_dirt" + ] + ], + "rewards": { + "recipes": [ + "conditional_recipe:cooking_false_conditions" + ] + }, + "sends_telemetry_event": false +} \ No newline at end of file diff --git a/src/generated_test/resources/conditional_recipe/data/conditional_recipe/advancements/recipes/misc/cooking_true_conditions.json b/src/generated_test/resources/conditional_recipe/data/conditional_recipe/advancements/recipes/misc/cooking_true_conditions.json new file mode 100644 index 0000000000..ff89ae85e5 --- /dev/null +++ b/src/generated_test/resources/conditional_recipe/data/conditional_recipe/advancements/recipes/misc/cooking_true_conditions.json @@ -0,0 +1,35 @@ +{ + "parent": "minecraft:recipes/root", + "criteria": { + "has_bee_nest": { + "conditions": { + "items": [ + { + "items": [ + "minecraft:bee_nest" + ] + } + ] + }, + "trigger": "minecraft:inventory_changed" + }, + "has_the_recipe": { + "conditions": { + "recipe": "conditional_recipe:cooking_true_conditions" + }, + "trigger": "minecraft:recipe_unlocked" + } + }, + "requirements": [ + [ + "has_the_recipe", + "has_bee_nest" + ] + ], + "rewards": { + "recipes": [ + "conditional_recipe:cooking_true_conditions" + ] + }, + "sends_telemetry_event": false +} \ No newline at end of file diff --git a/src/generated_test/resources/conditional_recipe/data/conditional_recipe/advancements/recipes/misc/shaped_false_conditions.json b/src/generated_test/resources/conditional_recipe/data/conditional_recipe/advancements/recipes/misc/shaped_false_conditions.json new file mode 100644 index 0000000000..75de699e90 --- /dev/null +++ b/src/generated_test/resources/conditional_recipe/data/conditional_recipe/advancements/recipes/misc/shaped_false_conditions.json @@ -0,0 +1,35 @@ +{ + "parent": "minecraft:recipes/root", + "criteria": { + "has_dirt": { + "conditions": { + "items": [ + { + "items": [ + "minecraft:dirt" + ] + } + ] + }, + "trigger": "minecraft:inventory_changed" + }, + "has_the_recipe": { + "conditions": { + "recipe": "conditional_recipe:shaped_false_conditions" + }, + "trigger": "minecraft:recipe_unlocked" + } + }, + "requirements": [ + [ + "has_the_recipe", + "has_dirt" + ] + ], + "rewards": { + "recipes": [ + "conditional_recipe:shaped_false_conditions" + ] + }, + "sends_telemetry_event": false +} \ No newline at end of file diff --git a/src/generated_test/resources/conditional_recipe/data/conditional_recipe/advancements/recipes/misc/shaped_true_conditions.json b/src/generated_test/resources/conditional_recipe/data/conditional_recipe/advancements/recipes/misc/shaped_true_conditions.json new file mode 100644 index 0000000000..9f03d0d081 --- /dev/null +++ b/src/generated_test/resources/conditional_recipe/data/conditional_recipe/advancements/recipes/misc/shaped_true_conditions.json @@ -0,0 +1,35 @@ +{ + "parent": "minecraft:recipes/root", + "criteria": { + "has_oak_log": { + "conditions": { + "items": [ + { + "items": [ + "minecraft:oak_log" + ] + } + ] + }, + "trigger": "minecraft:inventory_changed" + }, + "has_the_recipe": { + "conditions": { + "recipe": "conditional_recipe:shaped_true_conditions" + }, + "trigger": "minecraft:recipe_unlocked" + } + }, + "requirements": [ + [ + "has_the_recipe", + "has_oak_log" + ] + ], + "rewards": { + "recipes": [ + "conditional_recipe:shaped_true_conditions" + ] + }, + "sends_telemetry_event": false +} \ No newline at end of file diff --git a/src/generated_test/resources/conditional_recipe/data/conditional_recipe/advancements/recipes/misc/shapeless_false_conditions.json b/src/generated_test/resources/conditional_recipe/data/conditional_recipe/advancements/recipes/misc/shapeless_false_conditions.json new file mode 100644 index 0000000000..6660d7623d --- /dev/null +++ b/src/generated_test/resources/conditional_recipe/data/conditional_recipe/advancements/recipes/misc/shapeless_false_conditions.json @@ -0,0 +1,35 @@ +{ + "parent": "minecraft:recipes/root", + "criteria": { + "has_redstone_ore": { + "conditions": { + "items": [ + { + "items": [ + "minecraft:redstone_ore" + ] + } + ] + }, + "trigger": "minecraft:inventory_changed" + }, + "has_the_recipe": { + "conditions": { + "recipe": "conditional_recipe:shapeless_false_conditions" + }, + "trigger": "minecraft:recipe_unlocked" + } + }, + "requirements": [ + [ + "has_the_recipe", + "has_redstone_ore" + ] + ], + "rewards": { + "recipes": [ + "conditional_recipe:shapeless_false_conditions" + ] + }, + "sends_telemetry_event": false +} \ No newline at end of file diff --git a/src/generated_test/resources/conditional_recipe/data/conditional_recipe/advancements/recipes/misc/shapeless_true_conditions.json b/src/generated_test/resources/conditional_recipe/data/conditional_recipe/advancements/recipes/misc/shapeless_true_conditions.json new file mode 100644 index 0000000000..59ecfd6935 --- /dev/null +++ b/src/generated_test/resources/conditional_recipe/data/conditional_recipe/advancements/recipes/misc/shapeless_true_conditions.json @@ -0,0 +1,35 @@ +{ + "parent": "minecraft:recipes/root", + "criteria": { + "has_redstone_ore": { + "conditions": { + "items": [ + { + "items": [ + "minecraft:redstone_ore" + ] + } + ] + }, + "trigger": "minecraft:inventory_changed" + }, + "has_the_recipe": { + "conditions": { + "recipe": "conditional_recipe:shapeless_true_conditions" + }, + "trigger": "minecraft:recipe_unlocked" + } + }, + "requirements": [ + [ + "has_the_recipe", + "has_redstone_ore" + ] + ], + "rewards": { + "recipes": [ + "conditional_recipe:shapeless_true_conditions" + ] + }, + "sends_telemetry_event": false +} \ No newline at end of file diff --git a/src/generated_test/resources/conditional_recipe/data/conditional_recipe/advancements/recipes/misc/singe_item_true_conditions.json b/src/generated_test/resources/conditional_recipe/data/conditional_recipe/advancements/recipes/misc/singe_item_true_conditions.json new file mode 100644 index 0000000000..558a75f697 --- /dev/null +++ b/src/generated_test/resources/conditional_recipe/data/conditional_recipe/advancements/recipes/misc/singe_item_true_conditions.json @@ -0,0 +1,35 @@ +{ + "parent": "minecraft:recipes/root", + "criteria": { + "has_redstone_ore": { + "conditions": { + "items": [ + { + "items": [ + "minecraft:redstone_ore" + ] + } + ] + }, + "trigger": "minecraft:inventory_changed" + }, + "has_the_recipe": { + "conditions": { + "recipe": "conditional_recipe:singe_item_true_conditions" + }, + "trigger": "minecraft:recipe_unlocked" + } + }, + "requirements": [ + [ + "has_the_recipe", + "has_redstone_ore" + ] + ], + "rewards": { + "recipes": [ + "conditional_recipe:singe_item_true_conditions" + ] + }, + "sends_telemetry_event": false +} \ No newline at end of file diff --git a/src/generated_test/resources/conditional_recipe/data/conditional_recipe/advancements/recipes/misc/single_item_false_conditions.json b/src/generated_test/resources/conditional_recipe/data/conditional_recipe/advancements/recipes/misc/single_item_false_conditions.json new file mode 100644 index 0000000000..7baba2ba90 --- /dev/null +++ b/src/generated_test/resources/conditional_recipe/data/conditional_recipe/advancements/recipes/misc/single_item_false_conditions.json @@ -0,0 +1,35 @@ +{ + "parent": "minecraft:recipes/root", + "criteria": { + "has_dirt": { + "conditions": { + "items": [ + { + "items": [ + "minecraft:dirt" + ] + } + ] + }, + "trigger": "minecraft:inventory_changed" + }, + "has_the_recipe": { + "conditions": { + "recipe": "conditional_recipe:single_item_false_conditions" + }, + "trigger": "minecraft:recipe_unlocked" + } + }, + "requirements": [ + [ + "has_the_recipe", + "has_dirt" + ] + ], + "rewards": { + "recipes": [ + "conditional_recipe:single_item_false_conditions" + ] + }, + "sends_telemetry_event": false +} \ No newline at end of file diff --git a/src/generated_test/resources/conditional_recipe/data/conditional_recipe/advancements/recipes/misc/test_encode_all_conditions.json b/src/generated_test/resources/conditional_recipe/data/conditional_recipe/advancements/recipes/misc/test_encode_all_conditions.json new file mode 100644 index 0000000000..453eb36ee9 --- /dev/null +++ b/src/generated_test/resources/conditional_recipe/data/conditional_recipe/advancements/recipes/misc/test_encode_all_conditions.json @@ -0,0 +1,35 @@ +{ + "parent": "minecraft:recipes/root", + "criteria": { + "has_dirt": { + "conditions": { + "items": [ + { + "items": [ + "minecraft:dirt" + ] + } + ] + }, + "trigger": "minecraft:inventory_changed" + }, + "has_the_recipe": { + "conditions": { + "recipe": "conditional_recipe:test_encode_all_conditions" + }, + "trigger": "minecraft:recipe_unlocked" + } + }, + "requirements": [ + [ + "has_the_recipe", + "has_dirt" + ] + ], + "rewards": { + "recipes": [ + "conditional_recipe:test_encode_all_conditions" + ] + }, + "sends_telemetry_event": false +} \ No newline at end of file diff --git a/src/generated_test/resources/conditional_recipe/data/conditional_recipe/recipes/conditional_doesnt_load_empty.json b/src/generated_test/resources/conditional_recipe/data/conditional_recipe/recipes/conditional_doesnt_load_empty.json new file mode 100644 index 0000000000..d90ea07e5e --- /dev/null +++ b/src/generated_test/resources/conditional_recipe/data/conditional_recipe/recipes/conditional_doesnt_load_empty.json @@ -0,0 +1,26 @@ +{ + "type": "forge:conditional", + "forge:condition": { + "type": "forge:false", + "value": {} + }, + "recipes": [ + { + "type": "minecraft:crafting_shapeless", + "category": "misc", + "forge:condition": { + "type": "forge:false", + "value": {} + }, + "ingredients": [ + { + "item": "minecraft:dirt" + } + ], + "result": { + "count": 64, + "item": "minecraft:diamond_block" + } + } + ] +} \ No newline at end of file diff --git a/src/generated_test/resources/conditional_recipe/data/conditional_recipe/recipes/conditional_recipe_choice.json b/src/generated_test/resources/conditional_recipe/data/conditional_recipe/recipes/conditional_recipe_choice.json new file mode 100644 index 0000000000..c18f93b244 --- /dev/null +++ b/src/generated_test/resources/conditional_recipe/data/conditional_recipe/recipes/conditional_recipe_choice.json @@ -0,0 +1,49 @@ +{ + "type": "forge:conditional", + "recipes": [ + { + "type": "minecraft:crafting_shaped", + "category": "misc", + "forge:condition": { + "type": "forge:false", + "value": {} + }, + "key": { + "X": { + "item": "minecraft:dirt" + } + }, + "pattern": [ + "XXX", + "XX " + ], + "result": { + "count": 64, + "item": "minecraft:diamond_block" + }, + "show_notification": true + }, + { + "type": "minecraft:crafting_shaped", + "category": "misc", + "forge:condition": { + "type": "forge:true", + "value": {} + }, + "key": { + "X": { + "item": "minecraft:oak_log" + } + }, + "pattern": [ + "XXX", + "XX " + ], + "result": { + "count": 64, + "item": "minecraft:diamond_block" + }, + "show_notification": true + } + ] +} \ No newline at end of file diff --git a/src/generated_test/resources/conditional_recipe/data/conditional_recipe/recipes/cooking_false_conditions.json b/src/generated_test/resources/conditional_recipe/data/conditional_recipe/recipes/cooking_false_conditions.json new file mode 100644 index 0000000000..52dda96838 --- /dev/null +++ b/src/generated_test/resources/conditional_recipe/data/conditional_recipe/recipes/cooking_false_conditions.json @@ -0,0 +1,14 @@ +{ + "type": "minecraft:smelting", + "category": "blocks", + "cookingtime": 200, + "experience": 0.1, + "forge:condition": { + "type": "forge:false", + "value": {} + }, + "ingredient": { + "item": "minecraft:dirt" + }, + "result": "minecraft:diamond_block" +} \ No newline at end of file diff --git a/src/generated_test/resources/conditional_recipe/data/conditional_recipe/recipes/cooking_true_conditions.json b/src/generated_test/resources/conditional_recipe/data/conditional_recipe/recipes/cooking_true_conditions.json new file mode 100644 index 0000000000..9c9fb75101 --- /dev/null +++ b/src/generated_test/resources/conditional_recipe/data/conditional_recipe/recipes/cooking_true_conditions.json @@ -0,0 +1,14 @@ +{ + "type": "minecraft:smelting", + "category": "blocks", + "cookingtime": 200, + "experience": 0.1, + "forge:condition": { + "type": "forge:true", + "value": {} + }, + "ingredient": { + "item": "minecraft:bee_nest" + }, + "result": "minecraft:diamond_block" +} \ No newline at end of file diff --git a/src/generated_test/resources/conditional_recipe/data/conditional_recipe/recipes/shaped_false_conditions.json b/src/generated_test/resources/conditional_recipe/data/conditional_recipe/recipes/shaped_false_conditions.json new file mode 100644 index 0000000000..74252a0943 --- /dev/null +++ b/src/generated_test/resources/conditional_recipe/data/conditional_recipe/recipes/shaped_false_conditions.json @@ -0,0 +1,23 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "misc", + "forge:condition": { + "type": "forge:false", + "value": {} + }, + "key": { + "X": { + "item": "minecraft:dirt" + } + }, + "pattern": [ + "XXX", + "XXX", + "XXX" + ], + "result": { + "count": 64, + "item": "minecraft:diamond_block" + }, + "show_notification": true +} \ No newline at end of file diff --git a/src/generated_test/resources/conditional_recipe/data/conditional_recipe/recipes/shaped_true_conditions.json b/src/generated_test/resources/conditional_recipe/data/conditional_recipe/recipes/shaped_true_conditions.json new file mode 100644 index 0000000000..670efe05b1 --- /dev/null +++ b/src/generated_test/resources/conditional_recipe/data/conditional_recipe/recipes/shaped_true_conditions.json @@ -0,0 +1,23 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "misc", + "forge:condition": { + "type": "forge:true", + "value": {} + }, + "key": { + "X": { + "item": "minecraft:oak_log" + } + }, + "pattern": [ + "XXX", + "XXX", + "XXX" + ], + "result": { + "count": 64, + "item": "minecraft:diamond_block" + }, + "show_notification": true +} \ No newline at end of file diff --git a/src/generated_test/resources/conditional_recipe/data/conditional_recipe/recipes/shapeless_false_conditions.json b/src/generated_test/resources/conditional_recipe/data/conditional_recipe/recipes/shapeless_false_conditions.json new file mode 100644 index 0000000000..3935b8f0cc --- /dev/null +++ b/src/generated_test/resources/conditional_recipe/data/conditional_recipe/recipes/shapeless_false_conditions.json @@ -0,0 +1,22 @@ +{ + "type": "minecraft:crafting_shapeless", + "category": "misc", + "forge:condition": { + "type": "forge:false", + "value": {} + }, + "ingredients": [ + { + "item": "minecraft:redstone_ore" + }, + { + "item": "minecraft:redstone_ore" + }, + { + "item": "minecraft:redstone_ore" + } + ], + "result": { + "item": "minecraft:diamond_block" + } +} \ No newline at end of file diff --git a/src/generated_test/resources/conditional_recipe/data/conditional_recipe/recipes/shapeless_true_conditions.json b/src/generated_test/resources/conditional_recipe/data/conditional_recipe/recipes/shapeless_true_conditions.json new file mode 100644 index 0000000000..ab9ff4f848 --- /dev/null +++ b/src/generated_test/resources/conditional_recipe/data/conditional_recipe/recipes/shapeless_true_conditions.json @@ -0,0 +1,25 @@ +{ + "type": "minecraft:crafting_shapeless", + "category": "misc", + "forge:condition": { + "type": "forge:true", + "value": {} + }, + "ingredients": [ + { + "item": "minecraft:redstone_ore" + }, + { + "item": "minecraft:redstone_ore" + }, + { + "item": "minecraft:redstone_ore" + }, + { + "item": "minecraft:redstone_ore" + } + ], + "result": { + "item": "minecraft:diamond_block" + } +} \ No newline at end of file diff --git a/src/generated_test/resources/conditional_recipe/data/conditional_recipe/recipes/singe_item_true_conditions.json b/src/generated_test/resources/conditional_recipe/data/conditional_recipe/recipes/singe_item_true_conditions.json new file mode 100644 index 0000000000..1428baec93 --- /dev/null +++ b/src/generated_test/resources/conditional_recipe/data/conditional_recipe/recipes/singe_item_true_conditions.json @@ -0,0 +1,12 @@ +{ + "type": "minecraft:stonecutting", + "count": 1, + "forge:condition": { + "type": "forge:true", + "value": {} + }, + "ingredient": { + "item": "minecraft:redstone_ore" + }, + "result": "minecraft:diamond_block" +} \ No newline at end of file diff --git a/src/generated_test/resources/conditional_recipe/data/conditional_recipe/recipes/single_item_false_conditions.json b/src/generated_test/resources/conditional_recipe/data/conditional_recipe/recipes/single_item_false_conditions.json new file mode 100644 index 0000000000..5137643c75 --- /dev/null +++ b/src/generated_test/resources/conditional_recipe/data/conditional_recipe/recipes/single_item_false_conditions.json @@ -0,0 +1,12 @@ +{ + "type": "minecraft:stonecutting", + "count": 1, + "forge:condition": { + "type": "forge:false", + "value": {} + }, + "ingredient": { + "item": "minecraft:dirt" + }, + "result": "minecraft:diamond_block" +} \ No newline at end of file diff --git a/src/generated_test/resources/conditional_recipe/data/conditional_recipe/recipes/test_encode_all_conditions.json b/src/generated_test/resources/conditional_recipe/data/conditional_recipe/recipes/test_encode_all_conditions.json new file mode 100644 index 0000000000..0b9a70dc8d --- /dev/null +++ b/src/generated_test/resources/conditional_recipe/data/conditional_recipe/recipes/test_encode_all_conditions.json @@ -0,0 +1,52 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "misc", + "forge:condition": { + "type": "forge:and", + "values": [ + { + "type": "forge:or", + "values": [ + { + "type": "forge:not", + "value": { + "type": "forge:mod_loaded", + "modid": "minecraft" + } + }, + { + "type": "forge:item_exists", + "item": "minecraft:dirt" + }, + { + "type": "forge:true", + "value": {} + }, + { + "type": "forge:tag_empty", + "tag": "minecraft:dirt" + } + ] + }, + { + "type": "forge:false", + "value": {} + } + ] + }, + "key": { + "X": { + "item": "minecraft:dirt" + } + }, + "pattern": [ + "XXX", + "XXX", + "XXX" + ], + "result": { + "count": 64, + "item": "minecraft:diamond_block" + }, + "show_notification": true +} \ No newline at end of file diff --git a/src/generated_test/resources/empty/pack.mcmeta b/src/generated_test/resources/test_helper_mod/pack.mcmeta similarity index 100% rename from src/generated_test/resources/empty/pack.mcmeta rename to src/generated_test/resources/test_helper_mod/pack.mcmeta diff --git a/src/main/java/net/minecraftforge/common/ForgeHooks.java b/src/main/java/net/minecraftforge/common/ForgeHooks.java index 97fed331b3..6f11f62fea 100644 --- a/src/main/java/net/minecraftforge/common/ForgeHooks.java +++ b/src/main/java/net/minecraftforge/common/ForgeHooks.java @@ -22,12 +22,16 @@ import java.util.stream.Stream; import com.google.common.collect.ImmutableMap; import com.google.common.collect.Lists; import com.google.common.collect.Multimap; +import com.google.gson.JsonObject; +import com.google.gson.JsonParseException; +import com.google.gson.JsonSyntaxException; import com.mojang.datafixers.util.Pair; import com.mojang.serialization.Codec; import com.mojang.serialization.DataResult; import com.mojang.serialization.Decoder; import com.mojang.serialization.Dynamic; import com.mojang.serialization.DynamicOps; +import com.mojang.serialization.JsonOps; import com.mojang.serialization.Lifecycle; import it.unimi.dsi.fastutil.objects.ObjectArrayList; @@ -51,6 +55,7 @@ import net.minecraft.server.level.ServerLevel; import net.minecraft.tags.BlockTags; import net.minecraft.tags.TagKey; import net.minecraft.util.CrudeIncrementalIntIdentityHashBiMap; +import net.minecraft.util.GsonHelper; import net.minecraft.util.Mth; import net.minecraft.util.datafix.fixes.StructuresBecomeConfiguredFix; import net.minecraft.world.Difficulty; @@ -112,6 +117,7 @@ import net.minecraft.world.level.biome.Biome; import net.minecraft.world.level.biome.BiomeSpecialEffects; import net.minecraft.world.level.biome.BiomeGenerationSettings; import net.minecraft.world.level.biome.MobSpawnSettings; +import net.minecraftforge.common.crafting.conditions.ICondition; import net.minecraftforge.common.util.BlockSnapshot; import net.minecraftforge.common.util.BrainBuilder; import net.minecraftforge.common.util.Lazy; @@ -185,6 +191,7 @@ import org.apache.logging.log4j.Marker; import org.apache.logging.log4j.MarkerManager; import net.minecraft.ChatFormatting; +import net.minecraft.Util; import net.minecraft.resources.ResourceKey; import net.minecraft.resources.ResourceLocation; import net.minecraft.world.damagesource.DamageSource; @@ -1268,4 +1275,42 @@ public class ForgeHooks { var pkt = (Packet)NetworkDirection.PLAY_TO_CLIENT.buildPacket(data, play.getName()).getThis(); return pkt; } + + @ApiStatus.Internal + public static boolean readAndTestCondition(ICondition.IContext context, JsonObject json) { + if (!json.has(ICondition.DEFAULT_FIELD)) + return true; + + var condition = Util.getOrThrow(ICondition.SAFE_CODEC.parse(JsonOps.INSTANCE, json.getAsJsonObject(ICondition.DEFAULT_FIELD)), JsonParseException::new); + return condition.test(context); + } + + @ApiStatus.Internal + public static void writeCondition(ICondition condition, JsonObject out) { + if (condition == null) + return; + var data = ICondition.CODEC.encode(condition, JsonOps.INSTANCE, JsonOps.INSTANCE.empty()).getOrThrow(false, JsonSyntaxException::new); + out.add(ICondition.DEFAULT_FIELD, data); + } + + @Nullable + @ApiStatus.Internal + public static JsonObject readConditionalAdvancement(ICondition.IContext context, JsonObject json) { + var entries = GsonHelper.getAsJsonArray(json, "advancements", null); + if (entries == null) + return readAndTestCondition(context, json) ? json : null; + + int idx = 0; + for (var ele : entries) { + if (!ele.isJsonObject()) + throw new JsonSyntaxException("Invalid advancement entry at index " + idx + " Must be JsonObject"); + + if (readAndTestCondition(context, ele.getAsJsonObject())) + return GsonHelper.getAsJsonObject(ele.getAsJsonObject(), "advancement"); + + idx++; + } + + return null; + } } \ No newline at end of file diff --git a/src/main/java/net/minecraftforge/common/ForgeMod.java b/src/main/java/net/minecraftforge/common/ForgeMod.java index 5db01feadd..f027467780 100644 --- a/src/main/java/net/minecraftforge/common/ForgeMod.java +++ b/src/main/java/net/minecraftforge/common/ForgeMod.java @@ -28,6 +28,7 @@ import net.minecraft.world.entity.ai.attributes.RangedAttribute; import net.minecraft.world.entity.item.ItemEntity; import net.minecraft.world.item.ItemDisplayContext; import net.minecraft.world.item.crafting.Ingredient; +import net.minecraft.world.item.crafting.RecipeSerializer; import net.minecraft.world.level.BlockAndTintGetter; import net.minecraft.world.level.BlockGetter; import net.minecraft.world.level.biome.Biome; @@ -93,6 +94,7 @@ import org.apache.logging.log4j.Logger; import net.minecraft.data.DataGenerator; import net.minecraft.resources.ResourceLocation; +import net.minecraftforge.common.crafting.ConditionalRecipe; import net.minecraftforge.common.crafting.conditions.AndCondition; import net.minecraftforge.common.crafting.conditions.FalseCondition; import net.minecraftforge.common.crafting.conditions.ICondition; @@ -390,6 +392,11 @@ public class ForgeMod { CONDITION_SERIALIZERS.register("tag_empty", () -> TagEmptyCondition.CODEC); } + private static final DeferredRegister> RECIPE_SERIALIZERS = DeferredRegister.create(ForgeRegistries.Keys.RECIPE_SERIALIZERS, "forge"); + static { + RECIPE_SERIALIZERS.register("conditional", () -> ConditionalRecipe.SERIALZIER); + } + private static boolean enableMilkFluid = false; public static final RegistryObject BUCKET_EMPTY_MILK = RegistryObject.create(new ResourceLocation("item.bucket.empty_milk"), ForgeRegistries.SOUND_EVENTS); @@ -447,6 +454,7 @@ public class ForgeMod { VANILLA_FLUID_TYPES.register(modEventBus); LOOT_CONDITION_TYPES.register(modEventBus); CONDITION_SERIALIZERS.register(modEventBus); + RECIPE_SERIALIZERS.register(modEventBus); MinecraftForge.EVENT_BUS.addListener(this::serverStopping); ModLoadingContext.get().registerConfig(ModConfig.Type.CLIENT, ForgeConfig.clientSpec); ModLoadingContext.get().registerConfig(ModConfig.Type.SERVER, ForgeConfig.serverSpec); diff --git a/src/main/java/net/minecraftforge/common/crafting/ConditionalAdvancement.java b/src/main/java/net/minecraftforge/common/crafting/ConditionalAdvancement.java index bdd93903ae..c0f126d351 100644 --- a/src/main/java/net/minecraftforge/common/crafting/ConditionalAdvancement.java +++ b/src/main/java/net/minecraftforge/common/crafting/ConditionalAdvancement.java @@ -4,123 +4,113 @@ */ package net.minecraftforge.common.crafting; -/* + import java.util.ArrayList; import java.util.List; import java.util.function.Consumer; -import java.util.function.Supplier; - import com.google.gson.JsonArray; -import com.google.gson.JsonElement; import com.google.gson.JsonObject; -import com.google.gson.JsonSyntaxException; - import net.minecraft.advancements.Advancement; +import net.minecraft.advancements.AdvancementHolder; import net.minecraft.data.recipes.FinishedRecipe; -import net.minecraft.util.GsonHelper; +import net.minecraft.resources.ResourceLocation; +import net.minecraftforge.common.ForgeHooks; import net.minecraftforge.common.crafting.conditions.ICondition; -import org.jetbrains.annotations.Nullable; -public class ConditionalAdvancement -{ - public static Builder builder() - { +/** + * A `ConditionalAdvancement` is a single advancment file that contains multiple advancements, each having a condition. + * When loaded it will return the first advancement that the conditions pass. + * + * This allows for multiple variants of an advancement to share the same name in the registry. Which allows dependents + * to reference it without having to care about the conditions themselves. + * + * This is most likely useful when you have variants of a recipe based on what mods/resources are installed but want + * to maintain the same 'entry' in the advancement book. + */ +public class ConditionalAdvancement { + public static Builder builder() { return new Builder(); } - /** - * Processes the conditional advancement during loading. - * @param json The incoming json from the advancement file. - * @return The advancement that passed the conditions, or null if none did. - * / - @Nullable - public static JsonObject processConditional(JsonObject json, ICondition.IContext context) { - JsonArray entries = GsonHelper.getAsJsonArray(json, "advancements", null); - if (entries == null) - { - return CraftingHelper.processConditions(json, "conditions", context) ? json : null; + public static class Builder { + private static final ResourceLocation DOESNT_MATTER = new ResourceLocation("doesnt", "matter"); + + private List advancements = new ArrayList<>(); + private ICondition condition; + + public Builder condition(ICondition value) { + this.condition = value; + return this; } - int idx = 0; - for (JsonElement ele : entries) - { - if (!ele.isJsonObject()) - throw new JsonSyntaxException("Invalid advancement entry at index " + idx + " Must be JsonObject"); - if (CraftingHelper.processConditions(GsonHelper.getAsJsonArray(ele.getAsJsonObject(), "conditions"), context)) - return GsonHelper.getAsJsonObject(ele.getAsJsonObject(), "advancement"); - idx++; + public Builder advancement(Consumer> callable) { + callable.accept(this::advancement); + return this; + } + + public Builder advancement(Advancement.Builder builder) { + return advancement(builder.build(DOESNT_MATTER).value()); + } + + public Builder advancement(FinishedRecipe fromRecipe) { + return advancement(fromRecipe.advancement()); + } + + public Builder advancement(AdvancementHolder holder) { + return advancement(holder.value()); + } + + private Builder advancement(Advancement value) { + if (condition == null) + throw new IllegalStateException("Can not add a advancement with no conditions."); + + if (value == null) + throw new IllegalStateException("Can not add a null advancement"); + + this.advancements.add(new Pair(this.condition, value)); + this.condition = null; + + return this; + } + + public IResult build() { + return new Result(List.copyOf(advancements)); + } + + public JsonObject write() { + var json = new JsonObject(); + var array = new JsonArray(); + json.add("advancements", array); + + for (var pair : advancements) { + var holder = pair.adv().serializeToJson(); + if (holder.has(ICondition.DEFAULT_FIELD)) + throw new IllegalStateException("Recipe already serialized conditions!"); + ForgeHooks.writeCondition(pair.condition(), holder); + + array.add(holder); + } + return json; } - return null; } - public static class Builder - { - private List conditions = new ArrayList<>(); - private List> advancements = new ArrayList<>(); + public interface IResult { + JsonObject serializeToJson(); + } - private List currentConditions = new ArrayList<>(); - private boolean locked = false; - - public Builder addCondition(ICondition condition) - { - if (locked) - throw new IllegalStateException("Attempted to modify finished builder"); - currentConditions.add(condition); - return this; - } - - public Builder addAdvancement(Consumer> callable) - { - if (locked) - throw new IllegalStateException("Attempted to modify finished builder"); - callable.accept(this::addAdvancement); - return this; - } - - public Builder addAdvancement(Advancement.Builder advancement) - { - return addAdvancement(advancement::serializeToJson); - } - - public Builder addAdvancement(FinishedRecipe fromRecipe) - { - return addAdvancement(fromRecipe::serializeAdvancement); - } - - private Builder addAdvancement(Supplier jsonSupplier) - { - if (locked) - throw new IllegalStateException("Attempted to modify finished builder"); - if (currentConditions.isEmpty()) - throw new IllegalStateException("Can not add a advancement with no conditions."); - conditions.add(currentConditions.toArray(new ICondition[currentConditions.size()])); - advancements.add(jsonSupplier); - currentConditions.clear(); - return this; - } - - public JsonObject write() - { - if (!locked) - { - if (!currentConditions.isEmpty()) - throw new IllegalStateException("Invalid builder state: Orphaned conditions"); - if (advancements.isEmpty()) - throw new IllegalStateException("Invalid builder state: No Advancements"); - locked = true; - } - JsonObject json = new JsonObject(); - JsonArray array = new JsonArray(); + private record Pair(ICondition condition, Advancement adv) {} + private record Result(List advancements) implements IResult { + @Override + public JsonObject serializeToJson() { + var json = new JsonObject(); + var array = new JsonArray(); json.add("advancements", array); - for (int x = 0; x < conditions.size(); x++) - { - JsonObject holder = new JsonObject(); - JsonArray conds = new JsonArray(); - for (ICondition c : conditions.get(x)) - conds.add(CraftingHelper.serialize(c)); - holder.add("conditions", conds); - holder.add("advancement", advancements.get(x).get()); + for (var pair : advancements) { + var holder = pair.adv().serializeToJson(); + if (holder.has(ICondition.DEFAULT_FIELD)) + throw new IllegalStateException("Recipe already serialized conditions!"); + ForgeHooks.writeCondition(pair.condition(), holder); array.add(holder); } @@ -128,4 +118,3 @@ public class ConditionalAdvancement } } } -*/ diff --git a/src/main/java/net/minecraftforge/common/crafting/ConditionalRecipe.java b/src/main/java/net/minecraftforge/common/crafting/ConditionalRecipe.java index 12d1abedc7..f07eb75642 100644 --- a/src/main/java/net/minecraftforge/common/crafting/ConditionalRecipe.java +++ b/src/main/java/net/minecraftforge/common/crafting/ConditionalRecipe.java @@ -4,188 +4,159 @@ */ package net.minecraftforge.common.crafting; -/* TODO: Conditional Recipes + import java.util.ArrayList; import java.util.List; import java.util.function.Consumer; - import com.google.gson.JsonArray; -import com.google.gson.JsonElement; import com.google.gson.JsonObject; import com.google.gson.JsonSyntaxException; +import com.mojang.datafixers.util.Pair; +import com.mojang.serialization.Codec; +import com.mojang.serialization.DataResult; +import com.mojang.serialization.Dynamic; +import com.mojang.serialization.DynamicOps; +import com.mojang.serialization.JsonOps; +import net.minecraft.Util; +import net.minecraft.advancements.Advancement; import net.minecraft.advancements.AdvancementHolder; import net.minecraft.data.recipes.FinishedRecipe; +import net.minecraft.data.recipes.RecipeBuilder; +import net.minecraft.data.recipes.RecipeOutput; import net.minecraft.world.item.crafting.Recipe; import net.minecraft.world.item.crafting.RecipeSerializer; -import net.minecraft.world.item.crafting.RecipeManager; import net.minecraft.network.FriendlyByteBuf; -import net.minecraft.util.GsonHelper; import net.minecraft.resources.ResourceLocation; +import net.minecraft.util.GsonHelper; +import net.minecraftforge.common.ForgeHooks; +import net.minecraftforge.common.crafting.conditions.ConditionCodec; import net.minecraftforge.common.crafting.conditions.ICondition; -import net.minecraftforge.registries.ObjectHolder; +import net.minecraftforge.registries.ForgeRegistries; import org.jetbrains.annotations.Nullable; -public class ConditionalRecipe -{ - @ObjectHolder(registryName = "recipe_serializer", value = "forge:conditional") - public static final RecipeSerializer> SERIALZIER = null; - - public static Builder builder() - { +/** + * So, A 'ConditionalRecipe' differs from all normal recipes in the fact that in addition to the conditions + * disabling the entire recipe, it has sub-recipes that themselves have conditions. + * + * And when being deserialized it returns the first entry that passes the conditional check. + * This basically means that you can have muultiple variants all use the same recipe name, and + * only one will ever be loaded. + * + * This also means that you can wrap ALL recipes in a Conditional even those that don't explicitly + * have support for them in their data gen. + */ +public class ConditionalRecipe { + public static Builder builder() { return new Builder(); } - public static class Serializer> implements RecipeSerializer - { - @Override - public T fromJson(ResourceLocation recipeId, JsonObject json) - { - return fromJson(recipeId, json, ICondition.IContext.EMPTY); - } - - @SuppressWarnings("unchecked") // We return a nested one, so we can't know what type it is. - @Override - public T fromJson(ResourceLocation recipeId, JsonObject json, ICondition.IContext context) - { - JsonArray items = GsonHelper.getAsJsonArray(json, "recipes"); - int idx = 0; - for (JsonElement ele : items) - { - if (!ele.isJsonObject()) - throw new JsonSyntaxException("Invalid recipes entry at index " + idx + " Must be JsonObject"); - if (CraftingHelper.processConditions(GsonHelper.getAsJsonArray(ele.getAsJsonObject(), "conditions"), context)) - return (T)RecipeManager.fromJson(recipeId, GsonHelper.getAsJsonObject(ele.getAsJsonObject(), "recipe")); - idx++; + public static class Builder { + private List recipes = new ArrayList<>(); + private RecipeOutput bouncer = new RecipeOutput() { + @Override + public void accept(FinishedRecipe value) { + recipe(value); } - return null; - } - //Should never get here as we return one of the recipes we wrap. - @Override public T fromNetwork(ResourceLocation recipeId, FriendlyByteBuf buffer) { return null; } - @Override public void toNetwork(FriendlyByteBuf buffer, T recipe) {} - } + @SuppressWarnings("removal") + @Override + public Advancement.Builder advancement() { + return Advancement.Builder.recipeAdvancement().parent(RecipeBuilder.ROOT_RECIPE_ADVANCEMENT); + } + }; - public static class Builder - { - private List conditions = new ArrayList<>(); - private List recipes = new ArrayList<>(); - private ResourceLocation advId; - private ConditionalAdvancement.Builder adv; + @Nullable + private ICondition condition; + @Nullable + private ICondition mainCondition; - private List currentConditions = new ArrayList<>(); + @Nullable + private ResourceLocation advancementId; - public Builder addCondition(ICondition condition) - { - currentConditions.add(condition); + public Builder mainCondition(ICondition value) { + if (this.mainCondition != null) + throw new IllegalStateException("Attempted to overrride the main condition, only one is allowed to be set"); + this.mainCondition = value; return this; } - public Builder addRecipe(Consumer> callable) - { - callable.accept(this::addRecipe); + public Builder condition(ICondition value) { + if (this.condition != null) + throw new IllegalStateException("Attempted to override a previous set condition before adding a recipe"); + this.condition = value; return this; } - public Builder addRecipe(FinishedRecipe recipe) - { - if (currentConditions.isEmpty()) + public Builder recipe(Consumer callable) { + callable.accept(bouncer); + return this; + } + + public Builder recipe(FinishedRecipe recipe) { + if (condition == null) throw new IllegalStateException("Can not add a recipe with no conditions."); - conditions.add(currentConditions.toArray(new ICondition[currentConditions.size()])); - recipes.add(recipe); - currentConditions.clear(); + recipes.add(new RecipePair(this.condition, recipe)); + condition = null; return this; } - public Builder generateAdvancement() - { - return generateAdvancement(null); - } - - public Builder generateAdvancement(@Nullable ResourceLocation id) - { - ConditionalAdvancement.Builder builder = ConditionalAdvancement.builder(); - for(int i=0;i consumer, String namespace, String path) - { - build(consumer, new ResourceLocation(namespace, path)); + public void save(RecipeOutput out, String namespace, String path) { + save(out, new ResourceLocation(namespace, path)); } - public void build(Consumer consumer, ResourceLocation id) - { - if (!currentConditions.isEmpty()) + public void save(RecipeOutput out, ResourceLocation id) { + if (condition != null) throw new IllegalStateException("Invalid ConditionalRecipe builder, Orphaned conditions"); + if (recipes.isEmpty()) throw new IllegalStateException("Invalid ConditionalRecipe builder, No recipes"); - if (advId == null && adv != null) - { - advId = new ResourceLocation(id.getNamespace(), "recipes/" + id.getPath()); + var adv = ConditionalAdvancement.builder(); + var hasAdvancement = false; + for (var pair : recipes) { + if (pair.recipe().advancement() != null) { + adv.condition(pair.condition()) + .advancement(pair.recipe().advancement()); + hasAdvancement = true; + } } - consumer.accept(new Finished(id, conditions, recipes, advId, adv)); + if (advancementId == null) + advancementId = id.withPrefix("recipes/"); + + out.accept(new Finished(id, mainCondition, recipes, advancementId, hasAdvancement ? adv.write() : null)); } } - - private static class Finished implements FinishedRecipe - { - private final ResourceLocation id; - private final List conditions; - private final List recipes; - private final ResourceLocation advId; - private final ConditionalAdvancement.Builder adv; - - private Finished(ResourceLocation id, List conditions, List recipes, @Nullable ResourceLocation advId, @Nullable ConditionalAdvancement.Builder adv) - { - this.id = id; - this.conditions = conditions; - this.recipes = recipes; - this.advId = advId; - this.adv = adv; - } - + private record Finished( + ResourceLocation id, + @Nullable + ICondition mainCondition, + List recipes, + ResourceLocation advId, + JsonObject advData + ) implements FinishedRecipe { @Override public void serializeRecipeData(JsonObject json) { JsonArray array = new JsonArray(); + var main = mainCondition; + if (main == null && recipes.size() == 1) + main = recipes.get(0).condition(); + + ForgeHooks.writeCondition(main, json); + json.add("recipes", array); - for (int x = 0; x < conditions.size(); x++) - { - JsonObject holder = new JsonObject(); - - JsonArray conds = new JsonArray(); - for (ICondition c : conditions.get(x)) - conds.add(CraftingHelper.serialize(c)); - holder.add("conditions", conds); - holder.add("recipe", recipes.get(x).serializeRecipe()); - + for (var pair : recipes) { + var holder = pair.recipe().serializeRecipe(); + if (holder.has(ICondition.DEFAULT_FIELD)) + throw new IllegalStateException("Recipe already serialized conditions!"); + ForgeHooks.writeCondition(pair.condition(), holder); array.add(holder); } } @@ -196,22 +167,77 @@ public class ConditionalRecipe } @Override - public RecipeSerializer type() - { - return SERIALZIER; + public RecipeSerializer type() { + return ConditionalRecipe.SERIALZIER; } + @Nullable @Override public AdvancementHolder advancement() { - if (adv == null) - return null; - return new AdvancementHolder(advId, adv.) + return null; + } + + @Nullable + @Override + public AdvancementData advancementData() { + return advData == null ? null : new AdvancementData(advId, advData); + } + } + + private record RecipePair(ICondition condition, FinishedRecipe recipe) {} + + private static Codec> CODEC = new Codec>() { + @Override + public DataResult encode(Recipe input, DynamicOps ops, T prefix) { + throw new UnsupportedOperationException("ConditionRecipe.CODEC does not support encoding"); } @Override - public ResourceLocation getAdvancementId() { - return advId; + public DataResult, T>> decode(DynamicOps ops, T input) { + var context = ConditionCodec.getContext(ops); + var json = new Dynamic<>(ops, input).convert(JsonOps.INSTANCE).getValue(); + try { + var recipes = GsonHelper.getAsJsonArray(GsonHelper.convertToJsonObject(json.getAsJsonObject(), "root"), "recipes"); + int idx = 0; + for (var entry : recipes) { + var object = GsonHelper.convertToJsonObject(entry, "recipe[" + idx++ + "]"); + var conditionjson = GsonHelper.getAsJsonObject(object, ICondition.DEFAULT_FIELD); + var condition = Util.getOrThrow(ICondition.SAFE_CODEC.parse(JsonOps.INSTANCE, conditionjson), JsonSyntaxException::new); + if (!condition.test(context)) + continue; + var type = new ResourceLocation(GsonHelper.getAsString(object, "type")); + var serializer = ForgeRegistries.RECIPE_SERIALIZERS.getValue(type); + if (serializer == null) + return DataResult.error(() -> "Invalid or unsupported recipe type '" + type + "' Conditions were successful, but unknown type"); + var parsed = serializer.codec().parse(JsonOps.INSTANCE, object); + if (parsed.error().isPresent()) + return DataResult.error(parsed.error().get()::message); + else if (!parsed.result().isPresent()) + return DataResult.error(() -> "Recipe passed all conditions but did not parse a valid return"); + else + return DataResult.success(Pair.of(parsed.result().get(), ops.empty())); + } + return DataResult.error(() -> "No recipe passed conditions, if this is the case, you should have an outer condition."); + } catch (JsonSyntaxException e) { + return DataResult.error(() -> e.getMessage()); + } } - } + }.stable(); + + public static final RecipeSerializer> SERIALZIER = new RecipeSerializer>() { + @Override + public Codec> codec() { + return CODEC; + } + + @Override + public Recipe fromNetwork(FriendlyByteBuf buffer) { + throw new UnsupportedOperationException("ConditionaRecipe.SERIALIZER does not support decoding from network"); + } + + @Override + public void toNetwork(FriendlyByteBuf buffer, Recipe recipe) { + throw new UnsupportedOperationException("ConditionaRecipe.SERIALIZER does not support encoding to network"); + } + }; } -*/ diff --git a/src/main/java/net/minecraftforge/common/crafting/CraftingHelper.java b/src/main/java/net/minecraftforge/common/crafting/CraftingHelper.java index f2db032539..a35fdcbd0f 100644 --- a/src/main/java/net/minecraftforge/common/crafting/CraftingHelper.java +++ b/src/main/java/net/minecraftforge/common/crafting/CraftingHelper.java @@ -14,15 +14,18 @@ import com.google.gson.GsonBuilder; import com.google.gson.JsonArray; import com.google.gson.JsonElement; import com.google.gson.JsonObject; +import com.google.gson.JsonParseException; import com.google.gson.JsonSyntaxException; import com.mojang.brigadier.exceptions.CommandSyntaxException; import com.mojang.serialization.Codec; +import com.mojang.serialization.JsonOps; import net.minecraft.world.item.Item; import net.minecraft.world.item.ItemStack; import net.minecraft.world.item.Items; import net.minecraftforge.common.crafting.conditions.ICondition; import net.minecraft.nbt.TagParser; +import net.minecraft.Util; import net.minecraft.nbt.CompoundTag; import net.minecraft.util.GsonHelper; import net.minecraft.resources.ResourceLocation; @@ -171,30 +174,4 @@ public class CraftingHelper { return new ItemStack(item, GsonHelper.getAsInt(json, "count", 1)); } - - // CONDITIONS - private static final Map> conditions = new HashMap<>(); - public static void register(ResourceLocation key, Codec codec) { - if (conditions.containsKey(key)) - throw new IllegalStateException("Duplicate recipe condition serializer: " + key); - conditions.put(key, codec); - } - - public static boolean processConditions(JsonObject json, String memberName, ICondition.IContext context) { - return !json.has(memberName) || processConditions(GsonHelper.getAsJsonArray(json, memberName), context); - } - - public static boolean processConditions(JsonArray conditions, ICondition.IContext context) { - for (int x = 0; x < conditions.size(); x++) { - if (!conditions.get(x).isJsonObject()) - throw new JsonSyntaxException("Conditions must be an array of JsonObjects"); - - JsonObject json = conditions.get(x).getAsJsonObject(); - /* - if (!CraftingHelper.getCondition(json).test(context)) - return false; - */ - } - return true; - } } diff --git a/src/main/java/net/minecraftforge/common/crafting/SimpleCraftingContainer.java b/src/main/java/net/minecraftforge/common/crafting/SimpleCraftingContainer.java new file mode 100644 index 0000000000..caebe2025f --- /dev/null +++ b/src/main/java/net/minecraftforge/common/crafting/SimpleCraftingContainer.java @@ -0,0 +1,166 @@ +/* + * Copyright (c) Forge Development LLC and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + +package net.minecraftforge.common.crafting; + +import java.util.ArrayList; +import java.util.List; +import it.unimi.dsi.fastutil.chars.Char2ObjectMap; +import it.unimi.dsi.fastutil.chars.Char2ObjectOpenHashMap; +import it.unimi.dsi.fastutil.chars.CharArraySet; +import net.minecraft.core.NonNullList; +import net.minecraft.world.ContainerHelper; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.entity.player.StackedContents; +import net.minecraft.world.inventory.CraftingContainer; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.ItemLike; + +public class SimpleCraftingContainer implements CraftingContainer { + private final int width; + private final int height; + private final NonNullList items; + + public static Builder builder() { + return new Builder(); + } + + public SimpleCraftingContainer(int width, int height) { + this(width, height, NonNullList.withSize(width * height, ItemStack.EMPTY)); + } + + public SimpleCraftingContainer(int width, int height, NonNullList items) { + this.width = width; + this.height = height; + this.items = items; + if (items.size() != (width * height)) + throw new IllegalArgumentException("Invalid item list, must be same size inventory width * height, received " + items.size() + " expected " + (width * height)); + } + + @Override + public int getContainerSize() { + return this.items.size(); + } + + @Override + public boolean isEmpty() { + return this.items.stream().noneMatch(p -> !p.isEmpty()); + } + + @Override + public ItemStack getItem(int slot) { + return slot >= this.items.size() ? ItemStack.EMPTY : this.items.get(slot); + } + + @Override + public ItemStack removeItem(int slot, int count) { + return ContainerHelper.removeItem(this.items, slot, count); + } + + @Override + public ItemStack removeItemNoUpdate(int slot) { + return ContainerHelper.takeItem(this.items, slot); + } + + @Override + public void setItem(int slot, ItemStack stack) { + this.items.set(slot, stack); + } + + @Override + public void setChanged() { + } + + @Override + public boolean stillValid(Player player) { + return true; + } + + @Override + public void clearContent() { + this.items.clear(); + } + + @Override + public void fillStackedContents(StackedContents stacked) { + this.items.forEach(stacked::accountSimpleStack); + } + + @Override + public int getWidth() { + return width; + } + + @Override + public int getHeight() { + return height; + } + + @Override + public List getItems() { + return List.copyOf(this.items); + } + + public static class Builder { + private final List rows = new ArrayList<>(); + private final Char2ObjectMap keys = new Char2ObjectOpenHashMap<>(); + + private Builder() { + this.define(' ', ItemStack.EMPTY); + } + + public Builder pattern(String row) { + if (!this.rows.isEmpty() && row.length() != this.rows.get(0).length()) + throw new IllegalArgumentException("Pattern must be the same width on every line"); + this.rows.add(row); + return this; + } + + public Builder pattern(String... rows) { + for (var row : rows) + pattern(row); + return this; + } + + public Builder define(char key, ItemLike item) { + return define(key, new ItemStack(item)); + } + + public Builder define(char key, ItemStack stack) { + if (this.keys.containsKey(key)) + throw new IllegalArgumentException("key '" + key + "' is already defined."); + this.keys.put(key, stack); + return this; + } + + public SimpleCraftingContainer build() { + var unseen = new CharArraySet(this.keys.keySet()); + unseen.remove(' '); + + int height = this.rows.size(); + if (height == 0) + throw new IllegalStateException("Invalid builder, empty inventory"); + int width = this.rows.get(0).length(); + var items = NonNullList.withSize(width * height, ItemStack.EMPTY); + + int idx = 0; + for (var row : this.rows) { + for (int x = 0; x < width; x++) { + char key = row.charAt(x); + var stack = this.keys.get(key); + if (stack == null) + throw new IllegalStateException("Invalid builder pattern, missing value for key '" + key + "'"); + unseen.remove(key); + items.set(idx++, stack.copy()); + } + } + + if (!unseen.isEmpty()) + throw new IllegalStateException("Invalid builder, missing usage of defined keys: " + unseen); + + return new SimpleCraftingContainer(width, height, items); + } + } +} diff --git a/src/main/java/net/minecraftforge/common/crafting/conditions/AndCondition.java b/src/main/java/net/minecraftforge/common/crafting/conditions/AndCondition.java index 07fd147898..5d276112c4 100644 --- a/src/main/java/net/minecraftforge/common/crafting/conditions/AndCondition.java +++ b/src/main/java/net/minecraftforge/common/crafting/conditions/AndCondition.java @@ -27,7 +27,7 @@ public record AndCondition(List children) implements ICondition { @Override public String toString() { - return Joiner.on(" && ").join(children); + return '(' + Joiner.on(" && ").join(children) + ')'; } @Override diff --git a/src/main/java/net/minecraftforge/common/crafting/conditions/ICondition.java b/src/main/java/net/minecraftforge/common/crafting/conditions/ICondition.java index e33a2ba4a8..a77cc2bbae 100644 --- a/src/main/java/net/minecraftforge/common/crafting/conditions/ICondition.java +++ b/src/main/java/net/minecraftforge/common/crafting/conditions/ICondition.java @@ -29,6 +29,7 @@ public interface ICondition { Codec CODEC = ExtraCodecs.lazyInitializedCodec(() -> ForgeRegistries.CONDITION_SERIALIZERS.get().getCodec().dispatch(ICondition::codec, Function.identity())); String DEFAULT_FIELD = "forge:condition"; MapCodec> OPTIONAL_FEILD_CODEC = CODEC.optionalFieldOf(DEFAULT_FIELD); + Codec SAFE_CODEC = CODEC.orElse(FalseCondition.INSTANCE); boolean test(IContext context); diff --git a/src/main/java/net/minecraftforge/common/crafting/conditions/OrCondition.java b/src/main/java/net/minecraftforge/common/crafting/conditions/OrCondition.java index 86a9dee36d..18e4ebce13 100644 --- a/src/main/java/net/minecraftforge/common/crafting/conditions/OrCondition.java +++ b/src/main/java/net/minecraftforge/common/crafting/conditions/OrCondition.java @@ -12,9 +12,9 @@ import com.mojang.serialization.Codec; import com.mojang.serialization.codecs.RecordCodecBuilder; public record OrCondition(List children) implements ICondition { - public static final Codec CODEC = RecordCodecBuilder.create(b -> b.group( - ICondition.CODEC.listOf().fieldOf("values").forGetter(AndCondition::children) - ).apply(b, AndCondition::new)); + public static final Codec CODEC = RecordCodecBuilder.create(b -> b.group( + ICondition.CODEC.listOf().fieldOf("values").forGetter(OrCondition::children) + ).apply(b, OrCondition::new)); @Override @@ -29,7 +29,7 @@ public record OrCondition(List children) implements ICondition { @Override public String toString() { - return Joiner.on(" || ").join(children); + return '(' + Joiner.on(" || ").join(children) + ')'; } @Override diff --git a/src/main/java/net/minecraftforge/common/data/ForgeRecipeProvider.java b/src/main/java/net/minecraftforge/common/data/ForgeRecipeProvider.java index b6b1cf585d..105f940a91 100644 --- a/src/main/java/net/minecraftforge/common/data/ForgeRecipeProvider.java +++ b/src/main/java/net/minecraftforge/common/data/ForgeRecipeProvider.java @@ -133,7 +133,7 @@ public final class ForgeRecipeProvider extends VanillaRecipeProvider { @Nullable @Override - protected CompletableFuture saveAdvancement(CachedOutput output, JsonObject advancementJson, AdvancementHolder holder, FinishedRecipe finishedRecipe) { + protected CompletableFuture saveAdvancement(CachedOutput output, ResourceLocation advancementId, JsonObject advancementJson, FinishedRecipe finishedRecipe) { // NOOP - We don't replace any of the advancement things yet... return null; } diff --git a/src/main/java/net/minecraftforge/common/extensions/IForgeGameTestHelper.java b/src/main/java/net/minecraftforge/common/extensions/IForgeGameTestHelper.java index 379cb39824..017ccdbc72 100644 --- a/src/main/java/net/minecraftforge/common/extensions/IForgeGameTestHelper.java +++ b/src/main/java/net/minecraftforge/common/extensions/IForgeGameTestHelper.java @@ -6,10 +6,12 @@ package net.minecraftforge.common.extensions; import java.util.UUID; +import java.util.function.Supplier; import com.mojang.authlib.GameProfile; import io.netty.channel.embedded.EmbeddedChannel; +import net.minecraft.gametest.framework.GameTestAssertException; import net.minecraft.gametest.framework.GameTestHelper; import net.minecraft.network.Connection; import net.minecraft.network.ConnectionProtocol; @@ -23,6 +25,16 @@ public interface IForgeGameTestHelper { return (GameTestHelper)this; } + default void assertTrue(boolean value, Supplier message) { + if (!value) + throw new GameTestAssertException(message.get()); + } + + default void assertFalse(boolean value, Supplier message) { + if (value) + throw new GameTestAssertException(message.get()); + } + default ServerPlayer makeMockServerPlayer() { var level = self().getLevel(); var cookie = CommonListenerCookie.createInitial(new GameProfile(UUID.randomUUID(), "test-mock-player")); diff --git a/src/test/java/net/minecraftforge/debug/gameplay/crafting/ConditionalRecipeTest.java b/src/test/java/net/minecraftforge/debug/gameplay/crafting/ConditionalRecipeTest.java new file mode 100644 index 0000000000..9bf0eeb958 --- /dev/null +++ b/src/test/java/net/minecraftforge/debug/gameplay/crafting/ConditionalRecipeTest.java @@ -0,0 +1,353 @@ +/* + * Copyright (c) Forge Development LLC and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + +package net.minecraftforge.debug.gameplay.crafting; + +import net.minecraft.data.PackOutput; +import net.minecraft.data.recipes.RecipeCategory; +import net.minecraft.data.recipes.RecipeOutput; +import net.minecraft.data.recipes.RecipeProvider; +import net.minecraft.data.recipes.ShapedRecipeBuilder; +import net.minecraft.data.recipes.ShapelessRecipeBuilder; +import net.minecraft.data.recipes.SimpleCookingRecipeBuilder; +import net.minecraft.data.recipes.SingleItemRecipeBuilder; +import net.minecraft.gametest.framework.GameTest; +import net.minecraft.gametest.framework.GameTestHelper; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.tags.ItemTags; +import net.minecraft.world.Container; +import net.minecraft.world.item.crafting.Ingredient; +import net.minecraft.world.item.crafting.Recipe; +import net.minecraft.world.item.crafting.RecipeType; +import net.minecraft.world.level.block.Blocks; +import net.minecraftforge.common.crafting.ConditionalRecipe; +import net.minecraftforge.common.crafting.SimpleCraftingContainer; +import net.minecraftforge.common.crafting.conditions.IConditionBuilder; +import net.minecraftforge.data.event.GatherDataEvent; +import net.minecraftforge.eventbus.api.SubscribeEvent; +import net.minecraftforge.fml.common.Mod; +import net.minecraftforge.gametest.GameTestHolder; +import net.minecraftforge.test.BaseTestMod; + +@GameTestHolder("forge." + ConditionalRecipeTest.MODID) +@Mod(ConditionalRecipeTest.MODID) +public class ConditionalRecipeTest extends BaseTestMod { + static final String MODID = "conditional_recipe"; + + @SubscribeEvent + public void gatherData(GatherDataEvent event) { + var gen = event.getGenerator(); + gen.addProvider(event.includeServer(), new Recipes(gen.getPackOutput())); + } + + private static ResourceLocation rl(String path) { + return new ResourceLocation(MODID, path); + } + + private static > void assertFalse(GameTestHelper helper, RecipeType type, C container) { + var recipe = helper.getLevel().getRecipeManager().getRecipeFor(type, container, helper.getLevel()); + helper.assertTrue(recipe.isEmpty(), () -> "Found crafting recipe when unexpected: " + recipe.get().id()); + helper.succeed(); + } + + private static > void assertTrue(GameTestHelper helper, RecipeType type, C container) { + var recipe = helper.getLevel().getRecipeManager().getRecipeFor(type, container, helper.getLevel()); + helper.assertTrue(recipe.isPresent(), "Did not find crafting recipe when expected!"); + helper.assertTrue(recipe.get().id().getNamespace().equals(MODID), "It wasn't our recipe: " + recipe.get().id()); + helper.succeed(); + } + + @GameTest(template = "forge:empty3x3x3") + public static void shaped_false_conditions(GameTestHelper helper) { + assertFalse(helper, RecipeType.CRAFTING, SimpleCraftingContainer.builder() + .pattern( + "XXX", + "XXX", + "XXX" + ) + .define('X', Blocks.DIRT) + .build() + ); + } + + @GameTest(template = "forge:empty3x3x3") + public static void shaped_true_conditions(GameTestHelper helper) { + assertTrue(helper, RecipeType.CRAFTING, SimpleCraftingContainer.builder() + .pattern( + "XXX", + "XXX", + "XXX" + ) + .define('X', Blocks.OAK_LOG) + .build() + ); + } + + @GameTest(template = "forge:empty3x3x3") + public static void shapeless_false_conditions(GameTestHelper helper) { + assertFalse(helper, RecipeType.CRAFTING, SimpleCraftingContainer.builder() + .pattern("XXX") + .define('X', Blocks.REDSTONE_ORE) + .build() + ); + } + + @GameTest(template = "forge:empty3x3x3") + public static void shapeless_true_conditions(GameTestHelper helper) { + assertTrue(helper, RecipeType.CRAFTING, SimpleCraftingContainer.builder() + .pattern("XXXX") + .define('X', Blocks.REDSTONE_ORE) + .build() + ); + } + + @GameTest(template = "forge:empty3x3x3") + public static void cooking_false_conditions(GameTestHelper helper) { + assertFalse(helper, RecipeType.SMELTING, SimpleCraftingContainer.builder() + .pattern("X") + .define('X', Blocks.DIRT) + .build() + ); + } + + @GameTest(template = "forge:empty3x3x3") + public static void cooking_true_conditions(GameTestHelper helper) { + assertTrue(helper, RecipeType.SMELTING, SimpleCraftingContainer.builder() + .pattern("X") + .define('X', Blocks.BEE_NEST) + .build() + ); + } + + @GameTest(template = "forge:empty3x3x3") + public static void single_item_false_conditions(GameTestHelper helper) { + assertFalse(helper, RecipeType.STONECUTTING, SimpleCraftingContainer.builder() + .pattern("X") + .define('X', Blocks.DIRT) + .build() + ); + } + + @GameTest(template = "forge:empty3x3x3") + public static void single_item_true_conditions(GameTestHelper helper) { + assertTrue(helper, RecipeType.STONECUTTING, SimpleCraftingContainer.builder() + .pattern("X") + .define('X', Blocks.REDSTONE_ORE) + .build() + ); + } + + @GameTest(template = "forge:empty3x3x3") + public static void conditional_recipe_choice(GameTestHelper helper) { + assertFalse(helper, RecipeType.CRAFTING, SimpleCraftingContainer.builder() + .pattern("XXX") + .pattern("XX ") + .define('X', Blocks.DIRT) + .build() + ); + assertTrue(helper, RecipeType.CRAFTING, SimpleCraftingContainer.builder() + .pattern("XXX") + .pattern("XX ") + .define('X', Blocks.OAK_LOG) + .build() + ); + } + + public static class Recipes extends RecipeProvider implements IConditionBuilder { + public Recipes(PackOutput gen) { + super(gen); + } + + @Override + protected void buildRecipes(RecipeOutput out) { + // TODO: Move this to a Condition test instead of using recipe encoders. + ShapedRecipeBuilder.shaped(RecipeCategory.MISC, Blocks.DIAMOND_BLOCK, 64) + .pattern("XXX") + .pattern("XXX") + .pattern("XXX") + .define('X', Blocks.DIRT) + .unlockedBy(getHasName(Blocks.DIRT), has(Blocks.DIRT)) + .condition( + and ( + or ( + not(modLoaded("minecraft")), + itemExists("minecraft", "dirt"), + TRUE(), + tagEmpty(ItemTags.DIRT) + ), + FALSE() + ) + ) + .save(out, rl("test_encode_all_conditions")); + + ShapedRecipeBuilder.shaped(RecipeCategory.MISC, Blocks.DIAMOND_BLOCK, 64) + .pattern("XXX") + .pattern("XXX") + .pattern("XXX") + .define('X', Blocks.DIRT) + .unlockedBy(getHasName(Blocks.DIRT), has(Blocks.DIRT)) + .condition(FALSE()) + .save(out, rl("shaped_false_conditions")); + + ShapedRecipeBuilder.shaped(RecipeCategory.MISC, Blocks.DIAMOND_BLOCK, 64) + .pattern("XXX") + .pattern("XXX") + .pattern("XXX") + .define('X', Blocks.OAK_LOG) + .unlockedBy(getHasName(Blocks.OAK_LOG), has(Blocks.OAK_LOG)) + .condition(TRUE()) + .save(out, rl("shaped_true_conditions")); + + ShapelessRecipeBuilder.shapeless(RecipeCategory.MISC, Blocks.DIAMOND_BLOCK) + .requires(Blocks.REDSTONE_ORE) + .requires(Blocks.REDSTONE_ORE) + .requires(Blocks.REDSTONE_ORE) + .unlockedBy(getHasName(Blocks.REDSTONE_ORE), has(Blocks.REDSTONE_ORE)) + .condition(FALSE()) + .save(out, rl("shapeless_false_conditions")); + + ShapelessRecipeBuilder.shapeless(RecipeCategory.MISC, Blocks.DIAMOND_BLOCK) + .requires(Blocks.REDSTONE_ORE) + .requires(Blocks.REDSTONE_ORE) + .requires(Blocks.REDSTONE_ORE) + .requires(Blocks.REDSTONE_ORE) + .unlockedBy(getHasName(Blocks.REDSTONE_ORE), has(Blocks.REDSTONE_ORE)) + .condition(TRUE()) + .save(out, rl("shapeless_true_conditions")); + + SimpleCookingRecipeBuilder.smelting(Ingredient.of(Blocks.DIRT), RecipeCategory.MISC, Blocks.DIAMOND_BLOCK, 0.1F, 200) + .unlockedBy(getHasName(Blocks.DIRT), has(Blocks.DIRT)) + .condition(FALSE()) + .save(out, rl("cooking_false_conditions")); + + SimpleCookingRecipeBuilder.smelting(Ingredient.of(Blocks.BEE_NEST), RecipeCategory.MISC, Blocks.DIAMOND_BLOCK, 0.1F, 200) + .unlockedBy(getHasName(Blocks.BEE_NEST), has(Blocks.BEE_NEST)) + .condition(TRUE()) + .save(out, rl("cooking_true_conditions")); + + SingleItemRecipeBuilder.stonecutting(Ingredient.of(Blocks.DIRT), RecipeCategory.MISC, Blocks.DIAMOND_BLOCK) + .unlockedBy(getHasName(Blocks.DIRT), has(Blocks.DIRT)) + .condition(FALSE()) + .save(out, rl("single_item_false_conditions")); + + SingleItemRecipeBuilder.stonecutting(Ingredient.of(Blocks.REDSTONE_ORE), RecipeCategory.MISC, Blocks.DIAMOND_BLOCK) + .unlockedBy(getHasName(Blocks.REDSTONE_ORE), has(Blocks.REDSTONE_ORE)) + .condition(TRUE()) + .save(out, rl("singe_item_true_conditions")); + + // TODO SmithingTransformRecipeBuilder + // TODO SmithingTrimRecipeBuilder + + + ConditionalRecipe.builder() + .condition(FALSE()) + .recipe( + ShapedRecipeBuilder.shaped(RecipeCategory.MISC, Blocks.DIAMOND_BLOCK, 64) + .pattern("XXX") + .pattern("XX ") + .define('X', Blocks.DIRT) + .unlockedBy(getHasName(Blocks.DIRT), has(Blocks.DIRT)) + ::save + ) + .condition(TRUE()) + .recipe( + ShapedRecipeBuilder.shaped(RecipeCategory.MISC, Blocks.DIAMOND_BLOCK, 64) + .pattern("XXX") + .pattern("XX ") + .define('X', Blocks.OAK_LOG) + .unlockedBy(getHasName(Blocks.OAK_LOG), has(Blocks.OAK_LOG)) + ::save + ) + .save(out, rl("conditional_recipe_choice")); + + ConditionalRecipe.builder() + .condition(FALSE()) + .recipe( + ShapelessRecipeBuilder.shapeless(RecipeCategory.MISC, Blocks.DIAMOND_BLOCK, 64) + .requires(Blocks.DIRT) + .unlockedBy(getHasName(Blocks.DIRT), has(Blocks.DIRT)) + ::save + ) + .save(out, rl("conditional_doesnt_load_empty")); + + + /* + + // ingredient tests + // strict NBT match - should match an unnamed iron pickaxe that lost 3 durability + Ingredient nbtIngredient = StrictNBTIngredient.of(Util.make(() -> + { + ItemStack stack = new ItemStack(Items.IRON_PICKAXE); + stack.setDamageValue(3); + return stack; + })); + ShapelessRecipeBuilder.shapeless(RecipeCategory.TOOLS, Items.GOLDEN_PICKAXE) + .requires(nbtIngredient) + .unlockedBy("has_pickaxe", has(Items.IRON_PICKAXE)) + .save(consumer, new ResourceLocation("data_gen_test", "exact_nbt_ingredient")); + + // ingredient tests + // contains NBT match - should match a stone pickaxe that lost 3 durability, regardless of setting its name + ShapelessRecipeBuilder.shapeless(RecipeCategory.TOOLS, Items.IRON_PICKAXE) + .requires(PartialNBTIngredient.of(Items.STONE_PICKAXE, Util.make(() -> + { + CompoundTag nbt = new CompoundTag(); + nbt.putInt(ItemStack.TAG_DAMAGE, 3); + return nbt; + }))) + .unlockedBy("has_pickaxe", has(Items.STONE_PICKAXE)) + .save(consumer, new ResourceLocation("data_gen_test", "contains_nbt_ingredient_single_item")); + + // contains NBT match - should match a wood, stone, or iron pickaxe that was named "Diamond Pickaxe", regardless of damage + ShapelessRecipeBuilder.shapeless(RecipeCategory.TOOLS, Items.DIAMOND_PICKAXE) + .requires(PartialNBTIngredient.of(Util.make(() -> + { + CompoundTag nbt = new CompoundTag(); + CompoundTag display = new CompoundTag(); + display.putString(ItemStack.TAG_DISPLAY_NAME, "{\"text\":\"Diamond Pickaxe\"}"); + nbt.put(ItemStack.TAG_DISPLAY, display); + return nbt; + }), Items.WOODEN_PICKAXE, Items.STONE_PICKAXE, Items.IRON_PICKAXE)) + .unlockedBy("has_pickaxe", has(Items.WOODEN_PICKAXE)) + .save(consumer, new ResourceLocation("data_gen_test", "contains_nbt_ingredient_item_set")); + + // intersection - should match all non-flammable planks + ShapedRecipeBuilder.shaped(RecipeCategory.BUILDING_BLOCKS, Blocks.NETHERRACK) + .pattern("###") + .pattern("###") + .pattern(" # ") + .define('#', IntersectionIngredient.of(Ingredient.of(ItemTags.PLANKS), Ingredient.of(ItemTags.NON_FLAMMABLE_WOOD))) + .unlockedBy("has_planks", has(Items.CRIMSON_PLANKS)) + .save(consumer, new ResourceLocation("data_gen_test", "intersection_ingredient")); + + // difference - should match all flammable fences + ShapedRecipeBuilder.shaped(RecipeCategory.TOOLS, Items.FLINT_AND_STEEL) + .pattern(" # ") + .pattern("###") + .pattern(" # ") + .define('#', DifferenceIngredient.of(Ingredient.of(ItemTags.FENCES), Ingredient.of(ItemTags.NON_FLAMMABLE_WOOD))) + .unlockedBy("has_fence", has(Items.CRIMSON_FENCE)) + .save(consumer, new ResourceLocation("data_gen_test", "difference_ingredient")); + + // compound - should match planks, logs, or bedrock + ShapedRecipeBuilder.shaped(RecipeCategory.BUILDING_BLOCKS, Blocks.DIRT) + .pattern("###") + .pattern(" # ") + .define('#', CompoundIngredient.of(Ingredient.of(ItemTags.PLANKS), Ingredient.of(ItemTags.LOGS), Ingredient.of(Blocks.BEDROCK))) + .unlockedBy("has_planks", has(Items.CRIMSON_PLANKS)) + .save(consumer, new ResourceLocation("data_gen_test", "compound_ingredient_only_vanilla")); + + // compound - should match planks, logs, or a stone pickaxe with 3 damage + ShapedRecipeBuilder.shaped(RecipeCategory.BUILDING_BLOCKS, Blocks.GOLD_BLOCK) + .pattern("#") + .pattern("#") + .define('#', CompoundIngredient.of(Ingredient.of(ItemTags.PLANKS), Ingredient.of(ItemTags.LOGS), nbtIngredient)) + .unlockedBy("has_planks", has(Items.CRIMSON_PLANKS)) + .save(consumer, new ResourceLocation("data_gen_test", "compound_ingredient_custom_types")); + */ + } + } + +} diff --git a/src/test/java/net/minecraftforge/debug/gameplay/loot/GlobalLootModifiersTest.java b/src/test/java/net/minecraftforge/debug/gameplay/loot/GlobalLootModifiersTest.java index 0be93f1502..16fe622ceb 100644 --- a/src/test/java/net/minecraftforge/debug/gameplay/loot/GlobalLootModifiersTest.java +++ b/src/test/java/net/minecraftforge/debug/gameplay/loot/GlobalLootModifiersTest.java @@ -28,7 +28,6 @@ import net.minecraft.world.SimpleContainer; import net.minecraft.world.item.Item; import net.minecraft.world.item.ItemStack; import net.minecraft.world.item.Items; -import net.minecraft.world.item.CreativeModeTab.TabVisibility; import net.minecraft.world.item.crafting.RecipeType; import net.minecraft.world.level.storage.loot.LootContext; import net.minecraft.world.level.storage.loot.LootParams; @@ -44,11 +43,9 @@ import net.minecraftforge.common.loot.LootModifier; import net.minecraftforge.common.loot.LootTableIdCondition; import net.minecraftforge.eventbus.api.SubscribeEvent; import net.minecraftforge.registries.RegistryObject; -import net.minecraftforge.test.Empty; +import net.minecraftforge.test.BaseTestMod; import net.minecraftforge.fml.common.Mod; import net.minecraftforge.data.event.GatherDataEvent; -import net.minecraftforge.event.BuildCreativeModeTabContentsEvent; -import net.minecraftforge.fml.javafmlmod.FMLJavaModLoadingContext; import net.minecraftforge.gametest.GameTestHolder; import net.minecraftforge.items.ItemHandlerHelper; import net.minecraftforge.registries.DeferredRegister; @@ -61,45 +58,33 @@ import java.util.stream.Collectors; @GameTestHolder("forge.global_loot_modifiers") @Mod(GlobalLootModifiersTest.MODID) -public class GlobalLootModifiersTest { +public class GlobalLootModifiersTest extends BaseTestMod { public static final String MODID = "global_loot_test"; - public static final boolean ENABLE = true; - - public GlobalLootModifiersTest() { - if (!ENABLE) - return; - - var modBus = FMLJavaModLoadingContext.get().getModEventBus(); - GLM.register(modBus); - ENCHANTS.register(modBus); - modBus.register(this); - } private static final DeferredRegister> GLM = DeferredRegister.create(ForgeRegistries.Keys.GLOBAL_LOOT_MODIFIER_SERIALIZERS, MODID); - private static final DeferredRegister ENCHANTS = DeferredRegister.create(ForgeRegistries.ENCHANTMENTS, MODID); + static { + GLM.register("dungeon_loot", DungeonLootEnhancerModifier.CODEC); + GLM.register("smelting", SmeltingEnchantmentModifier.CODEC); + GLM.register("wheat_harvest", WheatSeedsConverterModifier.CODEC); + GLM.register("silk_touch_bamboo", SilkTouchTestModifier.CODEC); + } - public static final RegistryObject> DUNGEON_LOOT = GLM.register("dungeon_loot", DungeonLootEnhancerModifier.CODEC); - public static final RegistryObject> SMELTING = GLM.register("smelting", SmeltingEnchantmentModifier.CODEC); - public static final RegistryObject> WHEATSEEDS = GLM.register("wheat_harvest", WheatSeedsConverterModifier.CODEC); - public static final RegistryObject> SILKTOUCH = GLM.register("silk_touch_bamboo", SilkTouchTestModifier.CODEC); - public static final RegistryObject SMELT = ENCHANTS.register("smelt", () -> new SmelterEnchantment(Rarity.UNCOMMON, EnchantmentCategory.DIGGER, EquipmentSlot.MAINHAND)); + private static final DeferredRegister ENCHANTS = DeferredRegister.create(ForgeRegistries.ENCHANTMENTS, MODID); + private static final RegistryObject SMELT = ENCHANTS.register("smelt", () -> new SmelterEnchantment(Rarity.UNCOMMON, EnchantmentCategory.DIGGER, EquipmentSlot.MAINHAND)); + + public GlobalLootModifiersTest() { + testItem(() -> { + var smelt = new ItemStack(Items.IRON_AXE); + EnchantmentHelper.setEnchantments(Map.of(SMELT.get(), 1), smelt); + return smelt; + }); + } @SubscribeEvent public void runData(GatherDataEvent event) { event.getGenerator().addProvider(event.includeServer(), new DataProvider(event.getGenerator().getPackOutput(), MODID)); } - @SubscribeEvent - public void onCreativeModeTabBuildContents(BuildCreativeModeTabContentsEvent event) { - var entries = event.getEntries(); - var vis = TabVisibility.PARENT_AND_SEARCH_TABS; - if (event.getTabKey() == Empty.TAB) { - var smelt = new ItemStack(Items.IRON_AXE); - EnchantmentHelper.setEnchantments(Map.of(SMELT.get(), 1), smelt); - entries.put(smelt, vis); - } - } - @GameTest(template = "forge:empty3x3x3") public static void test_smelting_modifier(GameTestHelper helper) { var center = new BlockPos(1, 1, 1); diff --git a/src/test/java/net/minecraftforge/test/BaseTestMod.java b/src/test/java/net/minecraftforge/test/BaseTestMod.java new file mode 100644 index 0000000000..610c503e7c --- /dev/null +++ b/src/test/java/net/minecraftforge/test/BaseTestMod.java @@ -0,0 +1,72 @@ +/* + * Copyright (c) Forge Development LLC and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + +package net.minecraftforge.test; + +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.List; +import java.util.function.Supplier; + +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.CreativeModeTab.TabVisibility; +import net.minecraftforge.event.BuildCreativeModeTabContentsEvent; +import net.minecraftforge.eventbus.api.SubscribeEvent; +import net.minecraftforge.fml.javafmlmod.FMLJavaModLoadingContext; +import net.minecraftforge.registries.DeferredRegister; + +public abstract class BaseTestMod { + private List> testItems = new ArrayList<>(); + + protected BaseTestMod() { + // TODO: Some form of enable flag? + + var modBus = FMLJavaModLoadingContext.get().getModEventBus(); + modBus.register(this); + + Class cls = getClass(); + while (cls != BaseTestMod.class) { + for (var field : cls.getDeclaredFields()) { + if (Modifier.isStatic(field.getModifiers()) && field.getType() == DeferredRegister.class) { + field.setAccessible(true); + DeferredRegister dr = getField(field, null); + dr.register(modBus); + } + + } + cls = cls.getSuperclass(); + } + } + + protected Supplier testItem(Supplier supplier) { + this.testItems.add(supplier); + return supplier; + } + + + @SubscribeEvent + protected void onCreativeModeTabBuildContents(BuildCreativeModeTabContentsEvent event) { + var entries = event.getEntries(); + if (event.getTabKey() == TestHelperMod.TAB) { + for (var s : testItems) + entries.put(s.get(), TabVisibility.PARENT_AND_SEARCH_TABS); + } + } + + @SuppressWarnings("unchecked") + protected static R getField(Field field, Object instance) { + try { + return (R)field.get(instance); + } catch (IllegalArgumentException | IllegalAccessException e) { + return sneak(e); + } + } + + @SuppressWarnings("unchecked") + private static R sneak(Throwable e) throws E { + throw (E)e; + } +} diff --git a/src/test/java/net/minecraftforge/test/Empty.java b/src/test/java/net/minecraftforge/test/TestHelperMod.java similarity index 88% rename from src/test/java/net/minecraftforge/test/Empty.java rename to src/test/java/net/minecraftforge/test/TestHelperMod.java index 4aacb2d78f..183b33122e 100644 --- a/src/test/java/net/minecraftforge/test/Empty.java +++ b/src/test/java/net/minecraftforge/test/TestHelperMod.java @@ -21,20 +21,14 @@ import net.minecraft.world.item.Items; import net.minecraftforge.data.event.GatherDataEvent; import net.minecraftforge.eventbus.api.SubscribeEvent; import net.minecraftforge.fml.common.Mod; -import net.minecraftforge.fml.javafmlmod.FMLJavaModLoadingContext; import net.minecraftforge.registries.RegisterEvent; /* A place where I can put common utility stuff for now. Until I re-write the test codebase. */ -@Mod(Empty.MOD_ID) -public class Empty { - public static final String MOD_ID = "empty"; +@Mod(TestHelperMod.MOD_ID) +public class TestHelperMod extends BaseTestMod { + public static final String MOD_ID = "test_helper_mod"; public static final ResourceKey TAB = ResourceKey.create(Registries.CREATIVE_MODE_TAB, new ResourceLocation(MOD_ID, "test_items")); - public Empty() { - var modBus = FMLJavaModLoadingContext.get().getModEventBus(); - modBus.register(this); - } - @SubscribeEvent public void onRegister(RegisterEvent event) { event.register(Registries.CREATIVE_MODE_TAB, h -> { diff --git a/src/test/resources/conditional_recipe/data/conditional_recipe/recipes/shapeless_unknown_conditions.json b/src/test/resources/conditional_recipe/data/conditional_recipe/recipes/shapeless_unknown_conditions.json new file mode 100644 index 0000000000..d9e1857518 --- /dev/null +++ b/src/test/resources/conditional_recipe/data/conditional_recipe/recipes/shapeless_unknown_conditions.json @@ -0,0 +1,22 @@ +{ + "type": "minecraft:crafting_shapeless", + "category": "misc", + "forge:condition": { + "type": "forge:this_is_unknown_it_should_not_error_in_log_but_just_fail", + "value": {} + }, + "ingredients": [ + { + "item": "minecraft:redstone_ore" + }, + { + "item": "minecraft:redstone_ore" + }, + { + "item": "minecraft:redstone_ore" + } + ], + "result": { + "item": "minecraft:diamond_block" + } +} \ No newline at end of file diff --git a/src/test_old/java/net/minecraftforge/debug/DataGeneratorTest.java b/src/test_old/java/net/minecraftforge/debug/DataGeneratorTest.java index 1e90131105..d7ef9b5283 100644 --- a/src/test_old/java/net/minecraftforge/debug/DataGeneratorTest.java +++ b/src/test_old/java/net/minecraftforge/debug/DataGeneratorTest.java @@ -158,7 +158,6 @@ public class DataGeneratorTest { gen.addProvider(event.includeClient(), new SoundDefinitions(packOutput, event.getExistingFileHelper())); gen.addProvider(event.includeClient(), new ParticleDescriptions(packOutput, event.getExistingFileHelper())); - //gen.addProvider(event.includeServer(), new Recipes(packOutput)); gen.addProvider(event.includeServer(), new Tags(packOutput, lookupProvider, event.getExistingFileHelper())); gen.addProvider(event.includeServer(), new ForgeAdvancementProvider(packOutput, lookupProvider, event.getExistingFileHelper(), List.of(new Advancements()))); gen.addProvider(event.includeServer(), new DatapackBuiltinEntriesProvider(packOutput, lookupProvider, BUILDER, Set.of(MODID))); @@ -174,188 +173,6 @@ public class DataGeneratorTest { context.register(TEST_LEVEL_STEM, levelStem); } - /* - public static class Recipes extends RecipeProvider implements IConditionBuilder - { - public Recipes(PackOutput gen) - { - super(gen); - } - - @Override - protected void buildRecipes(Consumer consumer) - { - // conditional recipe - ResourceLocation ID = new ResourceLocation("data_gen_test", "conditional"); - - ConditionalRecipe.builder() - .addCondition( - and( - not(modLoaded("minecraft")), - itemExists("minecraft", "dirt"), - FALSE() - ) - ) - .addRecipe( - ShapedRecipeBuilder.shaped(RecipeCategory.BUILDING_BLOCKS, Blocks.DIAMOND_BLOCK, 64) - .pattern("XXX") - .pattern("XXX") - .pattern("XXX") - .define('X', Blocks.DIRT) - .group("") - .unlockedBy("has_dirt", has(Blocks.DIRT)) // DUMMY: Necessary, but not used when a custom advancement is provided through setAdvancement - ::save - ) - .setAdvancement(ID, - ConditionalAdvancement.builder() - .addCondition( - and( - not(modLoaded("minecraft")), - itemExists("minecraft", "dirt"), - FALSE() - ) - ) - .addAdvancement( - Advancement.Builder.advancement() - .parent(new ResourceLocation("minecraft", "root")) - .display(Blocks.DIAMOND_BLOCK, - Component.literal("Dirt2Diamonds"), - Component.literal("The BEST crafting recipe in the game!"), - null, FrameType.TASK, false, false, false - ) - .rewards(AdvancementRewards.Builder.recipe(ID)) - .addCriterion("has_dirt", has(Blocks.DIRT)) - ) - ) - .build(consumer, ID); - - ConditionalRecipe.builder() - .addCondition( - not( - and( - not(modLoaded("minecraft")), - itemExists("minecraft", "dirt"), - FALSE() - ) - ) - ) - .addRecipe( - ShapedRecipeBuilder.shaped(RecipeCategory.BUILDING_BLOCKS, Blocks.DIAMOND_BLOCK, 64) - .pattern("XXX") - .pattern("XXX") - .pattern("XXX") - .define('X', Blocks.DIRT) - .group("") - .unlockedBy("has_dirt", has(Blocks.DIRT)) - ::save - ) - .generateAdvancement() - .build(consumer, new ResourceLocation("data_gen_test", "conditional2")); - - ConditionalRecipe.builder() - .addCondition( - tagEmpty(ItemTags.PLANKS) - ) - .addRecipe( - ShapedRecipeBuilder.shaped(RecipeCategory.BUILDING_BLOCKS, Blocks.NETHERITE_BLOCK, 1) - .pattern("XX") - .pattern("XX") - .define('X', Blocks.DIAMOND_BLOCK) - .group("") - .unlockedBy("has_diamond_block", has(Blocks.DIAMOND_BLOCK)) - ::save - ) - .addCondition( - not( - tagEmpty(ItemTags.PLANKS) - ) - ) - .addRecipe( - ShapedRecipeBuilder.shaped(RecipeCategory.BUILDING_BLOCKS, Blocks.NETHERITE_BLOCK, 9) - .pattern("XX") - .pattern("XX") - .define('X', Blocks.DIAMOND_BLOCK) - .group("") - .unlockedBy("has_diamond_block", has(Blocks.DIAMOND_BLOCK)) - ::save - ) - .generateAdvancement() - .build(consumer, new ResourceLocation("data_gen_test", "conditional3")); - - // ingredient tests - // strict NBT match - should match an unnamed iron pickaxe that lost 3 durability - Ingredient nbtIngredient = StrictNBTIngredient.of(Util.make(() -> - { - ItemStack stack = new ItemStack(Items.IRON_PICKAXE); - stack.setDamageValue(3); - return stack; - })); - ShapelessRecipeBuilder.shapeless(RecipeCategory.TOOLS, Items.GOLDEN_PICKAXE) - .requires(nbtIngredient) - .unlockedBy("has_pickaxe", has(Items.IRON_PICKAXE)) - .save(consumer, new ResourceLocation("data_gen_test", "exact_nbt_ingredient")); - - // ingredient tests - // contains NBT match - should match a stone pickaxe that lost 3 durability, regardless of setting its name - ShapelessRecipeBuilder.shapeless(RecipeCategory.TOOLS, Items.IRON_PICKAXE) - .requires(PartialNBTIngredient.of(Items.STONE_PICKAXE, Util.make(() -> - { - CompoundTag nbt = new CompoundTag(); - nbt.putInt(ItemStack.TAG_DAMAGE, 3); - return nbt; - }))) - .unlockedBy("has_pickaxe", has(Items.STONE_PICKAXE)) - .save(consumer, new ResourceLocation("data_gen_test", "contains_nbt_ingredient_single_item")); - - // contains NBT match - should match a wood, stone, or iron pickaxe that was named "Diamond Pickaxe", regardless of damage - ShapelessRecipeBuilder.shapeless(RecipeCategory.TOOLS, Items.DIAMOND_PICKAXE) - .requires(PartialNBTIngredient.of(Util.make(() -> - { - CompoundTag nbt = new CompoundTag(); - CompoundTag display = new CompoundTag(); - display.putString(ItemStack.TAG_DISPLAY_NAME, "{\"text\":\"Diamond Pickaxe\"}"); - nbt.put(ItemStack.TAG_DISPLAY, display); - return nbt; - }), Items.WOODEN_PICKAXE, Items.STONE_PICKAXE, Items.IRON_PICKAXE)) - .unlockedBy("has_pickaxe", has(Items.WOODEN_PICKAXE)) - .save(consumer, new ResourceLocation("data_gen_test", "contains_nbt_ingredient_item_set")); - - // intersection - should match all non-flammable planks - ShapedRecipeBuilder.shaped(RecipeCategory.BUILDING_BLOCKS, Blocks.NETHERRACK) - .pattern("###") - .pattern("###") - .pattern(" # ") - .define('#', IntersectionIngredient.of(Ingredient.of(ItemTags.PLANKS), Ingredient.of(ItemTags.NON_FLAMMABLE_WOOD))) - .unlockedBy("has_planks", has(Items.CRIMSON_PLANKS)) - .save(consumer, new ResourceLocation("data_gen_test", "intersection_ingredient")); - - // difference - should match all flammable fences - ShapedRecipeBuilder.shaped(RecipeCategory.TOOLS, Items.FLINT_AND_STEEL) - .pattern(" # ") - .pattern("###") - .pattern(" # ") - .define('#', DifferenceIngredient.of(Ingredient.of(ItemTags.FENCES), Ingredient.of(ItemTags.NON_FLAMMABLE_WOOD))) - .unlockedBy("has_fence", has(Items.CRIMSON_FENCE)) - .save(consumer, new ResourceLocation("data_gen_test", "difference_ingredient")); - - // compound - should match planks, logs, or bedrock - ShapedRecipeBuilder.shaped(RecipeCategory.BUILDING_BLOCKS, Blocks.DIRT) - .pattern("###") - .pattern(" # ") - .define('#', CompoundIngredient.of(Ingredient.of(ItemTags.PLANKS), Ingredient.of(ItemTags.LOGS), Ingredient.of(Blocks.BEDROCK))) - .unlockedBy("has_planks", has(Items.CRIMSON_PLANKS)) - .save(consumer, new ResourceLocation("data_gen_test", "compound_ingredient_only_vanilla")); - - // compound - should match planks, logs, or a stone pickaxe with 3 damage - ShapedRecipeBuilder.shaped(RecipeCategory.BUILDING_BLOCKS, Blocks.GOLD_BLOCK) - .pattern("#") - .pattern("#") - .define('#', CompoundIngredient.of(Ingredient.of(ItemTags.PLANKS), Ingredient.of(ItemTags.LOGS), nbtIngredient)) - .unlockedBy("has_planks", has(Items.CRIMSON_PLANKS)) - .save(consumer, new ResourceLocation("data_gen_test", "compound_ingredient_custom_types")); - } - } - */ public static class SoundDefinitions extends SoundDefinitionsProvider {