Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
101 changes: 96 additions & 5 deletions src/main/java/net/tfminecraft/cooking/carve/CarvableRoastUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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<CarveCut> 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) {
Expand Down Expand Up @@ -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<CarveCut> 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) {
Expand Down
11 changes: 11 additions & 0 deletions src/main/java/net/tfminecraft/cooking/carve/CarveSequence.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,17 @@
public class CarveSequence {
private final String id;
private final int startRemaining;
private final int minFoodCuts;
private final List<CarveCut> cuts = new ArrayList<>();

public CarveSequence(String id, int startRemaining, List<CarveCut> cuts) {
this(id, startRemaining, 1, cuts);
}

public CarveSequence(String id, int startRemaining, int minFoodCuts, List<CarveCut> cuts) {
this.id = id;
this.startRemaining = startRemaining;
this.minFoodCuts = minFoodCuts;
this.cuts.addAll(cuts);
}

Expand All @@ -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<CarveCut> getCuts() {
return cuts;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<CarveCut> cuts = new ArrayList<>();

for (Map<?, ?> map : sec.getMapList("cuts")) {
Expand Down Expand Up @@ -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;
}
}
4 changes: 4 additions & 0 deletions src/main/resources/carve-sequences.yml
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
Expand Down
2 changes: 2 additions & 0 deletions src/main/resources/husbandry.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
140 changes: 140 additions & 0 deletions src/test/java/net/tfminecraft/cooking/item/RoastPortionTest.java
Original file line number Diff line number Diff line change
@@ -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<String> carve(FoodItem roast, CarveSequence sequence) {
List<String> 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)));
}
}
Original file line number Diff line number Diff line change
@@ -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"));
}
}