From a276d64a594718eae15179c2e65f2b0b56ac7081 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 31 Aug 2026 17:08:09 +0000 Subject: [PATCH 01/17] Add crafting plan progress and duration estimation API Adds the total crafting quantity and the estimated total and remaining tick durations to crafting plans, together with their (de)serialization and aggregation during plan flattening. These values are not filled in or displayed yet, so everything reports an unknown value for now. Refs #145 Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_0186zEXrjMNMVXQC7wcoSiMR --- .../crafting/ITerminalCraftingPlan.java | 27 ++++ .../crafting/ITerminalCraftingPlanFlat.java | 33 +++++ .../TerminalCraftingPlanFlatStatic.java | 61 ++++++++- .../crafting/TerminalCraftingPlanStatic.java | 124 +++++++++++++++++- ...raftingPlanCraftingJobDependencyGraph.java | 21 +++ ...redientCraftingHandlerCraftingNetwork.java | 3 + .../TestTerminalCraftingPlanStatic.java | 91 +++++++++++++ 7 files changed, 355 insertions(+), 5 deletions(-) create mode 100644 src/test/java/org/cyclops/integratedterminals/api/terminalstorage/crafting/TestTerminalCraftingPlanStatic.java diff --git a/src/main/java/org/cyclops/integratedterminals/api/terminalstorage/crafting/ITerminalCraftingPlan.java b/src/main/java/org/cyclops/integratedterminals/api/terminalstorage/crafting/ITerminalCraftingPlan.java index f1e430e8dd..8dd1f6e0ad 100644 --- a/src/main/java/org/cyclops/integratedterminals/api/terminalstorage/crafting/ITerminalCraftingPlan.java +++ b/src/main/java/org/cyclops/integratedterminals/api/terminalstorage/crafting/ITerminalCraftingPlan.java @@ -42,6 +42,16 @@ public interface ITerminalCraftingPlan { */ public long getCraftingQuantity(); + /** + * @return The number of instances that will be crafted in total by this job, + * including the ones that were crafted already. + * For jobs that have not started crafting yet, + * this is equal to {@link #getCraftingQuantity()}. + */ + public default long getCraftingQuantityTotal() { + return getCraftingQuantity(); + } + /** * @return The ingredients within the storage buffer. */ @@ -64,6 +74,23 @@ public interface ITerminalCraftingPlan { */ public long getTickDuration(); + /** + * Contrary to {@link #getTickDuration()}, which indicates how long this job has been running already, + * this is an estimation of how long this job takes from start to finish. + * + * @return The estimated total tick duration for this job. -1 indicates an unknown duration. + */ + public default long getEstimatedTickDurationTotal() { + return -1; + } + + /** + * @return The estimated tick duration until this job is finished. -1 indicates an unknown duration. + */ + public default long getEstimatedTickDurationRemaining() { + return -1; + } + /** * @return The channel id, or -1 for non-applicable. */ diff --git a/src/main/java/org/cyclops/integratedterminals/api/terminalstorage/crafting/ITerminalCraftingPlanFlat.java b/src/main/java/org/cyclops/integratedterminals/api/terminalstorage/crafting/ITerminalCraftingPlanFlat.java index 1e8a905248..acb1a53b43 100644 --- a/src/main/java/org/cyclops/integratedterminals/api/terminalstorage/crafting/ITerminalCraftingPlanFlat.java +++ b/src/main/java/org/cyclops/integratedterminals/api/terminalstorage/crafting/ITerminalCraftingPlanFlat.java @@ -47,6 +47,39 @@ public interface ITerminalCraftingPlanFlat { */ public long getTickDuration(); + /** + * @return The number of crafting operations in this plan, including the ones that finished already. + * 0 indicates an unknown quantity. + */ + public default long getCraftingQuantityTotal() { + return 0; + } + + /** + * @return The number of crafting operations in this plan that still have to be performed. + * 0 indicates an unknown quantity, or a plan without remaining operations. + */ + public default long getCraftingQuantityRemaining() { + return 0; + } + + /** + * Contrary to {@link #getTickDuration()}, which indicates how long this job has been running already, + * this is an estimation of how long this job takes from start to finish. + * + * @return The estimated total tick duration for this job. -1 indicates an unknown duration. + */ + public default long getEstimatedTickDurationTotal() { + return -1; + } + + /** + * @return The estimated tick duration until this job is finished. -1 indicates an unknown duration. + */ + public default long getEstimatedTickDurationRemaining() { + return -1; + } + /** * @return The channel id, or -1 for non-applicable. */ diff --git a/src/main/java/org/cyclops/integratedterminals/api/terminalstorage/crafting/TerminalCraftingPlanFlatStatic.java b/src/main/java/org/cyclops/integratedterminals/api/terminalstorage/crafting/TerminalCraftingPlanFlatStatic.java index 4446edf171..d5a80654d6 100644 --- a/src/main/java/org/cyclops/integratedterminals/api/terminalstorage/crafting/TerminalCraftingPlanFlatStatic.java +++ b/src/main/java/org/cyclops/integratedterminals/api/terminalstorage/crafting/TerminalCraftingPlanFlatStatic.java @@ -24,6 +24,10 @@ public class TerminalCraftingPlanFlatStatic implements ITerminalCraftingPlanF @Nullable private String unlocalizedLabelOverride; private final long tickDuration; + private final long craftingQuantityTotal; + private final long craftingQuantityRemaining; + private final long estimatedTickDurationTotal; + private final long estimatedTickDurationRemaining; private final int channel; @Nullable private final String initiatorName; @@ -36,6 +40,21 @@ public TerminalCraftingPlanFlatStatic(I id, long tickDuration, int channel, @Nullable String initiatorName) { + this(id, entries, outputs, status, label, tickDuration, 0, 0, -1, -1, channel, initiatorName); + } + + public TerminalCraftingPlanFlatStatic(I id, + List entries, + List> outputs, + TerminalCraftingJobStatus status, + TerminalCraftingPlanStatic.Label label, + long tickDuration, + long craftingQuantityTotal, + long craftingQuantityRemaining, + long estimatedTickDurationTotal, + long estimatedTickDurationRemaining, + int channel, + @Nullable String initiatorName) { this.id = id; this.entries = entries; this.outputs = outputs; @@ -43,6 +62,10 @@ public TerminalCraftingPlanFlatStatic(I id, this.label = label; this.unlocalizedLabelOverride = null; this.tickDuration = tickDuration; + this.craftingQuantityTotal = craftingQuantityTotal; + this.craftingQuantityRemaining = craftingQuantityRemaining; + this.estimatedTickDurationTotal = estimatedTickDurationTotal; + this.estimatedTickDurationRemaining = estimatedTickDurationRemaining; this.channel = channel; this.initiatorName = initiatorName; } @@ -93,6 +116,26 @@ public long getTickDuration() { return tickDuration; } + @Override + public long getCraftingQuantityTotal() { + return craftingQuantityTotal; + } + + @Override + public long getCraftingQuantityRemaining() { + return craftingQuantityRemaining; + } + + @Override + public long getEstimatedTickDurationTotal() { + return estimatedTickDurationTotal; + } + + @Override + public long getEstimatedTickDurationRemaining() { + return estimatedTickDurationRemaining; + } + @Override public int getChannel() { return channel; @@ -137,6 +180,12 @@ public static CompoundTag serialize(HolderLookup.Provider lookupProvider, Te tag.putLong("tickDuration", plan.getTickDuration()); + tag.putLong("craftingQuantityTotal", plan.getCraftingQuantityTotal()); + tag.putLong("craftingQuantityRemaining", plan.getCraftingQuantityRemaining()); + + tag.putLong("estimatedTickDurationTotal", plan.getEstimatedTickDurationTotal()); + tag.putLong("estimatedTickDurationRemaining", plan.getEstimatedTickDurationRemaining()); + tag.putInt("channel", plan.getChannel()); if (plan.getInitiatorName() != null) { @@ -195,6 +244,14 @@ public static TerminalCraftingPlanFlatStatic deserialize(HolderLookup.Pro long tickDuration = tag.getLong("tickDuration"); + long craftingQuantityTotal = tag.getLong("craftingQuantityTotal"); + long craftingQuantityRemaining = tag.getLong("craftingQuantityRemaining"); + + long estimatedTickDurationTotal = tag.contains("estimatedTickDurationTotal", Tag.TAG_LONG) + ? tag.getLong("estimatedTickDurationTotal") : -1; + long estimatedTickDurationRemaining = tag.contains("estimatedTickDurationRemaining", Tag.TAG_LONG) + ? tag.getLong("estimatedTickDurationRemaining") : -1; + int channel = tag.getInt("channel"); String initiatorName = null; @@ -202,7 +259,9 @@ public static TerminalCraftingPlanFlatStatic deserialize(HolderLookup.Pro initiatorName = tag.getString("initiatorName"); } - TerminalCraftingPlanFlatStatic plan = new TerminalCraftingPlanFlatStatic<>(id, entries, outputs, status, label, tickDuration, channel, initiatorName); + TerminalCraftingPlanFlatStatic plan = new TerminalCraftingPlanFlatStatic<>(id, entries, outputs, status, label, + tickDuration, craftingQuantityTotal, craftingQuantityRemaining, + estimatedTickDurationTotal, estimatedTickDurationRemaining, channel, initiatorName); if (unlocalizedLabelOverride != null) { plan.unlocalizedLabelOverride = unlocalizedLabelOverride; } diff --git a/src/main/java/org/cyclops/integratedterminals/api/terminalstorage/crafting/TerminalCraftingPlanStatic.java b/src/main/java/org/cyclops/integratedterminals/api/terminalstorage/crafting/TerminalCraftingPlanStatic.java index cad6a85e9b..b3733480dd 100644 --- a/src/main/java/org/cyclops/integratedterminals/api/terminalstorage/crafting/TerminalCraftingPlanStatic.java +++ b/src/main/java/org/cyclops/integratedterminals/api/terminalstorage/crafting/TerminalCraftingPlanStatic.java @@ -23,12 +23,15 @@ public class TerminalCraftingPlanStatic implements ITerminalCraftingPlan { private final List> outputs; private TerminalCraftingJobStatus status; private final long craftingQuantity; + private final long craftingQuantityTotal; private final List> bufferedIngredients; private final List>> lastMissingIngredients; private TerminalCraftingPlanStatic.Label label; @Nullable private String unlocalizedLabelOverride; private final long tickDuration; + private final long estimatedTickDurationTotal; + private final long estimatedTickDurationRemaining; private final int channel; @Nullable private final String initiatorName; @@ -44,16 +47,37 @@ public TerminalCraftingPlanStatic(I id, long tickDuration, int channel, @Nullable String initiatorName) { + this(id, dependencies, outputs, status, craftingQuantity, craftingQuantity, bufferedIngredients, + lastMissingIngredients, label, tickDuration, -1, -1, channel, initiatorName); + } + + public TerminalCraftingPlanStatic(I id, + List> dependencies, + List> outputs, + TerminalCraftingJobStatus status, + long craftingQuantity, + long craftingQuantityTotal, + List> bufferedIngredients, + List>> lastMissingIngredients, + TerminalCraftingPlanStatic.Label label, + long tickDuration, + long estimatedTickDurationTotal, + long estimatedTickDurationRemaining, + int channel, + @Nullable String initiatorName) { this.id = id; this.dependencies = dependencies; this.outputs = outputs; this.status = status; this.craftingQuantity = craftingQuantity; + this.craftingQuantityTotal = craftingQuantityTotal; this.bufferedIngredients = bufferedIngredients; this.lastMissingIngredients = lastMissingIngredients; this.label = label; this.unlocalizedLabelOverride = null; this.tickDuration = tickDuration; + this.estimatedTickDurationTotal = estimatedTickDurationTotal; + this.estimatedTickDurationRemaining = estimatedTickDurationRemaining; this.channel = channel; this.initiatorName = initiatorName; } @@ -83,6 +107,11 @@ public long getCraftingQuantity() { return craftingQuantity; } + @Override + public long getCraftingQuantityTotal() { + return craftingQuantityTotal; + } + @Override public List> getBufferedIngredients() { return bufferedIngredients; @@ -119,6 +148,16 @@ public long getTickDuration() { return tickDuration; } + @Override + public long getEstimatedTickDurationTotal() { + return estimatedTickDurationTotal; + } + + @Override + public long getEstimatedTickDurationRemaining() { + return estimatedTickDurationRemaining; + } + @Override public int getChannel() { return channel; @@ -143,6 +182,10 @@ public ITerminalCraftingPlanFlat flatten() { Set handledPlans = new HashSet<>(); groupDependenciesByPrototype(indexedEntries, handledPlans, this); + // Sum the crafting quantities of all jobs in this plan + CraftingQuantities craftingQuantities = new CraftingQuantities(); + sumCraftingQuantities(craftingQuantities, new HashSet<>(), this); + // Make plan TerminalCraftingPlanFlatStatic planFlat = new TerminalCraftingPlanFlatStatic<>( getId(), @@ -167,6 +210,10 @@ public ITerminalCraftingPlanFlat flatten() { getStatus(), getLabel(), getTickDuration(), + craftingQuantities.getTotal(), + craftingQuantities.getRemaining(), + getEstimatedTickDurationTotal(), + getEstimatedTickDurationRemaining(), getChannel(), getInitiatorName() ); @@ -242,9 +289,21 @@ public Collection getEntries() { } } + /** + * Since jobs can have multiple dependents due to job splitting, we only consider each job once during flattening. + * Jobs without a proper id, such as invalid jobs, are always considered. + * + * @param handledPlans The ids of the plans that were handled before. + * @param plan A plan. + * @return If the given plan was handled before. + * @param The type of identifier. + */ + protected static boolean isPlanHandled(Set handledPlans, ITerminalCraftingPlan plan) { + return (!(plan.getId() instanceof Integer id) || id > 0) && handledPlans.contains(plan.getId()); + } + protected static void groupDependenciesByPrototype(IndexedEntries indexedEntries, Set handledPlans, ITerminalCraftingPlan plan) { - // Since jobs can have multiple dependents due to job splitting, we only consider each job once during flattening. - if ((!(plan.getId() instanceof Integer id) || id > 0) && handledPlans.contains(plan.getId())) { + if (isPlanHandled(handledPlans, plan)) { return; } handledPlans.add(plan.getId()); @@ -297,6 +356,49 @@ protected static void groupDependenciesByPrototype(IndexedEntries indexedEnt } } + /** + * The number of crafting operations within a plan. + */ + public static class CraftingQuantities { + + private long total; + private long remaining; + + /** + * @return The number of crafting operations, including the ones that finished already. + */ + public long getTotal() { + return total; + } + + /** + * @return The number of crafting operations that still have to be performed. + */ + public long getRemaining() { + return remaining; + } + } + + protected static void sumCraftingQuantities(CraftingQuantities craftingQuantities, Set handledPlans, + ITerminalCraftingPlan plan) { + if (isPlanHandled(handledPlans, plan)) { + return; + } + handledPlans.add(plan.getId()); + + // Invalid jobs are not counted, as their crafting quantity refers to missing ingredients, + // and not to crafting operations that will be performed. + if (plan.getStatus().isValid()) { + craftingQuantities.total += plan.getCraftingQuantityTotal(); + craftingQuantities.remaining += plan.getCraftingQuantity(); + } + + // Recurse into dependencies + for (ITerminalCraftingPlan dependency : plan.getDependencies()) { + sumCraftingQuantities(craftingQuantities, handledPlans, dependency); + } + } + public static CompoundTag serialize(HolderLookup.Provider lookupProvider, TerminalCraftingPlanStatic plan, ITerminalStorageTabIngredientCraftingHandler handler) { CompoundTag tag = new CompoundTag(); @@ -319,6 +421,8 @@ public static CompoundTag serialize(HolderLookup.Provider lookupProvider, Te tag.putLong("craftingQuantity", plan.getCraftingQuantity()); + tag.putLong("craftingQuantityTotal", plan.getCraftingQuantityTotal()); + ListTag bufferedIngredients = new ListTag(); for (IPrototypedIngredient storageIngredient : plan.getBufferedIngredients()) { bufferedIngredients.add(IPrototypedIngredient.serialize(lookupProvider, (PrototypedIngredient) storageIngredient)); @@ -342,6 +446,9 @@ public static CompoundTag serialize(HolderLookup.Provider lookupProvider, Te tag.putLong("tickDuration", plan.getTickDuration()); + tag.putLong("estimatedTickDurationTotal", plan.getEstimatedTickDurationTotal()); + tag.putLong("estimatedTickDurationRemaining", plan.getEstimatedTickDurationRemaining()); + tag.putInt("channel", plan.getChannel()); if (plan.getInitiatorName() != null) { @@ -402,6 +509,9 @@ public static TerminalCraftingPlanStatic deserialize(HolderLookup.Provide long craftingQuantity = tag.getLong("craftingQuantity"); + long craftingQuantityTotal = tag.contains("craftingQuantityTotal", Tag.TAG_LONG) + ? tag.getLong("craftingQuantityTotal") : craftingQuantity; + ListTag bufferedIngredientsTag = tag.getList("bufferedIngredients", Tag.TAG_COMPOUND); List> bufferedIngredients = Lists.newArrayListWithExpectedSize(bufferedIngredientsTag.size()); for (Tag nbtBase : bufferedIngredientsTag) { @@ -428,6 +538,11 @@ public static TerminalCraftingPlanStatic deserialize(HolderLookup.Provide long tickDuration = tag.getLong("tickDuration"); + long estimatedTickDurationTotal = tag.contains("estimatedTickDurationTotal", Tag.TAG_LONG) + ? tag.getLong("estimatedTickDurationTotal") : -1; + long estimatedTickDurationRemaining = tag.contains("estimatedTickDurationRemaining", Tag.TAG_LONG) + ? tag.getLong("estimatedTickDurationRemaining") : -1; + int channel = tag.getInt("channel"); String initiatorName = null; @@ -435,8 +550,9 @@ public static TerminalCraftingPlanStatic deserialize(HolderLookup.Provide initiatorName = tag.getString("initiatorName"); } - TerminalCraftingPlanStatic plan = new TerminalCraftingPlanStatic<>(id, dependencies, outputs, status, craftingQuantity, bufferedIngredients, - lastMissingIngredients, label, tickDuration, channel, initiatorName); + TerminalCraftingPlanStatic plan = new TerminalCraftingPlanStatic<>(id, dependencies, outputs, status, craftingQuantity, + craftingQuantityTotal, bufferedIngredients, lastMissingIngredients, label, tickDuration, + estimatedTickDurationTotal, estimatedTickDurationRemaining, channel, initiatorName); if (unlocalizedLabelOverride != null) { plan.unlocalizedLabelOverride = unlocalizedLabelOverride; } diff --git a/src/main/java/org/cyclops/integratedterminals/modcompat/integratedcrafting/TerminalCraftingPlanCraftingJobDependencyGraph.java b/src/main/java/org/cyclops/integratedterminals/modcompat/integratedcrafting/TerminalCraftingPlanCraftingJobDependencyGraph.java index 9ab8d9245f..a93eac2594 100644 --- a/src/main/java/org/cyclops/integratedterminals/modcompat/integratedcrafting/TerminalCraftingPlanCraftingJobDependencyGraph.java +++ b/src/main/java/org/cyclops/integratedterminals/modcompat/integratedcrafting/TerminalCraftingPlanCraftingJobDependencyGraph.java @@ -34,6 +34,27 @@ public TerminalCraftingPlanCraftingJobDependencyGraph(int id, this.craftingJobDependencyGraph = craftingJobDependencyGraph; } + public TerminalCraftingPlanCraftingJobDependencyGraph(int id, + List> dependencies, + List> outputs, + TerminalCraftingJobStatus status, + long craftingQuantity, + long craftingQuantityTotal, + List> bufferedIngredients, + List>> lastMissingIngredients, + TerminalCraftingPlanStatic.Label label, + long tickDuration, + long estimatedTickDurationTotal, + long estimatedTickDurationRemaining, + int channel, + @Nullable String initiatorName, + CraftingJobDependencyGraph craftingJobDependencyGraph) { + super(id, dependencies, outputs, status, craftingQuantity, craftingQuantityTotal, bufferedIngredients, + lastMissingIngredients, label, tickDuration, estimatedTickDurationTotal, estimatedTickDurationRemaining, + channel, initiatorName); + this.craftingJobDependencyGraph = craftingJobDependencyGraph; + } + public CraftingJobDependencyGraph getCraftingJobDependencyGraph() { return craftingJobDependencyGraph; } diff --git a/src/main/java/org/cyclops/integratedterminals/modcompat/integratedcrafting/TerminalStorageTabIngredientCraftingHandlerCraftingNetwork.java b/src/main/java/org/cyclops/integratedterminals/modcompat/integratedcrafting/TerminalStorageTabIngredientCraftingHandlerCraftingNetwork.java index 36ac8e39ef..8f0223448a 100644 --- a/src/main/java/org/cyclops/integratedterminals/modcompat/integratedcrafting/TerminalStorageTabIngredientCraftingHandlerCraftingNetwork.java +++ b/src/main/java/org/cyclops/integratedterminals/modcompat/integratedcrafting/TerminalStorageTabIngredientCraftingHandlerCraftingNetwork.java @@ -438,10 +438,13 @@ public ITerminalCraftingPlan deserializeCraftingPlan(HolderLookup.Provi planStatic.getOutputs(), planStatic.getStatus(), planStatic.getCraftingQuantity(), + planStatic.getCraftingQuantityTotal(), planStatic.getBufferedIngredients(), planStatic.getLastMissingIngredients(), planStatic.getLabel(), planStatic.getTickDuration(), + planStatic.getEstimatedTickDurationTotal(), + planStatic.getEstimatedTickDurationRemaining(), planStatic.getChannel(), planStatic.getInitiatorName(), craftingJobDependencyGraph diff --git a/src/test/java/org/cyclops/integratedterminals/api/terminalstorage/crafting/TestTerminalCraftingPlanStatic.java b/src/test/java/org/cyclops/integratedterminals/api/terminalstorage/crafting/TestTerminalCraftingPlanStatic.java new file mode 100644 index 0000000000..bda0aeb60f --- /dev/null +++ b/src/test/java/org/cyclops/integratedterminals/api/terminalstorage/crafting/TestTerminalCraftingPlanStatic.java @@ -0,0 +1,91 @@ +package org.cyclops.integratedterminals.api.terminalstorage.crafting; + +import com.google.common.collect.Lists; +import org.junit.Test; + +import java.util.Collections; +import java.util.List; + +import static org.junit.Assert.assertEquals; + +/** + * @author rubensworks + */ +public class TestTerminalCraftingPlanStatic { + + protected static TerminalCraftingPlanStatic plan(int id, TerminalCraftingJobStatus status, + long craftingQuantity, long craftingQuantityTotal, + List> dependencies) { + return new TerminalCraftingPlanStatic<>(id, dependencies, Collections.emptyList(), status, + craftingQuantity, craftingQuantityTotal, Collections.emptyList(), Collections.emptyList(), + TerminalCraftingPlanStatic.Label.RUNNING, -1, -1, -1, 0, null); + } + + @Test + public void testCraftingQuantitiesWithoutDependencies() { + ITerminalCraftingPlanFlat flat = plan(1, TerminalCraftingJobStatus.CRAFTING, 3, 10, + Collections.emptyList()).flatten(); + + assertEquals(10, flat.getCraftingQuantityTotal()); + assertEquals(3, flat.getCraftingQuantityRemaining()); + } + + @Test + public void testCraftingQuantitiesWithDependencies() { + ITerminalCraftingPlanFlat flat = plan(1, TerminalCraftingJobStatus.CRAFTING, 3, 10, Lists.newArrayList( + plan(2, TerminalCraftingJobStatus.CRAFTING, 1, 2, Collections.emptyList()), + plan(3, TerminalCraftingJobStatus.FINISHED, 0, 5, Collections.emptyList()) + )).flatten(); + + assertEquals(17, flat.getCraftingQuantityTotal()); + assertEquals(4, flat.getCraftingQuantityRemaining()); + } + + @Test + public void testCraftingQuantitiesWithSharedDependency() { + // Due to job splitting, the same job can occur multiple times in a plan, but it may only be counted once. + ITerminalCraftingPlan shared = plan(4, TerminalCraftingJobStatus.CRAFTING, 1, 2, Collections.emptyList()); + ITerminalCraftingPlanFlat flat = plan(1, TerminalCraftingJobStatus.CRAFTING, 3, 10, Lists.newArrayList( + plan(2, TerminalCraftingJobStatus.CRAFTING, 1, 1, Lists.newArrayList(shared)), + plan(3, TerminalCraftingJobStatus.CRAFTING, 1, 1, Lists.newArrayList(shared)) + )).flatten(); + + assertEquals(14, flat.getCraftingQuantityTotal()); + assertEquals(6, flat.getCraftingQuantityRemaining()); + } + + @Test + public void testCraftingQuantitiesWithInvalidDependency() { + // Invalid jobs express missing ingredients in their crafting quantity, so they are not counted. + ITerminalCraftingPlanFlat flat = plan(1, TerminalCraftingJobStatus.UNSTARTED, 10, 10, Lists.newArrayList( + plan(0, TerminalCraftingJobStatus.INVALID, 64, 64, Collections.emptyList()) + )).flatten(); + + assertEquals(10, flat.getCraftingQuantityTotal()); + assertEquals(10, flat.getCraftingQuantityRemaining()); + } + + @Test + public void testEstimatedTickDurationsAreInheritedByFlatPlan() { + ITerminalCraftingPlanFlat flat = new TerminalCraftingPlanStatic<>(1, Collections.emptyList(), + Collections.emptyList(), TerminalCraftingJobStatus.CRAFTING, 3, 10, Collections.emptyList(), + Collections.emptyList(), TerminalCraftingPlanStatic.Label.RUNNING, 20, 200, 140, 0, null) + .flatten(); + + assertEquals(20, flat.getTickDuration()); + assertEquals(200, flat.getEstimatedTickDurationTotal()); + assertEquals(140, flat.getEstimatedTickDurationRemaining()); + } + + @Test + public void testUnknownValuesByDefault() { + TerminalCraftingPlanStatic plan = new TerminalCraftingPlanStatic<>(1, Collections.emptyList(), + Collections.emptyList(), TerminalCraftingJobStatus.UNSTARTED, 10, Collections.emptyList(), + Collections.emptyList(), TerminalCraftingPlanStatic.Label.VALID, -1, 0, null); + + assertEquals(10, plan.getCraftingQuantityTotal()); + assertEquals(-1, plan.getEstimatedTickDurationTotal()); + assertEquals(-1, plan.getEstimatedTickDurationRemaining()); + } + +} From f2e0efec1e6a82ac6702438d483480d52ef8f38c Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 2 Sep 2026 18:24:15 +0000 Subject: [PATCH 02/17] Address review: no default impls in the crafting plan API Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_0186zEXrjMNMVXQC7wcoSiMR --- .../crafting/ITerminalCraftingPlan.java | 12 +++--------- .../crafting/ITerminalCraftingPlanFlat.java | 16 ++++------------ 2 files changed, 7 insertions(+), 21 deletions(-) diff --git a/src/main/java/org/cyclops/integratedterminals/api/terminalstorage/crafting/ITerminalCraftingPlan.java b/src/main/java/org/cyclops/integratedterminals/api/terminalstorage/crafting/ITerminalCraftingPlan.java index 8dd1f6e0ad..0757eee0a2 100644 --- a/src/main/java/org/cyclops/integratedterminals/api/terminalstorage/crafting/ITerminalCraftingPlan.java +++ b/src/main/java/org/cyclops/integratedterminals/api/terminalstorage/crafting/ITerminalCraftingPlan.java @@ -48,9 +48,7 @@ public interface ITerminalCraftingPlan { * For jobs that have not started crafting yet, * this is equal to {@link #getCraftingQuantity()}. */ - public default long getCraftingQuantityTotal() { - return getCraftingQuantity(); - } + public long getCraftingQuantityTotal(); /** * @return The ingredients within the storage buffer. @@ -80,16 +78,12 @@ public default long getCraftingQuantityTotal() { * * @return The estimated total tick duration for this job. -1 indicates an unknown duration. */ - public default long getEstimatedTickDurationTotal() { - return -1; - } + public long getEstimatedTickDurationTotal(); /** * @return The estimated tick duration until this job is finished. -1 indicates an unknown duration. */ - public default long getEstimatedTickDurationRemaining() { - return -1; - } + public long getEstimatedTickDurationRemaining(); /** * @return The channel id, or -1 for non-applicable. diff --git a/src/main/java/org/cyclops/integratedterminals/api/terminalstorage/crafting/ITerminalCraftingPlanFlat.java b/src/main/java/org/cyclops/integratedterminals/api/terminalstorage/crafting/ITerminalCraftingPlanFlat.java index acb1a53b43..3040ae2ef9 100644 --- a/src/main/java/org/cyclops/integratedterminals/api/terminalstorage/crafting/ITerminalCraftingPlanFlat.java +++ b/src/main/java/org/cyclops/integratedterminals/api/terminalstorage/crafting/ITerminalCraftingPlanFlat.java @@ -51,17 +51,13 @@ public interface ITerminalCraftingPlanFlat { * @return The number of crafting operations in this plan, including the ones that finished already. * 0 indicates an unknown quantity. */ - public default long getCraftingQuantityTotal() { - return 0; - } + public long getCraftingQuantityTotal(); /** * @return The number of crafting operations in this plan that still have to be performed. * 0 indicates an unknown quantity, or a plan without remaining operations. */ - public default long getCraftingQuantityRemaining() { - return 0; - } + public long getCraftingQuantityRemaining(); /** * Contrary to {@link #getTickDuration()}, which indicates how long this job has been running already, @@ -69,16 +65,12 @@ public default long getCraftingQuantityRemaining() { * * @return The estimated total tick duration for this job. -1 indicates an unknown duration. */ - public default long getEstimatedTickDurationTotal() { - return -1; - } + public long getEstimatedTickDurationTotal(); /** * @return The estimated tick duration until this job is finished. -1 indicates an unknown duration. */ - public default long getEstimatedTickDurationRemaining() { - return -1; - } + public long getEstimatedTickDurationRemaining(); /** * @return The channel id, or -1 for non-applicable. From 41cc6dfe83b3bfde63f8a8e5c1cf2b4a2be30bfa Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 2 Sep 2026 18:40:48 +0000 Subject: [PATCH 03/17] Show crafting job progress and duration estimations Estimates the duration of a crafting plan by walking its dependency graph: a job takes the measured duration of its recipe times its amount, plus the longest estimation of the jobs it depends on, as dependencies have to finish first, while dependencies of the same job are crafted simultaneously. Running jobs subtract the time that their current crafting operation has been going on already. The crafting plan guis show the estimated total duration before a job is started, and the estimated remaining duration while it is running. The crafting jobs gui additionally shows the percentage of crafting operations that are finished. Refs #145 Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_0186zEXrjMNMVXQC7wcoSiMR --- .../ContainerScreenTerminalCraftingJobs.java | 14 +++ .../container/component/GuiCraftingPlan.java | 34 +++++++- .../component/GuiCraftingPlanFlat.java | 21 +++-- ...redientCraftingHandlerCraftingNetwork.java | 85 ++++++++++++++++--- .../integratedterminals/lang/en_us.json | 3 + ...redientCraftingHandlerCraftingNetwork.java | 75 ++++++++++++++++ 6 files changed, 216 insertions(+), 16 deletions(-) create mode 100644 src/test/java/org/cyclops/integratedterminals/modcompat/integratedcrafting/TestTerminalStorageTabIngredientCraftingHandlerCraftingNetwork.java diff --git a/src/main/java/org/cyclops/integratedterminals/client/gui/container/ContainerScreenTerminalCraftingJobs.java b/src/main/java/org/cyclops/integratedterminals/client/gui/container/ContainerScreenTerminalCraftingJobs.java index 731a1b0db1..11cc727acf 100644 --- a/src/main/java/org/cyclops/integratedterminals/client/gui/container/ContainerScreenTerminalCraftingJobs.java +++ b/src/main/java/org/cyclops/integratedterminals/client/gui/container/ContainerScreenTerminalCraftingJobs.java @@ -158,6 +158,20 @@ protected void drawCraftingPlan(GuiGraphics guiGraphics, HandlerWrappedTerminalC String durationString = GuiCraftingPlan.getDurationString(tickDuration); RenderHelpers.drawScaledString(guiGraphics.pose(), guiGraphics.bufferSource(), font, durationString, xOriginal + LINE_WIDTH - 80, y + 13, 0.5f, 16777215, true, Font.DisplayMode.NORMAL); } + + int progress = GuiCraftingPlan.getProgress(plan); + if (progress >= 0) { + String progressString = L10NHelpers.localize("gui.integratedterminals.terminal_crafting_job.craftingplan.progress", progress); + RenderHelpers.drawScaledString(guiGraphics.pose(), guiGraphics.bufferSource(), font, progressString, xOriginal + LINE_WIDTH - 40, y + 1, 0.5f, 16777215, true, Font.DisplayMode.NORMAL); + } + + long estimatedTickDurationRemaining = plan.getEstimatedTickDurationRemaining(); + if (estimatedTickDurationRemaining >= 0) { + String remainingString = GuiCraftingPlan.getDurationString( + "gui.integratedterminals.terminal_crafting_job.craftingplan.duration.remaining", + estimatedTickDurationRemaining); + RenderHelpers.drawScaledString(guiGraphics.pose(), guiGraphics.bufferSource(), font, remainingString, xOriginal + LINE_WIDTH - 40, y + 13, 0.5f, 16777215, true, Font.DisplayMode.NORMAL); + } } } diff --git a/src/main/java/org/cyclops/integratedterminals/client/gui/container/component/GuiCraftingPlan.java b/src/main/java/org/cyclops/integratedterminals/client/gui/container/component/GuiCraftingPlan.java index 03cf394fac..0f61bd8859 100644 --- a/src/main/java/org/cyclops/integratedterminals/client/gui/container/component/GuiCraftingPlan.java +++ b/src/main/java/org/cyclops/integratedterminals/client/gui/container/component/GuiCraftingPlan.java @@ -31,6 +31,7 @@ import org.cyclops.cyclopscore.helper.RenderHelpers; import org.cyclops.integratedterminals.Capabilities; import org.cyclops.integratedterminals.api.terminalstorage.crafting.ITerminalCraftingPlan; +import org.cyclops.integratedterminals.api.terminalstorage.crafting.ITerminalCraftingPlanFlat; import org.cyclops.integratedterminals.api.terminalstorage.crafting.TerminalCraftingJobStatus; import org.cyclops.integratedterminals.client.gui.container.ContainerScreenTerminalStorage; import org.cyclops.integratedterminals.core.client.gui.CraftingOptionGuiData; @@ -71,6 +72,8 @@ public class GuiCraftingPlan extends AbstractWidget { private final WidgetScrollBar scrollBar; private final String label; private final long tickDuration; + private final long estimatedTickDuration; + private final boolean estimatedTickDurationRemaining; private final int channel; @Nullable private final String initiatorName; @@ -89,6 +92,10 @@ public GuiCraftingPlan(AbstractContainerScreen parentGui, ITerminalCraftingPlan< this.scrollBar.setTotalRows(visibleElements.size()); this.label = L10NHelpers.localize(craftingPlan.getUnlocalizedLabel()); this.tickDuration = craftingPlan.getTickDuration(); + // Jobs that have not started yet can only show a total estimation, running jobs show what is left of it + this.estimatedTickDurationRemaining = craftingPlan.getStatus() != TerminalCraftingJobStatus.UNSTARTED; + this.estimatedTickDuration = this.estimatedTickDurationRemaining + ? craftingPlan.getEstimatedTickDurationRemaining() : craftingPlan.getEstimatedTickDurationTotal(); this.channel = craftingPlan.getChannel(); this.initiatorName = craftingPlan.getInitiatorName(); } @@ -247,11 +254,28 @@ protected static void renderItem(GuiGraphics guiGraphics, ItemStack itemStack, i } public static String getDurationString(long tickDuration) { + return getDurationString("gui.integratedterminals.terminal_crafting_job.craftingplan.duration", tickDuration); + } + + public static String getDurationString(String unlocalizedName, long tickDuration) { long durationMs = tickDuration * 1000 / MinecraftHelpers.SECOND_IN_TICKS; - return L10NHelpers.localize("gui.integratedterminals.terminal_crafting_job.craftingplan.duration", + return L10NHelpers.localize(unlocalizedName, DurationFormatUtils.formatDuration(durationMs, "H:mm:ss", true)); } + /** + * @param craftingPlan A flattened crafting plan. + * @return The percentage of crafting operations of the plan that are finished, or -1 if unknown. + */ + public static int getProgress(ITerminalCraftingPlanFlat craftingPlan) { + long total = craftingPlan.getCraftingQuantityTotal(); + if (total <= 0) { + return -1; + } + long finished = total - craftingPlan.getCraftingQuantityRemaining(); + return (int) (finished * 100 / total); + } + public void drawGuiContainerBackgroundLayer(GuiGraphics guiGraphics, float partialTicks, int mouseX, int mouseY) { Font fontRenderer = Minecraft.getInstance().font; @@ -276,6 +300,14 @@ public void drawGuiContainerBackgroundLayer(GuiGraphics guiGraphics, float parti RenderHelpers.drawScaledString(guiGraphics.pose(), guiGraphics.bufferSource(), fontRenderer, initiatorString, guiLeft + getX() - 4, guiTop + getY() - 14, 0.5f, 16777215, true, Font.DisplayMode.NORMAL); } + // Draw estimated duration + if (estimatedTickDuration >= 0) { + String estimatedDurationString = getDurationString(estimatedTickDurationRemaining + ? "gui.integratedterminals.terminal_crafting_job.craftingplan.duration.remaining" + : "gui.integratedterminals.terminal_crafting_job.craftingplan.duration.estimate", estimatedTickDuration); + RenderHelpers.drawScaledString(guiGraphics.pose(), guiGraphics.bufferSource(), fontRenderer, estimatedDurationString, guiLeft + getX() - 4, guiTop + getY() - 8, 0.5f, 16777215, true, Font.DisplayMode.NORMAL); + } + drawGuiContainerLayer(guiGraphics, guiLeft, guiTop, ContainerScreenTerminalStorage.DrawLayer.BACKGROUND, partialTicks, mouseX, mouseY); scrollBar.render(guiGraphics, mouseX, mouseY, partialTicks); } diff --git a/src/main/java/org/cyclops/integratedterminals/client/gui/container/component/GuiCraftingPlanFlat.java b/src/main/java/org/cyclops/integratedterminals/client/gui/container/component/GuiCraftingPlanFlat.java index ec32b8cbf1..077af56246 100644 --- a/src/main/java/org/cyclops/integratedterminals/client/gui/container/component/GuiCraftingPlanFlat.java +++ b/src/main/java/org/cyclops/integratedterminals/client/gui/container/component/GuiCraftingPlanFlat.java @@ -14,17 +14,16 @@ import net.minecraft.world.item.ItemStack; import net.minecraft.world.item.Items; import net.minecraft.world.level.block.Blocks; -import org.apache.commons.lang3.time.DurationFormatUtils; import org.cyclops.commoncapabilities.api.ingredient.IPrototypedIngredient; import org.cyclops.commoncapabilities.api.ingredient.IngredientComponent; import org.cyclops.cyclopscore.client.gui.GuiGraphicsExtended; import org.cyclops.cyclopscore.client.gui.component.WidgetScrollBar; import org.cyclops.cyclopscore.helper.Helpers; import org.cyclops.cyclopscore.helper.L10NHelpers; -import org.cyclops.cyclopscore.helper.MinecraftHelpers; import org.cyclops.cyclopscore.helper.RenderHelpers; import org.cyclops.integratedterminals.Capabilities; import org.cyclops.integratedterminals.api.terminalstorage.crafting.ITerminalCraftingPlanFlat; +import org.cyclops.integratedterminals.api.terminalstorage.crafting.TerminalCraftingJobStatus; import org.cyclops.integratedterminals.client.gui.container.ContainerScreenTerminalStorage; import org.cyclops.integratedterminals.core.client.gui.CraftingOptionGuiData; import org.lwjgl.opengl.GL11; @@ -63,6 +62,8 @@ public class GuiCraftingPlanFlat extends AbstractWidget { private final WidgetScrollBar scrollBar; private final String label; private final long tickDuration; + private final long estimatedTickDuration; + private final boolean estimatedTickDurationRemaining; private final int channel; @Nullable private final String initiatorName; @@ -81,6 +82,10 @@ public GuiCraftingPlanFlat(AbstractContainerScreen parentGui, ITerminalCraftingP refreshList(); this.label = L10NHelpers.localize(craftingPlan.getUnlocalizedLabel()); this.tickDuration = craftingPlan.getTickDuration(); + // Jobs that have not started yet can only show a total estimation, running jobs show what is left of it + this.estimatedTickDurationRemaining = craftingPlan.getStatus() != TerminalCraftingJobStatus.UNSTARTED; + this.estimatedTickDuration = this.estimatedTickDurationRemaining + ? craftingPlan.getEstimatedTickDurationRemaining() : craftingPlan.getEstimatedTickDurationTotal(); this.channel = craftingPlan.getChannel(); this.initiatorName = craftingPlan.getInitiatorName(); } @@ -210,9 +215,7 @@ protected static void renderItem(GuiGraphics guiGraphics, ItemStack itemStack, i } public static String getDurationString(long tickDuration) { - long durationMs = tickDuration * 1000 / MinecraftHelpers.SECOND_IN_TICKS; - return L10NHelpers.localize("gui.integratedterminals.terminal_crafting_job.craftingplan.duration", - DurationFormatUtils.formatDuration(durationMs, "H:mm:ss", true)); + return GuiCraftingPlan.getDurationString(tickDuration); } public void drawGuiContainerBackgroundLayer(GuiGraphics guiGraphics, float partialTicks, int mouseX, int mouseY) { @@ -239,6 +242,14 @@ public void drawGuiContainerBackgroundLayer(GuiGraphics guiGraphics, float parti RenderHelpers.drawScaledString(guiGraphics.pose(), guiGraphics.bufferSource(), fontRenderer, initiatorString, guiLeft + getX() - 4, guiTop + getY() - 14, 0.5f, 16777215, true, Font.DisplayMode.NORMAL); } + // Draw estimated duration + if (estimatedTickDuration >= 0) { + String estimatedDurationString = GuiCraftingPlan.getDurationString(estimatedTickDurationRemaining + ? "gui.integratedterminals.terminal_crafting_job.craftingplan.duration.remaining" + : "gui.integratedterminals.terminal_crafting_job.craftingplan.duration.estimate", estimatedTickDuration); + RenderHelpers.drawScaledString(guiGraphics.pose(), guiGraphics.bufferSource(), fontRenderer, estimatedDurationString, guiLeft + getX() - 4, guiTop + getY() - 8, 0.5f, 16777215, true, Font.DisplayMode.NORMAL); + } + drawGuiContainerLayer(guiGraphics, guiLeft, guiTop, ContainerScreenTerminalStorage.DrawLayer.BACKGROUND, partialTicks, mouseX, mouseY); scrollBar.render(guiGraphics, mouseX, mouseY, partialTicks); } diff --git a/src/main/java/org/cyclops/integratedterminals/modcompat/integratedcrafting/TerminalStorageTabIngredientCraftingHandlerCraftingNetwork.java b/src/main/java/org/cyclops/integratedterminals/modcompat/integratedcrafting/TerminalStorageTabIngredientCraftingHandlerCraftingNetwork.java index 8f0223448a..1ad99ba39d 100644 --- a/src/main/java/org/cyclops/integratedterminals/modcompat/integratedcrafting/TerminalStorageTabIngredientCraftingHandlerCraftingNetwork.java +++ b/src/main/java/org/cyclops/integratedterminals/modcompat/integratedcrafting/TerminalStorageTabIngredientCraftingHandlerCraftingNetwork.java @@ -28,6 +28,7 @@ import javax.annotation.Nullable; import java.util.*; +import java.util.function.ToLongFunction; import java.util.stream.Collectors; import java.util.stream.StreamSupport; @@ -91,26 +92,68 @@ public ITerminalCraftingPlan calculateCraftingPlan(INetwork network, in TerminalCraftingOptionRecipeDefinition safeCraftingOption = (TerminalCraftingOptionRecipeDefinition) craftingOption; IRecipeDefinition recipe = safeCraftingOption.getRecipe(); + ICraftingNetwork craftingNetwork = CraftingHelpers.getCraftingNetwork(network).orElse(null); CraftingJobDependencyGraph dependencyGraph = new CraftingJobDependencyGraph(); try { CraftingJob rootJob = CraftingHelpers.calculateCraftingJobs(network, channel, recipe, (int) quantity, true, CraftingHelpers.getGlobalCraftingJobIdentifier(), dependencyGraph, true); - return newCraftingPlan(rootJob, dependencyGraph, true); + return newCraftingPlan(craftingNetwork, rootJob, dependencyGraph, true); } catch (FailedCraftingRecipeException e) { - return newCraftingPlanFailed(e, dependencyGraph); + return newCraftingPlanFailed(craftingNetwork, e, dependencyGraph); } catch (RecursiveCraftingRecipeException e) { return newCraftingPlanErrorRecursive(Lists.reverse(e.getRecipeStack())); } } - protected static ITerminalCraftingPlan newCraftingPlan(CraftingJob craftingJob, + /** + * Estimate how long the given job takes, including the jobs it depends on. + * + * Dependencies have to finish before the job itself can start, + * so their estimations are added to the estimation of the job itself. + * Dependencies of the same job can be crafted simultaneously, so only the longest one is counted. + * Note that this does not take into account that a job can be distributed over multiple crafting interfaces. + * + * @param craftingNetwork The crafting network, or null if unavailable. + * @param craftingJob A crafting job. + * @param amount The number of crafting operations to estimate for. + * @param dependencies The plans of the jobs that the given job depends on. + * @return The estimated tick duration, or -1 if nothing within this plan was measured before. + */ + protected static long estimateTickDuration(@Nullable ICraftingNetwork craftingNetwork, CraftingJob craftingJob, + long amount, List> dependencies) { + return estimateTickDuration(craftingNetwork, craftingJob, amount, dependencies, + ITerminalCraftingPlan::getEstimatedTickDurationTotal); + } + + protected static long estimateTickDuration(@Nullable ICraftingNetwork craftingNetwork, CraftingJob craftingJob, + long amount, List> dependencies, + ToLongFunction> dependencyDuration) { + long dependenciesDuration = -1; + for (ITerminalCraftingPlan dependency : dependencies) { + dependenciesDuration = Math.max(dependenciesDuration, dependencyDuration.applyAsLong(dependency)); + } + + long recipeDuration = craftingNetwork == null ? -1 : craftingNetwork + .getEstimatedRecipeDuration(craftingJob.getChannel(), craftingJob.getRecipe()); + if (recipeDuration < 0 && dependenciesDuration < 0) { + return -1; + } + + return Math.max(recipeDuration, 0) * amount + Math.max(dependenciesDuration, 0); + } + + protected static ITerminalCraftingPlan newCraftingPlan(@Nullable ICraftingNetwork craftingNetwork, + CraftingJob craftingJob, CraftingJobDependencyGraph dependencyGraph, boolean root) { List recipeOutputs = IntegratedCraftingHelpers.getPrototypesFromIngredients(craftingJob.getRecipe().getOutput()); List> dependencies = dependencyGraph.getDependencies(craftingJob) .stream() - .map(subCraftingJob -> newCraftingPlan(subCraftingJob, dependencyGraph, false)) + .map(subCraftingJob -> newCraftingPlan(craftingNetwork, subCraftingJob, dependencyGraph, false)) .collect(Collectors.toList()); + // The job has not started yet, so its remaining duration is equal to its total duration + long estimatedTickDuration = estimateTickDuration(craftingNetwork, craftingJob, + craftingJob.getAmountTotal(), dependencies); if (root) { return new TerminalCraftingPlanCraftingJobDependencyGraph( craftingJob.getId(), @@ -118,10 +161,13 @@ protected static ITerminalCraftingPlan newCraftingPlan(CraftingJob craf CraftingHelpers.multiplyPrototypedIngredients(recipeOutputs, craftingJob.getAmount()), TerminalCraftingJobStatus.UNSTARTED, craftingJob.getAmount(), + craftingJob.getAmountTotal(), IntegratedCraftingHelpers.getPrototypesFromIngredients(craftingJob.getIngredientsStorageBuffer()), Collections.emptyList(), TerminalCraftingPlanStatic.Label.VALID, -1, + estimatedTickDuration, + estimatedTickDuration, craftingJob.getChannel(), null, dependencyGraph); @@ -132,26 +178,29 @@ protected static ITerminalCraftingPlan newCraftingPlan(CraftingJob craf CraftingHelpers.multiplyPrototypedIngredients(recipeOutputs, craftingJob.getAmount()), TerminalCraftingJobStatus.UNSTARTED, craftingJob.getAmount(), + craftingJob.getAmountTotal(), IntegratedCraftingHelpers.getPrototypesFromIngredients(craftingJob.getIngredientsStorageBuffer()), Collections.emptyList(), TerminalCraftingPlanStatic.Label.VALID, -1, + estimatedTickDuration, + estimatedTickDuration, craftingJob.getChannel(), null); } } - protected static ITerminalCraftingPlan newCraftingPlanUnknown(UnknownCraftingRecipeException exception, CraftingJobDependencyGraph dependencyGraph) { + protected static ITerminalCraftingPlan newCraftingPlanUnknown(@Nullable ICraftingNetwork craftingNetwork, UnknownCraftingRecipeException exception, CraftingJobDependencyGraph dependencyGraph) { List> dependencies = Lists.newArrayList(); // Add all valid jobs dependencies.addAll( exception.getPartialCraftingJobs() .stream() - .map(subCraftingJob -> newCraftingPlan(subCraftingJob, dependencyGraph, false)) + .map(subCraftingJob -> newCraftingPlan(craftingNetwork, subCraftingJob, dependencyGraph, false)) .collect(Collectors.toList())); // Add all sub-unknown jobs dependencies.addAll(exception.getMissingChildRecipes() .stream() - .map(subCraftingJob -> newCraftingPlanUnknown(subCraftingJob, dependencyGraph)) + .map(subCraftingJob -> newCraftingPlanUnknown(craftingNetwork, subCraftingJob, dependencyGraph)) .collect(Collectors.toList())); return new TerminalCraftingPlanStatic<>( 0, @@ -166,18 +215,18 @@ protected static ITerminalCraftingPlan newCraftingPlanUnknown(UnknownCr -1, null); } - protected static ITerminalCraftingPlan newCraftingPlanFailed(FailedCraftingRecipeException exception, CraftingJobDependencyGraph dependencyGraph) { + protected static ITerminalCraftingPlan newCraftingPlanFailed(@Nullable ICraftingNetwork craftingNetwork, FailedCraftingRecipeException exception, CraftingJobDependencyGraph dependencyGraph) { List> dependencies = Lists.newArrayList(); // Add all valid jobs dependencies.addAll( exception.getPartialCraftingJobs() .stream() - .map(subCraftingJob -> newCraftingPlan(subCraftingJob, dependencyGraph, false)) + .map(subCraftingJob -> newCraftingPlan(craftingNetwork, subCraftingJob, dependencyGraph, false)) .collect(Collectors.toList())); // Add all sub-unknown jobs dependencies.addAll(exception.getMissingChildRecipes() .stream() - .map(subCraftingJob -> newCraftingPlanUnknown(subCraftingJob, dependencyGraph)) + .map(subCraftingJob -> newCraftingPlanUnknown(craftingNetwork, subCraftingJob, dependencyGraph)) .collect(Collectors.toList())); List recipeOutputs = IntegratedCraftingHelpers.getPrototypesFromIngredients(exception.getRecipe().getOutput()); return new TerminalCraftingPlanStatic( @@ -352,16 +401,32 @@ protected static ITerminalCraftingPlan newActiveCraftingJob(ICraftingNe } } + long estimatedTickDurationTotal = estimateTickDuration(craftingNetwork, craftingJob, + craftingJob.getAmountTotal(), dependencies); + long estimatedTickDurationRemaining = estimateTickDuration(craftingNetwork, craftingJob, + craftingJob.getAmount(), dependencies, ITerminalCraftingPlan::getEstimatedTickDurationRemaining); + if (estimatedTickDurationRemaining > 0) { + // Subtract the time that the currently running crafting operation has been going on already + long entryStartTick = craftingInterface.getCraftingJobEntryStartTick(craftingJobId); + if (entryStartTick >= 0) { + long entryTickDuration = Math.max(0, CraftingHelpers.getCurrentTick() - entryStartTick); + estimatedTickDurationRemaining = Math.max(0, estimatedTickDurationRemaining - entryTickDuration); + } + } + return new TerminalCraftingPlanStatic<>( craftingJob.getId(), dependencies, pendingOutputs, jobStatus, craftingJob.getAmount(), + craftingJob.getAmountTotal(), IntegratedCraftingHelpers.getPrototypesFromIngredients(craftingJob.getIngredientsStorageBuffer()), lastMissingIngredients, TerminalCraftingPlanStatic.Label.RUNNING, craftingNetwork.getRunningTicks(craftingJob), + estimatedTickDurationTotal, + estimatedTickDurationRemaining, craftingJob.getChannel(), uuidToName(craftingJob.getInitiatorUuid())); } diff --git a/src/main/resources/assets/integratedterminals/lang/en_us.json b/src/main/resources/assets/integratedterminals/lang/en_us.json index b1acb63376..b9dcd28b5d 100644 --- a/src/main/resources/assets/integratedterminals/lang/en_us.json +++ b/src/main/resources/assets/integratedterminals/lang/en_us.json @@ -76,6 +76,9 @@ "gui.integratedterminals.terminal_crafting_job.craftingplan.cancel_all": "Cancel All Jobs", "gui.integratedterminals.terminal_crafting_job.craftingplan.empty": "Empty crafting job", "gui.integratedterminals.terminal_crafting_job.craftingplan.duration": "Duration: %s", + "gui.integratedterminals.terminal_crafting_job.craftingplan.duration.estimate": "Estimated: %s", + "gui.integratedterminals.terminal_crafting_job.craftingplan.duration.remaining": "Remaining: %s", + "gui.integratedterminals.terminal_crafting_job.craftingplan.progress": "Progress: %s%%", "gui.integratedterminals.terminal_crafting_job.craftingplan.crafting_channel": "Channel: %s", "gui.integratedterminals.terminal_crafting_job.craftingplan.owner": "Owner: %s", diff --git a/src/test/java/org/cyclops/integratedterminals/modcompat/integratedcrafting/TestTerminalStorageTabIngredientCraftingHandlerCraftingNetwork.java b/src/test/java/org/cyclops/integratedterminals/modcompat/integratedcrafting/TestTerminalStorageTabIngredientCraftingHandlerCraftingNetwork.java new file mode 100644 index 0000000000..8db1327ed8 --- /dev/null +++ b/src/test/java/org/cyclops/integratedterminals/modcompat/integratedcrafting/TestTerminalStorageTabIngredientCraftingHandlerCraftingNetwork.java @@ -0,0 +1,75 @@ +package org.cyclops.integratedterminals.modcompat.integratedcrafting; + +import com.google.common.collect.Lists; +import com.google.common.collect.Maps; +import org.cyclops.commoncapabilities.api.ingredient.MixedIngredients; +import org.cyclops.integratedcrafting.api.crafting.CraftingJob; +import org.cyclops.integratedterminals.api.terminalstorage.crafting.ITerminalCraftingPlan; +import org.cyclops.integratedterminals.api.terminalstorage.crafting.TerminalCraftingJobStatus; +import org.cyclops.integratedterminals.api.terminalstorage.crafting.TerminalCraftingPlanStatic; +import org.junit.Before; +import org.junit.Test; + +import java.util.Collections; +import java.util.List; + +import static org.junit.Assert.assertEquals; + +/** + * @author rubensworks + */ +public class TestTerminalStorageTabIngredientCraftingHandlerCraftingNetwork { + + private CraftingJob craftingJob; + + @Before + public void beforeEach() { + this.craftingJob = new CraftingJob(1, 0, null, 3, new MixedIngredients(Maps.newIdentityHashMap())); + } + + protected static ITerminalCraftingPlan planWithEstimations(long total, long remaining) { + return new TerminalCraftingPlanStatic<>(1, Collections.emptyList(), Collections.emptyList(), + TerminalCraftingJobStatus.CRAFTING, 1, 1, Collections.emptyList(), Collections.emptyList(), + TerminalCraftingPlanStatic.Label.RUNNING, -1, total, remaining, 0, null); + } + + @Test + public void testEstimateWithoutMeasurements() { + // Without a crafting network, no recipe durations are known + assertEquals(-1, TerminalStorageTabIngredientCraftingHandlerCraftingNetwork + .estimateTickDuration(null, craftingJob, 3, Collections.emptyList())); + } + + @Test + public void testEstimateFromDependencies() { + // Dependencies are crafted simultaneously, so only the longest one counts + List> dependencies = Lists.newArrayList( + planWithEstimations(100, 40), + planWithEstimations(250, 90)); + + assertEquals(250, TerminalStorageTabIngredientCraftingHandlerCraftingNetwork + .estimateTickDuration(null, craftingJob, 3, dependencies)); + } + + @Test + public void testEstimateFromRemainingDependencies() { + List> dependencies = Lists.newArrayList( + planWithEstimations(100, 40), + planWithEstimations(250, 90)); + + assertEquals(90, TerminalStorageTabIngredientCraftingHandlerCraftingNetwork + .estimateTickDuration(null, craftingJob, 3, dependencies, + ITerminalCraftingPlan::getEstimatedTickDurationRemaining)); + } + + @Test + public void testEstimateWithUnknownDependencies() { + List> dependencies = Lists.newArrayList( + planWithEstimations(-1, -1), + planWithEstimations(250, 90)); + + assertEquals(250, TerminalStorageTabIngredientCraftingHandlerCraftingNetwork + .estimateTickDuration(null, craftingJob, 3, dependencies)); + } + +} From 81b6ba1034c3283f65f595018299c70a835eb44a Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 2 Sep 2026 18:47:16 +0000 Subject: [PATCH 04/17] Update to the IntegratedCrafting dev build with duration measurements Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_0186zEXrjMNMVXQC7wcoSiMR --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 135d7157ee..1883a0970c 100644 --- a/gradle.properties +++ b/gradle.properties @@ -34,5 +34,5 @@ org.gradle.caching=true cyclopscore_version=1.26.2-808 integrateddynamics_version=1.32.0-1630 integratedterminalscompat_version=1.0.0-167 -integratedcrafting_version=1.4.1-442 +integratedcrafting_version=1.5.0-678 commoncapabilities_version=2.9.12-263 From 15921e9b7dea2870298649195d558f3329659fb9 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 2 Sep 2026 18:51:22 +0000 Subject: [PATCH 05/17] Ignore ClientDevBridge session state Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_0186zEXrjMNMVXQC7wcoSiMR --- .gitignore | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.gitignore b/.gitignore index bc7040e522..37f308c19c 100644 --- a/.gitignore +++ b/.gitignore @@ -28,3 +28,7 @@ logs/ secrets.properties build_number.properties changelog.txt + +# ClientDevBridge session state (golden images are intentionally kept) +.clientdevbridge/* +!.clientdevbridge/golden/ From 8466ea3ae27bef916828fc026255cc75608d8e16 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 2 Sep 2026 19:55:51 +0000 Subject: [PATCH 06/17] Widen the crafting job info columns The progress and remaining duration that were added to the crafting job list are drawn in a second column, which the status and duration of the first column ran into: at half scale "Status: Queueing" and "Duration: 0:00:00" are both wider than the 40 pixels that separated the two columns, so their last characters were overwritten. Widen both columns to fit their longest strings, the "Pending Dependencies" status and a two-digit hour duration. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_016gX5ZToD5ApLBXQkKW2QGj --- .../ContainerScreenTerminalCraftingJobs.java | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/src/main/java/org/cyclops/integratedterminals/client/gui/container/ContainerScreenTerminalCraftingJobs.java b/src/main/java/org/cyclops/integratedterminals/client/gui/container/ContainerScreenTerminalCraftingJobs.java index 11cc727acf..8c107dac09 100644 --- a/src/main/java/org/cyclops/integratedterminals/client/gui/container/ContainerScreenTerminalCraftingJobs.java +++ b/src/main/java/org/cyclops/integratedterminals/client/gui/container/ContainerScreenTerminalCraftingJobs.java @@ -43,6 +43,11 @@ public class ContainerScreenTerminalCraftingJobs extends ContainerScreenExtended public static int LINE_WIDTH = 221; + // Offsets from the end of a line for the two columns of half-scale info text. + // The left column has to fit the longest status label, the right one the longest duration. + private static final int COLUMN_LEFT = 124; + private static final int COLUMN_RIGHT = 48; + private final Player player; private WidgetScrollBar scrollBar; @@ -142,27 +147,27 @@ protected void drawCraftingPlan(GuiGraphics guiGraphics, HandlerWrappedTerminalC if (layer == ContainerScreenTerminalStorage.DrawLayer.BACKGROUND) { String statusString = L10NHelpers.localize("gui.integratedterminals.craftingplan.status", L10NHelpers.localize( "gui.integratedterminals.craftingplan.status." + plan.getStatus().name().toLowerCase(Locale.ENGLISH))); - RenderHelpers.drawScaledString(guiGraphics.pose(), guiGraphics.bufferSource(), font, statusString, xOriginal + LINE_WIDTH - 80, y + 1, 0.5f, 16777215, true, Font.DisplayMode.NORMAL); + RenderHelpers.drawScaledString(guiGraphics.pose(), guiGraphics.bufferSource(), font, statusString, xOriginal + LINE_WIDTH - COLUMN_LEFT, y + 1, 0.5f, 16777215, true, Font.DisplayMode.NORMAL); int dependencies = plan.getEntries().size(); String dependenciesString = L10NHelpers.localize("gui.integratedterminals.terminal_crafting_job.craftingplan.dependencies", dependencies); - RenderHelpers.drawScaledString(guiGraphics.pose(), guiGraphics.bufferSource(), font, dependenciesString, xOriginal + LINE_WIDTH - 80, y + 7, 0.5f, 16777215, true, Font.DisplayMode.NORMAL); + RenderHelpers.drawScaledString(guiGraphics.pose(), guiGraphics.bufferSource(), font, dependenciesString, xOriginal + LINE_WIDTH - COLUMN_LEFT, y + 7, 0.5f, 16777215, true, Font.DisplayMode.NORMAL); if (plan.getChannel() != -1) { String channelString = L10NHelpers.localize("gui.integratedterminals.terminal_crafting_job.craftingplan.crafting_channel", plan.getChannel()); - RenderHelpers.drawScaledString(guiGraphics.pose(), guiGraphics.bufferSource(), font, channelString, xOriginal + LINE_WIDTH - 40, y + 7, 0.5f, 16777215, true, Font.DisplayMode.NORMAL); + RenderHelpers.drawScaledString(guiGraphics.pose(), guiGraphics.bufferSource(), font, channelString, xOriginal + LINE_WIDTH - COLUMN_RIGHT, y + 7, 0.5f, 16777215, true, Font.DisplayMode.NORMAL); } long tickDuration = plan.getTickDuration(); if (tickDuration >= 0) { String durationString = GuiCraftingPlan.getDurationString(tickDuration); - RenderHelpers.drawScaledString(guiGraphics.pose(), guiGraphics.bufferSource(), font, durationString, xOriginal + LINE_WIDTH - 80, y + 13, 0.5f, 16777215, true, Font.DisplayMode.NORMAL); + RenderHelpers.drawScaledString(guiGraphics.pose(), guiGraphics.bufferSource(), font, durationString, xOriginal + LINE_WIDTH - COLUMN_LEFT, y + 13, 0.5f, 16777215, true, Font.DisplayMode.NORMAL); } int progress = GuiCraftingPlan.getProgress(plan); if (progress >= 0) { String progressString = L10NHelpers.localize("gui.integratedterminals.terminal_crafting_job.craftingplan.progress", progress); - RenderHelpers.drawScaledString(guiGraphics.pose(), guiGraphics.bufferSource(), font, progressString, xOriginal + LINE_WIDTH - 40, y + 1, 0.5f, 16777215, true, Font.DisplayMode.NORMAL); + RenderHelpers.drawScaledString(guiGraphics.pose(), guiGraphics.bufferSource(), font, progressString, xOriginal + LINE_WIDTH - COLUMN_RIGHT, y + 1, 0.5f, 16777215, true, Font.DisplayMode.NORMAL); } long estimatedTickDurationRemaining = plan.getEstimatedTickDurationRemaining(); @@ -170,7 +175,7 @@ protected void drawCraftingPlan(GuiGraphics guiGraphics, HandlerWrappedTerminalC String remainingString = GuiCraftingPlan.getDurationString( "gui.integratedterminals.terminal_crafting_job.craftingplan.duration.remaining", estimatedTickDurationRemaining); - RenderHelpers.drawScaledString(guiGraphics.pose(), guiGraphics.bufferSource(), font, remainingString, xOriginal + LINE_WIDTH - 40, y + 13, 0.5f, 16777215, true, Font.DisplayMode.NORMAL); + RenderHelpers.drawScaledString(guiGraphics.pose(), guiGraphics.bufferSource(), font, remainingString, xOriginal + LINE_WIDTH - COLUMN_RIGHT, y + 13, 0.5f, 16777215, true, Font.DisplayMode.NORMAL); } } } From 51d2662cd69e358f8aef5ea25dc12b0a4f771129 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 2 Sep 2026 20:12:41 +0000 Subject: [PATCH 07/17] Give the crafting job status column more room The longest status, "Pending Dependencies", is the one that nested jobs spend most of their time in, and it ended right against the progress column next to it. Widen the status column so the two are separated. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_016gX5ZToD5ApLBXQkKW2QGj --- .../gui/container/ContainerScreenTerminalCraftingJobs.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/cyclops/integratedterminals/client/gui/container/ContainerScreenTerminalCraftingJobs.java b/src/main/java/org/cyclops/integratedterminals/client/gui/container/ContainerScreenTerminalCraftingJobs.java index 8c107dac09..b681350fd7 100644 --- a/src/main/java/org/cyclops/integratedterminals/client/gui/container/ContainerScreenTerminalCraftingJobs.java +++ b/src/main/java/org/cyclops/integratedterminals/client/gui/container/ContainerScreenTerminalCraftingJobs.java @@ -45,7 +45,7 @@ public class ContainerScreenTerminalCraftingJobs extends ContainerScreenExtended // Offsets from the end of a line for the two columns of half-scale info text. // The left column has to fit the longest status label, the right one the longest duration. - private static final int COLUMN_LEFT = 124; + private static final int COLUMN_LEFT = 132; private static final int COLUMN_RIGHT = 48; private final Player player; From ebeb4ad6bec686360dd55ed6ff06e71403c7103c Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 2 Sep 2026 20:56:34 +0000 Subject: [PATCH 08/17] Show crafting job progress as a bar, and mark unknown estimates The crafting job terminal listed the status, size, channel, duration, progress and remaining time as six separate pieces of text per job. Replace the duration, progress and remaining time with a progress bar that carries all three: the time the job has been running on the left, how far it has come in the middle, and what is left on the right. The labelled values, along with the total estimate and the owner, now show up in a tooltip when hovering a job. An estimate that is not known yet was left out entirely, which made it indistinguishable from a job that had no line to show. Show a question mark for it instead, for any job that is not in an error state. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_016gX5ZToD5ApLBXQkKW2QGj --- .../ContainerScreenTerminalCraftingJobs.java | 145 ++++++++++++++---- .../container/component/GuiCraftingPlan.java | 19 ++- .../component/GuiCraftingPlanFlat.java | 5 +- .../integratedterminals/lang/en_us.json | 2 + .../TestGuiCraftingPlanProgress.java | 65 ++++++++ 5 files changed, 201 insertions(+), 35 deletions(-) create mode 100644 src/test/java/org/cyclops/integratedterminals/client/gui/container/component/TestGuiCraftingPlanProgress.java diff --git a/src/main/java/org/cyclops/integratedterminals/client/gui/container/ContainerScreenTerminalCraftingJobs.java b/src/main/java/org/cyclops/integratedterminals/client/gui/container/ContainerScreenTerminalCraftingJobs.java index b681350fd7..fd0dd92d57 100644 --- a/src/main/java/org/cyclops/integratedterminals/client/gui/container/ContainerScreenTerminalCraftingJobs.java +++ b/src/main/java/org/cyclops/integratedterminals/client/gui/container/ContainerScreenTerminalCraftingJobs.java @@ -1,5 +1,7 @@ package org.cyclops.integratedterminals.client.gui.container; +import com.google.common.collect.Lists; +import net.minecraft.ChatFormatting; import net.minecraft.client.Minecraft; import net.minecraft.client.gui.Font; import net.minecraft.client.gui.GuiGraphics; @@ -14,6 +16,7 @@ import org.cyclops.cyclopscore.client.gui.component.button.ButtonText; import org.cyclops.cyclopscore.client.gui.container.ContainerScreenExtended; import org.cyclops.cyclopscore.helper.GuiHelpers; +import org.cyclops.cyclopscore.helper.Helpers; import org.cyclops.cyclopscore.helper.L10NHelpers; import org.cyclops.cyclopscore.helper.RenderHelpers; import org.cyclops.integrateddynamics.api.part.PartPos; @@ -21,6 +24,7 @@ import org.cyclops.integratedterminals.IntegratedTerminals; import org.cyclops.integratedterminals.Reference; import org.cyclops.integratedterminals.api.terminalstorage.crafting.ITerminalCraftingPlanFlat; +import org.cyclops.integratedterminals.api.terminalstorage.crafting.TerminalCraftingJobStatus; import org.cyclops.integratedterminals.client.gui.container.component.GuiCraftingPlan; import org.cyclops.integratedterminals.core.client.gui.CraftingJobGuiData; import org.cyclops.integratedterminals.core.terminalstorage.crafting.HandlerWrappedTerminalCraftingPlan; @@ -43,10 +47,11 @@ public class ContainerScreenTerminalCraftingJobs extends ContainerScreenExtended public static int LINE_WIDTH = 221; - // Offsets from the end of a line for the two columns of half-scale info text. - // The left column has to fit the longest status label, the right one the longest duration. - private static final int COLUMN_LEFT = 132; - private static final int COLUMN_RIGHT = 48; + // Width of the info area at the end of a line, which holds the status line and the progress bar below it. + private static final int COLUMN_INFO = 132; + private static final int COLUMN_INFO_SPACING = 6; + private static final int PROGRESS_BAR_OFFSET_Y = 8; + private static final int PROGRESS_BAR_HEIGHT = 8; private final Player player; @@ -104,6 +109,14 @@ protected void renderBg(GuiGraphics guiGraphics, float partialTicks, int mouseX, protected void renderLabels(GuiGraphics guiGraphics, int mouseX, int mouseY) { // super.drawGuiContainerForegroundLayer(matrixStack, mouseX, mouseY); drawCraftingPlans(guiGraphics, 0, 0, 0, mouseX, mouseY, ContainerScreenTerminalStorage.DrawLayer.FOREGROUND); + + // The progress bar only has room for bare numbers, so the labelled values go in a tooltip. + // Outputs draw their own tooltip, which already contains these lines. + HandlerWrappedTerminalCraftingPlan hoveredPlan = getHoveredPlan(mouseX, mouseY); + if (hoveredPlan != null && !isHoveringOutputs(hoveredPlan, mouseX - leftPos)) { + guiGraphics.renderComponentTooltip(font, getPlanTooltipLines(hoveredPlan.getCraftingPlanFlat()), + mouseX - leftPos, mouseY - topPos); + } } protected List getVisiblePlans() { @@ -132,6 +145,8 @@ protected void drawCraftingPlan(GuiGraphics guiGraphics, HandlerWrappedTerminalC // Draw outputs + List tooltipLines = layer == ContainerScreenTerminalStorage.DrawLayer.FOREGROUND + ? getPlanTooltipLines(plan) : null; x += 4; for (IPrototypedIngredient output : plan.getOutputs()) { IngredientComponent ingredientComponent = output.getComponent(); @@ -139,45 +154,113 @@ protected void drawCraftingPlan(GuiGraphics guiGraphics, HandlerWrappedTerminalC int finalX = x; ingredientComponent.getCapability(Capabilities.IngredientComponentTerminalStorageHandler.INGREDIENT) .ifPresent(h -> h.drawInstance(guiGraphics, output.getPrototype(), quantity, - GuiHelpers.quantityToScaledString(quantity), this, layer, partialTick, finalX, y + 1, mouseX, mouseY, null)); + GuiHelpers.quantityToScaledString(quantity), this, layer, partialTick, finalX, y + 1, mouseX, mouseY, tooltipLines)); x += GuiHelpers.SLOT_SIZE_INNER; } - // Draw dependency count if (layer == ContainerScreenTerminalStorage.DrawLayer.BACKGROUND) { - String statusString = L10NHelpers.localize("gui.integratedterminals.craftingplan.status", - L10NHelpers.localize( "gui.integratedterminals.craftingplan.status." + plan.getStatus().name().toLowerCase(Locale.ENGLISH))); - RenderHelpers.drawScaledString(guiGraphics.pose(), guiGraphics.bufferSource(), font, statusString, xOriginal + LINE_WIDTH - COLUMN_LEFT, y + 1, 0.5f, 16777215, true, Font.DisplayMode.NORMAL); - - int dependencies = plan.getEntries().size(); - String dependenciesString = L10NHelpers.localize("gui.integratedterminals.terminal_crafting_job.craftingplan.dependencies", dependencies); - RenderHelpers.drawScaledString(guiGraphics.pose(), guiGraphics.bufferSource(), font, dependenciesString, xOriginal + LINE_WIDTH - COLUMN_LEFT, y + 7, 0.5f, 16777215, true, Font.DisplayMode.NORMAL); + int infoX = xOriginal + LINE_WIDTH - COLUMN_INFO; + // The size and channel are right-aligned, so that the status gets whatever room is left + int infoRight = xOriginal + LINE_WIDTH - 2; if (plan.getChannel() != -1) { String channelString = L10NHelpers.localize("gui.integratedterminals.terminal_crafting_job.craftingplan.crafting_channel", plan.getChannel()); - RenderHelpers.drawScaledString(guiGraphics.pose(), guiGraphics.bufferSource(), font, channelString, xOriginal + LINE_WIDTH - COLUMN_RIGHT, y + 7, 0.5f, 16777215, true, Font.DisplayMode.NORMAL); + infoRight -= scaledWidth(channelString); + RenderHelpers.drawScaledString(guiGraphics.pose(), guiGraphics.bufferSource(), font, channelString, infoRight, y + 1, 0.5f, 16777215, true, Font.DisplayMode.NORMAL); + infoRight -= COLUMN_INFO_SPACING; } - long tickDuration = plan.getTickDuration(); - if (tickDuration >= 0) { - String durationString = GuiCraftingPlan.getDurationString(tickDuration); - RenderHelpers.drawScaledString(guiGraphics.pose(), guiGraphics.bufferSource(), font, durationString, xOriginal + LINE_WIDTH - COLUMN_LEFT, y + 13, 0.5f, 16777215, true, Font.DisplayMode.NORMAL); - } + String dependenciesString = L10NHelpers.localize("gui.integratedterminals.terminal_crafting_job.craftingplan.dependencies", plan.getEntries().size()); + infoRight -= scaledWidth(dependenciesString); + RenderHelpers.drawScaledString(guiGraphics.pose(), guiGraphics.bufferSource(), font, dependenciesString, infoRight, y + 1, 0.5f, 16777215, true, Font.DisplayMode.NORMAL); - int progress = GuiCraftingPlan.getProgress(plan); - if (progress >= 0) { - String progressString = L10NHelpers.localize("gui.integratedterminals.terminal_crafting_job.craftingplan.progress", progress); - RenderHelpers.drawScaledString(guiGraphics.pose(), guiGraphics.bufferSource(), font, progressString, xOriginal + LINE_WIDTH - COLUMN_RIGHT, y + 1, 0.5f, 16777215, true, Font.DisplayMode.NORMAL); - } + String statusString = L10NHelpers.localize("gui.integratedterminals.craftingplan.status", + L10NHelpers.localize( "gui.integratedterminals.craftingplan.status." + plan.getStatus().name().toLowerCase(Locale.ENGLISH))); + RenderHelpers.drawScaledString(guiGraphics.pose(), guiGraphics.bufferSource(), font, statusString, infoX, y + 1, 0.5f, 16777215, true, Font.DisplayMode.NORMAL); - long estimatedTickDurationRemaining = plan.getEstimatedTickDurationRemaining(); - if (estimatedTickDurationRemaining >= 0) { - String remainingString = GuiCraftingPlan.getDurationString( - "gui.integratedterminals.terminal_crafting_job.craftingplan.duration.remaining", - estimatedTickDurationRemaining); - RenderHelpers.drawScaledString(guiGraphics.pose(), guiGraphics.bufferSource(), font, remainingString, xOriginal + LINE_WIDTH - COLUMN_RIGHT, y + 13, 0.5f, 16777215, true, Font.DisplayMode.NORMAL); - } + drawProgressBar(guiGraphics, plan, infoX, y + PROGRESS_BAR_OFFSET_Y, COLUMN_INFO); + } + } + + /** + * @return If the mouse is over one of the output instances of the given plan, which draw their own tooltip. + */ + protected boolean isHoveringOutputs(HandlerWrappedTerminalCraftingPlan plan, double mouseX) { + int outputs = plan.getCraftingPlanFlat().getOutputs().size(); + return mouseX >= OUTPUT_SLOT_X + 4 + && mouseX < OUTPUT_SLOT_X + 4 + GuiHelpers.SLOT_SIZE_INNER * outputs; + } + + /** + * Draw how far a job has come as a bar, with the time it has been running on the left, + * the completed percentage in the middle, and the estimated time until it is done on the right. + */ + protected void drawProgressBar(GuiGraphics guiGraphics, ITerminalCraftingPlanFlat plan, int x, int y, int width) { + int progress = GuiCraftingPlan.getProgress(plan); + + guiGraphics.fill(x, y, x + width, y + PROGRESS_BAR_HEIGHT, Helpers.RGBAToInt(0, 0, 0, 100)); + if (progress > 0) { + guiGraphics.fill(x, y, x + width * progress / 100, y + PROGRESS_BAR_HEIGHT, + TerminalCraftingJobStatus.CRAFTING.getColor()); + } + + long tickDuration = plan.getTickDuration(); + if (tickDuration >= 0) { + drawProgressBarString(guiGraphics, GuiCraftingPlan.getDurationValue(tickDuration), x + 2, y); + } + if (progress >= 0) { + String progressString = L10NHelpers.localize("gui.integratedterminals.terminal_crafting_job.craftingplan.progress.short", progress); + drawProgressBarString(guiGraphics, progressString, x + (width - scaledWidth(progressString)) / 2, y); + } + if (plan.getStatus().isValid()) { + String remainingString = GuiCraftingPlan.getDurationValue(plan.getEstimatedTickDurationRemaining()); + drawProgressBarString(guiGraphics, remainingString, x + width - 2 - scaledWidth(remainingString), y); + } + } + + protected int scaledWidth(String string) { + return font.width(string) / 2; + } + + protected void drawProgressBarString(GuiGraphics guiGraphics, String string, int x, int y) { + RenderHelpers.drawScaledString(guiGraphics.pose(), guiGraphics.bufferSource(), font, string, + x, y + 2, 0.5f, 16777215, true, Font.DisplayMode.NORMAL); + } + + protected List getPlanTooltipLines(ITerminalCraftingPlanFlat plan) { + List lines = Lists.newArrayList(); + String status = plan.getStatus().name().toLowerCase(Locale.ENGLISH); + lines.add(Component.translatable("gui.integratedterminals.craftingplan.status", + Component.translatable("gui.integratedterminals.craftingplan.status." + status))); + lines.add(Component.translatable("gui.integratedterminals.craftingplan.status." + status + ".desc") + .withStyle(ChatFormatting.GRAY)); + lines.add(Component.translatable("gui.integratedterminals.terminal_crafting_job.craftingplan.dependencies", + plan.getEntries().size())); + if (plan.getChannel() != -1) { + lines.add(Component.translatable("gui.integratedterminals.terminal_crafting_job.craftingplan.crafting_channel", + plan.getChannel())); + } + int progress = GuiCraftingPlan.getProgress(plan); + if (progress >= 0) { + lines.add(Component.translatable("gui.integratedterminals.terminal_crafting_job.craftingplan.progress", progress)); + } + long tickDuration = plan.getTickDuration(); + if (tickDuration >= 0) { + lines.add(Component.literal(GuiCraftingPlan.getDurationString(tickDuration))); + } + if (plan.getStatus().isValid()) { + lines.add(Component.literal(GuiCraftingPlan.getDurationString( + "gui.integratedterminals.terminal_crafting_job.craftingplan.duration.remaining", + plan.getEstimatedTickDurationRemaining()))); + lines.add(Component.literal(GuiCraftingPlan.getDurationString( + "gui.integratedterminals.terminal_crafting_job.craftingplan.duration.estimate", + plan.getEstimatedTickDurationTotal()))); + } + if (plan.getInitiatorName() != null) { + lines.add(Component.translatable("gui.integratedterminals.terminal_crafting_job.craftingplan.owner", + plan.getInitiatorName())); } + return lines; } private void cancelCraftingJobs() { diff --git a/src/main/java/org/cyclops/integratedterminals/client/gui/container/component/GuiCraftingPlan.java b/src/main/java/org/cyclops/integratedterminals/client/gui/container/component/GuiCraftingPlan.java index 0f61bd8859..686cb4ffba 100644 --- a/src/main/java/org/cyclops/integratedterminals/client/gui/container/component/GuiCraftingPlan.java +++ b/src/main/java/org/cyclops/integratedterminals/client/gui/container/component/GuiCraftingPlan.java @@ -74,6 +74,7 @@ public class GuiCraftingPlan extends AbstractWidget { private final long tickDuration; private final long estimatedTickDuration; private final boolean estimatedTickDurationRemaining; + private final boolean showEstimatedTickDuration; private final int channel; @Nullable private final String initiatorName; @@ -96,6 +97,8 @@ public GuiCraftingPlan(AbstractContainerScreen parentGui, ITerminalCraftingPlan< this.estimatedTickDurationRemaining = craftingPlan.getStatus() != TerminalCraftingJobStatus.UNSTARTED; this.estimatedTickDuration = this.estimatedTickDurationRemaining ? craftingPlan.getEstimatedTickDurationRemaining() : craftingPlan.getEstimatedTickDurationTotal(); + // Jobs that can not be crafted have nothing to estimate, other jobs show a placeholder until they are measured + this.showEstimatedTickDuration = craftingPlan.getStatus().isValid(); this.channel = craftingPlan.getChannel(); this.initiatorName = craftingPlan.getInitiatorName(); } @@ -258,9 +261,19 @@ public static String getDurationString(long tickDuration) { } public static String getDurationString(String unlocalizedName, long tickDuration) { + return L10NHelpers.localize(unlocalizedName, getDurationValue(tickDuration)); + } + + /** + * @param tickDuration A tick duration, where -1 indicates an unknown duration. + * @return The duration as H:mm:ss, or a placeholder if it is unknown. + */ + public static String getDurationValue(long tickDuration) { + if (tickDuration < 0) { + return L10NHelpers.localize("gui.integratedterminals.terminal_crafting_job.craftingplan.duration.unknown"); + } long durationMs = tickDuration * 1000 / MinecraftHelpers.SECOND_IN_TICKS; - return L10NHelpers.localize(unlocalizedName, - DurationFormatUtils.formatDuration(durationMs, "H:mm:ss", true)); + return DurationFormatUtils.formatDuration(durationMs, "H:mm:ss", true); } /** @@ -301,7 +314,7 @@ public void drawGuiContainerBackgroundLayer(GuiGraphics guiGraphics, float parti } // Draw estimated duration - if (estimatedTickDuration >= 0) { + if (showEstimatedTickDuration) { String estimatedDurationString = getDurationString(estimatedTickDurationRemaining ? "gui.integratedterminals.terminal_crafting_job.craftingplan.duration.remaining" : "gui.integratedterminals.terminal_crafting_job.craftingplan.duration.estimate", estimatedTickDuration); diff --git a/src/main/java/org/cyclops/integratedterminals/client/gui/container/component/GuiCraftingPlanFlat.java b/src/main/java/org/cyclops/integratedterminals/client/gui/container/component/GuiCraftingPlanFlat.java index 077af56246..88da82d388 100644 --- a/src/main/java/org/cyclops/integratedterminals/client/gui/container/component/GuiCraftingPlanFlat.java +++ b/src/main/java/org/cyclops/integratedterminals/client/gui/container/component/GuiCraftingPlanFlat.java @@ -64,6 +64,7 @@ public class GuiCraftingPlanFlat extends AbstractWidget { private final long tickDuration; private final long estimatedTickDuration; private final boolean estimatedTickDurationRemaining; + private final boolean showEstimatedTickDuration; private final int channel; @Nullable private final String initiatorName; @@ -86,6 +87,8 @@ public GuiCraftingPlanFlat(AbstractContainerScreen parentGui, ITerminalCraftingP this.estimatedTickDurationRemaining = craftingPlan.getStatus() != TerminalCraftingJobStatus.UNSTARTED; this.estimatedTickDuration = this.estimatedTickDurationRemaining ? craftingPlan.getEstimatedTickDurationRemaining() : craftingPlan.getEstimatedTickDurationTotal(); + // Jobs that can not be crafted have nothing to estimate, other jobs show a placeholder until they are measured + this.showEstimatedTickDuration = craftingPlan.getStatus().isValid(); this.channel = craftingPlan.getChannel(); this.initiatorName = craftingPlan.getInitiatorName(); } @@ -243,7 +246,7 @@ public void drawGuiContainerBackgroundLayer(GuiGraphics guiGraphics, float parti } // Draw estimated duration - if (estimatedTickDuration >= 0) { + if (showEstimatedTickDuration) { String estimatedDurationString = GuiCraftingPlan.getDurationString(estimatedTickDurationRemaining ? "gui.integratedterminals.terminal_crafting_job.craftingplan.duration.remaining" : "gui.integratedterminals.terminal_crafting_job.craftingplan.duration.estimate", estimatedTickDuration); diff --git a/src/main/resources/assets/integratedterminals/lang/en_us.json b/src/main/resources/assets/integratedterminals/lang/en_us.json index b9dcd28b5d..127890f15c 100644 --- a/src/main/resources/assets/integratedterminals/lang/en_us.json +++ b/src/main/resources/assets/integratedterminals/lang/en_us.json @@ -78,7 +78,9 @@ "gui.integratedterminals.terminal_crafting_job.craftingplan.duration": "Duration: %s", "gui.integratedterminals.terminal_crafting_job.craftingplan.duration.estimate": "Estimated: %s", "gui.integratedterminals.terminal_crafting_job.craftingplan.duration.remaining": "Remaining: %s", + "gui.integratedterminals.terminal_crafting_job.craftingplan.duration.unknown": "?", "gui.integratedterminals.terminal_crafting_job.craftingplan.progress": "Progress: %s%%", + "gui.integratedterminals.terminal_crafting_job.craftingplan.progress.short": "%s%%", "gui.integratedterminals.terminal_crafting_job.craftingplan.crafting_channel": "Channel: %s", "gui.integratedterminals.terminal_crafting_job.craftingplan.owner": "Owner: %s", diff --git a/src/test/java/org/cyclops/integratedterminals/client/gui/container/component/TestGuiCraftingPlanProgress.java b/src/test/java/org/cyclops/integratedterminals/client/gui/container/component/TestGuiCraftingPlanProgress.java new file mode 100644 index 0000000000..5b176f8335 --- /dev/null +++ b/src/test/java/org/cyclops/integratedterminals/client/gui/container/component/TestGuiCraftingPlanProgress.java @@ -0,0 +1,65 @@ +package org.cyclops.integratedterminals.client.gui.container.component; + +import org.cyclops.integratedterminals.api.terminalstorage.crafting.ITerminalCraftingPlan; +import org.cyclops.integratedterminals.api.terminalstorage.crafting.ITerminalCraftingPlanFlat; +import org.cyclops.integratedterminals.api.terminalstorage.crafting.TerminalCraftingJobStatus; +import org.cyclops.integratedterminals.api.terminalstorage.crafting.TerminalCraftingPlanStatic; +import org.junit.Test; + +import java.util.Collections; +import java.util.List; + +import static org.junit.Assert.assertEquals; + +/** + * @author rubensworks + */ +public class TestGuiCraftingPlanProgress { + + protected static TerminalCraftingPlanStatic plan(int id, TerminalCraftingJobStatus status, + long craftingQuantity, long craftingQuantityTotal, + List> dependencies) { + return new TerminalCraftingPlanStatic<>(id, dependencies, Collections.emptyList(), status, + craftingQuantity, craftingQuantityTotal, Collections.emptyList(), Collections.emptyList(), + TerminalCraftingPlanStatic.Label.RUNNING, -1, -1, -1, 0, null); + } + + protected static ITerminalCraftingPlanFlat flatPlan(TerminalCraftingJobStatus status, + long craftingQuantity, long craftingQuantityTotal, + List> dependencies) { + return plan(1, status, craftingQuantity, craftingQuantityTotal, dependencies).flatten(); + } + + @Test + public void testProgressIsUnknownWithoutQuantities() { + assertEquals(-1, GuiCraftingPlan.getProgress( + flatPlan(TerminalCraftingJobStatus.UNSTARTED, 0, 0, Collections.emptyList()))); + } + + @Test + public void testProgressAtStart() { + assertEquals(0, GuiCraftingPlan.getProgress( + flatPlan(TerminalCraftingJobStatus.CRAFTING, 10, 10, Collections.emptyList()))); + } + + @Test + public void testProgressHalfway() { + assertEquals(50, GuiCraftingPlan.getProgress( + flatPlan(TerminalCraftingJobStatus.CRAFTING, 5, 10, Collections.emptyList()))); + } + + @Test + public void testProgressWhenFinished() { + assertEquals(100, GuiCraftingPlan.getProgress( + flatPlan(TerminalCraftingJobStatus.FINISHED, 0, 10, Collections.emptyList()))); + } + + @Test + public void testProgressCountsDependencies() { + // 20 of the 30 operations remain, so a third is done + assertEquals(33, GuiCraftingPlan.getProgress(flatPlan(TerminalCraftingJobStatus.CRAFTING, 10, 20, + Collections.singletonList(plan(2, TerminalCraftingJobStatus.CRAFTING, 10, 10, + Collections.emptyList()))))); + } + +} From 7901e5f19990231a1b15db0482cafd959597f31f Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 2 Sep 2026 21:12:56 +0000 Subject: [PATCH 09/17] Call the current drawInstance overload from the crafting jobs gui The crafting jobs gui was still on the overload that master deprecated when it gained a tooltip component parameter. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_016gX5ZToD5ApLBXQkKW2QGj --- .../gui/container/ContainerScreenTerminalCraftingJobs.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/cyclops/integratedterminals/client/gui/container/ContainerScreenTerminalCraftingJobs.java b/src/main/java/org/cyclops/integratedterminals/client/gui/container/ContainerScreenTerminalCraftingJobs.java index fd0dd92d57..4611b18a89 100644 --- a/src/main/java/org/cyclops/integratedterminals/client/gui/container/ContainerScreenTerminalCraftingJobs.java +++ b/src/main/java/org/cyclops/integratedterminals/client/gui/container/ContainerScreenTerminalCraftingJobs.java @@ -154,7 +154,7 @@ protected void drawCraftingPlan(GuiGraphics guiGraphics, HandlerWrappedTerminalC int finalX = x; ingredientComponent.getCapability(Capabilities.IngredientComponentTerminalStorageHandler.INGREDIENT) .ifPresent(h -> h.drawInstance(guiGraphics, output.getPrototype(), quantity, - GuiHelpers.quantityToScaledString(quantity), this, layer, partialTick, finalX, y + 1, mouseX, mouseY, tooltipLines)); + GuiHelpers.quantityToScaledString(quantity), this, layer, partialTick, finalX, y + 1, mouseX, mouseY, tooltipLines, null)); x += GuiHelpers.SLOT_SIZE_INNER; } From f23e75c4c05fe526d431de3f1d54de55950d006c Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 3 Sep 2026 04:42:02 +0000 Subject: [PATCH 10/17] Show the estimated duration of a craft in the storage terminal Hovering a craftable in the storage terminal now says how long one crafting operation of it is expected to take. The client had no notion of recipe durations, and asking for a whole crafting plan per hovered item would be far too expensive. The duration of a single operation is only a lookup in the crafting network though, so the server can attach it to the crafting option it already sends. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_016gX5ZToD5ApLBXQkKW2QGj --- .../crafting/ITerminalCraftingOption.java | 10 +++++++++ ...alStorageSlotIngredientCraftingOption.java | 4 ++++ ...erminalCraftingOptionRecipeDefinition.java | 13 +++++++++++ ...redientCraftingHandlerCraftingNetwork.java | 13 ++++++++--- .../integratedterminals/lang/en_us.json | 1 + ...erminalCraftingOptionRecipeDefinition.java | 22 +++++++++++++++++++ 6 files changed, 60 insertions(+), 3 deletions(-) create mode 100644 src/test/java/org/cyclops/integratedterminals/modcompat/integratedcrafting/TestTerminalCraftingOptionRecipeDefinition.java diff --git a/src/main/java/org/cyclops/integratedterminals/api/terminalstorage/crafting/ITerminalCraftingOption.java b/src/main/java/org/cyclops/integratedterminals/api/terminalstorage/crafting/ITerminalCraftingOption.java index 14d5f1f9bd..ebff70e692 100644 --- a/src/main/java/org/cyclops/integratedterminals/api/terminalstorage/crafting/ITerminalCraftingOption.java +++ b/src/main/java/org/cyclops/integratedterminals/api/terminalstorage/crafting/ITerminalCraftingOption.java @@ -37,6 +37,16 @@ public interface ITerminalCraftingOption extends Comparable Collection getOutputs(IngredientComponent ingredientComponent); + /** + * An estimation of how long a single crafting operation of this option takes, + * based on how long the crafting interfaces of the network needed for it before. + * + * @return The estimated tick duration of one crafting operation. -1 indicates an unknown duration. + */ + public default long getEstimatedTickDuration() { + return -1; + } + /** * @return All input components. */ diff --git a/src/main/java/org/cyclops/integratedterminals/core/terminalstorage/slot/TerminalStorageSlotIngredientCraftingOption.java b/src/main/java/org/cyclops/integratedterminals/core/terminalstorage/slot/TerminalStorageSlotIngredientCraftingOption.java index 5bdc085bec..8e2847123a 100644 --- a/src/main/java/org/cyclops/integratedterminals/core/terminalstorage/slot/TerminalStorageSlotIngredientCraftingOption.java +++ b/src/main/java/org/cyclops/integratedterminals/core/terminalstorage/slot/TerminalStorageSlotIngredientCraftingOption.java @@ -15,6 +15,7 @@ import org.cyclops.integratedterminals.api.ingredient.IIngredientComponentTerminalStorageHandler; import org.cyclops.integratedterminals.api.terminalstorage.ITerminalStorageTabClient; import org.cyclops.integratedterminals.client.gui.container.ContainerScreenTerminalStorage; +import org.cyclops.integratedterminals.client.gui.container.component.GuiCraftingPlan; import org.cyclops.integratedterminals.client.gui.tooltip.CraftingOptionIngredientsTooltip; import org.cyclops.integratedterminals.client.gui.tooltip.TooltipRenderHelpers; import org.cyclops.integratedterminals.core.terminalstorage.TerminalStorageTabIngredientComponentClient; @@ -64,12 +65,15 @@ public void drawGuiContainerLayer(AbstractContainerScreen gui, GuiGraphics guiGr drawCraftingJobOverlay(guiGraphics, layer, x, y, pendingCraftingJobOutput); } + @OnlyIn(Dist.CLIENT) protected List getTooltipLines(@Nullable PendingCraftingJobOutput pendingCraftingJobOutput, List>> inputs) { List tooltipLines = Lists.newArrayList(); if (pendingCraftingJobOutput != null) { addCraftingJobTooltipLines(tooltipLines, pendingCraftingJobOutput); } + tooltipLines.add(Component.translatable("gui.integratedterminals.terminal_storage.tooltip.duration", + GuiCraftingPlan.getDurationValue(getCraftingOption().getCraftingOption().getEstimatedTickDuration()))); if (!inputs.isEmpty()) { tooltipLines.add(Component.translatable("gui.integratedterminals.terminal_storage.tooltip.requirements") .withStyle(ChatFormatting.YELLOW)); diff --git a/src/main/java/org/cyclops/integratedterminals/modcompat/integratedcrafting/TerminalCraftingOptionRecipeDefinition.java b/src/main/java/org/cyclops/integratedterminals/modcompat/integratedcrafting/TerminalCraftingOptionRecipeDefinition.java index c4261f811f..6db9f0285a 100644 --- a/src/main/java/org/cyclops/integratedterminals/modcompat/integratedcrafting/TerminalCraftingOptionRecipeDefinition.java +++ b/src/main/java/org/cyclops/integratedterminals/modcompat/integratedcrafting/TerminalCraftingOptionRecipeDefinition.java @@ -22,11 +22,24 @@ public class TerminalCraftingOptionRecipeDefinition implements ITerminalCr private final IngredientComponent ingredientComponent; private final IRecipeDefinition prioritizedRecipe; + private final long estimatedTickDuration; public TerminalCraftingOptionRecipeDefinition(IngredientComponent ingredientComponent, IRecipeDefinition prioritizedRecipe) { + this(ingredientComponent, prioritizedRecipe, -1); + } + + public TerminalCraftingOptionRecipeDefinition(IngredientComponent ingredientComponent, + IRecipeDefinition prioritizedRecipe, + long estimatedTickDuration) { this.ingredientComponent = ingredientComponent; this.prioritizedRecipe = prioritizedRecipe; + this.estimatedTickDuration = estimatedTickDuration; + } + + @Override + public long getEstimatedTickDuration() { + return estimatedTickDuration; } @Override diff --git a/src/main/java/org/cyclops/integratedterminals/modcompat/integratedcrafting/TerminalStorageTabIngredientCraftingHandlerCraftingNetwork.java b/src/main/java/org/cyclops/integratedterminals/modcompat/integratedcrafting/TerminalStorageTabIngredientCraftingHandlerCraftingNetwork.java index 1ad99ba39d..4aa534ef49 100644 --- a/src/main/java/org/cyclops/integratedterminals/modcompat/integratedcrafting/TerminalStorageTabIngredientCraftingHandlerCraftingNetwork.java +++ b/src/main/java/org/cyclops/integratedterminals/modcompat/integratedcrafting/TerminalStorageTabIngredientCraftingHandlerCraftingNetwork.java @@ -70,20 +70,27 @@ public int[] getChannels(TerminalStorageTabIngredientComponentServer Collection> getCraftingOptionsWithOutput(TerminalStorageTabIngredientComponentServer tab, int channel, T instance, M matchCondition) { IngredientComponent ingredientComponent = tab.getIngredientNetwork().getComponent(); IRecipeIndex recipeIndex = getRecipeIndex(tab.getNetwork(), channel); + ICraftingNetwork craftingNetwork = CraftingHelpers.getCraftingNetwork(tab.getNetwork()).orElse(null); Iterable recipes = () -> recipeIndex.getRecipes(ingredientComponent, instance, matchCondition); return StreamSupport.stream(recipes.spliterator(), false) - .map((recipe) -> new TerminalCraftingOptionRecipeDefinition<>(ingredientComponent, recipe)) + // The duration is only a lookup of what the interfaces measured, so it can be sent along with the option + .map((recipe) -> new TerminalCraftingOptionRecipeDefinition<>(ingredientComponent, recipe, + craftingNetwork == null ? -1 : craftingNetwork.getEstimatedRecipeDuration(channel, recipe))) .collect(Collectors.toList()); } @Override public CompoundTag serializeCraftingOption(HolderLookup.Provider lookupProvider, TerminalCraftingOptionRecipeDefinition craftingOption) { - return IRecipeDefinition.serialize(lookupProvider, craftingOption.getRecipe()); + CompoundTag tag = IRecipeDefinition.serialize(lookupProvider, craftingOption.getRecipe()); + tag.putLong("estimatedTickDuration", craftingOption.getEstimatedTickDuration()); + return tag; } @Override public TerminalCraftingOptionRecipeDefinition deserializeCraftingOption(HolderLookup.Provider lookupProvider, IngredientComponent ingredientComponent, CompoundTag tag) throws IllegalArgumentException { - return new TerminalCraftingOptionRecipeDefinition<>(ingredientComponent, IRecipeDefinition.deserialize(lookupProvider, tag)); + return new TerminalCraftingOptionRecipeDefinition<>(ingredientComponent, + IRecipeDefinition.deserialize(lookupProvider, tag), + tag.contains("estimatedTickDuration", Tag.TAG_LONG) ? tag.getLong("estimatedTickDuration") : -1); } @Override diff --git a/src/main/resources/assets/integratedterminals/lang/en_us.json b/src/main/resources/assets/integratedterminals/lang/en_us.json index ddb8b7312c..d689c6cd69 100644 --- a/src/main/resources/assets/integratedterminals/lang/en_us.json +++ b/src/main/resources/assets/integratedterminals/lang/en_us.json @@ -17,6 +17,7 @@ "gui.integratedterminals.terminal_storage.craft": "craft", "gui.integratedterminals.terminal_storage.tooltip.requirements": "Crafting Requirements:", "gui.integratedterminals.terminal_storage.tooltip.crafting": "Being crafted: %s", + "gui.integratedterminals.terminal_storage.tooltip.duration": "Estimated: %s per craft", "gui.integratedterminals.terminal_storage.start_crafting_job": "Start crafting job", "gui.integratedterminals.terminal_storage.craftingplan.label.valid": "Crafting plan - Valid", "gui.integratedterminals.terminal_storage.craftingplan.label.failed.incomplete": "Crafting plan - Incomplete", diff --git a/src/test/java/org/cyclops/integratedterminals/modcompat/integratedcrafting/TestTerminalCraftingOptionRecipeDefinition.java b/src/test/java/org/cyclops/integratedterminals/modcompat/integratedcrafting/TestTerminalCraftingOptionRecipeDefinition.java new file mode 100644 index 0000000000..80783477dd --- /dev/null +++ b/src/test/java/org/cyclops/integratedterminals/modcompat/integratedcrafting/TestTerminalCraftingOptionRecipeDefinition.java @@ -0,0 +1,22 @@ +package org.cyclops.integratedterminals.modcompat.integratedcrafting; + +import org.junit.Test; + +import static org.junit.Assert.assertEquals; + +/** + * @author rubensworks + */ +public class TestTerminalCraftingOptionRecipeDefinition { + + @Test + public void testDurationIsUnknownByDefault() { + assertEquals(-1, new TerminalCraftingOptionRecipeDefinition<>(null, null).getEstimatedTickDuration()); + } + + @Test + public void testDurationIsRemembered() { + assertEquals(200, new TerminalCraftingOptionRecipeDefinition<>(null, null, 200).getEstimatedTickDuration()); + } + +} From 5b4767eb2ab745ded734199cd5e96a2514b5e426 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 3 Sep 2026 04:53:18 +0000 Subject: [PATCH 11/17] Color the crafting job tooltip Every line was plain white, which made the tooltip a wall of text to read through. Give the status the same color that the plan guis paint it in, dim the labels so that the durations stand out from them, put the remaining time in a color of its own, and push the values that are only occasionally interesting to the back. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_016gX5ZToD5ApLBXQkKW2QGj --- .../ContainerScreenTerminalCraftingJobs.java | 63 ++++++++++++------- 1 file changed, 41 insertions(+), 22 deletions(-) diff --git a/src/main/java/org/cyclops/integratedterminals/client/gui/container/ContainerScreenTerminalCraftingJobs.java b/src/main/java/org/cyclops/integratedterminals/client/gui/container/ContainerScreenTerminalCraftingJobs.java index 4611b18a89..e03735080c 100644 --- a/src/main/java/org/cyclops/integratedterminals/client/gui/container/ContainerScreenTerminalCraftingJobs.java +++ b/src/main/java/org/cyclops/integratedterminals/client/gui/container/ContainerScreenTerminalCraftingJobs.java @@ -7,6 +7,7 @@ import net.minecraft.client.gui.GuiGraphics; import net.minecraft.nbt.CompoundTag; import net.minecraft.network.chat.Component; +import net.minecraft.network.chat.Style; import net.minecraft.resources.ResourceLocation; import net.minecraft.world.entity.player.Inventory; import net.minecraft.world.entity.player.Player; @@ -53,6 +54,9 @@ public class ContainerScreenTerminalCraftingJobs extends ContainerScreenExtended private static final int PROGRESS_BAR_OFFSET_Y = 8; private static final int PROGRESS_BAR_HEIGHT = 8; + private static final int TRACK_COLOR = Helpers.RGBAToInt(0, 0, 0, 100); + private static final int FILL_COLOR = TerminalCraftingJobStatus.CRAFTING.getColor() | 0xFF000000; + private final Player player; private WidgetScrollBar scrollBar; @@ -197,12 +201,10 @@ protected boolean isHoveringOutputs(HandlerWrappedTerminalCraftingPlan plan, dou */ protected void drawProgressBar(GuiGraphics guiGraphics, ITerminalCraftingPlanFlat plan, int x, int y, int width) { int progress = GuiCraftingPlan.getProgress(plan); + int filled = progress > 0 ? width * progress / 100 : 0; - guiGraphics.fill(x, y, x + width, y + PROGRESS_BAR_HEIGHT, Helpers.RGBAToInt(0, 0, 0, 100)); - if (progress > 0) { - guiGraphics.fill(x, y, x + width * progress / 100, y + PROGRESS_BAR_HEIGHT, - TerminalCraftingJobStatus.CRAFTING.getColor()); - } + guiGraphics.fill(x, y, x + width, y + PROGRESS_BAR_HEIGHT, TRACK_COLOR); + guiGraphics.fill(x, y, x + filled, y + PROGRESS_BAR_HEIGHT, FILL_COLOR); long tickDuration = plan.getTickDuration(); if (tickDuration >= 0) { @@ -229,40 +231,57 @@ protected void drawProgressBarString(GuiGraphics guiGraphics, String string, int protected List getPlanTooltipLines(ITerminalCraftingPlanFlat plan) { List lines = Lists.newArrayList(); + + // The status is shown in the same color that the plan guis use for it String status = plan.getStatus().name().toLowerCase(Locale.ENGLISH); lines.add(Component.translatable("gui.integratedterminals.craftingplan.status", - Component.translatable("gui.integratedterminals.craftingplan.status." + status))); - lines.add(Component.translatable("gui.integratedterminals.craftingplan.status." + status + ".desc") + Component.translatable("gui.integratedterminals.craftingplan.status." + status) + .withStyle(Style.EMPTY.withColor(plan.getStatus().getColor() & 0xFFFFFF))) .withStyle(ChatFormatting.GRAY)); - lines.add(Component.translatable("gui.integratedterminals.terminal_crafting_job.craftingplan.dependencies", - plan.getEntries().size())); - if (plan.getChannel() != -1) { - lines.add(Component.translatable("gui.integratedterminals.terminal_crafting_job.craftingplan.crafting_channel", - plan.getChannel())); - } + lines.add(Component.translatable("gui.integratedterminals.craftingplan.status." + status + ".desc") + .withStyle(ChatFormatting.DARK_GRAY)); + + // Durations are what this tooltip is here for, so they get the brightest colors int progress = GuiCraftingPlan.getProgress(plan); if (progress >= 0) { - lines.add(Component.translatable("gui.integratedterminals.terminal_crafting_job.craftingplan.progress", progress)); + lines.add(tooltipLine("gui.integratedterminals.terminal_crafting_job.craftingplan.progress", + String.valueOf(progress), ChatFormatting.WHITE)); } long tickDuration = plan.getTickDuration(); if (tickDuration >= 0) { - lines.add(Component.literal(GuiCraftingPlan.getDurationString(tickDuration))); + lines.add(tooltipLine("gui.integratedterminals.terminal_crafting_job.craftingplan.duration", + GuiCraftingPlan.getDurationValue(tickDuration), ChatFormatting.WHITE)); } if (plan.getStatus().isValid()) { - lines.add(Component.literal(GuiCraftingPlan.getDurationString( - "gui.integratedterminals.terminal_crafting_job.craftingplan.duration.remaining", - plan.getEstimatedTickDurationRemaining()))); - lines.add(Component.literal(GuiCraftingPlan.getDurationString( - "gui.integratedterminals.terminal_crafting_job.craftingplan.duration.estimate", - plan.getEstimatedTickDurationTotal()))); + lines.add(tooltipLine("gui.integratedterminals.terminal_crafting_job.craftingplan.duration.remaining", + GuiCraftingPlan.getDurationValue(plan.getEstimatedTickDurationRemaining()), ChatFormatting.AQUA)); + lines.add(tooltipLine("gui.integratedterminals.terminal_crafting_job.craftingplan.duration.estimate", + GuiCraftingPlan.getDurationValue(plan.getEstimatedTickDurationTotal()), ChatFormatting.WHITE)); + } + + // Everything that is only occasionally interesting stays in the background + lines.add(Component.translatable("gui.integratedterminals.terminal_crafting_job.craftingplan.dependencies", + plan.getEntries().size()).withStyle(ChatFormatting.DARK_GRAY)); + if (plan.getChannel() != -1) { + lines.add(Component.translatable("gui.integratedterminals.terminal_crafting_job.craftingplan.crafting_channel", + plan.getChannel()).withStyle(ChatFormatting.DARK_GRAY)); } if (plan.getInitiatorName() != null) { lines.add(Component.translatable("gui.integratedterminals.terminal_crafting_job.craftingplan.owner", - plan.getInitiatorName())); + plan.getInitiatorName()).withStyle(ChatFormatting.DARK_GRAY)); } + return lines; } + /** + * @return A tooltip line with a dimmed label, so that its value stands out. + */ + protected static Component tooltipLine(String unlocalizedName, String value, ChatFormatting valueColor) { + return Component.translatable(unlocalizedName, Component.literal(value).withStyle(valueColor)) + .withStyle(ChatFormatting.GRAY); + } + private void cancelCraftingJobs() { // Send packets to cancel crafting jobs for (HandlerWrappedTerminalCraftingPlan craftingJob : getMenu().getCraftingJobs()) { From 729c88e58bf73fafe2a346cd4fca686316c82464 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 3 Sep 2026 05:11:16 +0000 Subject: [PATCH 12/17] Sink the crafting job progress bar into the line The numbers inside the bar sat on a track that was close in brightness to the line behind it. Give the bar a border and a darker track, so that the times and the percentage read over both the track and the fill. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_016gX5ZToD5ApLBXQkKW2QGj --- .../container/ContainerScreenTerminalCraftingJobs.java | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/cyclops/integratedterminals/client/gui/container/ContainerScreenTerminalCraftingJobs.java b/src/main/java/org/cyclops/integratedterminals/client/gui/container/ContainerScreenTerminalCraftingJobs.java index e03735080c..2fca9c6621 100644 --- a/src/main/java/org/cyclops/integratedterminals/client/gui/container/ContainerScreenTerminalCraftingJobs.java +++ b/src/main/java/org/cyclops/integratedterminals/client/gui/container/ContainerScreenTerminalCraftingJobs.java @@ -54,6 +54,7 @@ public class ContainerScreenTerminalCraftingJobs extends ContainerScreenExtended private static final int PROGRESS_BAR_OFFSET_Y = 8; private static final int PROGRESS_BAR_HEIGHT = 8; + private static final int BORDER_COLOR = Helpers.RGBAToInt(0, 0, 0, 180); private static final int TRACK_COLOR = Helpers.RGBAToInt(0, 0, 0, 100); private static final int FILL_COLOR = TerminalCraftingJobStatus.CRAFTING.getColor() | 0xFF000000; @@ -203,8 +204,12 @@ protected void drawProgressBar(GuiGraphics guiGraphics, ITerminalCraftingPlanFla int progress = GuiCraftingPlan.getProgress(plan); int filled = progress > 0 ? width * progress / 100 : 0; - guiGraphics.fill(x, y, x + width, y + PROGRESS_BAR_HEIGHT, TRACK_COLOR); - guiGraphics.fill(x, y, x + filled, y + PROGRESS_BAR_HEIGHT, FILL_COLOR); + // A sunken track, so that the numbers inside the bar stay readable over both the track and the fill + guiGraphics.fill(x, y, x + width, y + PROGRESS_BAR_HEIGHT, BORDER_COLOR); + guiGraphics.fill(x + 1, y + 1, x + width - 1, y + PROGRESS_BAR_HEIGHT - 1, TRACK_COLOR); + if (filled > 1) { + guiGraphics.fill(x + 1, y + 1, x + filled - 1, y + PROGRESS_BAR_HEIGHT - 1, FILL_COLOR); + } long tickDuration = plan.getTickDuration(); if (tickDuration >= 0) { From 2f0ab5ecc10c6e74cff3c9c9a6ecb9290e7283b8 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 3 Sep 2026 05:54:50 +0000 Subject: [PATCH 13/17] Do not estimate a job whose own duration is unknown The operations of a job are added to the estimation of its dependencies. An unknown duration for them counted as zero, so a job on a crafting interface that had never crafted anything reported the duration of its dependencies as if that were the whole answer. Report an unknown estimation instead, which the guis already show as a question mark. Unknown dependencies keep being skipped: they only lower a maximum that the other dependencies can still win. The recipe duration is now passed in rather than looked up, so that the estimation itself can be tested without a crafting network, instead of through the network being absent. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_016gX5ZToD5ApLBXQkKW2QGj --- ...redientCraftingHandlerCraftingNetwork.java | 41 ++++++++----- ...redientCraftingHandlerCraftingNetwork.java | 58 +++++++++++++------ 2 files changed, 67 insertions(+), 32 deletions(-) diff --git a/src/main/java/org/cyclops/integratedterminals/modcompat/integratedcrafting/TerminalStorageTabIngredientCraftingHandlerCraftingNetwork.java b/src/main/java/org/cyclops/integratedterminals/modcompat/integratedcrafting/TerminalStorageTabIngredientCraftingHandlerCraftingNetwork.java index 4aa534ef49..8b1dc29246 100644 --- a/src/main/java/org/cyclops/integratedterminals/modcompat/integratedcrafting/TerminalStorageTabIngredientCraftingHandlerCraftingNetwork.java +++ b/src/main/java/org/cyclops/integratedterminals/modcompat/integratedcrafting/TerminalStorageTabIngredientCraftingHandlerCraftingNetwork.java @@ -120,35 +120,47 @@ public ITerminalCraftingPlan calculateCraftingPlan(INetwork network, in * Dependencies of the same job can be crafted simultaneously, so only the longest one is counted. * Note that this does not take into account that a job can be distributed over multiple crafting interfaces. * - * @param craftingNetwork The crafting network, or null if unavailable. - * @param craftingJob A crafting job. + * @param recipeDuration The estimated duration of one crafting operation of the job, + * where -1 indicates an unknown duration. * @param amount The number of crafting operations to estimate for. * @param dependencies The plans of the jobs that the given job depends on. - * @return The estimated tick duration, or -1 if nothing within this plan was measured before. + * @return The estimated tick duration, or -1 if it can not be determined. */ - protected static long estimateTickDuration(@Nullable ICraftingNetwork craftingNetwork, CraftingJob craftingJob, - long amount, List> dependencies) { - return estimateTickDuration(craftingNetwork, craftingJob, amount, dependencies, + protected static long estimateTickDuration(long recipeDuration, long amount, + List> dependencies) { + return estimateTickDuration(recipeDuration, amount, dependencies, ITerminalCraftingPlan::getEstimatedTickDurationTotal); } - protected static long estimateTickDuration(@Nullable ICraftingNetwork craftingNetwork, CraftingJob craftingJob, - long amount, List> dependencies, + protected static long estimateTickDuration(long recipeDuration, long amount, + List> dependencies, ToLongFunction> dependencyDuration) { long dependenciesDuration = -1; for (ITerminalCraftingPlan dependency : dependencies) { dependenciesDuration = Math.max(dependenciesDuration, dependencyDuration.applyAsLong(dependency)); } - long recipeDuration = craftingNetwork == null ? -1 : craftingNetwork - .getEstimatedRecipeDuration(craftingJob.getChannel(), craftingJob.getRecipe()); - if (recipeDuration < 0 && dependenciesDuration < 0) { + // The operations of the job itself are added to the estimation, so leaving them out when they are + // unknown would silently drop them. An unknown dependency only lowers a maximum that the others + // can still win, so those are skipped instead. + if (amount > 0 ? recipeDuration < 0 : dependenciesDuration < 0) { return -1; } return Math.max(recipeDuration, 0) * amount + Math.max(dependenciesDuration, 0); } + /** + * @param craftingNetwork The crafting network, or null if unavailable. + * @param craftingJob A crafting job. + * @return How long one crafting operation of the given job is estimated to take, + * or -1 if that is unknown. + */ + protected static long getEstimatedRecipeDuration(@Nullable ICraftingNetwork craftingNetwork, CraftingJob craftingJob) { + return craftingNetwork == null ? -1 : craftingNetwork + .getEstimatedRecipeDuration(craftingJob.getChannel(), craftingJob.getRecipe()); + } + protected static ITerminalCraftingPlan newCraftingPlan(@Nullable ICraftingNetwork craftingNetwork, CraftingJob craftingJob, CraftingJobDependencyGraph dependencyGraph, @@ -159,7 +171,7 @@ protected static ITerminalCraftingPlan newCraftingPlan(@Nullable ICraft .map(subCraftingJob -> newCraftingPlan(craftingNetwork, subCraftingJob, dependencyGraph, false)) .collect(Collectors.toList()); // The job has not started yet, so its remaining duration is equal to its total duration - long estimatedTickDuration = estimateTickDuration(craftingNetwork, craftingJob, + long estimatedTickDuration = estimateTickDuration(getEstimatedRecipeDuration(craftingNetwork, craftingJob), craftingJob.getAmountTotal(), dependencies); if (root) { return new TerminalCraftingPlanCraftingJobDependencyGraph( @@ -408,9 +420,10 @@ protected static ITerminalCraftingPlan newActiveCraftingJob(ICraftingNe } } - long estimatedTickDurationTotal = estimateTickDuration(craftingNetwork, craftingJob, + long recipeDuration = getEstimatedRecipeDuration(craftingNetwork, craftingJob); + long estimatedTickDurationTotal = estimateTickDuration(recipeDuration, craftingJob.getAmountTotal(), dependencies); - long estimatedTickDurationRemaining = estimateTickDuration(craftingNetwork, craftingJob, + long estimatedTickDurationRemaining = estimateTickDuration(recipeDuration, craftingJob.getAmount(), dependencies, ITerminalCraftingPlan::getEstimatedTickDurationRemaining); if (estimatedTickDurationRemaining > 0) { // Subtract the time that the currently running crafting operation has been going on already diff --git a/src/test/java/org/cyclops/integratedterminals/modcompat/integratedcrafting/TestTerminalStorageTabIngredientCraftingHandlerCraftingNetwork.java b/src/test/java/org/cyclops/integratedterminals/modcompat/integratedcrafting/TestTerminalStorageTabIngredientCraftingHandlerCraftingNetwork.java index 8db1327ed8..ca14e90011 100644 --- a/src/test/java/org/cyclops/integratedterminals/modcompat/integratedcrafting/TestTerminalStorageTabIngredientCraftingHandlerCraftingNetwork.java +++ b/src/test/java/org/cyclops/integratedterminals/modcompat/integratedcrafting/TestTerminalStorageTabIngredientCraftingHandlerCraftingNetwork.java @@ -1,13 +1,9 @@ package org.cyclops.integratedterminals.modcompat.integratedcrafting; import com.google.common.collect.Lists; -import com.google.common.collect.Maps; -import org.cyclops.commoncapabilities.api.ingredient.MixedIngredients; -import org.cyclops.integratedcrafting.api.crafting.CraftingJob; import org.cyclops.integratedterminals.api.terminalstorage.crafting.ITerminalCraftingPlan; import org.cyclops.integratedterminals.api.terminalstorage.crafting.TerminalCraftingJobStatus; import org.cyclops.integratedterminals.api.terminalstorage.crafting.TerminalCraftingPlanStatic; -import org.junit.Before; import org.junit.Test; import java.util.Collections; @@ -20,24 +16,23 @@ */ public class TestTerminalStorageTabIngredientCraftingHandlerCraftingNetwork { - private CraftingJob craftingJob; - - @Before - public void beforeEach() { - this.craftingJob = new CraftingJob(1, 0, null, 3, new MixedIngredients(Maps.newIdentityHashMap())); - } - protected static ITerminalCraftingPlan planWithEstimations(long total, long remaining) { return new TerminalCraftingPlanStatic<>(1, Collections.emptyList(), Collections.emptyList(), TerminalCraftingJobStatus.CRAFTING, 1, 1, Collections.emptyList(), Collections.emptyList(), TerminalCraftingPlanStatic.Label.RUNNING, -1, total, remaining, 0, null); } + @Test + public void testEstimateWithoutDependencies() { + assertEquals(150, TerminalStorageTabIngredientCraftingHandlerCraftingNetwork + .estimateTickDuration(50, 3, Collections.emptyList())); + } + @Test public void testEstimateWithoutMeasurements() { - // Without a crafting network, no recipe durations are known + // Without a measured recipe duration, there is nothing to base the estimation on assertEquals(-1, TerminalStorageTabIngredientCraftingHandlerCraftingNetwork - .estimateTickDuration(null, craftingJob, 3, Collections.emptyList())); + .estimateTickDuration(-1, 3, Collections.emptyList())); } @Test @@ -47,8 +42,8 @@ public void testEstimateFromDependencies() { planWithEstimations(100, 40), planWithEstimations(250, 90)); - assertEquals(250, TerminalStorageTabIngredientCraftingHandlerCraftingNetwork - .estimateTickDuration(null, craftingJob, 3, dependencies)); + assertEquals(400, TerminalStorageTabIngredientCraftingHandlerCraftingNetwork + .estimateTickDuration(50, 3, dependencies)); } @Test @@ -57,19 +52,46 @@ public void testEstimateFromRemainingDependencies() { planWithEstimations(100, 40), planWithEstimations(250, 90)); - assertEquals(90, TerminalStorageTabIngredientCraftingHandlerCraftingNetwork - .estimateTickDuration(null, craftingJob, 3, dependencies, + assertEquals(240, TerminalStorageTabIngredientCraftingHandlerCraftingNetwork + .estimateTickDuration(50, 3, dependencies, ITerminalCraftingPlan::getEstimatedTickDurationRemaining)); } @Test public void testEstimateWithUnknownDependencies() { + // An unknown dependency only lowers the maximum, which the other dependency still wins List> dependencies = Lists.newArrayList( planWithEstimations(-1, -1), planWithEstimations(250, 90)); + assertEquals(400, TerminalStorageTabIngredientCraftingHandlerCraftingNetwork + .estimateTickDuration(50, 3, dependencies)); + } + + @Test + public void testEstimateWithUnknownRecipeButKnownDependencies() { + // The operations of the job itself are missing from the estimation, so it can not be given + List> dependencies = Lists.newArrayList( + planWithEstimations(250, 90)); + + assertEquals(-1, TerminalStorageTabIngredientCraftingHandlerCraftingNetwork + .estimateTickDuration(-1, 3, dependencies)); + } + + @Test + public void testEstimateWithUnknownRecipeWithoutOperationsLeft() { + // Without operations left, the unknown recipe duration does not contribute anything anyway + List> dependencies = Lists.newArrayList( + planWithEstimations(250, 90)); + assertEquals(250, TerminalStorageTabIngredientCraftingHandlerCraftingNetwork - .estimateTickDuration(null, craftingJob, 3, dependencies)); + .estimateTickDuration(-1, 0, dependencies)); + } + + @Test + public void testEstimateWithoutOperationsLeftAndUnknownDependencies() { + assertEquals(-1, TerminalStorageTabIngredientCraftingHandlerCraftingNetwork + .estimateTickDuration(-1, 0, Collections.emptyList())); } } From 0b825b97b58766364584a261dcf6b03e01cf11d3 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 3 Sep 2026 06:42:26 +0000 Subject: [PATCH 14/17] Explain the unknown crafting duration estimate on hover The question mark on its own does not say whether the estimate is missing, zero, or broken. Hovering it now explains that the crafting interfaces have not crafted these recipes before. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_016gX5ZToD5ApLBXQkKW2QGj --- .../container/component/GuiCraftingPlan.java | 45 +++++++++++++++++-- .../component/GuiCraftingPlanFlat.java | 15 +++++-- .../integratedterminals/lang/en_us.json | 2 + 3 files changed, 54 insertions(+), 8 deletions(-) diff --git a/src/main/java/org/cyclops/integratedterminals/client/gui/container/component/GuiCraftingPlan.java b/src/main/java/org/cyclops/integratedterminals/client/gui/container/component/GuiCraftingPlan.java index 686cb4ffba..feda24b05e 100644 --- a/src/main/java/org/cyclops/integratedterminals/client/gui/container/component/GuiCraftingPlan.java +++ b/src/main/java/org/cyclops/integratedterminals/client/gui/container/component/GuiCraftingPlan.java @@ -5,6 +5,7 @@ import com.mojang.blaze3d.platform.Lighting; import com.mojang.blaze3d.systems.RenderSystem; import it.unimi.dsi.fastutil.ints.IntOpenHashSet; +import net.minecraft.ChatFormatting; import net.minecraft.client.Minecraft; import net.minecraft.client.gui.Font; import net.minecraft.client.gui.GuiGraphics; @@ -63,6 +64,8 @@ public class GuiCraftingPlan extends AbstractWidget { protected static final int TICK_DELAY = 30; + private static final int DURATION_LINE_HEIGHT = 6; + private final AbstractContainerScreen parentGui; private final int guiLeft; private final int guiTop; @@ -315,10 +318,7 @@ public void drawGuiContainerBackgroundLayer(GuiGraphics guiGraphics, float parti // Draw estimated duration if (showEstimatedTickDuration) { - String estimatedDurationString = getDurationString(estimatedTickDurationRemaining - ? "gui.integratedterminals.terminal_crafting_job.craftingplan.duration.remaining" - : "gui.integratedterminals.terminal_crafting_job.craftingplan.duration.estimate", estimatedTickDuration); - RenderHelpers.drawScaledString(guiGraphics.pose(), guiGraphics.bufferSource(), fontRenderer, estimatedDurationString, guiLeft + getX() - 4, guiTop + getY() - 8, 0.5f, 16777215, true, Font.DisplayMode.NORMAL); + RenderHelpers.drawScaledString(guiGraphics.pose(), guiGraphics.bufferSource(), fontRenderer, getEstimatedDurationString(), guiLeft + getX() - 4, guiTop + getY() - 8, 0.5f, 16777215, true, Font.DisplayMode.NORMAL); } drawGuiContainerLayer(guiGraphics, guiLeft, guiTop, ContainerScreenTerminalStorage.DrawLayer.BACKGROUND, partialTicks, mouseX, mouseY); @@ -327,6 +327,43 @@ public void drawGuiContainerBackgroundLayer(GuiGraphics guiGraphics, float parti public void drawGuiContainerForegroundLayer(GuiGraphics guiGraphics, int mouseX, int mouseY) { drawGuiContainerLayer(guiGraphics, 0, 0, ContainerScreenTerminalStorage.DrawLayer.FOREGROUND, 0, mouseX, mouseY); + if (showEstimatedTickDuration) { + drawUnknownDurationTooltip(this.parentGui, guiGraphics, getEstimatedDurationString(), estimatedTickDuration, + getX() - 4, getY() - 8, mouseX, mouseY); + } + } + + protected String getEstimatedDurationString() { + return getDurationString(estimatedTickDurationRemaining + ? "gui.integratedterminals.terminal_crafting_job.craftingplan.duration.remaining" + : "gui.integratedterminals.terminal_crafting_job.craftingplan.duration.estimate", estimatedTickDuration); + } + + /** + * Explain an estimated duration that could not be determined, when the mouse hovers over it. + * + * @param parentGui The gui that the duration is drawn in. + * @param guiGraphics The gui graphics. + * @param durationString The duration line as it is drawn, to derive its width from. + * @param tickDuration The estimated duration, where only an unknown one is explained. + * @param x The X position of the duration line, relative to the gui. + * @param y The Y position of the duration line, relative to the gui. + * @param mouseX The mouse X position. + * @param mouseY The mouse Y position. + */ + public static void drawUnknownDurationTooltip(AbstractContainerScreen parentGui, GuiGraphics guiGraphics, + String durationString, long tickDuration, + int x, int y, int mouseX, int mouseY) { + if (tickDuration >= 0) { + return; + } + // The line is drawn at half scale, so it only takes up half of the font's width + int width = Minecraft.getInstance().font.width(durationString) / 2; + GuiHelpers.renderTooltipOptional(parentGui, guiGraphics.pose(), x, y, width, DURATION_LINE_HEIGHT, + mouseX, mouseY, () -> Optional.of(Lists.newArrayList( + Component.translatable("gui.integratedterminals.terminal_crafting_job.craftingplan.duration.unknown.title"), + Component.translatable("gui.integratedterminals.terminal_crafting_job.craftingplan.duration.unknown.desc") + .withStyle(ChatFormatting.GRAY)))); } @Override diff --git a/src/main/java/org/cyclops/integratedterminals/client/gui/container/component/GuiCraftingPlanFlat.java b/src/main/java/org/cyclops/integratedterminals/client/gui/container/component/GuiCraftingPlanFlat.java index 88da82d388..7e056ea20c 100644 --- a/src/main/java/org/cyclops/integratedterminals/client/gui/container/component/GuiCraftingPlanFlat.java +++ b/src/main/java/org/cyclops/integratedterminals/client/gui/container/component/GuiCraftingPlanFlat.java @@ -247,10 +247,7 @@ public void drawGuiContainerBackgroundLayer(GuiGraphics guiGraphics, float parti // Draw estimated duration if (showEstimatedTickDuration) { - String estimatedDurationString = GuiCraftingPlan.getDurationString(estimatedTickDurationRemaining - ? "gui.integratedterminals.terminal_crafting_job.craftingplan.duration.remaining" - : "gui.integratedterminals.terminal_crafting_job.craftingplan.duration.estimate", estimatedTickDuration); - RenderHelpers.drawScaledString(guiGraphics.pose(), guiGraphics.bufferSource(), fontRenderer, estimatedDurationString, guiLeft + getX() - 4, guiTop + getY() - 8, 0.5f, 16777215, true, Font.DisplayMode.NORMAL); + RenderHelpers.drawScaledString(guiGraphics.pose(), guiGraphics.bufferSource(), fontRenderer, getEstimatedDurationString(), guiLeft + getX() - 4, guiTop + getY() - 8, 0.5f, 16777215, true, Font.DisplayMode.NORMAL); } drawGuiContainerLayer(guiGraphics, guiLeft, guiTop, ContainerScreenTerminalStorage.DrawLayer.BACKGROUND, partialTicks, mouseX, mouseY); @@ -259,6 +256,16 @@ public void drawGuiContainerBackgroundLayer(GuiGraphics guiGraphics, float parti public void drawGuiContainerForegroundLayer(GuiGraphics guiGraphics, int mouseX, int mouseY) { drawGuiContainerLayer(guiGraphics, 0, 0, ContainerScreenTerminalStorage.DrawLayer.FOREGROUND, 0, mouseX, mouseY); + if (showEstimatedTickDuration) { + GuiCraftingPlan.drawUnknownDurationTooltip(this.parentGui, guiGraphics, getEstimatedDurationString(), + estimatedTickDuration, getX() - 4, getY() - 8, mouseX, mouseY); + } + } + + protected String getEstimatedDurationString() { + return GuiCraftingPlan.getDurationString(estimatedTickDurationRemaining + ? "gui.integratedterminals.terminal_crafting_job.craftingplan.duration.remaining" + : "gui.integratedterminals.terminal_crafting_job.craftingplan.duration.estimate", estimatedTickDuration); } @Override diff --git a/src/main/resources/assets/integratedterminals/lang/en_us.json b/src/main/resources/assets/integratedterminals/lang/en_us.json index d689c6cd69..13f0763164 100644 --- a/src/main/resources/assets/integratedterminals/lang/en_us.json +++ b/src/main/resources/assets/integratedterminals/lang/en_us.json @@ -84,6 +84,8 @@ "gui.integratedterminals.terminal_crafting_job.craftingplan.duration.estimate": "Estimated: %s", "gui.integratedterminals.terminal_crafting_job.craftingplan.duration.remaining": "Remaining: %s", "gui.integratedterminals.terminal_crafting_job.craftingplan.duration.unknown": "?", + "gui.integratedterminals.terminal_crafting_job.craftingplan.duration.unknown.title": "No estimate yet", + "gui.integratedterminals.terminal_crafting_job.craftingplan.duration.unknown.desc": "The crafting interfaces have not crafted these recipes before.", "gui.integratedterminals.terminal_crafting_job.craftingplan.progress": "Progress: %s%%", "gui.integratedterminals.terminal_crafting_job.craftingplan.progress.short": "%s%%", "gui.integratedterminals.terminal_crafting_job.craftingplan.crafting_channel": "Channel: %s", From 8064171f0601989f5898b6c854e102009f8cb298 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 3 Sep 2026 16:33:22 +0000 Subject: [PATCH 15/17] Address review comments on the crafting option duration Make getEstimatedTickDuration a regular interface method, deprecate the constructor that does not take a duration, and drop a comment that does not carry its weight. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_016gX5ZToD5ApLBXQkKW2QGj --- .../api/terminalstorage/crafting/ITerminalCraftingOption.java | 4 +--- .../GameTestTerminalCraftingOptionIntegratedTerminals.java | 2 +- .../TerminalCraftingOptionRecipeDefinition.java | 1 + ...nalStorageTabIngredientCraftingHandlerCraftingNetwork.java | 1 - .../TestTerminalCraftingOptionRecipeDefinition.java | 1 + 5 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/main/java/org/cyclops/integratedterminals/api/terminalstorage/crafting/ITerminalCraftingOption.java b/src/main/java/org/cyclops/integratedterminals/api/terminalstorage/crafting/ITerminalCraftingOption.java index ebff70e692..6d581b64f5 100644 --- a/src/main/java/org/cyclops/integratedterminals/api/terminalstorage/crafting/ITerminalCraftingOption.java +++ b/src/main/java/org/cyclops/integratedterminals/api/terminalstorage/crafting/ITerminalCraftingOption.java @@ -43,9 +43,7 @@ public interface ITerminalCraftingOption extends Comparable> alternatives(Item private static ITerminalCraftingOption createCraftingOption(List>> inputs) { IRecipeDefinition recipe = RecipeDefinition.ofIngredients(IngredientComponent.ITEMSTACK, inputs, MixedIngredients.ofInstance(IngredientComponent.ITEMSTACK, new ItemStack(Items.STICK, 4))); - return new TerminalCraftingOptionRecipeDefinition<>(IngredientComponent.ITEMSTACK, recipe); + return new TerminalCraftingOptionRecipeDefinition<>(IngredientComponent.ITEMSTACK, recipe, -1); } @GameTest(template = "empty", templateNamespace = "cyclopscore") diff --git a/src/main/java/org/cyclops/integratedterminals/modcompat/integratedcrafting/TerminalCraftingOptionRecipeDefinition.java b/src/main/java/org/cyclops/integratedterminals/modcompat/integratedcrafting/TerminalCraftingOptionRecipeDefinition.java index 6db9f0285a..1358ffc2b0 100644 --- a/src/main/java/org/cyclops/integratedterminals/modcompat/integratedcrafting/TerminalCraftingOptionRecipeDefinition.java +++ b/src/main/java/org/cyclops/integratedterminals/modcompat/integratedcrafting/TerminalCraftingOptionRecipeDefinition.java @@ -24,6 +24,7 @@ public class TerminalCraftingOptionRecipeDefinition implements ITerminalCr private final IRecipeDefinition prioritizedRecipe; private final long estimatedTickDuration; + @Deprecated // TODO: rm in next major public TerminalCraftingOptionRecipeDefinition(IngredientComponent ingredientComponent, IRecipeDefinition prioritizedRecipe) { this(ingredientComponent, prioritizedRecipe, -1); diff --git a/src/main/java/org/cyclops/integratedterminals/modcompat/integratedcrafting/TerminalStorageTabIngredientCraftingHandlerCraftingNetwork.java b/src/main/java/org/cyclops/integratedterminals/modcompat/integratedcrafting/TerminalStorageTabIngredientCraftingHandlerCraftingNetwork.java index 8b1dc29246..bbe749c44d 100644 --- a/src/main/java/org/cyclops/integratedterminals/modcompat/integratedcrafting/TerminalStorageTabIngredientCraftingHandlerCraftingNetwork.java +++ b/src/main/java/org/cyclops/integratedterminals/modcompat/integratedcrafting/TerminalStorageTabIngredientCraftingHandlerCraftingNetwork.java @@ -73,7 +73,6 @@ public int[] getChannels(TerminalStorageTabIngredientComponentServer recipes = () -> recipeIndex.getRecipes(ingredientComponent, instance, matchCondition); return StreamSupport.stream(recipes.spliterator(), false) - // The duration is only a lookup of what the interfaces measured, so it can be sent along with the option .map((recipe) -> new TerminalCraftingOptionRecipeDefinition<>(ingredientComponent, recipe, craftingNetwork == null ? -1 : craftingNetwork.getEstimatedRecipeDuration(channel, recipe))) .collect(Collectors.toList()); diff --git a/src/test/java/org/cyclops/integratedterminals/modcompat/integratedcrafting/TestTerminalCraftingOptionRecipeDefinition.java b/src/test/java/org/cyclops/integratedterminals/modcompat/integratedcrafting/TestTerminalCraftingOptionRecipeDefinition.java index 80783477dd..83dc9063d5 100644 --- a/src/test/java/org/cyclops/integratedterminals/modcompat/integratedcrafting/TestTerminalCraftingOptionRecipeDefinition.java +++ b/src/test/java/org/cyclops/integratedterminals/modcompat/integratedcrafting/TestTerminalCraftingOptionRecipeDefinition.java @@ -10,6 +10,7 @@ public class TestTerminalCraftingOptionRecipeDefinition { @Test + @SuppressWarnings("deprecation") public void testDurationIsUnknownByDefault() { assertEquals(-1, new TerminalCraftingOptionRecipeDefinition<>(null, null).getEstimatedTickDuration()); } From 2eb56022ea38116b7b901af9aea81cc80e9ffb76 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 3 Sep 2026 16:57:52 +0000 Subject: [PATCH 16/17] Report a duration for recipes that crafting interfaces finish instantly Crafting interfaces only measure how long a recipe takes to produce its outputs after being started. Regular crafting recipes are done 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 to zero as well. Multiplied by the operations of a job, that reported a plan for 100 chests as taking no time, and left the remaining time of a running job at zero. An interface performs at most one operation per update though, so no recipe can go faster than that. Take the update interval as a lower bound. The durations are also no longer formatted as H:mm:ss throughout, which rounded everything below a minute away. Seconds are shown with the decimals that are meaningful at their magnitude, minutes as m:ss, and only durations of an hour and over as H:mm:ss. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_016gX5ZToD5ApLBXQkKW2QGj --- .../ContainerScreenTerminalCraftingJobs.java | 13 ++++-- .../container/component/GuiCraftingPlan.java | 37 +++++++++++++--- .../component/GuiCraftingPlanFlat.java | 6 +-- ...alStorageSlotIngredientCraftingOption.java | 8 +++- ...redientCraftingHandlerCraftingNetwork.java | 36 +++++++++++++-- .../integratedterminals/lang/en_us.json | 1 + .../TestGuiCraftingPlanDuration.java | 44 +++++++++++++++++++ ...redientCraftingHandlerCraftingNetwork.java | 18 ++++++++ 8 files changed, 146 insertions(+), 17 deletions(-) create mode 100644 src/test/java/org/cyclops/integratedterminals/client/gui/container/component/TestGuiCraftingPlanDuration.java diff --git a/src/main/java/org/cyclops/integratedterminals/client/gui/container/ContainerScreenTerminalCraftingJobs.java b/src/main/java/org/cyclops/integratedterminals/client/gui/container/ContainerScreenTerminalCraftingJobs.java index 2fca9c6621..d810f1890d 100644 --- a/src/main/java/org/cyclops/integratedterminals/client/gui/container/ContainerScreenTerminalCraftingJobs.java +++ b/src/main/java/org/cyclops/integratedterminals/client/gui/container/ContainerScreenTerminalCraftingJobs.java @@ -257,11 +257,16 @@ protected List getPlanTooltipLines(ITerminalCraftingPlanFlat plan) lines.add(tooltipLine("gui.integratedterminals.terminal_crafting_job.craftingplan.duration", GuiCraftingPlan.getDurationValue(tickDuration), ChatFormatting.WHITE)); } + // An unknown duration says nothing here, so it is left out rather than shown as a placeholder if (plan.getStatus().isValid()) { - lines.add(tooltipLine("gui.integratedterminals.terminal_crafting_job.craftingplan.duration.remaining", - GuiCraftingPlan.getDurationValue(plan.getEstimatedTickDurationRemaining()), ChatFormatting.AQUA)); - lines.add(tooltipLine("gui.integratedterminals.terminal_crafting_job.craftingplan.duration.estimate", - GuiCraftingPlan.getDurationValue(plan.getEstimatedTickDurationTotal()), ChatFormatting.WHITE)); + if (plan.getEstimatedTickDurationRemaining() >= 0) { + lines.add(tooltipLine("gui.integratedterminals.terminal_crafting_job.craftingplan.duration.remaining", + GuiCraftingPlan.getDurationValue(plan.getEstimatedTickDurationRemaining()), ChatFormatting.AQUA)); + } + if (plan.getEstimatedTickDurationTotal() >= 0) { + lines.add(tooltipLine("gui.integratedterminals.terminal_crafting_job.craftingplan.duration.estimate", + GuiCraftingPlan.getDurationValue(plan.getEstimatedTickDurationTotal()), ChatFormatting.WHITE)); + } } // Everything that is only occasionally interesting stays in the background diff --git a/src/main/java/org/cyclops/integratedterminals/client/gui/container/component/GuiCraftingPlan.java b/src/main/java/org/cyclops/integratedterminals/client/gui/container/component/GuiCraftingPlan.java index feda24b05e..aeaf3b565e 100644 --- a/src/main/java/org/cyclops/integratedterminals/client/gui/container/component/GuiCraftingPlan.java +++ b/src/main/java/org/cyclops/integratedterminals/client/gui/container/component/GuiCraftingPlan.java @@ -65,6 +65,8 @@ public class GuiCraftingPlan extends AbstractWidget { protected static final int TICK_DELAY = 30; private static final int DURATION_LINE_HEIGHT = 6; + private static final int SECONDS_IN_MINUTE = 60; + private static final int SECONDS_IN_HOUR = 60 * 60; private final AbstractContainerScreen parentGui; private final int guiLeft; @@ -269,14 +271,39 @@ public static String getDurationString(String unlocalizedName, long tickDuration /** * @param tickDuration A tick duration, where -1 indicates an unknown duration. - * @return The duration as H:mm:ss, or a placeholder if it is unknown. + * @return The duration in the coarsest unit that still shows it, or a placeholder if it is unknown. */ public static String getDurationValue(long tickDuration) { if (tickDuration < 0) { return L10NHelpers.localize("gui.integratedterminals.terminal_crafting_job.craftingplan.duration.unknown"); } + + // A clock would round most crafting jobs away to zero, so short durations are shown in seconds + if (tickDuration < MinecraftHelpers.SECOND_IN_TICKS * SECONDS_IN_MINUTE) { + return L10NHelpers.localize("gui.integratedterminals.terminal_crafting_job.craftingplan.duration.seconds", + getDurationSeconds(tickDuration)); + } + return getDurationClock(tickDuration); + } + + /** + * @param tickDuration A tick duration below a minute. + * @return The duration in seconds, with the decimals that are still meaningful at that magnitude. + */ + public static String getDurationSeconds(long tickDuration) { + String format = tickDuration < MinecraftHelpers.SECOND_IN_TICKS + ? "%.2f" : (tickDuration < MinecraftHelpers.SECOND_IN_TICKS * 10 ? "%.1f" : "%.0f"); + return String.format(Locale.ROOT, format, (double) tickDuration / MinecraftHelpers.SECOND_IN_TICKS); + } + + /** + * @param tickDuration A tick duration of at least a minute. + * @return The duration as m:ss, or as H:mm:ss from an hour onwards. + */ + public static String getDurationClock(long tickDuration) { long durationMs = tickDuration * 1000 / MinecraftHelpers.SECOND_IN_TICKS; - return DurationFormatUtils.formatDuration(durationMs, "H:mm:ss", true); + return DurationFormatUtils.formatDuration(durationMs, + tickDuration < MinecraftHelpers.SECOND_IN_TICKS * SECONDS_IN_HOUR ? "m:ss" : "H:mm:ss", true); } /** @@ -313,12 +340,12 @@ public void drawGuiContainerBackgroundLayer(GuiGraphics guiGraphics, float parti // Draw initiator if (initiatorName != null) { String initiatorString = L10NHelpers.localize("gui.integratedterminals.terminal_crafting_job.craftingplan.owner", initiatorName); - RenderHelpers.drawScaledString(guiGraphics.pose(), guiGraphics.bufferSource(), fontRenderer, initiatorString, guiLeft + getX() - 4, guiTop + getY() - 14, 0.5f, 16777215, true, Font.DisplayMode.NORMAL); + RenderHelpers.drawScaledString(guiGraphics.pose(), guiGraphics.bufferSource(), fontRenderer, initiatorString, guiLeft + getX() - 4, guiTop + getY() - 8, 0.5f, 16777215, true, Font.DisplayMode.NORMAL); } // Draw estimated duration if (showEstimatedTickDuration) { - RenderHelpers.drawScaledString(guiGraphics.pose(), guiGraphics.bufferSource(), fontRenderer, getEstimatedDurationString(), guiLeft + getX() - 4, guiTop + getY() - 8, 0.5f, 16777215, true, Font.DisplayMode.NORMAL); + RenderHelpers.drawScaledString(guiGraphics.pose(), guiGraphics.bufferSource(), fontRenderer, getEstimatedDurationString(), guiLeft + getX() - 4, guiTop + getY() - 14, 0.5f, 16777215, true, Font.DisplayMode.NORMAL); } drawGuiContainerLayer(guiGraphics, guiLeft, guiTop, ContainerScreenTerminalStorage.DrawLayer.BACKGROUND, partialTicks, mouseX, mouseY); @@ -329,7 +356,7 @@ public void drawGuiContainerForegroundLayer(GuiGraphics guiGraphics, int mouseX, drawGuiContainerLayer(guiGraphics, 0, 0, ContainerScreenTerminalStorage.DrawLayer.FOREGROUND, 0, mouseX, mouseY); if (showEstimatedTickDuration) { drawUnknownDurationTooltip(this.parentGui, guiGraphics, getEstimatedDurationString(), estimatedTickDuration, - getX() - 4, getY() - 8, mouseX, mouseY); + getX() - 4, getY() - 14, mouseX, mouseY); } } diff --git a/src/main/java/org/cyclops/integratedterminals/client/gui/container/component/GuiCraftingPlanFlat.java b/src/main/java/org/cyclops/integratedterminals/client/gui/container/component/GuiCraftingPlanFlat.java index 7e056ea20c..efbb8ef826 100644 --- a/src/main/java/org/cyclops/integratedterminals/client/gui/container/component/GuiCraftingPlanFlat.java +++ b/src/main/java/org/cyclops/integratedterminals/client/gui/container/component/GuiCraftingPlanFlat.java @@ -242,12 +242,12 @@ public void drawGuiContainerBackgroundLayer(GuiGraphics guiGraphics, float parti // Draw initiator if (initiatorName != null) { String initiatorString = L10NHelpers.localize("gui.integratedterminals.terminal_crafting_job.craftingplan.owner", initiatorName); - RenderHelpers.drawScaledString(guiGraphics.pose(), guiGraphics.bufferSource(), fontRenderer, initiatorString, guiLeft + getX() - 4, guiTop + getY() - 14, 0.5f, 16777215, true, Font.DisplayMode.NORMAL); + RenderHelpers.drawScaledString(guiGraphics.pose(), guiGraphics.bufferSource(), fontRenderer, initiatorString, guiLeft + getX() - 4, guiTop + getY() - 8, 0.5f, 16777215, true, Font.DisplayMode.NORMAL); } // Draw estimated duration if (showEstimatedTickDuration) { - RenderHelpers.drawScaledString(guiGraphics.pose(), guiGraphics.bufferSource(), fontRenderer, getEstimatedDurationString(), guiLeft + getX() - 4, guiTop + getY() - 8, 0.5f, 16777215, true, Font.DisplayMode.NORMAL); + RenderHelpers.drawScaledString(guiGraphics.pose(), guiGraphics.bufferSource(), fontRenderer, getEstimatedDurationString(), guiLeft + getX() - 4, guiTop + getY() - 14, 0.5f, 16777215, true, Font.DisplayMode.NORMAL); } drawGuiContainerLayer(guiGraphics, guiLeft, guiTop, ContainerScreenTerminalStorage.DrawLayer.BACKGROUND, partialTicks, mouseX, mouseY); @@ -258,7 +258,7 @@ public void drawGuiContainerForegroundLayer(GuiGraphics guiGraphics, int mouseX, drawGuiContainerLayer(guiGraphics, 0, 0, ContainerScreenTerminalStorage.DrawLayer.FOREGROUND, 0, mouseX, mouseY); if (showEstimatedTickDuration) { GuiCraftingPlan.drawUnknownDurationTooltip(this.parentGui, guiGraphics, getEstimatedDurationString(), - estimatedTickDuration, getX() - 4, getY() - 8, mouseX, mouseY); + estimatedTickDuration, getX() - 4, getY() - 14, mouseX, mouseY); } } diff --git a/src/main/java/org/cyclops/integratedterminals/core/terminalstorage/slot/TerminalStorageSlotIngredientCraftingOption.java b/src/main/java/org/cyclops/integratedterminals/core/terminalstorage/slot/TerminalStorageSlotIngredientCraftingOption.java index 8e2847123a..544bf8624d 100644 --- a/src/main/java/org/cyclops/integratedterminals/core/terminalstorage/slot/TerminalStorageSlotIngredientCraftingOption.java +++ b/src/main/java/org/cyclops/integratedterminals/core/terminalstorage/slot/TerminalStorageSlotIngredientCraftingOption.java @@ -72,8 +72,12 @@ protected List getTooltipLines(@Nullable PendingCraftingJobOutput if (pendingCraftingJobOutput != null) { addCraftingJobTooltipLines(tooltipLines, pendingCraftingJobOutput); } - tooltipLines.add(Component.translatable("gui.integratedterminals.terminal_storage.tooltip.duration", - GuiCraftingPlan.getDurationValue(getCraftingOption().getCraftingOption().getEstimatedTickDuration()))); + // An unknown duration says nothing here, so it is left out rather than shown as a placeholder + long estimatedTickDuration = getCraftingOption().getCraftingOption().getEstimatedTickDuration(); + if (estimatedTickDuration >= 0) { + tooltipLines.add(Component.translatable("gui.integratedterminals.terminal_storage.tooltip.duration", + GuiCraftingPlan.getDurationValue(estimatedTickDuration))); + } if (!inputs.isEmpty()) { tooltipLines.add(Component.translatable("gui.integratedterminals.terminal_storage.tooltip.requirements") .withStyle(ChatFormatting.YELLOW)); diff --git a/src/main/java/org/cyclops/integratedterminals/modcompat/integratedcrafting/TerminalStorageTabIngredientCraftingHandlerCraftingNetwork.java b/src/main/java/org/cyclops/integratedterminals/modcompat/integratedcrafting/TerminalStorageTabIngredientCraftingHandlerCraftingNetwork.java index bbe749c44d..f875262b8c 100644 --- a/src/main/java/org/cyclops/integratedterminals/modcompat/integratedcrafting/TerminalStorageTabIngredientCraftingHandlerCraftingNetwork.java +++ b/src/main/java/org/cyclops/integratedterminals/modcompat/integratedcrafting/TerminalStorageTabIngredientCraftingHandlerCraftingNetwork.java @@ -15,6 +15,7 @@ import org.cyclops.commoncapabilities.api.ingredient.IPrototypedIngredient; import org.cyclops.commoncapabilities.api.ingredient.IngredientComponent; import org.cyclops.commoncapabilities.api.ingredient.PrototypedIngredient; +import org.cyclops.integratedcrafting.GeneralConfig; import org.cyclops.integratedcrafting.api.crafting.*; import org.cyclops.integratedcrafting.api.network.ICraftingNetwork; import org.cyclops.integratedcrafting.api.recipe.IRecipeIndex; @@ -74,7 +75,7 @@ public int[] getChannels(TerminalStorageTabIngredientComponentServer recipes = () -> recipeIndex.getRecipes(ingredientComponent, instance, matchCondition); return StreamSupport.stream(recipes.spliterator(), false) .map((recipe) -> new TerminalCraftingOptionRecipeDefinition<>(ingredientComponent, recipe, - craftingNetwork == null ? -1 : craftingNetwork.getEstimatedRecipeDuration(channel, recipe))) + craftingNetwork == null ? -1 : getEstimatedRecipeDuration(craftingNetwork, channel, recipe))) .collect(Collectors.toList()); } @@ -156,8 +157,37 @@ protected static long estimateTickDuration(long recipeDuration, long amount, * or -1 if that is unknown. */ protected static long getEstimatedRecipeDuration(@Nullable ICraftingNetwork craftingNetwork, CraftingJob craftingJob) { - return craftingNetwork == null ? -1 : craftingNetwork - .getEstimatedRecipeDuration(craftingJob.getChannel(), craftingJob.getRecipe()); + return craftingNetwork == null ? -1 : getEstimatedRecipeDuration(craftingNetwork, + craftingJob.getChannel(), craftingJob.getRecipe()); + } + + /** + * @param craftingNetwork A crafting network. + * @param channel A crafting channel. + * @param recipe A recipe. + * @return How long one crafting operation of the given recipe is estimated to take, + * or -1 if that is unknown. + */ + protected static long getEstimatedRecipeDuration(ICraftingNetwork craftingNetwork, int channel, IRecipeDefinition recipe) { + return withCraftingInterfaceOverhead(craftingNetwork.getEstimatedRecipeDuration(channel, recipe), + GeneralConfig.minCraftingInterfaceUpdateFreq); + } + + /** + * Crafting interfaces only measure how long a recipe takes to produce its outputs after being started. + * Recipes that are done within the tick they are started in, such as regular crafting recipes, + * are therefore measured as taking no time at all, which would estimate whole jobs away. + * + * A crafting interface performs at most one crafting operation per update though, + * so no recipe can go faster than that, which makes it a lower bound for those recipes. + * + * @param recipeDuration The measured duration of one crafting operation, + * where -1 indicates an unknown duration. + * @param updateInterval The number of ticks between two updates of a crafting interface. + * @return The duration that one crafting operation takes at the very least. + */ + protected static long withCraftingInterfaceOverhead(long recipeDuration, long updateInterval) { + return recipeDuration < 0 ? -1 : Math.max(recipeDuration, updateInterval); } protected static ITerminalCraftingPlan newCraftingPlan(@Nullable ICraftingNetwork craftingNetwork, diff --git a/src/main/resources/assets/integratedterminals/lang/en_us.json b/src/main/resources/assets/integratedterminals/lang/en_us.json index 13f0763164..3fddcdde1e 100644 --- a/src/main/resources/assets/integratedterminals/lang/en_us.json +++ b/src/main/resources/assets/integratedterminals/lang/en_us.json @@ -84,6 +84,7 @@ "gui.integratedterminals.terminal_crafting_job.craftingplan.duration.estimate": "Estimated: %s", "gui.integratedterminals.terminal_crafting_job.craftingplan.duration.remaining": "Remaining: %s", "gui.integratedterminals.terminal_crafting_job.craftingplan.duration.unknown": "?", + "gui.integratedterminals.terminal_crafting_job.craftingplan.duration.seconds": "%ss", "gui.integratedterminals.terminal_crafting_job.craftingplan.duration.unknown.title": "No estimate yet", "gui.integratedterminals.terminal_crafting_job.craftingplan.duration.unknown.desc": "The crafting interfaces have not crafted these recipes before.", "gui.integratedterminals.terminal_crafting_job.craftingplan.progress": "Progress: %s%%", diff --git a/src/test/java/org/cyclops/integratedterminals/client/gui/container/component/TestGuiCraftingPlanDuration.java b/src/test/java/org/cyclops/integratedterminals/client/gui/container/component/TestGuiCraftingPlanDuration.java new file mode 100644 index 0000000000..136717fa3c --- /dev/null +++ b/src/test/java/org/cyclops/integratedterminals/client/gui/container/component/TestGuiCraftingPlanDuration.java @@ -0,0 +1,44 @@ +package org.cyclops.integratedterminals.client.gui.container.component; + +import org.junit.Test; + +import static org.junit.Assert.assertEquals; + +/** + * @author rubensworks + */ +public class TestGuiCraftingPlanDuration { + + @Test + public void testSecondsBelowOneSecond() { + assertEquals("0.05", GuiCraftingPlan.getDurationSeconds(1)); + assertEquals("0.25", GuiCraftingPlan.getDurationSeconds(5)); + assertEquals("0.95", GuiCraftingPlan.getDurationSeconds(19)); + } + + @Test + public void testSecondsBelowTenSeconds() { + assertEquals("1.0", GuiCraftingPlan.getDurationSeconds(20)); + assertEquals("4.5", GuiCraftingPlan.getDurationSeconds(90)); + } + + @Test + public void testSecondsBelowOneMinute() { + assertEquals("10", GuiCraftingPlan.getDurationSeconds(200)); + assertEquals("59", GuiCraftingPlan.getDurationSeconds(1180)); + } + + @Test + public void testClockBelowOneHour() { + assertEquals("1:00", GuiCraftingPlan.getDurationClock(1200)); + assertEquals("4:15", GuiCraftingPlan.getDurationClock(5100)); + assertEquals("59:59", GuiCraftingPlan.getDurationClock(71980)); + } + + @Test + public void testClockFromOneHour() { + assertEquals("1:00:00", GuiCraftingPlan.getDurationClock(72000)); + assertEquals("2:03:20", GuiCraftingPlan.getDurationClock(148000)); + } + +} diff --git a/src/test/java/org/cyclops/integratedterminals/modcompat/integratedcrafting/TestTerminalStorageTabIngredientCraftingHandlerCraftingNetwork.java b/src/test/java/org/cyclops/integratedterminals/modcompat/integratedcrafting/TestTerminalStorageTabIngredientCraftingHandlerCraftingNetwork.java index ca14e90011..0b07323b18 100644 --- a/src/test/java/org/cyclops/integratedterminals/modcompat/integratedcrafting/TestTerminalStorageTabIngredientCraftingHandlerCraftingNetwork.java +++ b/src/test/java/org/cyclops/integratedterminals/modcompat/integratedcrafting/TestTerminalStorageTabIngredientCraftingHandlerCraftingNetwork.java @@ -94,4 +94,22 @@ public void testEstimateWithoutOperationsLeftAndUnknownDependencies() { .estimateTickDuration(-1, 0, Collections.emptyList())); } + @Test + public void testOverheadRaisesAnInstantRecipeToOneUpdate() { + assertEquals(5, TerminalStorageTabIngredientCraftingHandlerCraftingNetwork + .withCraftingInterfaceOverhead(0, 5)); + } + + @Test + public void testOverheadKeepsARecipeThatIsSlowerThanOneUpdate() { + assertEquals(200, TerminalStorageTabIngredientCraftingHandlerCraftingNetwork + .withCraftingInterfaceOverhead(200, 5)); + } + + @Test + public void testOverheadKeepsAnUnknownRecipeUnknown() { + assertEquals(-1, TerminalStorageTabIngredientCraftingHandlerCraftingNetwork + .withCraftingInterfaceOverhead(-1, 5)); + } + } From 160592c0481e9bd0e14bb2ca076e9b39ea3d8a6d Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 4 Sep 2026 15:51:10 +0000 Subject: [PATCH 17/17] Let Integrated Crafting bound instant recipe durations 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 Claude-Session: https://claude.ai/code/session_016gX5ZToD5ApLBXQkKW2QGj --- gradle.properties | 2 +- ...redientCraftingHandlerCraftingNetwork.java | 36 ++----------------- ...redientCraftingHandlerCraftingNetwork.java | 18 ---------- 3 files changed, 4 insertions(+), 52 deletions(-) diff --git a/gradle.properties b/gradle.properties index 95674389d4..3b2135f720 100644 --- a/gradle.properties +++ b/gradle.properties @@ -34,6 +34,6 @@ org.gradle.caching=true cyclopscore_version=1.26.2-808 integrateddynamics_version=1.32.0-1630 integratedterminalscompat_version=1.0.0-167 -integratedcrafting_version=1.5.0-678 +integratedcrafting_version=1.5.0-689 integratedtunnels_version=1.8.44-484 commoncapabilities_version=2.9.12-263 diff --git a/src/main/java/org/cyclops/integratedterminals/modcompat/integratedcrafting/TerminalStorageTabIngredientCraftingHandlerCraftingNetwork.java b/src/main/java/org/cyclops/integratedterminals/modcompat/integratedcrafting/TerminalStorageTabIngredientCraftingHandlerCraftingNetwork.java index f875262b8c..bbe749c44d 100644 --- a/src/main/java/org/cyclops/integratedterminals/modcompat/integratedcrafting/TerminalStorageTabIngredientCraftingHandlerCraftingNetwork.java +++ b/src/main/java/org/cyclops/integratedterminals/modcompat/integratedcrafting/TerminalStorageTabIngredientCraftingHandlerCraftingNetwork.java @@ -15,7 +15,6 @@ import org.cyclops.commoncapabilities.api.ingredient.IPrototypedIngredient; import org.cyclops.commoncapabilities.api.ingredient.IngredientComponent; import org.cyclops.commoncapabilities.api.ingredient.PrototypedIngredient; -import org.cyclops.integratedcrafting.GeneralConfig; import org.cyclops.integratedcrafting.api.crafting.*; import org.cyclops.integratedcrafting.api.network.ICraftingNetwork; import org.cyclops.integratedcrafting.api.recipe.IRecipeIndex; @@ -75,7 +74,7 @@ public int[] getChannels(TerminalStorageTabIngredientComponentServer recipes = () -> recipeIndex.getRecipes(ingredientComponent, instance, matchCondition); return StreamSupport.stream(recipes.spliterator(), false) .map((recipe) -> new TerminalCraftingOptionRecipeDefinition<>(ingredientComponent, recipe, - craftingNetwork == null ? -1 : getEstimatedRecipeDuration(craftingNetwork, channel, recipe))) + craftingNetwork == null ? -1 : craftingNetwork.getEstimatedRecipeDuration(channel, recipe))) .collect(Collectors.toList()); } @@ -157,37 +156,8 @@ protected static long estimateTickDuration(long recipeDuration, long amount, * or -1 if that is unknown. */ protected static long getEstimatedRecipeDuration(@Nullable ICraftingNetwork craftingNetwork, CraftingJob craftingJob) { - return craftingNetwork == null ? -1 : getEstimatedRecipeDuration(craftingNetwork, - craftingJob.getChannel(), craftingJob.getRecipe()); - } - - /** - * @param craftingNetwork A crafting network. - * @param channel A crafting channel. - * @param recipe A recipe. - * @return How long one crafting operation of the given recipe is estimated to take, - * or -1 if that is unknown. - */ - protected static long getEstimatedRecipeDuration(ICraftingNetwork craftingNetwork, int channel, IRecipeDefinition recipe) { - return withCraftingInterfaceOverhead(craftingNetwork.getEstimatedRecipeDuration(channel, recipe), - GeneralConfig.minCraftingInterfaceUpdateFreq); - } - - /** - * Crafting interfaces only measure how long a recipe takes to produce its outputs after being started. - * Recipes that are done within the tick they are started in, such as regular crafting recipes, - * are therefore measured as taking no time at all, which would estimate whole jobs away. - * - * A crafting interface performs at most one crafting operation per update though, - * so no recipe can go faster than that, which makes it a lower bound for those recipes. - * - * @param recipeDuration The measured duration of one crafting operation, - * where -1 indicates an unknown duration. - * @param updateInterval The number of ticks between two updates of a crafting interface. - * @return The duration that one crafting operation takes at the very least. - */ - protected static long withCraftingInterfaceOverhead(long recipeDuration, long updateInterval) { - return recipeDuration < 0 ? -1 : Math.max(recipeDuration, updateInterval); + return craftingNetwork == null ? -1 : craftingNetwork + .getEstimatedRecipeDuration(craftingJob.getChannel(), craftingJob.getRecipe()); } protected static ITerminalCraftingPlan newCraftingPlan(@Nullable ICraftingNetwork craftingNetwork, diff --git a/src/test/java/org/cyclops/integratedterminals/modcompat/integratedcrafting/TestTerminalStorageTabIngredientCraftingHandlerCraftingNetwork.java b/src/test/java/org/cyclops/integratedterminals/modcompat/integratedcrafting/TestTerminalStorageTabIngredientCraftingHandlerCraftingNetwork.java index 0b07323b18..ca14e90011 100644 --- a/src/test/java/org/cyclops/integratedterminals/modcompat/integratedcrafting/TestTerminalStorageTabIngredientCraftingHandlerCraftingNetwork.java +++ b/src/test/java/org/cyclops/integratedterminals/modcompat/integratedcrafting/TestTerminalStorageTabIngredientCraftingHandlerCraftingNetwork.java @@ -94,22 +94,4 @@ public void testEstimateWithoutOperationsLeftAndUnknownDependencies() { .estimateTickDuration(-1, 0, Collections.emptyList())); } - @Test - public void testOverheadRaisesAnInstantRecipeToOneUpdate() { - assertEquals(5, TerminalStorageTabIngredientCraftingHandlerCraftingNetwork - .withCraftingInterfaceOverhead(0, 5)); - } - - @Test - public void testOverheadKeepsARecipeThatIsSlowerThanOneUpdate() { - assertEquals(200, TerminalStorageTabIngredientCraftingHandlerCraftingNetwork - .withCraftingInterfaceOverhead(200, 5)); - } - - @Test - public void testOverheadKeepsAnUnknownRecipeUnknown() { - assertEquals(-1, TerminalStorageTabIngredientCraftingHandlerCraftingNetwork - .withCraftingInterfaceOverhead(-1, 5)); - } - }