Skip to content

Expose crafting job progress and measured recipe durations - #220

Open
rubensworks wants to merge 7 commits into
master-1.21-ltsfrom
feature/craftingjob-duration-statistics
Open

Expose crafting job progress and measured recipe durations#220
rubensworks wants to merge 7 commits into
master-1.21-ltsfrom
feature/craftingjob-duration-statistics

Conversation

@rubensworks

@rubensworks rubensworks commented Aug 31, 2026

Copy link
Copy Markdown
Member

Groundwork for CyclopsMC/IntegratedTerminals#145, which asks for the estimated/remaining time and the completion percentage of crafting jobs in the crafting terminal. Neither can be derived from what is exposed today:

  • A job's amount is decremented while crafting and finished jobs are removed from the network, so the part of a job that is done is not recoverable.
  • Nothing knows how long a recipe takes: for processing recipes that is a property of the machine, so it can only be measured.

Crafting job progress

CraftingJob now also keeps the amount it started with, next to the amount that still has to be crafted:

  • CraftingJob#getAmountTotal(), maintained when jobs are split over multiple interfaces (CraftingHelpers#splitCraftingJobs) and when jobs are merged (CraftingJobDependencyGraph#mergeCraftingJobs), and serialized. Jobs from an older world load with the total set to their remaining amount.

Recipe durations

CraftingJobHandler records the tick at which each crafting operation is inserted into its target, and measures the duration once that operation's outputs come back in. Durations are smoothed with a running average (weight 0.25), as machine speeds can vary.

These measurements are kept by RecipeDurationStatistics, which is deliberately bounded:

  • Recipe-specific durations live in memory only, in a least-recently-used cache of at most craftingInterfaceRecipeDurationEntries (default 32) recipes. The number of recipes a crafting interface can craft is unbounded — an attuned interface exposes every recipe of its target machine, and reconfiguring a regular interface leaves entries for recipes it no longer exposes — and part states are also written into the item when a part is broken, so serializing them per recipe would grow both the world and the dropped item indefinitely.
  • Only the average duration over all recipes is serialized, which is two values per crafting interface, regardless of how much has been crafted. After loading, estimations start from that average, and become recipe-specific again as soon as recipes are crafted. Unmeasured recipes fall back to it as well, so an interface can estimate a recipe it has never crafted.
  • Measurements are forgotten once they are older than craftingInterfaceRecipeDurationMaxAge (default 24000 ticks, one in-game day). A player who speeds up their network would otherwise keep seeing estimations from the old setup for recipes that are not crafted often. An outdated measurement is replaced rather than smoothed into, so the new duration takes effect immediately.

This is exposed as:

  • ICraftingInterface#getEstimatedRecipeDuration(recipe): the measured duration of one crafting operation of a recipe in that interface.
  • ICraftingInterface#getCraftingJobEntryStartTick(craftingJobId): when the oldest running operation of a job started, so that the remaining time of a running operation can be shown.
  • ICraftingNetwork#getEstimatedRecipeDuration(channel, recipe): averaged over the interfaces that expose the recipe and have measured it.

All of these return -1 when nothing was measured yet, and the new methods on the API interfaces are default, so implementations outside of this repository keep compiling.

Also, CraftingNetwork#getCurrentTick moved to CraftingHelpers#getCurrentTick, as the crafting job handler needs it too.

Tests

  • Unit tests for the statistics: smoothing, per-recipe tracking, the fallback to the average, least-recently-used eviction, expiry (including that an outdated measurement does not slow down its replacement, and that measurements from a future tick are dropped when the game time moves backwards), the disabled and never-expiring configurations, and serialization.
  • Unit tests for the handler: a full crafting operation with a controlled clock, parallel operations, cleanup when a job is cancelled or finishes, and that serializing after crafting a hundred distinct recipes produces a byte-identical tag.
  • Unit tests for the total amount after splitting and merging jobs, and that it is unaffected by crafting.
  • A game test asserting that a duration is measured for a crafted recipe.

Heads-up: I could not build or run this locally, as all org.cyclops dependencies resolve to GitHub Packages, which returns 401 in my environment. Compilation, the unit tests and the game tests are verified by CI here.

