From 50715f0dfba178c1aa058bde46dbca19dd729059 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 3 Sep 2026 18:29:13 +0000 Subject: [PATCH] Estimate a duration for recipes that are crafted instantly Only the time between starting a crafting operation and its outputs coming back in is measured. Regular crafting recipes produce their outputs within the tick they are started in, so they measure as taking no time at all, and their average drags every other recipe on the interface down with them. Consumers multiply this by the number of operations of a job, so a plan for a hundred chests came out as taking no time. In blocking mode a crafting interface starts one operation per update, so an operation occupies it for a full update interval however quickly the recipe itself is done. Take that as a lower bound. Non-blocking mode starts as many operations as the target accepts, so no bound applies there. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_016gX5ZToD5ApLBXQkKW2QGj --- .../api/crafting/ICraftingInterface.java | 2 + .../api/network/ICraftingNetwork.java | 2 + .../core/CraftingJobHandler.java | 18 ++++++- .../part/PartTypeInterfaceCraftingBase.java | 2 +- .../gametest/GameTestsItemsCraft.java | 6 ++- .../core/TestCraftingJobHandler.java | 48 +++++++++++++++---- 6 files changed, 63 insertions(+), 15 deletions(-) diff --git a/src/main/java/org/cyclops/integratedcrafting/api/crafting/ICraftingInterface.java b/src/main/java/org/cyclops/integratedcrafting/api/crafting/ICraftingInterface.java index 29a43b7d..2bda7f00 100644 --- a/src/main/java/org/cyclops/integratedcrafting/api/crafting/ICraftingInterface.java +++ b/src/main/java/org/cyclops/integratedcrafting/api/crafting/ICraftingInterface.java @@ -91,6 +91,8 @@ public interface ICraftingInterface { * This may fall back to the average duration over all recipes of this interface, * as recipe-specific durations are only remembered for a limited number of recipes, * and are forgotten once they become outdated. + * This is never shorter than the interval at which this interface can start operations, + * so that recipes that produce their outputs instantly are not estimated as taking no time. */ public long getEstimatedRecipeDuration(IRecipeDefinition recipe); diff --git a/src/main/java/org/cyclops/integratedcrafting/api/network/ICraftingNetwork.java b/src/main/java/org/cyclops/integratedcrafting/api/network/ICraftingNetwork.java index 8a6fbcf4..daf84b7e 100644 --- a/src/main/java/org/cyclops/integratedcrafting/api/network/ICraftingNetwork.java +++ b/src/main/java/org/cyclops/integratedcrafting/api/network/ICraftingNetwork.java @@ -168,6 +168,8 @@ public Iterator getCraftingJobs(int channel, IngredientCompo * @param recipe A recipe. * @return The estimated duration in ticks of a single crafting operation of the given recipe, * based on the operations that the crafting interfaces performed before, or -1 if unknown. + * As with {@link ICraftingInterface#getEstimatedRecipeDuration(IRecipeDefinition)}, + * this accounts for the interval at which those interfaces start operations. */ public long getEstimatedRecipeDuration(int channel, IRecipeDefinition recipe); diff --git a/src/main/java/org/cyclops/integratedcrafting/core/CraftingJobHandler.java b/src/main/java/org/cyclops/integratedcrafting/core/CraftingJobHandler.java index 78dc63be..21dd3e66 100644 --- a/src/main/java/org/cyclops/integratedcrafting/core/CraftingJobHandler.java +++ b/src/main/java/org/cyclops/integratedcrafting/core/CraftingJobHandler.java @@ -350,14 +350,28 @@ public long getCraftingJobEntryStartTick(int craftingJobId) { } /** + * Only the time between starting a crafting operation and its outputs coming back in is measured. + * Recipes that produce their outputs within the tick they are started in, such as regular crafting + * recipes, therefore measure as taking no time at all, which would estimate whole crafting jobs away. + * + * In blocking mode, a single operation is started per update, so an operation occupies this handler + * for a full update interval, however quickly the recipe itself is done. + * In non-blocking mode, as many operations are started as the target accepts, so there is no such + * lower bound, and the given interval is ignored. + * * @param recipe A recipe. + * @param updateInterval The number of ticks between two updates of this handler. * @return The estimated duration in ticks of a single crafting operation of the given recipe, * based on the operations that were performed by this handler before, or -1 if unknown. * This falls back to the average duration over all recipes * when the given recipe itself was not crafted recently. */ - public long getEstimatedRecipeDuration(IRecipeDefinition recipe) { - return getRecipeDurationStatistics().getEstimatedDuration(recipe, getCurrentTick()); + public long getEstimatedRecipeDuration(IRecipeDefinition recipe, long updateInterval) { + long recipeDuration = getRecipeDurationStatistics().getEstimatedDuration(recipe, getCurrentTick()); + if (recipeDuration < 0) { + return -1; + } + return isBlockingJobsMode() ? Math.max(recipeDuration, updateInterval) : recipeDuration; } /** diff --git a/src/main/java/org/cyclops/integratedcrafting/core/part/PartTypeInterfaceCraftingBase.java b/src/main/java/org/cyclops/integratedcrafting/core/part/PartTypeInterfaceCraftingBase.java index d1decd8f..f2ff716b 100644 --- a/src/main/java/org/cyclops/integratedcrafting/core/part/PartTypeInterfaceCraftingBase.java +++ b/src/main/java/org/cyclops/integratedcrafting/core/part/PartTypeInterfaceCraftingBase.java @@ -381,7 +381,7 @@ public long getCraftingJobEntryStartTick(int craftingJobId) { @Override public long getEstimatedRecipeDuration(IRecipeDefinition recipe) { - return craftingJobHandler.getEstimatedRecipeDuration(recipe); + return craftingJobHandler.getEstimatedRecipeDuration(recipe, getUpdateInterval()); } @Override diff --git a/src/main/java/org/cyclops/integratedcrafting/gametest/GameTestsItemsCraft.java b/src/main/java/org/cyclops/integratedcrafting/gametest/GameTestsItemsCraft.java index dc1d7cc9..e7261320 100644 --- a/src/main/java/org/cyclops/integratedcrafting/gametest/GameTestsItemsCraft.java +++ b/src/main/java/org/cyclops/integratedcrafting/gametest/GameTestsItemsCraft.java @@ -118,10 +118,12 @@ public void testItemsCraftChestOneRecipeDuration(GameTestHelper helper) { // Check if items have been crafted helper.assertValueEqual(chestIn.getItem(1).getItem(), Items.CHEST, "Slot 1 item is incorrect"); - // Check if the duration of the crafted recipe was measured + // Check if the duration of the crafted recipe was measured. + // A chest is crafted within the tick it is started in, so its measured duration is zero, + // and the interval at which the interface starts operations is what is left of it. PartTypeInterfaceCrafting.State interfaceState = positions.interfaceStates().get(0); for (IRecipeDefinition recipe : interfaceState.getRecipes()) { - helper.assertTrue(interfaceState.getEstimatedRecipeDuration(recipe) >= 0, + helper.assertTrue(interfaceState.getEstimatedRecipeDuration(recipe) >= interfaceState.getUpdateInterval(), "No crafting duration was measured for the crafted recipe"); } }); diff --git a/src/test/java/org/cyclops/integratedcrafting/core/TestCraftingJobHandler.java b/src/test/java/org/cyclops/integratedcrafting/core/TestCraftingJobHandler.java index df2ee0e6..f11f4449 100644 --- a/src/test/java/org/cyclops/integratedcrafting/core/TestCraftingJobHandler.java +++ b/src/test/java/org/cyclops/integratedcrafting/core/TestCraftingJobHandler.java @@ -63,28 +63,52 @@ protected CraftingJob newCraftingJob(int id, int amount) { @Test public void testRecipeDurationUnknown() { - assertThat(handler.getEstimatedRecipeDuration(recipeA), equalTo(-1L)); + assertThat(handler.getEstimatedRecipeDuration(recipeA, 0), equalTo(-1L)); } @Test public void testRecipeDurationSingle() { handler.reportRecipeDuration(recipeA, 100); - assertThat(handler.getEstimatedRecipeDuration(recipeA), equalTo(100L)); + assertThat(handler.getEstimatedRecipeDuration(recipeA, 0), equalTo(100L)); } @Test public void testRecipeDurationSmoothed() { handler.reportRecipeDuration(recipeA, 100); handler.reportRecipeDuration(recipeA, 200); - assertThat(handler.getEstimatedRecipeDuration(recipeA), equalTo(125L)); + assertThat(handler.getEstimatedRecipeDuration(recipeA, 0), equalTo(125L)); handler.reportRecipeDuration(recipeA, 200); - assertThat(handler.getEstimatedRecipeDuration(recipeA), equalTo(144L)); + assertThat(handler.getEstimatedRecipeDuration(recipeA, 0), equalTo(144L)); + } + + @Test + public void testRecipeDurationIsAtLeastOneUpdate() { + handler.reportRecipeDuration(recipeA, 0); + assertThat(handler.getEstimatedRecipeDuration(recipeA, 5), equalTo(5L)); + } + + @Test + public void testRecipeDurationLongerThanOneUpdateIsKept() { + handler.reportRecipeDuration(recipeA, 100); + assertThat(handler.getEstimatedRecipeDuration(recipeA, 5), equalTo(100L)); + } + + @Test + public void testUnknownRecipeDurationStaysUnknownWithUpdateInterval() { + assertThat(handler.getEstimatedRecipeDuration(recipeA, 5), equalTo(-1L)); + } + + @Test + public void testRecipeDurationIsNotBoundedInNonBlockingMode() { + TickingCraftingJobHandler nonBlockingHandler = new TickingCraftingJobHandler(false); + nonBlockingHandler.reportRecipeDuration(recipeA, 0); + assertThat(nonBlockingHandler.getEstimatedRecipeDuration(recipeA, 5), equalTo(0L)); } @Test public void testRecipeDurationFallsBackToAverage() { handler.reportRecipeDuration(recipeA, 100); - assertThat(handler.getEstimatedRecipeDuration(recipeB), equalTo(100L)); + assertThat(handler.getEstimatedRecipeDuration(recipeB, 0), equalTo(100L)); } @Test @@ -130,7 +154,7 @@ public void testCraftingOperationIsMeasured() { assertThat(craftingJob.getAmount(), equalTo(1)); assertThat(craftingJob.getAmountTotal(), equalTo(2)); - assertThat(handler.getEstimatedRecipeDuration(recipeA), equalTo(60L)); + assertThat(handler.getEstimatedRecipeDuration(recipeA, 0), equalTo(60L)); assertThat(handler.getCraftingJobEntryStartTick(1), equalTo(-1L)); } @@ -147,12 +171,12 @@ public void testParallelCraftingOperationsAreMeasured() { handler.setCurrentTick(200); handler.onCraftingJobEntryFinished(craftingNetwork, 1); - assertThat(handler.getEstimatedRecipeDuration(recipeA), equalTo(100L)); + assertThat(handler.getEstimatedRecipeDuration(recipeA, 0), equalTo(100L)); assertThat(handler.getCraftingJobEntryStartTick(1), equalTo(120L)); handler.setCurrentTick(220); handler.onCraftingJobEntryFinished(craftingNetwork, 1); - assertThat(handler.getEstimatedRecipeDuration(recipeA), equalTo(100L)); + assertThat(handler.getEstimatedRecipeDuration(recipeA, 0), equalTo(100L)); assertThat(handler.getCraftingJobEntryStartTick(1), equalTo(-1L)); } @@ -210,7 +234,7 @@ public void testRecipeDurationsSurviveSerialization() { TickingCraftingJobHandler deserialized = new TickingCraftingJobHandler(); deserialized.readFromNBT(null, tag); - assertThat(deserialized.getEstimatedRecipeDuration(recipeA), equalTo(100L)); + assertThat(deserialized.getEstimatedRecipeDuration(recipeA, 0), equalTo(100L)); assertThat(deserialized.getRecipeDurationStatistics().getEntryCount(), equalTo(0)); } @@ -219,7 +243,11 @@ protected static class TickingCraftingJobHandler extends CraftingJobHandler { private long currentTick; public TickingCraftingJobHandler() { - super(1, true, Collections.emptyList(), new ICraftingResultsSink() { + this(true); + } + + public TickingCraftingJobHandler(boolean blockingJobsMode) { + super(1, blockingJobsMode, Collections.emptyList(), new ICraftingResultsSink() { @Override public void addResult(IngredientComponent ingredientComponent, T instance) {