Estimate a duration for recipes that are crafted instantly - #223
Merged
Conversation
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 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016gX5ZToD5ApLBXQkKW2QGj
rubensworks
pushed a commit
to CyclopsMC/IntegratedTerminals
that referenced
this pull request
Sep 4, 2026
Recipes that are crafted within the tick they are started in were measured as taking no time, which was worked around here by taking the crafting interface update interval as a lower bound. CyclopsMC/IntegratedCrafting#223 moved that bound to where the numbers come from, where it also knows that non-blocking interfaces start as many operations as their target accepts, and that an interface can be configured to update less often than the configured minimum. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016gX5ZToD5ApLBXQkKW2QGj
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Follow-up to #220, found while using its measurements in CyclopsMC/IntegratedTerminals#211.
The problem
CraftingJobHandlermeasures a recipe duration as the time between inserting a crafting operation into the target and its outputs coming back in. A regular crafting recipe produces its outputs within the tick it is started in, so it measures as 0.That zero then spreads: it is smoothed into the interface's average duration, which is the fallback for every recipe that interface has never crafted. One instant craft is enough to make every estimate on that interface report zero.
Observed on a fresh network, after a single oak stairs craft:
Consumers multiply this per-operation duration by the number of operations in a job, so a plan for a hundred chests came out as taking no time at all, and the remaining time of a running job stayed at zero for its whole life.
The fix
The measurement is a latency, and on its own it cannot answer how long a job takes, because it says nothing about how often the interface can start an operation. In blocking mode
CraftingJobHandler#updatestarts exactly one operation per call (itbreaks out of the pending-jobs loop as soon as one is inserted), so an operation occupies the handler for a full update interval however quickly the recipe itself is done. That is a lower bound on the duration of one operation.CraftingJobHandler#getEstimatedRecipeDurationtherefore takes the update interval and bounds the measurement by it. Non-blocking mode pushes as many operations into the target as it accepts within one update, so no such bound applies there and the interval is ignored. An unknown duration stays-1.PartTypeInterfaceCraftingBase.Statepasses its owngetUpdateInterval(), which defaults tominCraftingInterfaceUpdateFreqbut is configurable per part, so an interface that was slowed down estimates accordingly.CraftingNetwork#getEstimatedRecipeDurationneeds no change: it averages over the interfaces that expose a recipe, and each of them now contributes its own bound.With the default interval of 5 ticks, a plan for 100 chests goes from
0to500ticks, which matches the throughput I measured in-game (2 operations per 10 ticks on one interface).The javadoc of
ICraftingInterface#getEstimatedRecipeDurationandICraftingNetwork#getEstimatedRecipeDurationnow states this bound. This is a behaviour change for existing callers rather than a signature change; the only signature that changed isCraftingJobHandler#getEstimatedRecipeDuration, which is not part of the API.Tests
TestCraftingJobHandler: a measured duration of 0 estimates as one update interval, a longer measurement is kept as-is, an unknown duration stays unknown, and non-blocking mode is not bounded. The existing statistics tests pass an interval of 0, as they are about the smoothing and expiry rather than the bound.GameTestsItemsCraft#testItemsCraftChestOneRecipeDurationnow asserts>= getUpdateInterval()instead of>= 0. A chest is the exact case this fixes, so the old assertion passed against a zero. I verified this test fails on the unfixed code (testitemscraftchestonerecipeduration failed ... No crafting duration was measured for the crafted recipe) and passes with the fix../gradlew buildand./gradlew runGameTestServerboth pass locally (18 handler unit tests, all 53 game tests).🤖 Generated with Claude Code
https://claude.ai/code/session_016gX5ZToD5ApLBXQkKW2QGj
Generated by Claude Code