claude added 3 commits August 31, 2026 17:18
Crafting jobs now remember the amount they started with, so that the
part of a job that was crafted already can be derived.

Crafting interfaces measure how long each crafting operation takes,
and expose a smoothed duration per recipe. This allows the duration of
crafting jobs to be estimated, which is not possible otherwise, as the
time that a machine needs for a recipe is unknown upfront.

Refs CyclopsMC/IntegratedTerminals#145

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0186zEXrjMNMVXQC7wcoSiMR
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0186zEXrjMNMVXQC7wcoSiMR
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0186zEXrjMNMVXQC7wcoSiMR

Copy link
Copy Markdown
Member Author

Status on the checks:

  • Build failed on the first push because CraftingHelpers already imports org.apache.logging.log4j.Level, which my net.minecraft.world.level.Level import made ambiguous. Fixed in 278dd6c by dropping the import and qualifying the single reference. Build is green again on the pushes since.
  • coverage/coveralls reports -0.03%. That one is mine, but I do not think it is worth chasing: the bulk of the new lines are the duration measurement in CraftingJobHandler (its NBT, the operation start/finish hooks) and CraftingNetwork#getEstimatedRecipeDuration, which need a running server and are therefore covered by the game test in this PR rather than by unit tests, and game test coverage does not feed into the coveralls run (./gradlew test jacocoTestReport coveralls). The parts that can be unit tested are: the running average, the total amount after splitting and merging jobs, and (added in 3b2ed4a) that a job's total amount is unaffected by crafting.

Happy to add more coverage if you would rather see that number stay flat.


Generated by Claude Code

claude added 2 commits August 31, 2026 17:33
Makes the current tick overridable in the crafting job handler,
so that measuring the duration of crafting operations can be tested
without a running server.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0186zEXrjMNMVXQC7wcoSiMR

Copy link
Copy Markdown
Member Author

Correcting my previous comment: I claimed game test coverage does not feed into coveralls. That is wrong — jacocoTestReport aggregates jacoco/*.exec and depends on runGameTestServer, so game tests are included. The coverage drop is therefore real, and here are the actual numbers:

Build Covered Relevant %
master-1.21-lts (8fcb621) 2982 4424 63.899
this branch, push run (c1ebbd9) 3002 4512 63.033
this branch, pull_request run (c1ebbd9) 3047 4512 63.993

So the branch adds 88 relevant lines, of which the push run counts ~20 as covered. That is what the -0.9% is.

Two caveats on those numbers. The two runs for the same commit disagree by 45 lines, and that divergence is not new — on the #218 branch the push build reported 2967 and the merge build 2982 for the same work — so some of this is run-to-run variance in what the game test run records. And the comparison coveralls picked is the push build against the push base, which is the like-for-like one, so I am treating the decrease as real rather than as an artifact.

What I have added since: unit tests driving a full crafting operation through the handler with a controlled clock (start tick recorded, duration measured, amount decremented, parallel operations, cleanup), and one that measured durations survive a serialization round trip.

What stays uncovered by unit tests: CraftingNetwork#getEstimatedRecipeDuration and the crafting interface delegation both need registered interfaces with a real world position, and CraftingHelpers#getCurrentTick needs a server. I can push a game test that asserts the network-level average across two interfaces if you want those lines covered too — say the word and I will, otherwise I would leave the remaining delta as is.


Generated by Claude Code

claude added 2 commits August 31, 2026 17:50
The measured durations were stored per recipe and serialized with the
crafting interface. As the number of recipes that an interface can craft
is unbounded (attuned interfaces expose all recipes of their target, and
reconfigured interfaces leave behind entries for old recipes), and part
states are also stored in the item when a part is broken, this could grow
the crafting interface indefinitely.

Recipe-specific durations are now kept in memory only, in a bounded
least-recently-used cache, and only the average duration over all recipes
is serialized. After loading, estimations start from that average, and
become recipe-specific again as soon as recipes are crafted.

Measurements are also forgotten once they become too old, so that
estimations follow changes to the network, such as machines becoming
faster.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0186zEXrjMNMVXQC7wcoSiMR
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants