From 5cb64283ea2112055fc908522818dcd9791fee2c Mon Sep 17 00:00:00 2001 From: Drefvelin <54400154+Drefvelin@users.noreply.github.com> Date: Thu, 24 Sep 2026 13:30:47 +0000 Subject: [PATCH] fix: start slaughtered roasts whole, then bone after the meat Low genetics skipped to the last carve step, so the drop was only bone. Roasts now begin on the full model, keep a meat floor (one cut, or two legs and a filet for poultry), and close on the bone. Co-authored-by: Cursor --- .../cooking/carve/CarvableRoastUtils.java | 101 ++++++++++++- .../cooking/carve/CarveSequence.java | 11 ++ .../cooking/husbandry/HusbandryHarvest.java | 14 +- .../cooking/loader/CarveSequenceLoader.java | 13 +- src/main/resources/carve-sequences.yml | 4 + src/main/resources/husbandry.yml | 2 + .../cooking/item/RoastPortionTest.java | 140 ++++++++++++++++++ .../loader/CarveSequenceLoaderTest.java | 14 ++ 8 files changed, 282 insertions(+), 17 deletions(-) create mode 100644 src/test/java/net/tfminecraft/cooking/item/RoastPortionTest.java create mode 100644 src/test/java/net/tfminecraft/cooking/loader/CarveSequenceLoaderTest.java diff --git a/src/main/java/net/tfminecraft/cooking/carve/CarvableRoastUtils.java b/src/main/java/net/tfminecraft/cooking/carve/CarvableRoastUtils.java index 5d2d936..9780f31 100644 --- a/src/main/java/net/tfminecraft/cooking/carve/CarvableRoastUtils.java +++ b/src/main/java/net/tfminecraft/cooking/carve/CarvableRoastUtils.java @@ -77,7 +77,7 @@ public static double remainingFood(FoodItem item, CarveSequence seq) { int edible = Math.max(1, countFoodCuts(seq)); return item.getBaseFood() * countFoodCutsFrom(seq, item.getCarveNextIndex()) / (double) edible; } - return seq.sumRemainingFood(item.getCarveNextIndex()); + return budgetedFood(seq, item.getCarveNextIndex(), budgetedFoodCuts(item, seq)); } public static double portionFood(double totalFood, int edibleCuts) { @@ -103,9 +103,62 @@ public static int countFoodCutsFrom(CarveSequence seq, int fromIndex) { return count; } - /** Visual stage for IA models: raw_1 = whole bird, raw_6 = mostly carved. */ + /** + * Meat portions a slaughtered animal yields, then the closing bone when the sequence has one. + * The roast always starts at the first cut. Genetics only changes how many meat cuts follow + * before that bone, never below the sequence floor. + */ + public record RoastPortion(int nextIndex, int remaining) {} + + public static RoastPortion portion(CarveSequence seq, int requestedMeatCuts) { + int food = countFoodCuts(seq); + int min = food == 0 ? 0 : Math.min(food, Math.max(1, seq.getMinFoodCuts())); + int meat = Math.max(min, Math.min(food, requestedMeatCuts)); + int bone = trailingItemIndex(seq) >= 0 ? 1 : 0; + return new RoastPortion(0, meat + bone); + } + + /** Index of a non-food cut that closes the sequence, or -1 when every cut is meat. */ + public static int trailingItemIndex(CarveSequence seq) { + if (seq == null) return -1; + List cuts = seq.getCuts(); + for (int i = cuts.size() - 1; i >= 0; i--) { + CarveCut cut = cuts.get(i); + if (cut.isItemCut()) return i; + if (cut.isFoodCut()) return -1; + } + return -1; + } + + /** Visual stage for IA models: stage 1 is the whole roast, the last stage is bone. */ public static int getVisualCarveStage(FoodItem item) { - return item.getCarveNextIndex() + 1; + return visualCarveStage(item, getSequence(item)); + } + + public static int visualCarveStage(FoodItem item, CarveSequence seq) { + if (item == null) return 1; + if (seq == null || seq.getCuts().isEmpty()) { + return Math.max(1, item.getCarveNextIndex() + 1); + } + int maxStage = Math.max(seq.getStartRemaining(), seq.getCuts().size()); + maxStage = Math.max(1, maxStage); + CarveCut next = seq.getCut(item.getCarveNextIndex()); + if (next != null && !next.isFoodCut()) { + return maxStage; + } + boolean bone = trailingItemIndex(seq) >= 0; + int meatLeft = bone + ? Math.max(0, item.getCarveRemaining() - 1) + : Math.max(0, item.getCarveRemaining()); + int meatTaken = Math.max(0, item.getCarveNextIndex()); + int meatTotal = meatTaken + meatLeft; + if (meatTotal <= 0 || meatTaken <= 0) { + return meatTaken <= 0 ? 1 : maxStage; + } + int span = bone ? meatTotal : Math.max(1, meatTotal - 1); + int stage = 1 + (int) Math.round(meatTaken * (double) (maxStage - 1) / span); + if (stage < 1) return 1; + return Math.min(maxStage, stage); } public static ModelData getStageModelData(FoodItem item) { @@ -135,8 +188,46 @@ public static String resolveCookTag(FoodItem item) { } public static void advanceAfterCarve(FoodItem item) { - item.setCarveNextIndex(item.getCarveNextIndex() + 1); - item.setCarveRemaining(Math.max(0, item.getCarveRemaining() - 1)); + advanceAfterCarve(item, getSequence(item)); + } + + public static void advanceAfterCarve(FoodItem item, CarveSequence seq) { + int next = item.getCarveNextIndex() + 1; + int remaining = Math.max(0, item.getCarveRemaining() - 1); + if (remaining == 1 && seq != null) { + int bone = trailingItemIndex(seq); + CarveCut upcoming = seq.getCut(next); + if (bone >= 0 && (upcoming == null || upcoming.isFoodCut())) { + next = bone; + } + } + item.setCarveNextIndex(next); + item.setCarveRemaining(remaining); + } + + private static int budgetedFoodCuts(FoodItem item, CarveSequence seq) { + CarveCut next = seq.getCut(item.getCarveNextIndex()); + if (next != null && !next.isFoodCut()) { + return 0; + } + if (trailingItemIndex(seq) >= 0) { + return Math.max(0, item.getCarveRemaining() - 1); + } + return Math.max(0, item.getCarveRemaining()); + } + + private static double budgetedFood(CarveSequence seq, int fromIndex, int foodCuts) { + if (seq == null || foodCuts <= 0) return 0; + double total = 0; + int counted = 0; + List cuts = seq.getCuts(); + for (int i = Math.max(0, fromIndex); i < cuts.size() && counted < foodCuts; i++) { + CarveCut cut = cuts.get(i); + if (!cut.isFoodCut()) continue; + total += cut.getFood(); + counted++; + } + return total; } public static void copyInheritedTracks(FoodItem parent, FoodItem child) { diff --git a/src/main/java/net/tfminecraft/cooking/carve/CarveSequence.java b/src/main/java/net/tfminecraft/cooking/carve/CarveSequence.java index 20a6295..0dbf788 100644 --- a/src/main/java/net/tfminecraft/cooking/carve/CarveSequence.java +++ b/src/main/java/net/tfminecraft/cooking/carve/CarveSequence.java @@ -6,11 +6,17 @@ public class CarveSequence { private final String id; private final int startRemaining; + private final int minFoodCuts; private final List cuts = new ArrayList<>(); public CarveSequence(String id, int startRemaining, List cuts) { + this(id, startRemaining, 1, cuts); + } + + public CarveSequence(String id, int startRemaining, int minFoodCuts, List cuts) { this.id = id; this.startRemaining = startRemaining; + this.minFoodCuts = minFoodCuts; this.cuts.addAll(cuts); } @@ -22,6 +28,11 @@ public int getStartRemaining() { return startRemaining; } + /** Meat portions a slaughtered animal of this sequence always keeps before the bone. */ + public int getMinFoodCuts() { + return minFoodCuts; + } + public List getCuts() { return cuts; } diff --git a/src/main/java/net/tfminecraft/cooking/husbandry/HusbandryHarvest.java b/src/main/java/net/tfminecraft/cooking/husbandry/HusbandryHarvest.java index abdc0ac..ff0aa07 100644 --- a/src/main/java/net/tfminecraft/cooking/husbandry/HusbandryHarvest.java +++ b/src/main/java/net/tfminecraft/cooking/husbandry/HusbandryHarvest.java @@ -14,7 +14,6 @@ import net.tfminecraft.cooking.carve.CarvableRoastUtils; import net.tfminecraft.cooking.carve.CarveSequence; import net.tfminecraft.cooking.item.FoodItem; -import net.tfminecraft.cooking.item.model.ModelData; import net.tfminecraft.cooking.loader.CarveSequenceLoader; import net.tfminecraft.cooking.utils.FoodParser; import net.tfminecraft.cooking.utils.InventoryAdder; @@ -190,21 +189,14 @@ public static ItemStack buildFood(HusbandryAnimal animal, String foodString, Ite if (seq == null) { return stack; } - int maxCuts = Math.max(1, seq.getStartRemaining()); - int cuts = Math.min(maxCuts, HusbandryConfig.roastCutsFor(HusbandryConfig.effectiveGenetics(animal))); - int nextIndex = Math.max(0, maxCuts - cuts); - item.setCarveState(seqId, nextIndex, cuts); + int requested = HusbandryConfig.roastCutsFor(HusbandryConfig.effectiveGenetics(animal)); + CarvableRoastUtils.RoastPortion portion = CarvableRoastUtils.portion(seq, requested); + item.setCarveState(seqId, portion.nextIndex(), portion.remaining()); CarvableRoastUtils.writeCarveState(stack, item); ItemStack updated = ItemUpdater.applyItemUpdate(stack, item, null); if (updated != null) { stack = updated; } - ModelData staged = item.getModel() == null - ? null - : item.getModel().getModelByStageAndTag(cuts, CarvableRoastUtils.resolveCookTag(item)); - if (staged != null) { - stack = staged.apply(null, stack); - } return stack; } diff --git a/src/main/java/net/tfminecraft/cooking/loader/CarveSequenceLoader.java b/src/main/java/net/tfminecraft/cooking/loader/CarveSequenceLoader.java index 76db702..81633eb 100644 --- a/src/main/java/net/tfminecraft/cooking/loader/CarveSequenceLoader.java +++ b/src/main/java/net/tfminecraft/cooking/loader/CarveSequenceLoader.java @@ -41,6 +41,9 @@ public void load(File file) { if (sec == null) continue; int startRemaining = sec.getInt("start-remaining", 0); + int minFoodCuts = sec.contains("min-food-cuts") + ? sec.getInt("min-food-cuts") + : defaultMinFoodCuts(key); List cuts = new ArrayList<>(); for (Map map : sec.getMapList("cuts")) { @@ -70,7 +73,15 @@ public void load(File file) { } } - sequences.put(key.toLowerCase(), new CarveSequence(key, startRemaining, cuts)); + sequences.put(key.toLowerCase(), new CarveSequence(key, startRemaining, minFoodCuts, cuts)); } } + + /** Used when an older carve-sequences.yml has no min-food-cuts key. */ + static int defaultMinFoodCuts(String key) { + if ("poultry".equalsIgnoreCase(key)) { + return 3; + } + return 1; + } } diff --git a/src/main/resources/carve-sequences.yml b/src/main/resources/carve-sequences.yml index f699ff5..7fe0ee3 100644 --- a/src/main/resources/carve-sequences.yml +++ b/src/main/resources/carve-sequences.yml @@ -1,5 +1,7 @@ poultry: start-remaining: 6 + # Slaughter never drops below two legs and one filet, then the bone. + min-food-cuts: 3 cuts: - output: "meat(type=meat_poultry_leg;origin=Chicken)" food: 20.0 @@ -23,6 +25,8 @@ poultry: red_meat: start-remaining: 8 + # Slaughter never drops below one steak, then the bone. + min-food-cuts: 1 cuts: - output: "meat(type=meat_red_meat;origin=Beef)" food: 18.0 diff --git a/src/main/resources/husbandry.yml b/src/main/resources/husbandry.yml index a15e8cb..62b9ac0 100644 --- a/src/main/resources/husbandry.yml +++ b/src/main/resources/husbandry.yml @@ -35,6 +35,8 @@ exp: initial-genetic-max: 20 max-genetics: 1000 +# Meat portions from effective genetics, before the closing bone. +# Poultry raises this floor to two legs and one filet (carve-sequences.yml). min-roast-cuts: 1 # Bump this string to wild-reset animals with a missing or old id (genetics + care). stats-revision: "1" diff --git a/src/test/java/net/tfminecraft/cooking/item/RoastPortionTest.java b/src/test/java/net/tfminecraft/cooking/item/RoastPortionTest.java new file mode 100644 index 0000000..d5a64d5 --- /dev/null +++ b/src/test/java/net/tfminecraft/cooking/item/RoastPortionTest.java @@ -0,0 +1,140 @@ +package net.tfminecraft.cooking.item; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.ArrayList; +import java.util.List; + +import org.junit.jupiter.api.Test; + +import net.tfminecraft.cooking.carve.CarveCut; +import net.tfminecraft.cooking.carve.CarveSequence; +import net.tfminecraft.cooking.carve.CarvableRoastUtils; + +class RoastPortionTest { + + @Test + void redMeatFloorIsOneSteakThenBoneAndStartsWhole() { + CarveSequence sequence = redMeat(); + FoodItem roast = place(sequence, 1); + + assertEquals(0, roast.getCarveNextIndex()); + assertEquals(2, roast.getCarveRemaining()); + assertEquals(1, CarvableRoastUtils.visualCarveStage(roast, sequence)); + assertEquals(18, CarvableRoastUtils.remainingFood(roast, sequence)); + assertEquals(List.of("meat_red_meat", "v.bone"), carve(roast, sequence)); + } + + @Test + void poultryFloorIsTwoLegsOneFiletThenBone() { + CarveSequence sequence = poultry(); + FoodItem roast = place(sequence, 1); + + assertEquals(0, roast.getCarveNextIndex()); + assertEquals(4, roast.getCarveRemaining()); + assertEquals(1, CarvableRoastUtils.visualCarveStage(roast, sequence)); + assertEquals(58, CarvableRoastUtils.remainingFood(roast, sequence)); + assertEquals(List.of( + "meat_poultry_leg", + "meat_poultry_leg", + "meat_poultry", + "v.bone"), carve(roast, sequence)); + } + + @Test + void higherGeneticsAddsMeatButStillClosesOnBone() { + FoodItem beef = place(redMeat(), 4); + assertEquals(5, beef.getCarveRemaining()); + assertEquals(List.of( + "meat_red_meat", + "meat_red_meat", + "meat_red_meat", + "meat_red_meat", + "v.bone"), carve(beef, redMeat())); + + FoodItem chicken = place(poultry(), 4); + assertEquals(5, chicken.getCarveRemaining()); + assertEquals(List.of( + "meat_poultry_leg", + "meat_poultry_leg", + "meat_poultry", + "meat_poultry", + "v.bone"), carve(chicken, poultry())); + } + + @Test + void fullSequencesKeepOneStagePerCut() { + assertStages(redMeat(), 8); + assertStages(poultry(), 6); + assertStages(sausage(), 5); + } + + private static void assertStages(CarveSequence sequence, int cuts) { + FoodItem roast = new FoodItem("roast", "Roast", true); + CarvableRoastUtils.RoastPortion portion = CarvableRoastUtils.portion(sequence, cuts); + roast.setCarveState(sequence.getId(), portion.nextIndex(), portion.remaining()); + for (int taken = 0; taken < cuts; taken++) { + int expected = sequence.getCut(roast.getCarveNextIndex()).isFoodCut() + ? taken + 1 + : cuts; + assertEquals(expected, CarvableRoastUtils.visualCarveStage(roast, sequence), + sequence.getId() + " after " + taken); + CarvableRoastUtils.advanceAfterCarve(roast, sequence); + } + assertEquals(0, roast.getCarveRemaining()); + } + + private static FoodItem place(CarveSequence sequence, int requestedMeat) { + FoodItem roast = new FoodItem("roast", "Roast", true); + CarvableRoastUtils.RoastPortion portion = CarvableRoastUtils.portion(sequence, requestedMeat); + roast.setCarveState(sequence.getId(), portion.nextIndex(), portion.remaining()); + return roast; + } + + private static List carve(FoodItem roast, CarveSequence sequence) { + List yields = new ArrayList<>(); + while (roast.getCarveRemaining() > 0) { + CarveCut cut = sequence.getCut(roast.getCarveNextIndex()); + yields.add(cut.isFoodCut() ? cut.getOutput() : cut.getItemRef()); + if (yields.size() == 1) { + assertEquals(1, CarvableRoastUtils.visualCarveStage(roast, sequence)); + } + if (!cut.isFoodCut()) { + assertEquals(sequence.getCuts().size(), CarvableRoastUtils.visualCarveStage(roast, sequence)); + } + CarvableRoastUtils.advanceAfterCarve(roast, sequence); + } + return yields; + } + + private static CarveSequence poultry() { + return new CarveSequence("poultry", 6, 3, List.of( + CarveCut.food("meat_poultry_leg", 20, 8), + CarveCut.food("meat_poultry_leg", 20, 8), + CarveCut.food("meat_poultry", 18, 8), + CarveCut.food("meat_poultry", 18, 8), + CarveCut.food("meat_poultry", 18, 8), + CarveCut.item("v.bone", 2, 0, 0))); + } + + private static CarveSequence redMeat() { + return new CarveSequence("red_meat", 8, 1, List.of( + CarveCut.food("meat_red_meat", 18, 8), + CarveCut.food("meat_red_meat", 18, 8), + CarveCut.food("meat_red_meat", 18, 8), + CarveCut.food("meat_red_meat", 18, 8), + CarveCut.food("meat_red_meat", 18, 8), + CarveCut.food("meat_red_meat", 18, 8), + CarveCut.food("meat_red_meat", 18, 8), + CarveCut.item("v.bone", 4, 0, 0))); + } + + private static CarveSequence sausage() { + return new CarveSequence("sausage", 5, 1, List.of( + CarveCut.food("sausage", 18, 8), + CarveCut.food("sausage", 18, 8), + CarveCut.food("sausage", 18, 8), + CarveCut.food("sausage", 18, 8), + CarveCut.food("sausage", 18, 8))); + } +} diff --git a/src/test/java/net/tfminecraft/cooking/loader/CarveSequenceLoaderTest.java b/src/test/java/net/tfminecraft/cooking/loader/CarveSequenceLoaderTest.java new file mode 100644 index 0000000..e4628fb --- /dev/null +++ b/src/test/java/net/tfminecraft/cooking/loader/CarveSequenceLoaderTest.java @@ -0,0 +1,14 @@ +package net.tfminecraft.cooking.loader; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.api.Test; + +class CarveSequenceLoaderTest { + + @Test + void missingFloorKeepsPoultryAtTwoLegsAndAFilet() { + assertEquals(3, CarveSequenceLoader.defaultMinFoodCuts("poultry")); + assertEquals(1, CarveSequenceLoader.defaultMinFoodCuts("red_meat")); + } +}