diff --git a/gradle.properties b/gradle.properties index 5f7082b966..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.4.1-442 +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/api/terminalstorage/crafting/ITerminalCraftingOption.java b/src/main/java/org/cyclops/integratedterminals/api/terminalstorage/crafting/ITerminalCraftingOption.java index 14d5f1f9bd..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 @@ -37,6 +37,14 @@ 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 long getEstimatedTickDuration(); + /** * @return All input components. */ 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..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 @@ -42,6 +42,14 @@ 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 long getCraftingQuantityTotal(); + /** * @return The ingredients within the storage buffer. */ @@ -64,6 +72,19 @@ 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 long getEstimatedTickDurationTotal(); + + /** + * @return The estimated tick duration until this job is finished. -1 indicates an unknown duration. + */ + 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 1e8a905248..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 @@ -47,6 +47,31 @@ 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 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 long getCraftingQuantityRemaining(); + + /** + * 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 long getEstimatedTickDurationTotal(); + + /** + * @return The estimated tick duration until this job is finished. -1 indicates an unknown duration. + */ + public long getEstimatedTickDurationRemaining(); + /** * @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/client/gui/container/ContainerScreenTerminalCraftingJobs.java b/src/main/java/org/cyclops/integratedterminals/client/gui/container/ContainerScreenTerminalCraftingJobs.java index 731a1b0db1..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 @@ -1,10 +1,13 @@ 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; 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; @@ -14,6 +17,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 +25,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,6 +48,16 @@ public class ContainerScreenTerminalCraftingJobs extends ContainerScreenExtended public static int LINE_WIDTH = 221; + // 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 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; + private final Player player; private WidgetScrollBar scrollBar; @@ -99,6 +114,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() { @@ -127,6 +150,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(); @@ -134,31 +159,137 @@ 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, null)); 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 - 80, 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); + 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 - 40, 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 - 80, 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); + + 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); + + 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); + int filled = progress > 0 ? width * progress / 100 : 0; + + // 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) { + 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(); + + // 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) + .withStyle(Style.EMPTY.withColor(plan.getStatus().getColor() & 0xFFFFFF))) + .withStyle(ChatFormatting.GRAY)); + 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(tooltipLine("gui.integratedterminals.terminal_crafting_job.craftingplan.progress", + String.valueOf(progress), ChatFormatting.WHITE)); + } + long tickDuration = plan.getTickDuration(); + if (tickDuration >= 0) { + 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()) { + 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 + 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()).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() { 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..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 @@ -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; @@ -31,6 +32,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; @@ -62,6 +64,10 @@ 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; private final int guiTop; @@ -71,6 +77,9 @@ 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 boolean showEstimatedTickDuration; private final int channel; @Nullable private final String initiatorName; @@ -89,6 +98,12 @@ 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(); + // 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(); } @@ -247,9 +262,61 @@ 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) { + return L10NHelpers.localize(unlocalizedName, getDurationValue(tickDuration)); + } + + /** + * @param tickDuration A tick duration, where -1 indicates an unknown duration. + * @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 L10NHelpers.localize("gui.integratedterminals.terminal_crafting_job.craftingplan.duration", - 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); + } + + /** + * @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) { @@ -273,7 +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() - 14, 0.5f, 16777215, true, Font.DisplayMode.NORMAL); } drawGuiContainerLayer(guiGraphics, guiLeft, guiTop, ContainerScreenTerminalStorage.DrawLayer.BACKGROUND, partialTicks, mouseX, mouseY); @@ -282,6 +354,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() - 14, 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 ec32b8cbf1..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 @@ -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,9 @@ 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 boolean showEstimatedTickDuration; private final int channel; @Nullable private final String initiatorName; @@ -81,6 +83,12 @@ 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(); + // 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(); } @@ -210,9 +218,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) { @@ -236,7 +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() - 14, 0.5f, 16777215, true, Font.DisplayMode.NORMAL); } drawGuiContainerLayer(guiGraphics, guiLeft, guiTop, ContainerScreenTerminalStorage.DrawLayer.BACKGROUND, partialTicks, mouseX, mouseY); @@ -245,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() - 14, 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/java/org/cyclops/integratedterminals/core/terminalstorage/slot/TerminalStorageSlotIngredientCraftingOption.java b/src/main/java/org/cyclops/integratedterminals/core/terminalstorage/slot/TerminalStorageSlotIngredientCraftingOption.java index 5bdc085bec..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 @@ -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,19 @@ 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); } + // 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/gametest/GameTestTerminalCraftingOptionIntegratedTerminals.java b/src/main/java/org/cyclops/integratedterminals/gametest/GameTestTerminalCraftingOptionIntegratedTerminals.java index bfc38de613..4f1ac7cca9 100644 --- a/src/main/java/org/cyclops/integratedterminals/gametest/GameTestTerminalCraftingOptionIntegratedTerminals.java +++ b/src/main/java/org/cyclops/integratedterminals/gametest/GameTestTerminalCraftingOptionIntegratedTerminals.java @@ -40,7 +40,7 @@ private static List> 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 c4261f811f..1358ffc2b0 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,25 @@ public class TerminalCraftingOptionRecipeDefinition implements ITerminalCr private final IngredientComponent ingredientComponent; private final IRecipeDefinition prioritizedRecipe; + private final long estimatedTickDuration; + @Deprecated // TODO: rm in next major 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/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..bbe749c44d 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; @@ -69,20 +70,26 @@ 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)) + .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 @@ -91,26 +98,80 @@ 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 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 it can not be determined. + */ + protected static long estimateTickDuration(long recipeDuration, long amount, + List> dependencies) { + return estimateTickDuration(recipeDuration, amount, dependencies, + ITerminalCraftingPlan::getEstimatedTickDurationTotal); + } + + 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)); + } + + // 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, 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(getEstimatedRecipeDuration(craftingNetwork, craftingJob), + craftingJob.getAmountTotal(), dependencies); if (root) { return new TerminalCraftingPlanCraftingJobDependencyGraph( craftingJob.getId(), @@ -118,10 +179,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 +196,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 +233,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 +419,33 @@ protected static ITerminalCraftingPlan newActiveCraftingJob(ICraftingNe } } + long recipeDuration = getEstimatedRecipeDuration(craftingNetwork, craftingJob); + long estimatedTickDurationTotal = estimateTickDuration(recipeDuration, + craftingJob.getAmountTotal(), dependencies); + 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 + 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())); } @@ -438,10 +522,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/main/resources/assets/integratedterminals/lang/en_us.json b/src/main/resources/assets/integratedterminals/lang/en_us.json index 75140886c7..3fddcdde1e 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", @@ -80,6 +81,14 @@ "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.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%%", + "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/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()); + } + +} 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/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()))))); + } + +} 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..83dc9063d5 --- /dev/null +++ b/src/test/java/org/cyclops/integratedterminals/modcompat/integratedcrafting/TestTerminalCraftingOptionRecipeDefinition.java @@ -0,0 +1,23 @@ +package org.cyclops.integratedterminals.modcompat.integratedcrafting; + +import org.junit.Test; + +import static org.junit.Assert.assertEquals; + +/** + * @author rubensworks + */ +public class TestTerminalCraftingOptionRecipeDefinition { + + @Test + @SuppressWarnings("deprecation") + public void testDurationIsUnknownByDefault() { + assertEquals(-1, new TerminalCraftingOptionRecipeDefinition<>(null, null).getEstimatedTickDuration()); + } + + @Test + public void testDurationIsRemembered() { + assertEquals(200, new TerminalCraftingOptionRecipeDefinition<>(null, null, 200).getEstimatedTickDuration()); + } + +} 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..ca14e90011 --- /dev/null +++ b/src/test/java/org/cyclops/integratedterminals/modcompat/integratedcrafting/TestTerminalStorageTabIngredientCraftingHandlerCraftingNetwork.java @@ -0,0 +1,97 @@ +package org.cyclops.integratedterminals.modcompat.integratedcrafting; + +import com.google.common.collect.Lists; +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.Test; + +import java.util.Collections; +import java.util.List; + +import static org.junit.Assert.assertEquals; + +/** + * @author rubensworks + */ +public class TestTerminalStorageTabIngredientCraftingHandlerCraftingNetwork { + + 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 measured recipe duration, there is nothing to base the estimation on + assertEquals(-1, TerminalStorageTabIngredientCraftingHandlerCraftingNetwork + .estimateTickDuration(-1, 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(400, TerminalStorageTabIngredientCraftingHandlerCraftingNetwork + .estimateTickDuration(50, 3, dependencies)); + } + + @Test + public void testEstimateFromRemainingDependencies() { + List> dependencies = Lists.newArrayList( + planWithEstimations(100, 40), + planWithEstimations(250, 90)); + + 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(-1, 0, dependencies)); + } + + @Test + public void testEstimateWithoutOperationsLeftAndUnknownDependencies() { + assertEquals(-1, TerminalStorageTabIngredientCraftingHandlerCraftingNetwork + .estimateTickDuration(-1, 0, Collections.emptyList())); + } + +}