From baa1972289219c2184ec620d3025693f01ad1848 Mon Sep 17 00:00:00 2001 From: feldenr <135638674+feldenr@users.noreply.github.com> Date: Mon, 20 Jul 2026 23:57:21 +0200 Subject: [PATCH] fix(create): diamond dupe via mixer ignoring ingredient count Create 6.x deserializes item ingredients of processing recipes with plain Ingredient.CODEC, silently ignoring the "count" field. The mixing recipe (9 shards -> 1 diamond, written as one ingredient with count: 9) only consumed 1 shard per diamond; combined with crushing/milling (1 diamond -> ~6 shards) this created an infinite diamond duplication loop. - List the shard 9 times as single-item ingredients (BasinRecipe consumes exactly 1 item per ingredient entry) - Add CreateCompatRecipesTest guarding the invariants: 9 shards per diamond in mixing+crafting, and crushing/milling expected yield stays below 9 shards per diamond (loop can never be net-positive) --- .../mixing/diamond_shard_to_diamond.json | 29 +++- .../customoregen/CreateCompatRecipesTest.java | 139 ++++++++++++++++++ 2 files changed, 165 insertions(+), 3 deletions(-) create mode 100644 src/test/java/net/mcreator/customoregen/CreateCompatRecipesTest.java diff --git a/src/main/resources/data/create/recipe/mixing/diamond_shard_to_diamond.json b/src/main/resources/data/create/recipe/mixing/diamond_shard_to_diamond.json index 71d30b0d..293faa28 100644 --- a/src/main/resources/data/create/recipe/mixing/diamond_shard_to_diamond.json +++ b/src/main/resources/data/create/recipe/mixing/diamond_shard_to_diamond.json @@ -8,8 +8,31 @@ "type": "create:mixing", "ingredients": [ { - "item": "custom_ore_gen:diamondshard", - "count": 9 + "item": "custom_ore_gen:diamondshard" + }, + { + "item": "custom_ore_gen:diamondshard" + }, + { + "item": "custom_ore_gen:diamondshard" + }, + { + "item": "custom_ore_gen:diamondshard" + }, + { + "item": "custom_ore_gen:diamondshard" + }, + { + "item": "custom_ore_gen:diamondshard" + }, + { + "item": "custom_ore_gen:diamondshard" + }, + { + "item": "custom_ore_gen:diamondshard" + }, + { + "item": "custom_ore_gen:diamondshard" } ], "results": [ @@ -19,4 +42,4 @@ } ], "processing_time": 100 -} \ No newline at end of file +} diff --git a/src/test/java/net/mcreator/customoregen/CreateCompatRecipesTest.java b/src/test/java/net/mcreator/customoregen/CreateCompatRecipesTest.java new file mode 100644 index 00000000..413ec541 --- /dev/null +++ b/src/test/java/net/mcreator/customoregen/CreateCompatRecipesTest.java @@ -0,0 +1,139 @@ +package net.mcreator.customoregen; + +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.io.InputStream; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Regression tests for the Create mod compatibility recipes (diamond shard cycle). + * + * Background: Create 6.x deserializes ITEM ingredients of processing recipes with + * plain {@code Ingredient.CODEC} (see ProcessingRecipeParams), which silently + * IGNORES the "count" field. A mixing recipe written as a single ingredient with + * "count": 9 was therefore consuming only 1 shard per diamond. Combined with the + * crushing/milling recipes (1 diamond -> ~6 shards), this created an infinite + * diamond duplication loop. + * + * Fix: the mixing recipe must list the shard 9 times as separate single-item + * ingredients (BasinRecipe consumes exactly 1 item per ingredient entry). + * + * These tests guard the invariant: SHARDS_PER_DIAMOND = 9 everywhere, and the + * expected yield of diamond -> shards processing must stay BELOW 9 so the cycle + * can never be net-positive. + */ +@DisplayName("Create Compat Recipes Tests") +class CreateCompatRecipesTest { + + private static final int SHARDS_PER_DIAMOND = 9; + private static final String SHARD_ITEM = "custom_ore_gen:diamondshard"; + private static final String DIAMOND_ITEM = "minecraft:diamond"; + + private static final String MIXING_RECIPE = "/data/create/recipe/mixing/diamond_shard_to_diamond.json"; + private static final String CRUSHING_RECIPE = "/data/create/recipe/crushing/diamond_to_shards.json"; + private static final String MILLING_RECIPE = "/data/create/recipe/milling/diamond_to_shards.json"; + private static final String CRAFTING_RECIPE = "/data/custom_ore_gen/recipe/diamondshardtodiamond.json"; + + private static JsonObject loadRecipe(String path) { + InputStream stream = CreateCompatRecipesTest.class.getResourceAsStream(path); + assertNotNull(stream, "Recipe resource missing from classpath: " + path); + return JsonParser.parseReader(new InputStreamReader(stream, StandardCharsets.UTF_8)).getAsJsonObject(); + } + + private static double expectedShardYield(JsonObject recipe) { + double yield = 0.0; + for (JsonElement element : recipe.getAsJsonArray("results")) { + JsonObject result = element.getAsJsonObject(); + if (!result.has("id") || !SHARD_ITEM.equals(result.get("id").getAsString())) { + continue; + } + int count = result.has("count") ? result.get("count").getAsInt() : 1; + double chance = result.has("chance") ? result.get("chance").getAsDouble() : 1.0; + yield += count * chance; + } + return yield; + } + + @Test + @DisplayName("Mixing recipe consumes exactly 9 shards as 9 single-item ingredients") + void testMixingRecipe_ShouldConsumeNineShardsViaNineEntries() { + JsonObject recipe = loadRecipe(MIXING_RECIPE); + assertEquals("create:mixing", recipe.get("type").getAsString()); + + JsonArray ingredients = recipe.getAsJsonArray("ingredients"); + assertNotNull(ingredients, "Mixing recipe must declare ingredients"); + assertEquals(SHARDS_PER_DIAMOND, ingredients.size(), + "Mixing recipe must list the shard 9 times (Create ignores 'count' on item ingredients)"); + + for (JsonElement element : ingredients) { + JsonObject ingredient = element.getAsJsonObject(); + assertEquals(SHARD_ITEM, ingredient.get("item").getAsString(), + "Every mixing ingredient must be a diamond shard"); + assertFalse(ingredient.has("count"), + "Never use 'count' on Create item ingredients: it is silently ignored (dupe exploit)"); + } + } + + @Test + @DisplayName("Mixing recipe outputs exactly 1 diamond") + void testMixingRecipe_ShouldOutputOneDiamond() { + JsonObject recipe = loadRecipe(MIXING_RECIPE); + JsonArray results = recipe.getAsJsonArray("results"); + assertEquals(1, results.size(), "Mixing recipe must have a single result"); + JsonObject result = results.get(0).getAsJsonObject(); + assertEquals(DIAMOND_ITEM, result.get("id").getAsString()); + assertEquals(1, result.get("count").getAsInt(), "9 shards must yield exactly 1 diamond"); + } + + @Test + @DisplayName("Vanilla crafting recipe uses 9 shards for 1 diamond") + void testCraftingRecipe_ShouldUseNineShards() { + JsonObject recipe = loadRecipe(CRAFTING_RECIPE); + assertEquals("minecraft:crafting_shaped", recipe.get("type").getAsString()); + + JsonObject key = recipe.getAsJsonObject("key"); + assertEquals(SHARD_ITEM, key.getAsJsonObject("a").get("item").getAsString()); + + int shardSlots = 0; + for (JsonElement row : recipe.getAsJsonArray("pattern")) { + for (char c : row.getAsString().toCharArray()) { + if (c == 'a') { + shardSlots++; + } + } + } + assertEquals(SHARDS_PER_DIAMOND, shardSlots, "Crafting pattern must contain 9 shard slots"); + + JsonObject result = recipe.getAsJsonObject("result"); + assertEquals(DIAMOND_ITEM, result.get("id").getAsString()); + assertEquals(1, result.get("count").getAsInt()); + } + + @Test + @DisplayName("Crushing a diamond yields less than 9 shards on average (no dupe loop)") + void testCrushingRecipe_ShouldStayBelowDupeThreshold() { + double yield = expectedShardYield(loadRecipe(CRUSHING_RECIPE)); + assertTrue(yield > 0, "Crushing recipe must produce shards"); + assertTrue(yield < SHARDS_PER_DIAMOND, + "Crushing yield (" + yield + ") must stay below " + SHARDS_PER_DIAMOND + + " shards per diamond or the mixer/crusher cycle duplicates diamonds"); + } + + @Test + @DisplayName("Milling a diamond yields less than 9 shards on average (no dupe loop)") + void testMillingRecipe_ShouldStayBelowDupeThreshold() { + double yield = expectedShardYield(loadRecipe(MILLING_RECIPE)); + assertTrue(yield > 0, "Milling recipe must produce shards"); + assertTrue(yield < SHARDS_PER_DIAMOND, + "Milling yield (" + yield + ") must stay below " + SHARDS_PER_DIAMOND + + " shards per diamond or the mixer/millstone cycle duplicates diamonds"); + } +}