From 4a25fa30792a1a25fae291758532f052c8b7d728 Mon Sep 17 00:00:00 2001 From: Factorio Blueprints Date: Sat, 25 Jul 2026 04:12:32 +0000 Subject: [PATCH 1/5] Stop total raw recipe calculation from recursing through cycles. --- .../demod/factorio/TotalRawCalculator.java | 53 +++++++++++-------- 1 file changed, 31 insertions(+), 22 deletions(-) diff --git a/FactorioDataWrapper/src/com/demod/factorio/TotalRawCalculator.java b/FactorioDataWrapper/src/com/demod/factorio/TotalRawCalculator.java index 1858fcc..0a7a2e9 100644 --- a/FactorioDataWrapper/src/com/demod/factorio/TotalRawCalculator.java +++ b/FactorioDataWrapper/src/com/demod/factorio/TotalRawCalculator.java @@ -1,6 +1,7 @@ package com.demod.factorio; import java.util.LinkedHashMap; +import java.util.LinkedHashSet; import java.util.Map; import java.util.Map.Entry; import java.util.Optional; @@ -10,39 +11,47 @@ public class TotalRawCalculator { public static final String RAW_TIME = "_TIME_"; private final Map recipes; - private final Map> recipeTotalRaws = new LinkedHashMap<>(); public TotalRawCalculator(Map recipes) { this.recipes = recipes; } public Map compute(RecipePrototype recipe) { + return compute(recipe, new LinkedHashSet<>()); + } + + private Map compute(RecipePrototype recipe, Set expandedRecipeNames) { Map totalRaw = new LinkedHashMap<>(); - recipeTotalRaws.put(recipe.getName(), totalRaw); totalRaw.put(TotalRawCalculator.RAW_TIME, recipe.getEnergyRequired()); + expandedRecipeNames.add(recipe.getName()); - for (Entry entry : recipe.getInputs().entrySet()) { - String input = entry.getKey(); - Optional findRecipe = recipes.values().stream() - // XXX the nutrients-from-fish filter is here to match the bad Factorio behavior - // of picking nutrients from biter eggs - .filter(r -> !r.getName().equals("nutrients-from-fish")).filter(RecipePrototype::isHandCraftable) - .filter(r -> r.getOutputs().keySet().stream().anyMatch(i -> { - return i.equals(input); - })).findFirst(); - if (findRecipe.isPresent()) { - RecipePrototype inputRecipe = findRecipe.get(); - Map inputTotalRaw = compute(inputRecipe); - Double inputRunYield = inputRecipe.getOutputs().get(input); - double inputRunCount = entry.getValue() / inputRunYield; - inputTotalRaw.forEach((k, v) -> { - totalRaw.put(k, totalRaw.getOrDefault(k, 0.0) + v * inputRunCount); - }); - } else { - totalRaw.put(input, totalRaw.getOrDefault(input, 0.0) + entry.getValue()); + try { + for (Entry entry : recipe.getInputs().entrySet()) { + String input = entry.getKey(); + Optional findRecipe = recipes.values().stream() + // XXX the nutrients-from-fish filter is here to match the bad Factorio behavior + // of picking nutrients from biter eggs + .filter(r -> !r.getName().equals("nutrients-from-fish")) + .filter(RecipePrototype::isHandCraftable) + .filter(r -> r.getOutputs().keySet().stream().anyMatch(i -> { + return i.equals(input); + })).findFirst(); + if (findRecipe.isPresent() && !expandedRecipeNames.contains(findRecipe.get().getName())) { + RecipePrototype inputRecipe = findRecipe.get(); + Map inputTotalRaw = compute(inputRecipe, expandedRecipeNames); + Double inputRunYield = inputRecipe.getOutputs().get(input); + double inputRunCount = entry.getValue() / inputRunYield; + inputTotalRaw.forEach((k, v) -> { + totalRaw.put(k, totalRaw.getOrDefault(k, 0.0) + v * inputRunCount); + }); + } else { + totalRaw.put(input, totalRaw.getOrDefault(input, 0.0) + entry.getValue()); + } } + } finally { + expandedRecipeNames.remove(recipe.getName()); } return totalRaw; } -} \ No newline at end of file +} From 7dc013d3f4bd9e17a933a5791a7b7f981d405a92 Mon Sep 17 00:00:00 2001 From: Factorio Blueprints Date: Sun, 26 Jul 2026 03:14:24 +0000 Subject: [PATCH 2/5] Honor recipe decomposition rules when calculating total raw materials. --- FactorioDataWrapper/pom.xml | 7 ++ .../demod/factorio/TotalRawCalculator.java | 8 +- .../factorio/prototype/RecipePrototype.java | 6 ++ .../factorio/TotalRawCalculatorTest.java | 81 +++++++++++++++++++ 4 files changed, 98 insertions(+), 4 deletions(-) create mode 100644 FactorioDataWrapper/test/com/demod/factorio/TotalRawCalculatorTest.java diff --git a/FactorioDataWrapper/pom.xml b/FactorioDataWrapper/pom.xml index d6e9293..f91fc80 100644 --- a/FactorioDataWrapper/pom.xml +++ b/FactorioDataWrapper/pom.xml @@ -5,6 +5,7 @@ 0.0.1-SNAPSHOT src + test lua @@ -40,5 +41,11 @@ guava 21.0 + + junit + junit + 4.13.2 + test + diff --git a/FactorioDataWrapper/src/com/demod/factorio/TotalRawCalculator.java b/FactorioDataWrapper/src/com/demod/factorio/TotalRawCalculator.java index 0a7a2e9..32a0c85 100644 --- a/FactorioDataWrapper/src/com/demod/factorio/TotalRawCalculator.java +++ b/FactorioDataWrapper/src/com/demod/factorio/TotalRawCalculator.java @@ -32,11 +32,11 @@ private Map compute(RecipePrototype recipe, Set expanded // XXX the nutrients-from-fish filter is here to match the bad Factorio behavior // of picking nutrients from biter eggs .filter(r -> !r.getName().equals("nutrients-from-fish")) + .filter(RecipePrototype::isDecomposable) .filter(RecipePrototype::isHandCraftable) - .filter(r -> r.getOutputs().keySet().stream().anyMatch(i -> { - return i.equals(input); - })).findFirst(); - if (findRecipe.isPresent() && !expandedRecipeNames.contains(findRecipe.get().getName())) { + .filter(r -> r.getOutputs().containsKey(input)) + .filter(r -> !expandedRecipeNames.contains(r.getName())).findFirst(); + if (findRecipe.isPresent()) { RecipePrototype inputRecipe = findRecipe.get(); Map inputTotalRaw = compute(inputRecipe, expandedRecipeNames); Double inputRunYield = inputRecipe.getOutputs().get(input); diff --git a/FactorioDataWrapper/src/com/demod/factorio/prototype/RecipePrototype.java b/FactorioDataWrapper/src/com/demod/factorio/prototype/RecipePrototype.java index 08ed51c..8ad9444 100644 --- a/FactorioDataWrapper/src/com/demod/factorio/prototype/RecipePrototype.java +++ b/FactorioDataWrapper/src/com/demod/factorio/prototype/RecipePrototype.java @@ -14,6 +14,7 @@ public class RecipePrototype extends DataPrototype { private final String category; private final Map inputs = new LinkedHashMap<>(); private final Map outputs = new LinkedHashMap<>(); + private final boolean decomposable; private final double energyRequired; private final boolean handCraftable; private final boolean recycling; @@ -47,6 +48,7 @@ public RecipePrototype(LuaTable lua) { } energyRequired = lua.get("energy_required").optdouble(0.5); + decomposable = lua.get("allow_decomposition").optboolean(true); category = lua.get("category").optjstring("crafting"); // FIXME get these from the character prototype handCraftable = category.equals("crafting") || category.equals("electronics") || category.equals("pressing") @@ -59,6 +61,10 @@ public String getCategory() { return category; } + public boolean isDecomposable() { + return decomposable; + } + public double getEnergyRequired() { return energyRequired; } diff --git a/FactorioDataWrapper/test/com/demod/factorio/TotalRawCalculatorTest.java b/FactorioDataWrapper/test/com/demod/factorio/TotalRawCalculatorTest.java new file mode 100644 index 0000000..908ba97 --- /dev/null +++ b/FactorioDataWrapper/test/com/demod/factorio/TotalRawCalculatorTest.java @@ -0,0 +1,81 @@ +package com.demod.factorio; + +import static org.junit.Assert.assertEquals; + +import java.util.LinkedHashMap; +import java.util.Map; + +import org.json.JSONArray; +import org.json.JSONObject; +import org.junit.Test; + +import com.demod.factorio.fakelua.LuaTable; +import com.demod.factorio.prototype.RecipePrototype; + +public class TotalRawCalculatorTest { + + @Test + public void ignoresRecipesThatDisallowDecomposition() { + RecipePrototype root = recipe("root", "crafting", true, 1, map("intermediate", 2), + map("root", 1)); + RecipePrototype recycling = recipe("intermediate-recycling", "recycling", false, 1, + map("intermediate", 1), map("intermediate", 1)); + RecipePrototype intermediate = recipe("intermediate", "crafting", true, 3, map("ore", 4), + map("intermediate", 1)); + Map recipes = recipes(root, recycling, intermediate); + + Map totalRaw = new TotalRawCalculator(recipes).compute(root); + + assertEquals(Map.of(TotalRawCalculator.RAW_TIME, 7.0, "ore", 8.0), totalRaw); + } + + @Test + public void treatsIngredientsAsRawWhenTheirRecipesDisallowDecomposition() { + RecipePrototype root = recipe("root", "crafting", true, 2, map("asteroid-chunk", 3), + map("root", 1)); + RecipePrototype crushing = recipe("asteroid-crushing", "crafting", false, 5, + map("asteroid-chunk", 1), map("asteroid-chunk", 1)); + Map recipes = recipes(root, crushing); + + Map totalRaw = new TotalRawCalculator(recipes).compute(root); + + assertEquals(Map.of(TotalRawCalculator.RAW_TIME, 2.0, "asteroid-chunk", 3.0), totalRaw); + } + + private static Map map(String name, int amount) { + return Map.of(name, amount); + } + + private static Map recipes(RecipePrototype... recipes) { + Map result = new LinkedHashMap<>(); + for (RecipePrototype recipe : recipes) { + result.put(recipe.getName(), recipe); + } + return result; + } + + private static RecipePrototype recipe(String name, String category, boolean decomposable, double energyRequired, + Map inputs, Map outputs) { + JSONObject json = new JSONObject(); + json.put("type", "recipe"); + json.put("name", name); + json.put("category", category); + json.put("allow_decomposition", decomposable); + json.put("energy_required", energyRequired); + json.put("ingredients", ingredients(inputs)); + json.put("results", results(outputs)); + return new RecipePrototype(new LuaTable(json)); + } + + private static JSONArray ingredients(Map inputs) { + JSONArray ingredients = new JSONArray(); + inputs.forEach((name, amount) -> ingredients.put(new JSONObject().put("name", name).put("amount", amount))); + return ingredients; + } + + private static JSONArray results(Map outputs) { + JSONArray results = new JSONArray(); + outputs.forEach((name, amount) -> results.put(new JSONObject().put("name", name).put("amount", amount))); + return results; + } +} From 25f6e7ed4f50f30e27fecb7492daa45f26575da6 Mon Sep 17 00:00:00 2001 From: Factorio Blueprints Date: Sun, 26 Jul 2026 14:16:40 +0000 Subject: [PATCH 3/5] Document Factorio total raw recipe selection behavior. --- .../src/com/demod/factorio/TotalRawCalculator.java | 6 ++++-- .../com/demod/factorio/TotalRawCalculatorTest.java | 14 ++++++++++++++ 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/FactorioDataWrapper/src/com/demod/factorio/TotalRawCalculator.java b/FactorioDataWrapper/src/com/demod/factorio/TotalRawCalculator.java index 32a0c85..c8cfc96 100644 --- a/FactorioDataWrapper/src/com/demod/factorio/TotalRawCalculator.java +++ b/FactorioDataWrapper/src/com/demod/factorio/TotalRawCalculator.java @@ -29,9 +29,11 @@ private Map compute(RecipePrototype recipe, Set expanded for (Entry entry : recipe.getInputs().entrySet()) { String input = entry.getKey(); Optional findRecipe = recipes.values().stream() - // XXX the nutrients-from-fish filter is here to match the bad Factorio behavior - // of picking nutrients from biter eggs + // Factorio prefers nutrients-from-biter-egg over nutrients-from-fish for Total raw. .filter(r -> !r.getName().equals("nutrients-from-fish")) + // Factorio's allow_decomposition controls whether a recipe is expanded for the tooltip's + // "Total raw" calculation: + // https://lua-api.factorio.com/latest/prototypes/RecipePrototype.html#allow_decomposition .filter(RecipePrototype::isDecomposable) .filter(RecipePrototype::isHandCraftable) .filter(r -> r.getOutputs().containsKey(input)) diff --git a/FactorioDataWrapper/test/com/demod/factorio/TotalRawCalculatorTest.java b/FactorioDataWrapper/test/com/demod/factorio/TotalRawCalculatorTest.java index 908ba97..e0befb6 100644 --- a/FactorioDataWrapper/test/com/demod/factorio/TotalRawCalculatorTest.java +++ b/FactorioDataWrapper/test/com/demod/factorio/TotalRawCalculatorTest.java @@ -42,6 +42,20 @@ public void treatsIngredientsAsRawWhenTheirRecipesDisallowDecomposition() { assertEquals(Map.of(TotalRawCalculator.RAW_TIME, 2.0, "asteroid-chunk", 3.0), totalRaw); } + @Test + public void prefersNutrientsFromBiterEggsOverNutrientsFromFish() { + RecipePrototype root = recipe("root", "crafting", true, 1, map("nutrients", 20), map("root", 1)); + RecipePrototype fish = recipe("nutrients-from-fish", "crafting", true, 2, map("raw-fish", 1), + map("nutrients", 20)); + RecipePrototype biterEgg = recipe("nutrients-from-biter-egg", "crafting", true, 3, map("biter-egg", 1), + map("nutrients", 20)); + Map recipes = recipes(root, fish, biterEgg); + + Map totalRaw = new TotalRawCalculator(recipes).compute(root); + + assertEquals(Map.of(TotalRawCalculator.RAW_TIME, 4.0, "biter-egg", 1.0), totalRaw); + } + private static Map map(String name, int amount) { return Map.of(name, amount); } From 6b555d304e956ae9dbede8e8aaf0c1320f7a9b91 Mon Sep 17 00:00:00 2001 From: Factorio Blueprints Date: Sun, 26 Jul 2026 14:38:46 +0000 Subject: [PATCH 4/5] Restore hand craftability, reject recycling recipes, and keep nutrients terminal. --- .../demod/factorio/TotalRawCalculator.java | 27 +++++++---- .../factorio/TotalRawCalculatorTest.java | 46 +++++++++++++++++-- 2 files changed, 59 insertions(+), 14 deletions(-) diff --git a/FactorioDataWrapper/src/com/demod/factorio/TotalRawCalculator.java b/FactorioDataWrapper/src/com/demod/factorio/TotalRawCalculator.java index c8cfc96..58e0bea 100644 --- a/FactorioDataWrapper/src/com/demod/factorio/TotalRawCalculator.java +++ b/FactorioDataWrapper/src/com/demod/factorio/TotalRawCalculator.java @@ -5,6 +5,7 @@ import java.util.Map; import java.util.Map.Entry; import java.util.Optional; +import java.util.Set; import com.demod.factorio.prototype.RecipePrototype; @@ -28,16 +29,7 @@ private Map compute(RecipePrototype recipe, Set expanded try { for (Entry entry : recipe.getInputs().entrySet()) { String input = entry.getKey(); - Optional findRecipe = recipes.values().stream() - // Factorio prefers nutrients-from-biter-egg over nutrients-from-fish for Total raw. - .filter(r -> !r.getName().equals("nutrients-from-fish")) - // Factorio's allow_decomposition controls whether a recipe is expanded for the tooltip's - // "Total raw" calculation: - // https://lua-api.factorio.com/latest/prototypes/RecipePrototype.html#allow_decomposition - .filter(RecipePrototype::isDecomposable) - .filter(RecipePrototype::isHandCraftable) - .filter(r -> r.getOutputs().containsKey(input)) - .filter(r -> !expandedRecipeNames.contains(r.getName())).findFirst(); + Optional findRecipe = findRecipe(input, expandedRecipeNames); if (findRecipe.isPresent()) { RecipePrototype inputRecipe = findRecipe.get(); Map inputTotalRaw = compute(inputRecipe, expandedRecipeNames); @@ -56,4 +48,19 @@ private Map compute(RecipePrototype recipe, Set expanded return totalRaw; } + + private Optional findRecipe(String input, Set expandedRecipeNames) { + // Nutrients have several context-dependent production recipes, so no one recipe is canonical. + if (input.equals("nutrients")) { + return Optional.empty(); + } + return recipes.values().stream() + // Factorio's allow_decomposition controls whether a recipe is expanded for the tooltip's + // "Total raw" calculation: + // https://lua-api.factorio.com/latest/prototypes/RecipePrototype.html#allow_decomposition + .filter(RecipePrototype::isDecomposable) + .filter(RecipePrototype::isHandCraftable).filter(r -> !r.isRecycling()) + .filter(r -> r.getOutputs().containsKey(input)) + .filter(r -> !expandedRecipeNames.contains(r.getName())).findFirst(); + } } diff --git a/FactorioDataWrapper/test/com/demod/factorio/TotalRawCalculatorTest.java b/FactorioDataWrapper/test/com/demod/factorio/TotalRawCalculatorTest.java index e0befb6..2ef64b0 100644 --- a/FactorioDataWrapper/test/com/demod/factorio/TotalRawCalculatorTest.java +++ b/FactorioDataWrapper/test/com/demod/factorio/TotalRawCalculatorTest.java @@ -43,17 +43,55 @@ public void treatsIngredientsAsRawWhenTheirRecipesDisallowDecomposition() { } @Test - public void prefersNutrientsFromBiterEggsOverNutrientsFromFish() { + public void treatsIngredientsAsRawWhenTheirRecipesAreNotHandCraftable() { + RecipePrototype root = recipe("root", "crafting", true, 2, map("intermediate", 3), map("root", 1)); + RecipePrototype machineRecipe = recipe("intermediate", "metallurgy", true, 5, map("ore", 4), + map("intermediate", 1)); + Map recipes = recipes(root, machineRecipe); + + Map totalRaw = new TotalRawCalculator(recipes).compute(root); + + assertEquals(Map.of(TotalRawCalculator.RAW_TIME, 2.0, "intermediate", 3.0), totalRaw); + } + + @Test + public void ignoresRecyclingRecipes() { + RecipePrototype root = recipe("root", "crafting", true, 1, map("intermediate", 2), map("root", 1)); + RecipePrototype recycling = recipe("intermediate-recycling", "recycling", true, 1, + map("intermediate", 1), map("intermediate", 1)); + RecipePrototype intermediate = recipe("intermediate", "crafting", true, 3, map("ore", 4), + map("intermediate", 1)); + Map recipes = recipes(root, recycling, intermediate); + + Map totalRaw = new TotalRawCalculator(recipes).compute(root); + + assertEquals(Map.of(TotalRawCalculator.RAW_TIME, 7.0, "ore", 8.0), totalRaw); + } + + @Test + public void treatsTheRepeatedIngredientAsRawWhenRecipesFormACycle() { + RecipePrototype root = recipe("root", "crafting", true, 1, map("first", 1), map("root", 1)); + RecipePrototype first = recipe("first", "crafting", true, 2, map("second", 1), map("first", 1)); + RecipePrototype second = recipe("second", "crafting", true, 3, map("first", 1), map("second", 1)); + Map recipes = recipes(root, first, second); + + Map totalRaw = new TotalRawCalculator(recipes).compute(root); + + assertEquals(Map.of(TotalRawCalculator.RAW_TIME, 6.0, "first", 1.0), totalRaw); + } + + @Test + public void treatsNutrientsAsRawDespiteDecomposableRecipes() { RecipePrototype root = recipe("root", "crafting", true, 1, map("nutrients", 20), map("root", 1)); RecipePrototype fish = recipe("nutrients-from-fish", "crafting", true, 2, map("raw-fish", 1), map("nutrients", 20)); - RecipePrototype biterEgg = recipe("nutrients-from-biter-egg", "crafting", true, 3, map("biter-egg", 1), + RecipePrototype bioflux = recipe("nutrients-from-bioflux", "organic", true, 3, map("bioflux", 5), map("nutrients", 20)); - Map recipes = recipes(root, fish, biterEgg); + Map recipes = recipes(root, fish, bioflux); Map totalRaw = new TotalRawCalculator(recipes).compute(root); - assertEquals(Map.of(TotalRawCalculator.RAW_TIME, 4.0, "biter-egg", 1.0), totalRaw); + assertEquals(Map.of(TotalRawCalculator.RAW_TIME, 1.0, "nutrients", 20.0), totalRaw); } private static Map map(String name, int amount) { From 3b07b893237ebae27c440570493cedad473ae949 Mon Sep 17 00:00:00 2001 From: Factorio Blueprints Date: Sun, 26 Jul 2026 19:25:43 +0000 Subject: [PATCH 5/5] Match Factorio nutrient decomposition and recipe cycle selection. --- .../demod/factorio/TotalRawCalculator.java | 12 +++---- .../factorio/TotalRawCalculatorTest.java | 31 +++++++++++++++++-- 2 files changed, 32 insertions(+), 11 deletions(-) diff --git a/FactorioDataWrapper/src/com/demod/factorio/TotalRawCalculator.java b/FactorioDataWrapper/src/com/demod/factorio/TotalRawCalculator.java index 58e0bea..ca91c56 100644 --- a/FactorioDataWrapper/src/com/demod/factorio/TotalRawCalculator.java +++ b/FactorioDataWrapper/src/com/demod/factorio/TotalRawCalculator.java @@ -29,8 +29,8 @@ private Map compute(RecipePrototype recipe, Set expanded try { for (Entry entry : recipe.getInputs().entrySet()) { String input = entry.getKey(); - Optional findRecipe = findRecipe(input, expandedRecipeNames); - if (findRecipe.isPresent()) { + Optional findRecipe = findRecipe(input); + if (findRecipe.isPresent() && !expandedRecipeNames.contains(findRecipe.get().getName())) { RecipePrototype inputRecipe = findRecipe.get(); Map inputTotalRaw = compute(inputRecipe, expandedRecipeNames); Double inputRunYield = inputRecipe.getOutputs().get(input); @@ -49,11 +49,7 @@ private Map compute(RecipePrototype recipe, Set expanded return totalRaw; } - private Optional findRecipe(String input, Set expandedRecipeNames) { - // Nutrients have several context-dependent production recipes, so no one recipe is canonical. - if (input.equals("nutrients")) { - return Optional.empty(); - } + private Optional findRecipe(String input) { return recipes.values().stream() // Factorio's allow_decomposition controls whether a recipe is expanded for the tooltip's // "Total raw" calculation: @@ -61,6 +57,6 @@ private Optional findRecipe(String input, Set expandedR .filter(RecipePrototype::isDecomposable) .filter(RecipePrototype::isHandCraftable).filter(r -> !r.isRecycling()) .filter(r -> r.getOutputs().containsKey(input)) - .filter(r -> !expandedRecipeNames.contains(r.getName())).findFirst(); + .max(RecipePrototype::compareTo); } } diff --git a/FactorioDataWrapper/test/com/demod/factorio/TotalRawCalculatorTest.java b/FactorioDataWrapper/test/com/demod/factorio/TotalRawCalculatorTest.java index 2ef64b0..e86935f 100644 --- a/FactorioDataWrapper/test/com/demod/factorio/TotalRawCalculatorTest.java +++ b/FactorioDataWrapper/test/com/demod/factorio/TotalRawCalculatorTest.java @@ -4,6 +4,7 @@ import java.util.LinkedHashMap; import java.util.Map; +import java.util.Optional; import org.json.JSONArray; import org.json.JSONObject; @@ -81,7 +82,23 @@ public void treatsTheRepeatedIngredientAsRawWhenRecipesFormACycle() { } @Test - public void treatsNutrientsAsRawDespiteDecomposableRecipes() { + public void decomposesNutrientsUsingTheLastEligibleRecipeInPrototypeOrder() { + RecipePrototype root = recipe("root", "crafting", true, 1, map("nutrients", 100), map("root", 1)); + RecipePrototype fish = recipe("nutrients-from-fish", "c[nutrients-from-fish]", "crafting", true, 2, + map("raw-fish", 1), map("nutrients", 20)); + RecipePrototype biterEgg = recipe("nutrients-from-biter-egg", "d[nutrients-from-biter-egg]", "crafting", true, + 2, map("biter-egg", 1), map("nutrients", 20)); + RecipePrototype biterEggProduction = recipe("biter-egg", "captive-spawner-process", true, 10, Map.of(), + map("biter-egg", 5)); + Map recipes = recipes(root, biterEgg, fish, biterEggProduction); + + Map totalRaw = new TotalRawCalculator(recipes).compute(root); + + assertEquals(Map.of(TotalRawCalculator.RAW_TIME, 11.0, "biter-egg", 5.0), totalRaw); + } + + @Test + public void decomposesNutrientsThroughFishWhenThatIsTheOnlyEligibleRecipe() { RecipePrototype root = recipe("root", "crafting", true, 1, map("nutrients", 20), map("root", 1)); RecipePrototype fish = recipe("nutrients-from-fish", "crafting", true, 2, map("raw-fish", 1), map("nutrients", 20)); @@ -91,7 +108,7 @@ public void treatsNutrientsAsRawDespiteDecomposableRecipes() { Map totalRaw = new TotalRawCalculator(recipes).compute(root); - assertEquals(Map.of(TotalRawCalculator.RAW_TIME, 1.0, "nutrients", 20.0), totalRaw); + assertEquals(Map.of(TotalRawCalculator.RAW_TIME, 3.0, "raw-fish", 1.0), totalRaw); } private static Map map(String name, int amount) { @@ -108,15 +125,23 @@ private static Map recipes(RecipePrototype... recipes) private static RecipePrototype recipe(String name, String category, boolean decomposable, double energyRequired, Map inputs, Map outputs) { + return recipe(name, name, category, decomposable, energyRequired, inputs, outputs); + } + + private static RecipePrototype recipe(String name, String order, String category, boolean decomposable, + double energyRequired, Map inputs, Map outputs) { JSONObject json = new JSONObject(); json.put("type", "recipe"); json.put("name", name); + json.put("order", order); json.put("category", category); json.put("allow_decomposition", decomposable); json.put("energy_required", energyRequired); json.put("ingredients", ingredients(inputs)); json.put("results", results(outputs)); - return new RecipePrototype(new LuaTable(json)); + RecipePrototype recipe = new RecipePrototype(new LuaTable(json)); + recipe.setGroup(Optional.empty()); + return recipe; } private static JSONArray ingredients(Map inputs) {