From acff9fa4bdc63d299b3c593ac8d371e8508e63a2 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 27 Aug 2026 19:05:47 +0000 Subject: [PATCH 1/3] Show a spinner overlay on ingredients that are being crafted The storage terminal now indicates which ingredients are being produced by running crafting jobs, by drawing an animated spinner over their slot. The spinner is colored by the crafting job status, and the tooltip shows the quantity that is still being crafted together with the job status. The server-side ingredient tab periodically collects the outputs that all running crafting jobs are still expected to produce, and sends them to the client. Just like the crafting jobs gui, this is throttled by guiTerminalCraftingJobsUpdateFrequency, and nothing is sent as long as no crafting jobs are running. Closes #138 Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01FzSYcpDAVUfCpTPpjMmeoz --- .../client/gui/image/Images.java | 10 ++ ...alStorageTabIngredientComponentClient.java | 36 +++++ ...alStorageTabIngredientComponentServer.java | 66 +++++++++ .../crafting/PendingCraftingJobOutput.java | 53 ++++++++ .../PendingCraftingJobOutputEntry.java | 16 +++ .../crafting/PendingCraftingJobOutputs.java | 107 +++++++++++++++ .../slot/TerminalStorageSlotIngredient.java | 88 +++++++++++- ...alStorageSlotIngredientCraftingOption.java | 14 +- .../GameTestPendingCraftingJobOutputs.java | 117 ++++++++++++++++ ...alStorageIngredientCraftingJobsPacket.java | 126 ++++++++++++++++++ .../proxy/CommonProxy.java | 1 + .../integratedterminals/lang/en_us.json | 2 + .../textures/gui/icons.png | Bin 5541 -> 6248 bytes .../info/terminals_info.xml | 1 + 14 files changed, 632 insertions(+), 5 deletions(-) create mode 100644 src/main/java/org/cyclops/integratedterminals/core/terminalstorage/crafting/PendingCraftingJobOutput.java create mode 100644 src/main/java/org/cyclops/integratedterminals/core/terminalstorage/crafting/PendingCraftingJobOutputEntry.java create mode 100644 src/main/java/org/cyclops/integratedterminals/core/terminalstorage/crafting/PendingCraftingJobOutputs.java create mode 100644 src/main/java/org/cyclops/integratedterminals/gametest/GameTestPendingCraftingJobOutputs.java create mode 100644 src/main/java/org/cyclops/integratedterminals/network/packet/TerminalStorageIngredientCraftingJobsPacket.java diff --git a/src/main/java/org/cyclops/integratedterminals/client/gui/image/Images.java b/src/main/java/org/cyclops/integratedterminals/client/gui/image/Images.java index 45510d23f..359c3b3d2 100644 --- a/src/main/java/org/cyclops/integratedterminals/client/gui/image/Images.java +++ b/src/main/java/org/cyclops/integratedterminals/client/gui/image/Images.java @@ -46,4 +46,14 @@ public class Images { public static final Image BUTTON_SMALL_OVERLAY_CROSS = new Image(ICONS, 0, 44, 8, 8); public static final Image BUTTON_SMALL_OVERLAY_SQUARE = new Image(ICONS, 8, 44, 8, 8); + /** + * The animation frames of the spinner that is shown on ingredients that are being crafted. + */ + public static final Image[] SPINNER = new Image[8]; + static { + for (int frame = 0; frame < SPINNER.length; frame++) { + SPINNER[frame] = new Image(ICONS, frame * 8, 64, 8, 8); + } + } + } diff --git a/src/main/java/org/cyclops/integratedterminals/core/terminalstorage/TerminalStorageTabIngredientComponentClient.java b/src/main/java/org/cyclops/integratedterminals/core/terminalstorage/TerminalStorageTabIngredientComponentClient.java index f0e127387..5ab3478ff 100644 --- a/src/main/java/org/cyclops/integratedterminals/core/terminalstorage/TerminalStorageTabIngredientComponentClient.java +++ b/src/main/java/org/cyclops/integratedterminals/core/terminalstorage/TerminalStorageTabIngredientComponentClient.java @@ -58,6 +58,9 @@ import org.cyclops.integratedterminals.core.terminalstorage.button.TerminalButtonScaleGui; import org.cyclops.integratedterminals.core.terminalstorage.button.TerminalButtonSort; import org.cyclops.integratedterminals.core.terminalstorage.crafting.HandlerWrappedTerminalCraftingOption; +import org.cyclops.integratedterminals.core.terminalstorage.crafting.PendingCraftingJobOutput; +import org.cyclops.integratedterminals.core.terminalstorage.crafting.PendingCraftingJobOutputEntry; +import org.cyclops.integratedterminals.core.terminalstorage.crafting.PendingCraftingJobOutputs; import org.cyclops.integratedterminals.core.terminalstorage.crafting.TerminalStorageTabIngredientCraftingHandlers; import org.cyclops.integratedterminals.core.terminalstorage.query.IIngredientQuery; import org.cyclops.integratedterminals.core.terminalstorage.slot.TerminalStorageSlotIngredient; @@ -106,6 +109,7 @@ public class TerminalStorageTabIngredientComponentClient private final Int2ObjectMap>> filteredIngredientsViews; private final Int2ObjectMap>> lastFilteredIngredientsViews; private final Int2ObjectMap>> craftingOptions; + private PendingCraftingJobOutputs pendingCraftingJobOutputs; private final Int2LongMap maxQuantities; private final Int2LongMap totalQuantities; @@ -152,6 +156,7 @@ public TerminalStorageTabIngredientComponentClient(ContainerTerminalStorageBase this.filteredIngredientsViews = new Int2ObjectOpenHashMap<>(); this.lastFilteredIngredientsViews = new Int2ObjectOpenHashMap<>(); this.craftingOptions = new Int2ObjectOpenHashMap<>(); + this.pendingCraftingJobOutputs = new PendingCraftingJobOutputs<>(this.ingredientComponent); this.maxQuantities = new Int2LongOpenHashMap(); this.totalQuantities = new Int2LongOpenHashMap(); @@ -305,6 +310,37 @@ public Collection> getCraftingOptions(in return craftingOptions.get(channel); } + /** + * Called by the server when the outputs that running crafting jobs are still expected to produce have changed. + * @param entries All pending crafting job outputs of all ingredient components. + */ + public synchronized void setPendingCraftingJobOutputs(List entries) { + PendingCraftingJobOutputs pendingCraftingJobOutputs = new PendingCraftingJobOutputs<>(this.ingredientComponent); + for (PendingCraftingJobOutputEntry entry : entries) { + if (entry.ingredient().getComponent() == this.ingredientComponent) { + T instance = (T) entry.ingredient().getPrototype(); + pendingCraftingJobOutputs.add(entry.channel(), instance, entry.status()); + + // Also aggregate into the wildcard channel, as that channel shows the contents of all channels. + if (entry.channel() != IPositionedAddonsNetwork.WILDCARD_CHANNEL) { + pendingCraftingJobOutputs.add(IPositionedAddonsNetwork.WILDCARD_CHANNEL, instance, entry.status()); + } + } + } + this.pendingCraftingJobOutputs = pendingCraftingJobOutputs; + } + + /** + * Get the quantity and status of the running crafting jobs that will produce the given instance. + * @param channel A channel id. + * @param instance An instance. + * @return The pending crafting job output, or null if the given instance is not being crafted. + */ + @Nullable + public PendingCraftingJobOutput getPendingCraftingJobOutput(int channel, T instance) { + return this.pendingCraftingJobOutputs.get(channel, instance); + } + public List> createUnfilteredIngredientsView(int channel) { // Convert raw ingredients view to list List> enrichedIngredients = Lists.newArrayList(); diff --git a/src/main/java/org/cyclops/integratedterminals/core/terminalstorage/TerminalStorageTabIngredientComponentServer.java b/src/main/java/org/cyclops/integratedterminals/core/terminalstorage/TerminalStorageTabIngredientComponentServer.java index cf0932b8d..859175274 100644 --- a/src/main/java/org/cyclops/integratedterminals/core/terminalstorage/TerminalStorageTabIngredientComponentServer.java +++ b/src/main/java/org/cyclops/integratedterminals/core/terminalstorage/TerminalStorageTabIngredientComponentServer.java @@ -12,6 +12,7 @@ import net.neoforged.neoforge.server.ServerLifecycleHooks; import org.apache.commons.lang3.tuple.Pair; import org.cyclops.commoncapabilities.api.ingredient.IIngredientMatcher; +import org.cyclops.commoncapabilities.api.ingredient.IPrototypedIngredient; import org.cyclops.commoncapabilities.api.ingredient.IngredientComponent; import org.cyclops.commoncapabilities.api.ingredient.storage.IIngredientComponentStorage; import org.cyclops.cyclopscore.ingredient.collection.IIngredientCollapsedCollectionMutable; @@ -30,6 +31,7 @@ import org.cyclops.integrateddynamics.api.ingredient.IIngredientPositionsIndex; import org.cyclops.integrateddynamics.api.ingredient.capability.IIngredientComponentValueHandler; import org.cyclops.integrateddynamics.api.network.INetwork; +import org.cyclops.integrateddynamics.api.network.IPositionedAddonsNetwork; import org.cyclops.integrateddynamics.api.network.IPositionedAddonsNetworkIngredients; import org.cyclops.integrateddynamics.core.evaluate.variable.ValueHelpers; import org.cyclops.integrateddynamics.core.evaluate.variable.ValueTypeBoolean; @@ -43,10 +45,14 @@ import org.cyclops.integratedterminals.api.terminalstorage.ITerminalStorageTabServer; import org.cyclops.integratedterminals.api.terminalstorage.TerminalClickType; import org.cyclops.integratedterminals.api.terminalstorage.crafting.ITerminalCraftingOption; +import org.cyclops.integratedterminals.api.terminalstorage.crafting.ITerminalCraftingPlan; import org.cyclops.integratedterminals.api.terminalstorage.crafting.ITerminalStorageTabIngredientCraftingHandler; +import org.cyclops.integratedterminals.api.terminalstorage.crafting.TerminalCraftingJobStatus; import org.cyclops.integratedterminals.core.terminalstorage.crafting.HandlerWrappedTerminalCraftingOption; +import org.cyclops.integratedterminals.core.terminalstorage.crafting.PendingCraftingJobOutputs; import org.cyclops.integratedterminals.core.terminalstorage.crafting.TerminalStorageTabIngredientCraftingHandlers; import org.cyclops.integratedterminals.network.packet.TerminalStorageIngredientChangeEventPacket; +import org.cyclops.integratedterminals.network.packet.TerminalStorageIngredientCraftingJobsPacket; import org.cyclops.integratedterminals.network.packet.TerminalStorageIngredientCraftingOptionsPacket; import org.cyclops.integratedterminals.network.packet.TerminalStorageIngredientMaxQuantityPacket; import org.cyclops.integratedterminals.network.packet.TerminalStorageIngredientUpdateActiveStorageIngredientPacket; @@ -85,6 +91,8 @@ public class TerminalStorageTabIngredientComponentServer implements ITermi private final Int2ObjectMap> filteredDiffManagers; private boolean initialized; // True if the first change event has been sent to the client. private boolean sentCraftingOptionsFiltered; + private long nextCraftingJobsUpdate; + private boolean sentCraftingJobs; // True if a non-empty set of pending crafting job outputs was sent to the client. public TerminalStorageTabIngredientComponentServer(ResourceLocation name, INetwork network, IngredientComponent ingredientComponent, @@ -102,6 +110,8 @@ public TerminalStorageTabIngredientComponentServer(ResourceLocation name, INetwo this.ingredientsFilter = null; this.unfilteredIngredientsViews = new Int2ObjectOpenHashMap<>(); this.filteredDiffManagers = new Int2ObjectOpenHashMap<>(); + this.nextCraftingJobsUpdate = 0; + this.sentCraftingJobs = false; // Schedule an observation on creation, as the channel may not have been indexed yet. ingredientNetwork.scheduleObservation(); @@ -171,6 +181,62 @@ public void deInit() { @Override public void updateActive() { this.ingredientNetwork.scheduleObservation(); + updatePendingCraftingJobOutputs(); + } + + /** + * Collect the outputs that all running crafting jobs are still expected to produce, + * and send them to the client, so that they can be indicated in the storage terminal. + * + * As crafting job statuses change frequently, + * this is throttled by {@link GeneralConfig#guiTerminalCraftingJobsUpdateFrequency}. + */ + protected void updatePendingCraftingJobOutputs() { + if (System.currentTimeMillis() < this.nextCraftingJobsUpdate) { + return; + } + this.nextCraftingJobsUpdate = System.currentTimeMillis() + GeneralConfig.guiTerminalCraftingJobsUpdateFrequency; + + PendingCraftingJobOutputs pendingCraftingJobOutputs = new PendingCraftingJobOutputs<>(this.ingredientComponent); + for (ITerminalStorageTabIngredientCraftingHandler handler : TerminalStorageTabIngredientCraftingHandlers.REGISTRY.getHandlers()) { + Set handledPlans = Sets.newHashSet(); + for (ITerminalCraftingPlan craftingJob : handler.getCraftingJobs(this.network, IPositionedAddonsNetwork.WILDCARD_CHANNEL)) { + collectPendingCraftingJobOutputs(craftingJob, handledPlans, pendingCraftingJobOutputs); + } + } + + // Don't send anything as long as no crafting jobs are running, + // but do send one final (empty) update once the last job has finished. + boolean hasCraftingJobs = !pendingCraftingJobOutputs.isEmpty(); + if (!hasCraftingJobs && !this.sentCraftingJobs) { + return; + } + this.sentCraftingJobs = hasCraftingJobs; + + IntegratedTerminals._instance.getPacketHandler().sendToPlayer( + new TerminalStorageIngredientCraftingJobsPacket(player.level().registryAccess(), + this.getName().toString(), pendingCraftingJobOutputs), player); + } + + protected void collectPendingCraftingJobOutputs(ITerminalCraftingPlan craftingPlan, Set handledPlans, + PendingCraftingJobOutputs pendingCraftingJobOutputs) { + // Jobs can occur multiple times within a plan due to job splitting, so only take each of them into account once. + if ((!(craftingPlan.getId() instanceof Integer id) || id > 0) && !handledPlans.add(craftingPlan.getId())) { + return; + } + + // Finished jobs have already produced all their outputs, so nothing is pending for them anymore. + if (craftingPlan.getStatus() != TerminalCraftingJobStatus.FINISHED) { + for (IPrototypedIngredient output : craftingPlan.getOutputs()) { + if (output.getComponent() == this.ingredientComponent) { + pendingCraftingJobOutputs.add(craftingPlan.getChannel(), (T) output.getPrototype(), craftingPlan.getStatus()); + } + } + } + + for (ITerminalCraftingPlan dependency : craftingPlan.getDependencies()) { + collectPendingCraftingJobOutputs(dependency, handledPlans, pendingCraftingJobOutputs); + } } protected IIngredientCollapsedCollectionMutable getUnfilteredIngredientsView(int channel) { diff --git a/src/main/java/org/cyclops/integratedterminals/core/terminalstorage/crafting/PendingCraftingJobOutput.java b/src/main/java/org/cyclops/integratedterminals/core/terminalstorage/crafting/PendingCraftingJobOutput.java new file mode 100644 index 000000000..52dfc3f25 --- /dev/null +++ b/src/main/java/org/cyclops/integratedterminals/core/terminalstorage/crafting/PendingCraftingJobOutput.java @@ -0,0 +1,53 @@ +package org.cyclops.integratedterminals.core.terminalstorage.crafting; + +import org.cyclops.integratedterminals.api.terminalstorage.crafting.TerminalCraftingJobStatus; + +/** + * The aggregated pending output of all running crafting jobs for a single ingredient instance. + * @param The instance type. + * @author rubensworks + */ +public class PendingCraftingJobOutput { + + private final T instance; + private final TerminalCraftingJobStatus status; + + public PendingCraftingJobOutput(T instance, TerminalCraftingJobStatus status) { + this.instance = instance; + this.status = status; + } + + /** + * @return The pending instance, where the quantity indicates how much is still expected to be crafted. + */ + public T getInstance() { + return instance; + } + + /** + * @return The most relevant status over all crafting jobs that will produce this instance. + */ + public TerminalCraftingJobStatus getStatus() { + return status; + } + + /** + * Determine how relevant the given status is when multiple crafting jobs produce the same instance. + * + * Statuses that require the attention of the player take precedence over statuses that don't. + * + * @param status A crafting job status. + * @return The priority of the given status, where a higher number indicates a higher priority. + */ + public static int getStatusPriority(TerminalCraftingJobStatus status) { + return switch (status) { + case ERROR, INVALID, INVALID_INPUTS -> 5; + case PENDING_INPUTS -> 4; + case CRAFTING -> 3; + case PENDING_DEPENDENCIES, QUEUEING -> 2; + case UNSTARTED -> 1; + case FINISHED -> 0; + }; + } + +} diff --git a/src/main/java/org/cyclops/integratedterminals/core/terminalstorage/crafting/PendingCraftingJobOutputEntry.java b/src/main/java/org/cyclops/integratedterminals/core/terminalstorage/crafting/PendingCraftingJobOutputEntry.java new file mode 100644 index 000000000..d5aab28ae --- /dev/null +++ b/src/main/java/org/cyclops/integratedterminals/core/terminalstorage/crafting/PendingCraftingJobOutputEntry.java @@ -0,0 +1,16 @@ +package org.cyclops.integratedterminals.core.terminalstorage.crafting; + +import org.cyclops.commoncapabilities.api.ingredient.IPrototypedIngredient; +import org.cyclops.integratedterminals.api.terminalstorage.crafting.TerminalCraftingJobStatus; + +/** + * A single component-agnostic pending crafting job output, as it is sent from server to client. + * + * @param channel The channel the crafting job is running in. + * @param ingredient The pending output, where the quantity indicates how much is still expected to be crafted. + * @param status The status of the crafting job that will produce the ingredient. + * @author rubensworks + */ +public record PendingCraftingJobOutputEntry(int channel, IPrototypedIngredient ingredient, + TerminalCraftingJobStatus status) { +} diff --git a/src/main/java/org/cyclops/integratedterminals/core/terminalstorage/crafting/PendingCraftingJobOutputs.java b/src/main/java/org/cyclops/integratedterminals/core/terminalstorage/crafting/PendingCraftingJobOutputs.java new file mode 100644 index 000000000..2d41a468c --- /dev/null +++ b/src/main/java/org/cyclops/integratedterminals/core/terminalstorage/crafting/PendingCraftingJobOutputs.java @@ -0,0 +1,107 @@ +package org.cyclops.integratedterminals.core.terminalstorage.crafting; + +import it.unimi.dsi.fastutil.ints.Int2ObjectMap; +import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; +import org.cyclops.commoncapabilities.api.ingredient.IIngredientMatcher; +import org.cyclops.commoncapabilities.api.ingredient.IngredientComponent; +import org.cyclops.integratedterminals.api.terminalstorage.crafting.TerminalCraftingJobStatus; + +import javax.annotation.Nullable; +import java.util.Map; +import java.util.TreeMap; + +/** + * The pending outputs of all running crafting jobs of a single ingredient component, indexed by channel. + * + * Instances are indexed independent of their quantity, + * so that they can be looked up by the instances that are shown in the storage terminal. + * + * @param The instance type. + * @param The matching condition parameter. + * @author rubensworks + */ +public class PendingCraftingJobOutputs { + + private final IngredientComponent ingredientComponent; + private final Int2ObjectMap>> channeledOutputs; + + public PendingCraftingJobOutputs(IngredientComponent ingredientComponent) { + this.ingredientComponent = ingredientComponent; + this.channeledOutputs = new Int2ObjectOpenHashMap<>(); + } + + public IngredientComponent getIngredientComponent() { + return ingredientComponent; + } + + /** + * Add a pending crafting job output. + * + * If the given instance is already pending in the given channel, + * the quantities are summed, and the most relevant status is kept. + * + * @param channel A channel id. + * @param instance An instance, where the quantity indicates the pending quantity. + * @param status The status of the crafting job that will produce the given instance. + */ + public void add(int channel, T instance, TerminalCraftingJobStatus status) { + IIngredientMatcher matcher = this.ingredientComponent.getMatcher(); + if (matcher.isEmpty(instance)) { + return; + } + + Map> outputs = this.channeledOutputs + .computeIfAbsent(channel, (c) -> new TreeMap<>(matcher)); + T key = matcher.withQuantity(instance, 1); + PendingCraftingJobOutput existingOutput = outputs.get(key); + if (existingOutput != null) { + instance = matcher.withQuantity(instance, addQuantities(matcher, + matcher.getQuantity(existingOutput.getInstance()), matcher.getQuantity(instance))); + if (PendingCraftingJobOutput.getStatusPriority(existingOutput.getStatus()) + >= PendingCraftingJobOutput.getStatusPriority(status)) { + status = existingOutput.getStatus(); + } + } + outputs.put(key, new PendingCraftingJobOutput<>(instance, status)); + } + + /** + * Get the pending output for the given instance, independent of the instance's quantity. + * @param channel A channel id. + * @param instance An instance. + * @return The pending output, or null if the given instance is not being crafted. + */ + @Nullable + public PendingCraftingJobOutput get(int channel, T instance) { + Map> outputs = this.channeledOutputs.get(channel); + if (outputs == null) { + return null; + } + IIngredientMatcher matcher = this.ingredientComponent.getMatcher(); + return matcher.isEmpty(instance) ? null : outputs.get(matcher.withQuantity(instance, 1)); + } + + /** + * @return All pending outputs, indexed by channel. + */ + public Int2ObjectMap>> getChanneledOutputs() { + return channeledOutputs; + } + + /** + * @return If no crafting job outputs are pending. + */ + public boolean isEmpty() { + return this.channeledOutputs.isEmpty(); + } + + private static long addQuantities(IIngredientMatcher matcher, long quantity, long quantityToAdd) { + long maxQuantity = matcher.getMaximumQuantity(); + try { + return Math.min(maxQuantity, Math.addExact(quantity, quantityToAdd)); + } catch (ArithmeticException e) { + return maxQuantity; + } + } + +} diff --git a/src/main/java/org/cyclops/integratedterminals/core/terminalstorage/slot/TerminalStorageSlotIngredient.java b/src/main/java/org/cyclops/integratedterminals/core/terminalstorage/slot/TerminalStorageSlotIngredient.java index 2ec46a6a3..2e4c987cb 100644 --- a/src/main/java/org/cyclops/integratedterminals/core/terminalstorage/slot/TerminalStorageSlotIngredient.java +++ b/src/main/java/org/cyclops/integratedterminals/core/terminalstorage/slot/TerminalStorageSlotIngredient.java @@ -1,16 +1,26 @@ package org.cyclops.integratedterminals.core.terminalstorage.slot; +import com.google.common.collect.Lists; +import com.mojang.blaze3d.platform.GlStateManager; +import net.minecraft.ChatFormatting; import net.minecraft.client.gui.GuiGraphics; import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen; +import net.minecraft.network.chat.Component; import net.neoforged.api.distmarker.Dist; import net.neoforged.api.distmarker.OnlyIn; +import org.apache.commons.lang3.tuple.Triple; +import org.cyclops.cyclopscore.helper.Helpers; import org.cyclops.integratedterminals.api.ingredient.IIngredientComponentTerminalStorageHandler; import org.cyclops.integratedterminals.api.terminalstorage.ITerminalStorageSlot; import org.cyclops.integratedterminals.api.terminalstorage.ITerminalStorageTabClient; import org.cyclops.integratedterminals.client.gui.container.ContainerScreenTerminalStorage; +import org.cyclops.integratedterminals.client.gui.image.Images; import org.cyclops.integratedterminals.core.terminalstorage.TerminalStorageTabIngredientComponentClient; +import org.cyclops.integratedterminals.core.terminalstorage.crafting.PendingCraftingJobOutput; import javax.annotation.Nullable; +import java.util.List; +import java.util.Locale; /** * An ingredient slot. @@ -20,6 +30,11 @@ */ public class TerminalStorageSlotIngredient implements ITerminalStorageSlot { + /** + * The duration in milliseconds of a single frame of the crafting spinner. + */ + private static final long SPINNER_FRAME_DURATION = 100; + private final IIngredientComponentTerminalStorageHandler ingredientComponentViewHandler; private final T instance; @@ -34,7 +49,10 @@ public void drawGuiContainerLayer(AbstractContainerScreen gui, GuiGraphics guiGr float partialTick, int x, int y, int mouseX, int mouseY, ITerminalStorageTabClient tab, int channel, @Nullable String label) { long maxQuantity = ((TerminalStorageTabIngredientComponentClient) tab).getMaxQuantity(channel); - ingredientComponentViewHandler.drawInstance(guiGraphics, instance, maxQuantity, label, gui, layer, partialTick, x, y, mouseX, mouseY, null); + PendingCraftingJobOutput pendingCraftingJobOutput = getPendingCraftingJobOutput(tab, channel, label); + ingredientComponentViewHandler.drawInstance(guiGraphics, instance, maxQuantity, label, gui, layer, partialTick, x, y, mouseX, mouseY, + createCraftingJobTooltipLines(pendingCraftingJobOutput)); + drawCraftingJobOverlay(guiGraphics, layer, x, y, pendingCraftingJobOutput); } public IIngredientComponentTerminalStorageHandler getIngredientComponentViewHandler() { @@ -44,4 +62,72 @@ public IIngredientComponentTerminalStorageHandler getIngredientComponentVi public T getInstance() { return instance; } + + /** + * Get the pending output of the running crafting jobs that will produce this slot's instance. + * @param tab The tab this slot is being rendered in. + * @param channel The channel this slot is being rendered in. + * @param label An optional label that is rendered instead of the quantity. + * Slots with such a label are not part of the storage overview, + * such as the instance that is being moved around by the player, + * so they don't get a crafting indication. + * @return The pending crafting job output, or null if this slot's instance is not being crafted. + */ + @Nullable + @OnlyIn(Dist.CLIENT) + protected PendingCraftingJobOutput getPendingCraftingJobOutput(ITerminalStorageTabClient tab, int channel, + @Nullable String label) { + return label == null + ? ((TerminalStorageTabIngredientComponentClient) tab).getPendingCraftingJobOutput(channel, getInstance()) + : null; + } + + @Nullable + @OnlyIn(Dist.CLIENT) + protected List createCraftingJobTooltipLines(@Nullable PendingCraftingJobOutput pendingCraftingJobOutput) { + if (pendingCraftingJobOutput == null) { + return null; + } + List tooltipLines = Lists.newArrayList(); + addCraftingJobTooltipLines(tooltipLines, pendingCraftingJobOutput); + return tooltipLines; + } + + @OnlyIn(Dist.CLIENT) + protected void addCraftingJobTooltipLines(List tooltipLines, + PendingCraftingJobOutput pendingCraftingJobOutput) { + tooltipLines.add(Component.translatable("gui.integratedterminals.terminal_storage.tooltip.crafting", + getIngredientComponentViewHandler().formatQuantity(pendingCraftingJobOutput.getInstance())) + .withStyle(ChatFormatting.AQUA)); + String unlocalizedStatus = "gui.integratedterminals.craftingplan.status." + + pendingCraftingJobOutput.getStatus().name().toLowerCase(Locale.ENGLISH); + tooltipLines.add(Component.translatable("gui.integratedterminals.craftingplan.status", + Component.translatable(unlocalizedStatus)) + .withStyle(ChatFormatting.GRAY)); + tooltipLines.add(Component.translatable(unlocalizedStatus + ".desc").withStyle(ChatFormatting.DARK_GRAY)); + } + + /** + * Draw a spinner over this slot when its instance is being crafted. + * The spinner is colored based on the status of the crafting jobs. + */ + @OnlyIn(Dist.CLIENT) + protected void drawCraftingJobOverlay(GuiGraphics guiGraphics, ContainerScreenTerminalStorage.DrawLayer layer, + int x, int y, @Nullable PendingCraftingJobOutput pendingCraftingJobOutput) { + if (layer != ContainerScreenTerminalStorage.DrawLayer.BACKGROUND || pendingCraftingJobOutput == null) { + return; + } + + Triple color = Helpers.intToRGB(pendingCraftingJobOutput.getStatus().getColor()); + int frame = (int) ((System.currentTimeMillis() / SPINNER_FRAME_DURATION) % Images.SPINNER.length); + + guiGraphics.pose().pushPose(); + // Draw in front of the instance, which is rendered as a 3D item for some ingredient components. + guiGraphics.pose().translate(0, 0, 300); + GlStateManager._enableBlend(); + Images.SPINNER[frame].drawWithColor(guiGraphics, x, y, + color.getLeft(), color.getMiddle(), color.getRight(), 1F); + guiGraphics.pose().popPose(); + } + } 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 fbef14cf6..19ad10e5e 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 @@ -18,6 +18,7 @@ import org.cyclops.integratedterminals.client.gui.container.ContainerScreenTerminalStorage; import org.cyclops.integratedterminals.core.terminalstorage.TerminalStorageTabIngredientComponentClient; import org.cyclops.integratedterminals.core.terminalstorage.crafting.HandlerWrappedTerminalCraftingOption; +import org.cyclops.integratedterminals.core.terminalstorage.crafting.PendingCraftingJobOutput; import javax.annotation.Nullable; import java.util.List; @@ -43,18 +44,23 @@ public void drawGuiContainerLayer(AbstractContainerScreen gui, GuiGraphics guiGr float partialTick, int x, int y, int mouseX, int mouseY, ITerminalStorageTabClient tab, int channel, @Nullable String label) { IIngredientComponentTerminalStorageHandler viewHandler = getIngredientComponentViewHandler(); + long maxQuantity = ((TerminalStorageTabIngredientComponentClient) tab).getMaxQuantity(channel); + PendingCraftingJobOutput pendingCraftingJobOutput = getPendingCraftingJobOutput(tab, channel, label); if (layer == ContainerScreenTerminalStorage.DrawLayer.BACKGROUND) { - long maxQuantity = ((TerminalStorageTabIngredientComponentClient) tab).getMaxQuantity(channel); viewHandler.drawInstance(guiGraphics, getInstance(), maxQuantity, null, gui, layer, partialTick, x, y, mouseX, mouseY, null); drawCraftLabel(guiGraphics, x, y); } else { - long maxQuantity = ((TerminalStorageTabIngredientComponentClient) tab).getMaxQuantity(channel); - getIngredientComponentViewHandler().drawInstance(guiGraphics, getInstance(), maxQuantity, label, gui, layer, partialTick, x, y, mouseX, mouseY, getTooltipLines()); + viewHandler.drawInstance(guiGraphics, getInstance(), maxQuantity, label, gui, layer, partialTick, x, y, mouseX, mouseY, + getTooltipLines(pendingCraftingJobOutput)); } + drawCraftingJobOverlay(guiGraphics, layer, x, y, pendingCraftingJobOutput); } - protected List getTooltipLines() { + protected List getTooltipLines(@Nullable PendingCraftingJobOutput pendingCraftingJobOutput) { List tooltipLines = Lists.newArrayList(); + if (pendingCraftingJobOutput != null) { + addCraftingJobTooltipLines(tooltipLines, pendingCraftingJobOutput); + } tooltipLines.add(Component.translatable("gui.integratedterminals.terminal_storage.tooltip.requirements") .withStyle(ChatFormatting.YELLOW)); ITerminalCraftingOption option = getCraftingOption().getCraftingOption(); diff --git a/src/main/java/org/cyclops/integratedterminals/gametest/GameTestPendingCraftingJobOutputs.java b/src/main/java/org/cyclops/integratedterminals/gametest/GameTestPendingCraftingJobOutputs.java new file mode 100644 index 000000000..4aeb5a420 --- /dev/null +++ b/src/main/java/org/cyclops/integratedterminals/gametest/GameTestPendingCraftingJobOutputs.java @@ -0,0 +1,117 @@ +package org.cyclops.integratedterminals.gametest; + +import net.minecraft.gametest.framework.GameTest; +import net.minecraft.gametest.framework.GameTestHelper; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.neoforged.neoforge.gametest.GameTestHolder; +import net.neoforged.neoforge.gametest.PrefixGameTestTemplate; +import org.cyclops.commoncapabilities.IngredientComponents; +import org.cyclops.integratedterminals.Reference; +import org.cyclops.integratedterminals.api.terminalstorage.crafting.TerminalCraftingJobStatus; +import org.cyclops.integratedterminals.core.terminalstorage.crafting.PendingCraftingJobOutput; +import org.cyclops.integratedterminals.core.terminalstorage.crafting.PendingCraftingJobOutputs; + +/** + * Game tests for the aggregation of pending crafting job outputs in the storage terminal. + * @author rubensworks + */ +@GameTestHolder(Reference.MOD_ID) +@PrefixGameTestTemplate(false) +public class GameTestPendingCraftingJobOutputs { + + private static PendingCraftingJobOutputs createOutputs() { + return new PendingCraftingJobOutputs<>(IngredientComponents.ITEMSTACK); + } + + @GameTest(template = "empty", templateNamespace = "cyclopscore") + public void testEmpty(GameTestHelper helper) { + PendingCraftingJobOutputs outputs = createOutputs(); + + helper.assertTrue(outputs.isEmpty(), "No outputs should be pending"); + helper.assertTrue(outputs.get(0, new ItemStack(Items.STONE)) == null, + "No output should be pending for stone"); + + helper.succeed(); + } + + @GameTest(template = "empty", templateNamespace = "cyclopscore") + public void testLookupIgnoresQuantity(GameTestHelper helper) { + PendingCraftingJobOutputs outputs = createOutputs(); + outputs.add(0, new ItemStack(Items.STONE, 5), TerminalCraftingJobStatus.CRAFTING); + + helper.assertTrue(!outputs.isEmpty(), "Outputs should be pending"); + PendingCraftingJobOutput output = outputs.get(0, new ItemStack(Items.STONE, 64)); + helper.assertTrue(output != null, "An output should be pending for stone of any quantity"); + helper.assertTrue(output.getInstance().getCount() == 5, "5 stone should be pending"); + helper.assertTrue(output.getStatus() == TerminalCraftingJobStatus.CRAFTING, "Stone should be crafting"); + + helper.assertTrue(outputs.get(0, new ItemStack(Items.DIRT)) == null, + "No output should be pending for another item"); + helper.assertTrue(outputs.get(1, new ItemStack(Items.STONE)) == null, + "No output should be pending in another channel"); + + helper.succeed(); + } + + @GameTest(template = "empty", templateNamespace = "cyclopscore") + public void testQuantitiesAreSummed(GameTestHelper helper) { + PendingCraftingJobOutputs outputs = createOutputs(); + outputs.add(0, new ItemStack(Items.STONE, 5), TerminalCraftingJobStatus.CRAFTING); + outputs.add(0, new ItemStack(Items.STONE, 7), TerminalCraftingJobStatus.CRAFTING); + + helper.assertTrue(outputs.get(0, new ItemStack(Items.STONE)).getInstance().getCount() == 12, + "12 stone should be pending"); + + helper.succeed(); + } + + @GameTest(template = "empty", templateNamespace = "cyclopscore") + public void testMostRelevantStatusIsKept(GameTestHelper helper) { + PendingCraftingJobOutputs outputs = createOutputs(); + outputs.add(0, new ItemStack(Items.STONE), TerminalCraftingJobStatus.CRAFTING); + outputs.add(0, new ItemStack(Items.STONE), TerminalCraftingJobStatus.PENDING_INPUTS); + helper.assertTrue(outputs.get(0, new ItemStack(Items.STONE)).getStatus() == TerminalCraftingJobStatus.PENDING_INPUTS, + "Missing inputs should take precedence over crafting"); + + PendingCraftingJobOutputs outputsReversed = createOutputs(); + outputsReversed.add(0, new ItemStack(Items.STONE), TerminalCraftingJobStatus.PENDING_INPUTS); + outputsReversed.add(0, new ItemStack(Items.STONE), TerminalCraftingJobStatus.CRAFTING); + helper.assertTrue(outputsReversed.get(0, new ItemStack(Items.STONE)).getStatus() == TerminalCraftingJobStatus.PENDING_INPUTS, + "Missing inputs should take precedence over crafting, independent of insertion order"); + + PendingCraftingJobOutputs outputsQueueing = createOutputs(); + outputsQueueing.add(0, new ItemStack(Items.STONE), TerminalCraftingJobStatus.QUEUEING); + outputsQueueing.add(0, new ItemStack(Items.STONE), TerminalCraftingJobStatus.CRAFTING); + helper.assertTrue(outputsQueueing.get(0, new ItemStack(Items.STONE)).getStatus() == TerminalCraftingJobStatus.CRAFTING, + "Crafting should take precedence over queueing"); + + helper.succeed(); + } + + @GameTest(template = "empty", templateNamespace = "cyclopscore") + public void testEmptyInstancesAreIgnored(GameTestHelper helper) { + PendingCraftingJobOutputs outputs = createOutputs(); + outputs.add(0, ItemStack.EMPTY, TerminalCraftingJobStatus.CRAFTING); + + helper.assertTrue(outputs.isEmpty(), "Empty instances should not be pending"); + + helper.succeed(); + } + + @GameTest(template = "empty", templateNamespace = "cyclopscore") + public void testMultipleChannels(GameTestHelper helper) { + PendingCraftingJobOutputs outputs = createOutputs(); + outputs.add(0, new ItemStack(Items.STONE, 5), TerminalCraftingJobStatus.CRAFTING); + outputs.add(1, new ItemStack(Items.STONE, 3), TerminalCraftingJobStatus.QUEUEING); + + helper.assertTrue(outputs.getChanneledOutputs().size() == 2, "Two channels should have pending outputs"); + helper.assertTrue(outputs.get(0, new ItemStack(Items.STONE)).getInstance().getCount() == 5, + "5 stone should be pending in channel 0"); + helper.assertTrue(outputs.get(1, new ItemStack(Items.STONE)).getInstance().getCount() == 3, + "3 stone should be pending in channel 1"); + + helper.succeed(); + } + +} diff --git a/src/main/java/org/cyclops/integratedterminals/network/packet/TerminalStorageIngredientCraftingJobsPacket.java b/src/main/java/org/cyclops/integratedterminals/network/packet/TerminalStorageIngredientCraftingJobsPacket.java new file mode 100644 index 000000000..263624cee --- /dev/null +++ b/src/main/java/org/cyclops/integratedterminals/network/packet/TerminalStorageIngredientCraftingJobsPacket.java @@ -0,0 +1,126 @@ +package org.cyclops.integratedterminals.network.packet; + +import com.google.common.collect.Lists; +import it.unimi.dsi.fastutil.ints.Int2ObjectMap; +import net.minecraft.client.Minecraft; +import net.minecraft.core.HolderLookup; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.ListTag; +import net.minecraft.nbt.Tag; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.level.Level; +import net.neoforged.api.distmarker.Dist; +import net.neoforged.api.distmarker.OnlyIn; +import org.cyclops.commoncapabilities.IngredientComponents; +import org.cyclops.commoncapabilities.api.ingredient.IIngredientMatcher; +import org.cyclops.commoncapabilities.api.ingredient.IPrototypedIngredient; +import org.cyclops.commoncapabilities.api.ingredient.PrototypedIngredient; +import org.cyclops.cyclopscore.network.CodecField; +import org.cyclops.cyclopscore.network.PacketCodec; +import org.cyclops.integratedterminals.GeneralConfig; +import org.cyclops.integratedterminals.Reference; +import org.cyclops.integratedterminals.api.terminalstorage.crafting.TerminalCraftingJobStatus; +import org.cyclops.integratedterminals.core.terminalstorage.TerminalStorageTabIngredientComponentClient; +import org.cyclops.integratedterminals.core.terminalstorage.TerminalStorageTabIngredientComponentItemStackCrafting; +import org.cyclops.integratedterminals.core.terminalstorage.crafting.PendingCraftingJobOutput; +import org.cyclops.integratedterminals.core.terminalstorage.crafting.PendingCraftingJobOutputEntry; +import org.cyclops.integratedterminals.core.terminalstorage.crafting.PendingCraftingJobOutputs; +import org.cyclops.integratedterminals.inventory.container.ContainerTerminalStorageBase; + +import java.util.List; +import java.util.Map; + +/** + * Packet for sending the pending outputs of all running crafting jobs from server to client. + * + * This is used to indicate the ingredients that are being crafted in the storage terminal. + * + * @author rubensworks + * + */ +public class TerminalStorageIngredientCraftingJobsPacket extends PacketCodec { + + public static final Type ID = new Type<>(ResourceLocation.fromNamespaceAndPath(Reference.MOD_ID, "terminal_storage_ingredient_crafting_jobs")); + public static final StreamCodec CODEC = getCodec(TerminalStorageIngredientCraftingJobsPacket::new); + + @CodecField + private String tabId; + @CodecField + private CompoundTag data; + + public TerminalStorageIngredientCraftingJobsPacket() { + super(ID); + } + + public TerminalStorageIngredientCraftingJobsPacket(HolderLookup.Provider lookupProvider, String tabId, + PendingCraftingJobOutputs pendingCraftingJobOutputs) { + super(ID); + this.tabId = tabId; + this.data = new CompoundTag(); + + IIngredientMatcher matcher = pendingCraftingJobOutputs.getIngredientComponent().getMatcher(); + ListTag list = new ListTag(); + for (Int2ObjectMap.Entry>> channelEntry + : pendingCraftingJobOutputs.getChanneledOutputs().int2ObjectEntrySet()) { + for (PendingCraftingJobOutput output : channelEntry.getValue().values()) { + CompoundTag tag = new CompoundTag(); + tag.putInt("channel", channelEntry.getIntKey()); + tag.put("ingredient", IPrototypedIngredient.serialize(lookupProvider, + new PrototypedIngredient<>(pendingCraftingJobOutputs.getIngredientComponent(), + output.getInstance(), matcher.getExactMatchNoQuantityCondition()))); + tag.putInt("status", output.getStatus().ordinal()); + list.add(tag); + } + } + this.data.put("craftingJobOutputs", list); + } + + @Override + public boolean isAsync() { + return GeneralConfig.packetDeserializationEnableMultithreading; + } + + @Override + @OnlyIn(Dist.CLIENT) + public void actionClient(Level world, Player player) { + ListTag list = this.data.getList("craftingJobOutputs", Tag.TAG_COMPOUND); + List outputs = Lists.newArrayListWithExpectedSize(list.size()); + for (int i = 0; i < list.size(); i++) { + CompoundTag tag = list.getCompound(i); + outputs.add(new PendingCraftingJobOutputEntry( + tag.getInt("channel"), + IPrototypedIngredient.deserialize(world.registryAccess(), tag.getCompound("ingredient")), + TerminalCraftingJobStatus.values()[tag.getInt("status")])); + } + + // Run the following code in the render thread, since this packet runs in a different thread. (isAsync is true) + Minecraft.getInstance().execute(() -> { + if (player.containerMenu instanceof ContainerTerminalStorageBase container) { + TerminalStorageTabIngredientComponentClient tab = (TerminalStorageTabIngredientComponentClient) container.getTabClient(tabId); + if (tab != null) { + tab.setPendingCraftingJobOutputs(outputs); + } + + // Hard-coded crafting tab + // TODO: abstract this as "auxiliary" tabs + if (tabId.equals(IngredientComponents.ITEMSTACK.getName().toString())) { + TerminalStorageTabIngredientComponentClient tabCrafting = (TerminalStorageTabIngredientComponentClient) container + .getTabClient(TerminalStorageTabIngredientComponentItemStackCrafting.NAME.toString()); + if (tabCrafting != null) { + tabCrafting.setPendingCraftingJobOutputs(outputs); + } + } + } + }); + } + + @Override + public void actionServer(Level world, ServerPlayer player) { + + } + +} diff --git a/src/main/java/org/cyclops/integratedterminals/proxy/CommonProxy.java b/src/main/java/org/cyclops/integratedterminals/proxy/CommonProxy.java index a43729e7b..d654ed546 100644 --- a/src/main/java/org/cyclops/integratedterminals/proxy/CommonProxy.java +++ b/src/main/java/org/cyclops/integratedterminals/proxy/CommonProxy.java @@ -30,6 +30,7 @@ public void registerPacketHandlers(PacketHandler packetHandler) { packetHandler.register(TerminalStorageChangeGuiState.ID, TerminalStorageChangeGuiState.CODEC); packetHandler.register(TerminalStorageIngredientChangeEventPacket.ID, TerminalStorageIngredientChangeEventPacket.CODEC); packetHandler.register(TerminalStorageIngredientCraftingOptionsPacket.ID, TerminalStorageIngredientCraftingOptionsPacket.CODEC); + packetHandler.register(TerminalStorageIngredientCraftingJobsPacket.ID, TerminalStorageIngredientCraftingJobsPacket.CODEC); packetHandler.register(TerminalStorageIngredientMaxQuantityPacket.ID, TerminalStorageIngredientMaxQuantityPacket.CODEC); packetHandler.register(TerminalStorageIngredientSlotClickPacket.ID, TerminalStorageIngredientSlotClickPacket.CODEC); packetHandler.register(TerminalStorageIngredientOpenCraftingPlanGuiPacket.ID, TerminalStorageIngredientOpenCraftingPlanGuiPacket.CODEC); diff --git a/src/main/resources/assets/integratedterminals/lang/en_us.json b/src/main/resources/assets/integratedterminals/lang/en_us.json index b1acb6337..3b8c7f4ae 100644 --- a/src/main/resources/assets/integratedterminals/lang/en_us.json +++ b/src/main/resources/assets/integratedterminals/lang/en_us.json @@ -14,6 +14,7 @@ "gui.integratedterminals.terminal_storage.channel": "Chan:", "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.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", @@ -176,6 +177,7 @@ "info_book.integratedterminals.storage_terminal.autocrafting.text2": "If your network contains crafting recipes, then you will be able to start crafting jobs right from within your &lStorage Terminal&r.", "info_book.integratedterminals.storage_terminal.autocrafting.text3": "All available recipes will show up in their respective tabs and channels. Clicking on a recipe will open a gui to select the amount of times the recipe should be executed. After that, a crafting plan will be shown that lists all required or missing ingredients. After that, the job will be started. All running crafting jobs will be visible in the &lCrafting Job Terminal&r.", "info_book.integratedterminals.storage_terminal.autocrafting.text4": "Power users can shift-click on recipes in the storage terminal to skip the crafting amount gui, and go immediately to the crafting plan overview.", + "info_book.integratedterminals.storage_terminal.autocrafting.text5": "Ingredients that are being produced by a running crafting job are marked with a spinner in the &lStorage Terminal&r. The color of this spinner indicates the status of the crafting job, which is also shown when hovering over the ingredient, together with the quantity that is still being crafted.", "info_book.integratedterminals.storage_terminal.power_usage": "Power Users", "info_book.integratedterminals.storage_terminal.power_usage.text1": "Power users tend to dislike clicking too much as it takes too much time. The &lStorage Terminal&r offers a couple of &ohotkeys&r to speed up the usage of common tasks such as selecting the search field, browsing through tabs, clearing the crafting grid, and balancing crafting grid slots. These hotkeys are listed at the end of this section.", diff --git a/src/main/resources/assets/integratedterminals/textures/gui/icons.png b/src/main/resources/assets/integratedterminals/textures/gui/icons.png index 3ab58e8af6ad7f291c7bd320e826168d60f0c7d5..6ebd0a329047b8df02d3c87c4b8a4a0afedf19ad 100644 GIT binary patch literal 6248 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�I&4ycd{q+ek6W>) zo4XM|vwu=91Tv+;daXVE^9MWj!iz7L3SQ)wPEpj2)W4JTTZxXZ>co{uwjYe$O)DEd>r~antXZz3BTf-#r=6di5BrMH9o&l`np+{T6Z>6I_GkRFPEE$ zoOFwDSVH`3FXb|By~KL>I(*s5x39u6yoT_}J-PwLkxcseQSIxB3l;(t^~!HoO)PJY z^O4rT9nGBD2BN-5rIhE~9@y-!{WASpGHW~cZ1ZBr#rcH#q8B=50_^6?Efua(HL_m* z(-aGg7jD0}M^YjC@nt=q+o(bLrdeuLaLe@oy+S zn8Pfvb#^B&vRUOBqh3}qOf#i2KAX5XRT>N1WIr}L7JnWbkWnysW3p#*Vj{8i zZMM#C{K$QHi#4$MKbrSu z_7Hs|!jtmQ)Q>lQA7%e0XJQ;Tc6&pEriZhYkiCr*pq8LEeU;VylgCrle{t=Q9DB$? zEj{HM6Id}%I>PD84rhnCDrf9usAt~Iz**T_H5v5F509y>?|ek;)J!j3 zGW%$jQ7vubwvJerB_xg*^q{lQ8C8wwFNDj{KwfO(Jd^R?kBPBQ6 zvK!!W_~~#TdwDk4`v!in4?T&zu~pe@&}_4(xT!R$G8-|fU`3dj*q(Xjib3Y>4$o*D zl7xp15#smbXjLx6rN{NXpnkEDb4OlE>ZgJ?e(khaW$5A1^&BQb0imyo=`t+%Xmn$I za5!x^KXoTHvCZvi5q=%Baiw~?yQ7<<^|PO-@Poa3xXpIt43d4j5KC*Y_>}9}%`@lY z_xpFh7UL4HBqp8_6Q%8*I*1BPpYJ!h95jvf00_7S04fZC`0M=~*kWK@D z$s@t$llu8K^sAPtl8N8cuh~atmyjHoKl5WPCROtVU)Nh-$f5P<=^vr3=#;Qy90%?0 z<#pz-3$Ct@JveN4S89ijzTB`n*$mSXQiGE7KJSb0k%wPT6ne|OJMhdYK`e^mR99D5 zRU&h?HYzGAGLm?LQ!AKgF1mx-59X~v5aI$T0HA;j{2O>C5eW$k8zn?))=WPB+>o1= zrkTJSRXIik^;Cqu=G96n%L2S>ophNzKgdG?+dGi7)A-yQ`q~Cgz`|lsH^`u#7+;7t ztMf1}emYNQ>cY*LBg&tt9uOHS6uqJVk>Qc;_7d&8(e2HBT6AV1&QaFRq1a4&Ll}BzhX*A9{aqfKdQ3dwa%8c? zDS>QyUxB^7V6t3a{d_EOcC;##9y3g#X#7RUv;2l`yd#bgRk@||7>TVYZr*=9l|6Ba zRtFJszzXlBGr9Uw_SP2`qhEXLCQ-nIkGJQE+#r-K3_Py$N7hm%Npf9q&9w%T+>T} z{Jw2a>U#;5Fjm`#;{&5K71eSz6tTF7^rA)FC9vhtD};x8oY;Oesbfl@ROA*|pCgW7ssk z|LYGw#uv9`g%2vf0?=TNoS9LM+F6d#%Ak}KiL9G<=LjEjl{^0$P}|jD#W5ecgIRW> z-!6)ZKoPtoEVeH95nbr{qoWkJAWu(*I$wsrB!D9a`P{*tj(F^K&IVDoB3b>+p;;#X z6{jTsHsAdy#aMOl2>&2^r4fp~pDoW}ww>BUcOOWxe{ z0R^EDA?Q0wPa$7O89l0HEB3qX!%Jvp2(LQOkOeaM+>l>TlNF+CqzERiGG@SoTG&y~ zTX;ulm=D;}tk09((`EU0>XC%Lr$qx9hSmz|s-$D@R%_?tYCf4N4`RLr zud1nkMR@tqK(DK{dM&*=zIdi_NoJ69p#f^f2Ley7=i-mlxo9R`Fxt(bX7axt18vo!{ptrGLTy4fxIV|C_3oEl*D~7d&z5BM&pqT?4YEzIxVTNPdc*%W-zk0G3y9`m zoW|_eZl_-!M}JxhYOcd%?isH4`IGorszW9783cg8wyFc4OVBlNBM{}z#6 z@!2X$MK9Z^JGsk=?7|9fBQvoERTSVmg828mpIERE$1A3u!Zmwof#w;6mu!Z-E=-bP zZY%6h2`5Y32)0TCYX~)5ZaK^F@zbs#vvz7KU^*tYBjN=??B(U%oxTit&Caj5-~L%mXZ$A|@^MH`xwyo`XR&jnLxtNmLnCXQ2tszXaSQE; z<96`AtVrz;F>sZJGP6Z5&MbXb3^^@-7JS^;R}G_$Rr7Y|Pxz(z&|M+AKE2L4_gHGm zz0%D&H|TQu^pHeK!_X;P`Xqc%i29#m~%Jk?^>06Iy3XlXHZ#RDlQiQo17pkJwqs~mOCeekNjG}$@R zGHMJK{Bo=`x%X)!OM3cvpNxB@`t+VFqQ64Dr*hP9gOd^p#N|kY#8~#j3Wq&|#nwq- zYC-N(p1}we{PD0eGv7y9zgvHuB0?-JhxdLiW+$&^TSeu_D17>izCA356Vo54>?%c< zCL+zwDQh=9cE1+je#S6cYrrGgaxQ(E9#b^&)kdf-IYciN%Nko>T0HFSBPIPdM}1?_ z`@`&0md!@TO3Bnhab%;zx(h;A;Z;iJ@kUPfw!A+-FdT6JD}rjS4;PZEKh{kdptwZpf??218#uqT)bv%Wlx;)*0dv6nTmS$7 diff --git a/src/main/resources/data/integratedterminals/info/terminals_info.xml b/src/main/resources/data/integratedterminals/info/terminals_info.xml index 758988025..3e9f2b0bc 100644 --- a/src/main/resources/data/integratedterminals/info/terminals_info.xml +++ b/src/main/resources/data/integratedterminals/info/terminals_info.xml @@ -40,6 +40,7 @@ info_book.integratedterminals.storage_terminal.autocrafting.text2 info_book.integratedterminals.storage_terminal.autocrafting.text3 info_book.integratedterminals.storage_terminal.autocrafting.text4 + info_book.integratedterminals.storage_terminal.autocrafting.text5
From dfb7b4ecaeee581aad39130cc191846d1f394c44 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 28 Aug 2026 18:52:40 +0000 Subject: [PATCH 2/3] Keep indicating ingredients that are being crafted for the whole job Running crafting jobs report the FINISHED status in-between the batches that they hand to their crafting interface. As the storage terminal skipped the outputs of finished jobs, the spinner disappeared as soon as the job started producing, even though the job was still running. Such a job is not actually done: jobs that are done are not exposed as running jobs anymore. Their remaining outputs are now shown as actively being crafted. Ingredients that are both stored and craftable were showing the spinner twice. The crafting option slot now defers to the stored ingredient slot when the same instance is also shown as a stored ingredient. The collecting of pending crafting job outputs moved into PendingCraftingJobOutputs, so that it can be covered by a game test that runs an actual crafting job. This test needs a network with item storage, for which Integrated Tunnels was added as a dependency, just like Integrated Crafting does for its own game tests. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01FzSYcpDAVUfCpTPpjMmeoz --- build.gradle | 5 + gradle.properties | 1 + ...alStorageTabIngredientComponentClient.java | 18 +++ ...alStorageTabIngredientComponentServer.java | 34 +---- .../crafting/PendingCraftingJobOutputs.java | 54 ++++++++ ...alStorageSlotIngredientCraftingOption.java | 11 ++ .../GameTestCraftingJobIndication.java | 122 ++++++++++++++++++ .../integratedterminals/structure/empty10.nbt | Bin 0 -> 2471 bytes 8 files changed, 213 insertions(+), 32 deletions(-) create mode 100644 src/main/java/org/cyclops/integratedterminals/gametest/GameTestCraftingJobIndication.java create mode 100644 src/main/resources/data/integratedterminals/structure/empty10.nbt diff --git a/build.gradle b/build.gradle index 1e4bc7ac2..758c5b1b2 100644 --- a/build.gradle +++ b/build.gradle @@ -150,6 +150,11 @@ dependencies { } } + // Integrated Tunnels is only needed to set up networks with item storage in game tests. + implementation ("org.cyclops.integratedtunnels:integratedtunnels-${project.minecraft_version}-neoforge:${project.integratedtunnels_version}:deobf") { + transitive = false + } + // Add something like 'integratedterminalscompat_version_local=0.1.0-DEV' to your secrets.properties if you want to use a custom local Integrated Tunnels Compat version. if(secrets.integratedterminalscompat_version_local) { shadow("org.cyclops.integratedterminalscompat:integratedterminalscompat-${project.minecraft_version}-neoforge:${secrets.integratedterminalscompat_version_local}") { diff --git a/gradle.properties b/gradle.properties index 135d7157e..364f91d7e 100644 --- a/gradle.properties +++ b/gradle.properties @@ -35,4 +35,5 @@ cyclopscore_version=1.26.2-808 integrateddynamics_version=1.32.0-1630 integratedterminalscompat_version=1.0.0-167 integratedcrafting_version=1.4.1-442 +integratedtunnels_version=1.8.44-484 commoncapabilities_version=2.9.12-263 diff --git a/src/main/java/org/cyclops/integratedterminals/core/terminalstorage/TerminalStorageTabIngredientComponentClient.java b/src/main/java/org/cyclops/integratedterminals/core/terminalstorage/TerminalStorageTabIngredientComponentClient.java index 5ab3478ff..6b6816776 100644 --- a/src/main/java/org/cyclops/integratedterminals/core/terminalstorage/TerminalStorageTabIngredientComponentClient.java +++ b/src/main/java/org/cyclops/integratedterminals/core/terminalstorage/TerminalStorageTabIngredientComponentClient.java @@ -341,6 +341,24 @@ public PendingCraftingJobOutput getPendingCraftingJobOutput(int channel, T in return this.pendingCraftingJobOutputs.get(channel, instance); } + /** + * Check if the given instance is currently shown as a stored ingredient in the given channel. + * + * An instance can be shown twice: once as a stored ingredient, and once for each crafting option producing it. + * This allows crafting option slots to defer to the stored ingredient slot for things + * that apply to the instance as a whole, such as the indication of running crafting jobs. + * + * @param channel A channel id. + * @param instance An instance. + * @return If a stored ingredient slot is shown for the given instance. + */ + public boolean isShownAsStoredInstance(int channel, T instance) { + IIngredientMatcher matcher = this.ingredientComponent.getMatcher(); + return getInstanceFilterMetadata().test(new InstanceWithMetadata<>(instance, null)) + && getRawUnfilteredIngredientsView(channel) + .contains(instance, matcher.getExactMatchNoQuantityCondition()); + } + public List> createUnfilteredIngredientsView(int channel) { // Convert raw ingredients view to list List> enrichedIngredients = Lists.newArrayList(); diff --git a/src/main/java/org/cyclops/integratedterminals/core/terminalstorage/TerminalStorageTabIngredientComponentServer.java b/src/main/java/org/cyclops/integratedterminals/core/terminalstorage/TerminalStorageTabIngredientComponentServer.java index 859175274..1d88edac3 100644 --- a/src/main/java/org/cyclops/integratedterminals/core/terminalstorage/TerminalStorageTabIngredientComponentServer.java +++ b/src/main/java/org/cyclops/integratedterminals/core/terminalstorage/TerminalStorageTabIngredientComponentServer.java @@ -12,7 +12,6 @@ import net.neoforged.neoforge.server.ServerLifecycleHooks; import org.apache.commons.lang3.tuple.Pair; import org.cyclops.commoncapabilities.api.ingredient.IIngredientMatcher; -import org.cyclops.commoncapabilities.api.ingredient.IPrototypedIngredient; import org.cyclops.commoncapabilities.api.ingredient.IngredientComponent; import org.cyclops.commoncapabilities.api.ingredient.storage.IIngredientComponentStorage; import org.cyclops.cyclopscore.ingredient.collection.IIngredientCollapsedCollectionMutable; @@ -31,7 +30,6 @@ import org.cyclops.integrateddynamics.api.ingredient.IIngredientPositionsIndex; import org.cyclops.integrateddynamics.api.ingredient.capability.IIngredientComponentValueHandler; import org.cyclops.integrateddynamics.api.network.INetwork; -import org.cyclops.integrateddynamics.api.network.IPositionedAddonsNetwork; import org.cyclops.integrateddynamics.api.network.IPositionedAddonsNetworkIngredients; import org.cyclops.integrateddynamics.core.evaluate.variable.ValueHelpers; import org.cyclops.integrateddynamics.core.evaluate.variable.ValueTypeBoolean; @@ -45,9 +43,7 @@ import org.cyclops.integratedterminals.api.terminalstorage.ITerminalStorageTabServer; import org.cyclops.integratedterminals.api.terminalstorage.TerminalClickType; import org.cyclops.integratedterminals.api.terminalstorage.crafting.ITerminalCraftingOption; -import org.cyclops.integratedterminals.api.terminalstorage.crafting.ITerminalCraftingPlan; import org.cyclops.integratedterminals.api.terminalstorage.crafting.ITerminalStorageTabIngredientCraftingHandler; -import org.cyclops.integratedterminals.api.terminalstorage.crafting.TerminalCraftingJobStatus; import org.cyclops.integratedterminals.core.terminalstorage.crafting.HandlerWrappedTerminalCraftingOption; import org.cyclops.integratedterminals.core.terminalstorage.crafting.PendingCraftingJobOutputs; import org.cyclops.integratedterminals.core.terminalstorage.crafting.TerminalStorageTabIngredientCraftingHandlers; @@ -197,13 +193,8 @@ protected void updatePendingCraftingJobOutputs() { } this.nextCraftingJobsUpdate = System.currentTimeMillis() + GeneralConfig.guiTerminalCraftingJobsUpdateFrequency; - PendingCraftingJobOutputs pendingCraftingJobOutputs = new PendingCraftingJobOutputs<>(this.ingredientComponent); - for (ITerminalStorageTabIngredientCraftingHandler handler : TerminalStorageTabIngredientCraftingHandlers.REGISTRY.getHandlers()) { - Set handledPlans = Sets.newHashSet(); - for (ITerminalCraftingPlan craftingJob : handler.getCraftingJobs(this.network, IPositionedAddonsNetwork.WILDCARD_CHANNEL)) { - collectPendingCraftingJobOutputs(craftingJob, handledPlans, pendingCraftingJobOutputs); - } - } + PendingCraftingJobOutputs pendingCraftingJobOutputs = PendingCraftingJobOutputs + .collectFromNetwork(this.ingredientComponent, this.network); // Don't send anything as long as no crafting jobs are running, // but do send one final (empty) update once the last job has finished. @@ -218,27 +209,6 @@ protected void updatePendingCraftingJobOutputs() { this.getName().toString(), pendingCraftingJobOutputs), player); } - protected void collectPendingCraftingJobOutputs(ITerminalCraftingPlan craftingPlan, Set handledPlans, - PendingCraftingJobOutputs pendingCraftingJobOutputs) { - // Jobs can occur multiple times within a plan due to job splitting, so only take each of them into account once. - if ((!(craftingPlan.getId() instanceof Integer id) || id > 0) && !handledPlans.add(craftingPlan.getId())) { - return; - } - - // Finished jobs have already produced all their outputs, so nothing is pending for them anymore. - if (craftingPlan.getStatus() != TerminalCraftingJobStatus.FINISHED) { - for (IPrototypedIngredient output : craftingPlan.getOutputs()) { - if (output.getComponent() == this.ingredientComponent) { - pendingCraftingJobOutputs.add(craftingPlan.getChannel(), (T) output.getPrototype(), craftingPlan.getStatus()); - } - } - } - - for (ITerminalCraftingPlan dependency : craftingPlan.getDependencies()) { - collectPendingCraftingJobOutputs(dependency, handledPlans, pendingCraftingJobOutputs); - } - } - protected IIngredientCollapsedCollectionMutable getUnfilteredIngredientsView(int channel) { IIngredientCollapsedCollectionMutable ingredientsView = unfilteredIngredientsViews.get(channel); if (ingredientsView == null) { diff --git a/src/main/java/org/cyclops/integratedterminals/core/terminalstorage/crafting/PendingCraftingJobOutputs.java b/src/main/java/org/cyclops/integratedterminals/core/terminalstorage/crafting/PendingCraftingJobOutputs.java index 2d41a468c..84a7b3072 100644 --- a/src/main/java/org/cyclops/integratedterminals/core/terminalstorage/crafting/PendingCraftingJobOutputs.java +++ b/src/main/java/org/cyclops/integratedterminals/core/terminalstorage/crafting/PendingCraftingJobOutputs.java @@ -1,13 +1,20 @@ package org.cyclops.integratedterminals.core.terminalstorage.crafting; +import com.google.common.collect.Sets; import it.unimi.dsi.fastutil.ints.Int2ObjectMap; import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; import org.cyclops.commoncapabilities.api.ingredient.IIngredientMatcher; +import org.cyclops.commoncapabilities.api.ingredient.IPrototypedIngredient; import org.cyclops.commoncapabilities.api.ingredient.IngredientComponent; +import org.cyclops.integrateddynamics.api.network.INetwork; +import org.cyclops.integrateddynamics.api.network.IPositionedAddonsNetwork; +import org.cyclops.integratedterminals.api.terminalstorage.crafting.ITerminalCraftingPlan; +import org.cyclops.integratedterminals.api.terminalstorage.crafting.ITerminalStorageTabIngredientCraftingHandler; import org.cyclops.integratedterminals.api.terminalstorage.crafting.TerminalCraftingJobStatus; import javax.annotation.Nullable; import java.util.Map; +import java.util.Set; import java.util.TreeMap; /** @@ -25,6 +32,26 @@ public class PendingCraftingJobOutputs { private final IngredientComponent ingredientComponent; private final Int2ObjectMap>> channeledOutputs; + /** + * Collect the outputs that all running crafting jobs in the given network are still expected to produce. + * @param ingredientComponent The ingredient component to collect the outputs for. + * @param network A network. + * @param The instance type. + * @param The matching condition parameter. + * @return The pending crafting job outputs. + */ + public static PendingCraftingJobOutputs collectFromNetwork(IngredientComponent ingredientComponent, + INetwork network) { + PendingCraftingJobOutputs pendingCraftingJobOutputs = new PendingCraftingJobOutputs<>(ingredientComponent); + for (ITerminalStorageTabIngredientCraftingHandler handler : TerminalStorageTabIngredientCraftingHandlers.REGISTRY.getHandlers()) { + Set handledPlans = Sets.newHashSet(); + for (ITerminalCraftingPlan craftingJob : handler.getCraftingJobs(network, IPositionedAddonsNetwork.WILDCARD_CHANNEL)) { + pendingCraftingJobOutputs.addCraftingPlan(craftingJob, handledPlans); + } + } + return pendingCraftingJobOutputs; + } + public PendingCraftingJobOutputs(IngredientComponent ingredientComponent) { this.ingredientComponent = ingredientComponent; this.channeledOutputs = new Int2ObjectOpenHashMap<>(); @@ -65,6 +92,33 @@ public void add(int channel, T instance, TerminalCraftingJobStatus status) { outputs.put(key, new PendingCraftingJobOutput<>(instance, status)); } + /** + * Add all outputs that the given crafting plan and its dependencies are still expected to produce. + * @param craftingPlan A crafting plan. + * @param handledPlans The ids of the plans that were already taken into account. + */ + public void addCraftingPlan(ITerminalCraftingPlan craftingPlan, Set handledPlans) { + // Jobs can occur multiple times within a plan due to job splitting, so only take each of them into account once. + if ((!(craftingPlan.getId() instanceof Integer id) || id > 0) && !handledPlans.add(craftingPlan.getId())) { + return; + } + + // Jobs report FINISHED in-between the batches that they hand to their crafting interface, + // so this status does not mean that the job is done: a job that is done is not exposed as a running job anymore. + // As the outputs of such a job are still pending, we show them as actively being crafted. + TerminalCraftingJobStatus status = craftingPlan.getStatus() == TerminalCraftingJobStatus.FINISHED + ? TerminalCraftingJobStatus.CRAFTING : craftingPlan.getStatus(); + for (IPrototypedIngredient output : craftingPlan.getOutputs()) { + if (output.getComponent() == this.ingredientComponent) { + add(craftingPlan.getChannel(), (T) output.getPrototype(), status); + } + } + + for (ITerminalCraftingPlan dependency : craftingPlan.getDependencies()) { + addCraftingPlan(dependency, handledPlans); + } + } + /** * Get the pending output for the given instance, independent of the instance's quantity. * @param channel A channel id. 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 19ad10e5e..5f65d622d 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 @@ -76,6 +76,17 @@ protected List getTooltipLines(@Nullable PendingCraftingJobOutput return tooltipLines; } + @Nullable + @Override + @OnlyIn(Dist.CLIENT) + protected PendingCraftingJobOutput getPendingCraftingJobOutput(ITerminalStorageTabClient tab, int channel, + @Nullable String label) { + // The same instance can also be shown as a stored ingredient. + // In that case, only that slot indicates the running crafting jobs, to avoid indicating them twice. + return ((TerminalStorageTabIngredientComponentClient) tab).isShownAsStoredInstance(channel, getInstance()) + ? null : super.getPendingCraftingJobOutput(tab, channel, label); + } + public HandlerWrappedTerminalCraftingOption getCraftingOption() { return craftingOption; } diff --git a/src/main/java/org/cyclops/integratedterminals/gametest/GameTestCraftingJobIndication.java b/src/main/java/org/cyclops/integratedterminals/gametest/GameTestCraftingJobIndication.java new file mode 100644 index 000000000..de97ee03e --- /dev/null +++ b/src/main/java/org/cyclops/integratedterminals/gametest/GameTestCraftingJobIndication.java @@ -0,0 +1,122 @@ +package org.cyclops.integratedterminals.gametest; + +import net.minecraft.core.BlockPos; +import net.minecraft.gametest.framework.GameTest; +import net.minecraft.gametest.framework.GameTestHelper; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraft.world.item.crafting.RecipeType; +import net.minecraft.world.level.block.entity.ChestBlockEntity; +import net.neoforged.neoforge.gametest.GameTestHolder; +import net.neoforged.neoforge.gametest.PrefixGameTestTemplate; +import org.apache.commons.lang3.mutable.MutableInt; +import org.apache.commons.lang3.tuple.Triple; +import org.cyclops.commoncapabilities.IngredientComponents; +import org.cyclops.commoncapabilities.api.capability.itemhandler.ItemMatch; +import org.cyclops.integratedcrafting.api.crafting.CraftingJob; +import org.cyclops.integratedcrafting.core.CraftingHelpers; +import org.cyclops.integratedcrafting.gametest.GameTestHelpersIntegratedCrafting; +import org.cyclops.integratedcrafting.part.PartTypeInterfaceCrafting; +import org.cyclops.integrateddynamics.api.network.INetwork; +import org.cyclops.integrateddynamics.api.network.IPositionedAddonsNetworkIngredients; +import org.cyclops.integrateddynamics.core.helper.NetworkHelpers; +import org.cyclops.integratedterminals.Reference; +import org.cyclops.integratedterminals.core.terminalstorage.crafting.PendingCraftingJobOutputs; + +import java.util.Iterator; + +/** + * Game tests for the indication of running crafting jobs in the storage terminal. + * @author rubensworks + */ +@GameTestHolder(Reference.MOD_ID) +@PrefixGameTestTemplate(false) +public class GameTestCraftingJobIndication { + + public static final BlockPos POS = BlockPos.ZERO.offset(2, 0, 2); + public static final int CRAFT_AMOUNT = 4; + + /** + * Craft a batch of chests, and check on every tick that the crafted item is indicated as being crafted + * for as long as the crafting job is running. + */ + @GameTest(template = "empty10", templateNamespace = Reference.MOD_ID, timeoutTicks = 2000) + public void testCraftingIndicationWhileJobIsRunning(GameTestHelper helper) { + GameTestHelpersIntegratedCrafting.INetworkPositions positions = + GameTestHelpersIntegratedCrafting.createBasicNetwork(helper, POS); + + // Insert crafting inputs in the interface chest + ChestBlockEntity chest = helper.getBlockEntity(POS.east()); + chest.setItem(0, new ItemStack(Items.OAK_PLANKS, 64)); + + // Add the chest recipe to the crafting interface + positions.interfaceRecipeAdders().get(0).accept(Triple.of(0, RecipeType.CRAFTING, + ResourceLocation.fromNamespaceAndPath("minecraft", "chest"))); + + MutableInt ticksRunning = new MutableInt(); + MutableInt ticksIndicated = new MutableInt(); + MutableInt ticksMissingIndication = new MutableInt(); + + helper.startSequence() + .thenIdle(20) + .thenExecute(() -> { + INetwork network = getNetwork(helper); + helper.assertTrue(CraftingHelpers.calculateAndScheduleCraftingJob(network, + IPositionedAddonsNetworkIngredients.DEFAULT_CHANNEL, IngredientComponents.ITEMSTACK, + new ItemStack(Items.CHEST, CRAFT_AMOUNT), ItemMatch.ITEM, true, true, + CraftingHelpers.getGlobalCraftingJobIdentifier(), null) != null, + "Crafting job could not be scheduled"); + }) + .thenExecuteFor(400, () -> { + INetwork network = getNetwork(helper); + if (!hasPendingCraftingJob(network)) { + // Nothing is left to be crafted, so nothing has to be indicated + return; + } + + ticksRunning.increment(); + if (PendingCraftingJobOutputs + .collectFromNetwork(IngredientComponents.ITEMSTACK, network) + .get(IPositionedAddonsNetworkIngredients.DEFAULT_CHANNEL, new ItemStack(Items.CHEST)) != null) { + ticksIndicated.increment(); + } else { + ticksMissingIndication.increment(); + } + }) + .thenExecute(() -> { + helper.assertTrue(ticksRunning.intValue() > 0, "The crafting job never started running"); + helper.assertTrue(ticksIndicated.intValue() > 0, + "The crafted item was never indicated as being crafted during the " + + ticksRunning.intValue() + " ticks in which the crafting job was running"); + helper.assertTrue(ticksMissingIndication.intValue() == 0, + "The crafted item was not indicated as being crafted during " + + ticksMissingIndication.intValue() + " of the " + ticksRunning.intValue() + + " ticks in which the crafting job was running"); + GameTestHelpersIntegratedCrafting.chestContains(helper, chest, + new ItemStack(Items.CHEST, CRAFT_AMOUNT)); + }) + .thenSucceed(); + } + + /** + * @param network A network. + * @return If a crafting job with remaining outputs is present in the given network. + */ + private static boolean hasPendingCraftingJob(INetwork network) { + Iterator craftingJobs = CraftingHelpers.getCraftingNetworkChecked(network) + .getCraftingJobs(IPositionedAddonsNetworkIngredients.WILDCARD_CHANNEL); + while (craftingJobs.hasNext()) { + if (craftingJobs.next().getAmount() > 0) { + return true; + } + } + return false; + } + + private static INetwork getNetwork(GameTestHelper helper) { + return NetworkHelpers.getNetwork(helper.getLevel(), helper.absolutePos(POS), null) + .orElseThrow(() -> new IllegalStateException("Could not find a network")); + } + +} diff --git a/src/main/resources/data/integratedterminals/structure/empty10.nbt b/src/main/resources/data/integratedterminals/structure/empty10.nbt new file mode 100644 index 0000000000000000000000000000000000000000..11ea2c5de23a9a4f5a26add27ff4c5a124eaa1c2 GIT binary patch literal 2471 zcmb2|=3oGW|Gn4u_9=%-xL*9U|1QTzyCcHFlV(qx7m(}k-ns(p5=Hmx`7jGCIv~oA zi((iBGv{+=E21cU9gZ~tYlF!Gjqc36z*0mC$mBva6xIuo?1-XAa z*l&kG9tH_w@dl0ELQ;?t=avV2plGtT0LI9>2f+B*(G1iF5hOP5z!urxyWItfb6H@p zlmnys4lv$74~qEX2gRQ)DE_{K7mBmNjc@y8B|KRHnRRWyL&4=hMh{Nc}@ z#HA%dnVbw$4l08pt{)U}HlU36fddqqU_m^k2^kg*P-0LAMch1a#5I8;juRA{U_sRK zap?F6CDok=M;s`}ec%B_oG~ai!Ggq83fMjU^Ra!7E2tv*%Qf(s_yH>@!o)$b7zd8U zMo=t*1yKTGNG8@<|E=cli`{=muK)Js-FGW*{xAAl6z>% literal 0 HcmV?d00001 From 8cc3cec6e180dc97f166147b41d021bb24cd31b9 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 29 Aug 2026 13:12:05 +0000 Subject: [PATCH 3/3] Collect crafting job indications for the shown channel The storage terminal always shows a single channel, so the pending crafting job outputs are now collected for the selected channel instead of for all channels at once. This channel is propagated from the container into the server tab, and the client only applies the received outputs as long as that channel is being shown. The priority that decides which status is shown when multiple crafting jobs produce the same ingredient is now a field of TerminalCraftingJobStatus. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01FzSYcpDAVUfCpTPpjMmeoz --- .../ITerminalStorageTabServer.java | 3 +- .../crafting/TerminalCraftingJobStatus.java | 34 +++++++--- ...alStorageTabIngredientComponentClient.java | 21 +++---- ...alStorageTabIngredientComponentServer.java | 17 +++-- .../crafting/PendingCraftingJobOutput.java | 19 ------ .../PendingCraftingJobOutputEntry.java | 6 +- .../crafting/PendingCraftingJobOutputs.java | 63 +++++++++---------- .../GameTestCraftingJobIndication.java | 8 ++- .../GameTestPendingCraftingJobOutputs.java | 55 ++++++++-------- .../ContainerTerminalStorageBase.java | 2 +- ...alStorageIngredientCraftingJobsPacket.java | 28 ++++----- 11 files changed, 124 insertions(+), 132 deletions(-) diff --git a/src/main/java/org/cyclops/integratedterminals/api/terminalstorage/ITerminalStorageTabServer.java b/src/main/java/org/cyclops/integratedterminals/api/terminalstorage/ITerminalStorageTabServer.java index 4aaa076d1..3bedd8831 100644 --- a/src/main/java/org/cyclops/integratedterminals/api/terminalstorage/ITerminalStorageTabServer.java +++ b/src/main/java/org/cyclops/integratedterminals/api/terminalstorage/ITerminalStorageTabServer.java @@ -25,7 +25,8 @@ public interface ITerminalStorageTabServer { /** * Called on each tick this tab is active. + * @param channel The channel that is being shown in the terminal. */ - public void updateActive(); + public void updateActive(int channel); } diff --git a/src/main/java/org/cyclops/integratedterminals/api/terminalstorage/crafting/TerminalCraftingJobStatus.java b/src/main/java/org/cyclops/integratedterminals/api/terminalstorage/crafting/TerminalCraftingJobStatus.java index 5a4eab42c..aacef366c 100644 --- a/src/main/java/org/cyclops/integratedterminals/api/terminalstorage/crafting/TerminalCraftingJobStatus.java +++ b/src/main/java/org/cyclops/integratedterminals/api/terminalstorage/crafting/TerminalCraftingJobStatus.java @@ -10,49 +10,51 @@ public enum TerminalCraftingJobStatus { /** * A generic job error state. */ - ERROR(Helpers.RGBAToInt(250, 0, 0, 150), false), + ERROR(Helpers.RGBAToInt(250, 0, 0, 150), false, 5), /** * If this job, or its dependencies, have missing storage instances. */ - INVALID(Helpers.RGBAToInt(250, 10, 13, 150), false), + INVALID(Helpers.RGBAToInt(250, 10, 13, 150), false, 5), /** * No outputs have been crafted yet, and they are not scheduled yet for crafting. */ - UNSTARTED(Helpers.RGBAToInt(225, 225, 225, 150), true), + UNSTARTED(Helpers.RGBAToInt(225, 225, 225, 150), true, 1), /** * The crafting job has been scheduled, * but is not processing yet because other jobs are still processing. */ - QUEUEING(Helpers.RGBAToInt(243, 245, 150, 150), true), + QUEUEING(Helpers.RGBAToInt(243, 245, 150, 150), true, 2), /** * The crafting job has been scheduled, * but is not processing yet because a dependency is still being processed. */ - PENDING_DEPENDENCIES(Helpers.RGBAToInt(243, 245, 4, 150), true), + PENDING_DEPENDENCIES(Helpers.RGBAToInt(243, 245, 4, 150), true, 2), /** * The crafting job has been scheduled, * but is not processing yet because input ingredients are missing. */ - PENDING_INPUTS(Helpers.RGBAToInt(245, 172, 3, 150), true), + PENDING_INPUTS(Helpers.RGBAToInt(245, 172, 3, 150), true, 4), /** * The recipe inputs could not be inserted into the crafting handler. */ - INVALID_INPUTS(Helpers.RGBAToInt(250, 10, 13, 150), true), + INVALID_INPUTS(Helpers.RGBAToInt(250, 10, 13, 150), true, 5), /** * The output is actively being crafted. */ - CRAFTING(Helpers.RGBAToInt(43, 174, 231, 150), true), + CRAFTING(Helpers.RGBAToInt(43, 174, 231, 150), true, 3), /** * All expected outputs are crafted. */ - FINISHED(Helpers.RGBAToInt(43, 231, 47, 150), true); + FINISHED(Helpers.RGBAToInt(43, 231, 47, 150), true, 0); private final int color; private final boolean valid; + private final int priority; - private TerminalCraftingJobStatus(int color, boolean valid) { + private TerminalCraftingJobStatus(int color, boolean valid, int priority) { this.color = color; this.valid = valid; + this.priority = priority; } public int getColor() { @@ -62,4 +64,16 @@ public int getColor() { public boolean isValid() { return valid; } + + /** + * The relevance of this status when a single ingredient is produced by multiple crafting jobs, + * in which case only the status with the highest priority is shown. + * + * Statuses that require the attention of the player take precedence over statuses that don't. + * + * @return The priority, where a higher number indicates a higher priority. + */ + public int getPriority() { + return priority; + } } diff --git a/src/main/java/org/cyclops/integratedterminals/core/terminalstorage/TerminalStorageTabIngredientComponentClient.java b/src/main/java/org/cyclops/integratedterminals/core/terminalstorage/TerminalStorageTabIngredientComponentClient.java index 6b6816776..65842e472 100644 --- a/src/main/java/org/cyclops/integratedterminals/core/terminalstorage/TerminalStorageTabIngredientComponentClient.java +++ b/src/main/java/org/cyclops/integratedterminals/core/terminalstorage/TerminalStorageTabIngredientComponentClient.java @@ -156,7 +156,8 @@ public TerminalStorageTabIngredientComponentClient(ContainerTerminalStorageBase this.filteredIngredientsViews = new Int2ObjectOpenHashMap<>(); this.lastFilteredIngredientsViews = new Int2ObjectOpenHashMap<>(); this.craftingOptions = new Int2ObjectOpenHashMap<>(); - this.pendingCraftingJobOutputs = new PendingCraftingJobOutputs<>(this.ingredientComponent); + this.pendingCraftingJobOutputs = new PendingCraftingJobOutputs<>(this.ingredientComponent, + IPositionedAddonsNetwork.WILDCARD_CHANNEL); this.maxQuantities = new Int2LongOpenHashMap(); this.totalQuantities = new Int2LongOpenHashMap(); @@ -312,19 +313,14 @@ public Collection> getCraftingOptions(in /** * Called by the server when the outputs that running crafting jobs are still expected to produce have changed. + * @param channel The channel the outputs were collected for. * @param entries All pending crafting job outputs of all ingredient components. */ - public synchronized void setPendingCraftingJobOutputs(List entries) { - PendingCraftingJobOutputs pendingCraftingJobOutputs = new PendingCraftingJobOutputs<>(this.ingredientComponent); + public synchronized void setPendingCraftingJobOutputs(int channel, List entries) { + PendingCraftingJobOutputs pendingCraftingJobOutputs = new PendingCraftingJobOutputs<>(this.ingredientComponent, channel); for (PendingCraftingJobOutputEntry entry : entries) { if (entry.ingredient().getComponent() == this.ingredientComponent) { - T instance = (T) entry.ingredient().getPrototype(); - pendingCraftingJobOutputs.add(entry.channel(), instance, entry.status()); - - // Also aggregate into the wildcard channel, as that channel shows the contents of all channels. - if (entry.channel() != IPositionedAddonsNetwork.WILDCARD_CHANNEL) { - pendingCraftingJobOutputs.add(IPositionedAddonsNetwork.WILDCARD_CHANNEL, instance, entry.status()); - } + pendingCraftingJobOutputs.add((T) entry.ingredient().getPrototype(), entry.status()); } } this.pendingCraftingJobOutputs = pendingCraftingJobOutputs; @@ -338,7 +334,10 @@ public synchronized void setPendingCraftingJobOutputs(List getPendingCraftingJobOutput(int channel, T instance) { - return this.pendingCraftingJobOutputs.get(channel, instance); + // The outputs are collected for the channel that is shown in the terminal, + // so they don't apply anymore right after the shown channel has changed. + return this.pendingCraftingJobOutputs.getChannel() == channel + ? this.pendingCraftingJobOutputs.get(instance) : null; } /** diff --git a/src/main/java/org/cyclops/integratedterminals/core/terminalstorage/TerminalStorageTabIngredientComponentServer.java b/src/main/java/org/cyclops/integratedterminals/core/terminalstorage/TerminalStorageTabIngredientComponentServer.java index 1d88edac3..b47df5107 100644 --- a/src/main/java/org/cyclops/integratedterminals/core/terminalstorage/TerminalStorageTabIngredientComponentServer.java +++ b/src/main/java/org/cyclops/integratedterminals/core/terminalstorage/TerminalStorageTabIngredientComponentServer.java @@ -30,6 +30,7 @@ import org.cyclops.integrateddynamics.api.ingredient.IIngredientPositionsIndex; import org.cyclops.integrateddynamics.api.ingredient.capability.IIngredientComponentValueHandler; import org.cyclops.integrateddynamics.api.network.INetwork; +import org.cyclops.integrateddynamics.api.network.IPositionedAddonsNetwork; import org.cyclops.integrateddynamics.api.network.IPositionedAddonsNetworkIngredients; import org.cyclops.integrateddynamics.core.evaluate.variable.ValueHelpers; import org.cyclops.integrateddynamics.core.evaluate.variable.ValueTypeBoolean; @@ -88,6 +89,7 @@ public class TerminalStorageTabIngredientComponentServer implements ITermi private boolean initialized; // True if the first change event has been sent to the client. private boolean sentCraftingOptionsFiltered; private long nextCraftingJobsUpdate; + private int craftingJobsChannel; // The channel the last pending crafting job outputs were collected for. private boolean sentCraftingJobs; // True if a non-empty set of pending crafting job outputs was sent to the client. public TerminalStorageTabIngredientComponentServer(ResourceLocation name, INetwork network, @@ -107,6 +109,7 @@ public TerminalStorageTabIngredientComponentServer(ResourceLocation name, INetwo this.unfilteredIngredientsViews = new Int2ObjectOpenHashMap<>(); this.filteredDiffManagers = new Int2ObjectOpenHashMap<>(); this.nextCraftingJobsUpdate = 0; + this.craftingJobsChannel = IPositionedAddonsNetwork.WILDCARD_CHANNEL; this.sentCraftingJobs = false; // Schedule an observation on creation, as the channel may not have been indexed yet. @@ -175,9 +178,9 @@ public void deInit() { } @Override - public void updateActive() { + public void updateActive(int channel) { this.ingredientNetwork.scheduleObservation(); - updatePendingCraftingJobOutputs(); + updatePendingCraftingJobOutputs(channel); } /** @@ -186,15 +189,19 @@ public void updateActive() { * * As crafting job statuses change frequently, * this is throttled by {@link GeneralConfig#guiTerminalCraftingJobsUpdateFrequency}. + * + * @param channel The channel that is being shown in the terminal. */ - protected void updatePendingCraftingJobOutputs() { - if (System.currentTimeMillis() < this.nextCraftingJobsUpdate) { + protected void updatePendingCraftingJobOutputs(int channel) { + // Don't wait for the next update when the shown channel changed, as the client has no outputs for it yet. + if (channel == this.craftingJobsChannel && System.currentTimeMillis() < this.nextCraftingJobsUpdate) { return; } + this.craftingJobsChannel = channel; this.nextCraftingJobsUpdate = System.currentTimeMillis() + GeneralConfig.guiTerminalCraftingJobsUpdateFrequency; PendingCraftingJobOutputs pendingCraftingJobOutputs = PendingCraftingJobOutputs - .collectFromNetwork(this.ingredientComponent, this.network); + .collectFromNetwork(this.ingredientComponent, this.network, channel); // Don't send anything as long as no crafting jobs are running, // but do send one final (empty) update once the last job has finished. diff --git a/src/main/java/org/cyclops/integratedterminals/core/terminalstorage/crafting/PendingCraftingJobOutput.java b/src/main/java/org/cyclops/integratedterminals/core/terminalstorage/crafting/PendingCraftingJobOutput.java index 52dfc3f25..3339735a1 100644 --- a/src/main/java/org/cyclops/integratedterminals/core/terminalstorage/crafting/PendingCraftingJobOutput.java +++ b/src/main/java/org/cyclops/integratedterminals/core/terminalstorage/crafting/PendingCraftingJobOutput.java @@ -31,23 +31,4 @@ public TerminalCraftingJobStatus getStatus() { return status; } - /** - * Determine how relevant the given status is when multiple crafting jobs produce the same instance. - * - * Statuses that require the attention of the player take precedence over statuses that don't. - * - * @param status A crafting job status. - * @return The priority of the given status, where a higher number indicates a higher priority. - */ - public static int getStatusPriority(TerminalCraftingJobStatus status) { - return switch (status) { - case ERROR, INVALID, INVALID_INPUTS -> 5; - case PENDING_INPUTS -> 4; - case CRAFTING -> 3; - case PENDING_DEPENDENCIES, QUEUEING -> 2; - case UNSTARTED -> 1; - case FINISHED -> 0; - }; - } - } diff --git a/src/main/java/org/cyclops/integratedterminals/core/terminalstorage/crafting/PendingCraftingJobOutputEntry.java b/src/main/java/org/cyclops/integratedterminals/core/terminalstorage/crafting/PendingCraftingJobOutputEntry.java index d5aab28ae..dab0925c1 100644 --- a/src/main/java/org/cyclops/integratedterminals/core/terminalstorage/crafting/PendingCraftingJobOutputEntry.java +++ b/src/main/java/org/cyclops/integratedterminals/core/terminalstorage/crafting/PendingCraftingJobOutputEntry.java @@ -6,11 +6,9 @@ /** * A single component-agnostic pending crafting job output, as it is sent from server to client. * - * @param channel The channel the crafting job is running in. * @param ingredient The pending output, where the quantity indicates how much is still expected to be crafted. - * @param status The status of the crafting job that will produce the ingredient. + * @param status The status of the crafting jobs that will produce the ingredient. * @author rubensworks */ -public record PendingCraftingJobOutputEntry(int channel, IPrototypedIngredient ingredient, - TerminalCraftingJobStatus status) { +public record PendingCraftingJobOutputEntry(IPrototypedIngredient ingredient, TerminalCraftingJobStatus status) { } diff --git a/src/main/java/org/cyclops/integratedterminals/core/terminalstorage/crafting/PendingCraftingJobOutputs.java b/src/main/java/org/cyclops/integratedterminals/core/terminalstorage/crafting/PendingCraftingJobOutputs.java index 84a7b3072..c385c591c 100644 --- a/src/main/java/org/cyclops/integratedterminals/core/terminalstorage/crafting/PendingCraftingJobOutputs.java +++ b/src/main/java/org/cyclops/integratedterminals/core/terminalstorage/crafting/PendingCraftingJobOutputs.java @@ -1,24 +1,22 @@ package org.cyclops.integratedterminals.core.terminalstorage.crafting; import com.google.common.collect.Sets; -import it.unimi.dsi.fastutil.ints.Int2ObjectMap; -import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; import org.cyclops.commoncapabilities.api.ingredient.IIngredientMatcher; import org.cyclops.commoncapabilities.api.ingredient.IPrototypedIngredient; import org.cyclops.commoncapabilities.api.ingredient.IngredientComponent; import org.cyclops.integrateddynamics.api.network.INetwork; -import org.cyclops.integrateddynamics.api.network.IPositionedAddonsNetwork; import org.cyclops.integratedterminals.api.terminalstorage.crafting.ITerminalCraftingPlan; import org.cyclops.integratedterminals.api.terminalstorage.crafting.ITerminalStorageTabIngredientCraftingHandler; import org.cyclops.integratedterminals.api.terminalstorage.crafting.TerminalCraftingJobStatus; import javax.annotation.Nullable; +import java.util.Collection; import java.util.Map; import java.util.Set; import java.util.TreeMap; /** - * The pending outputs of all running crafting jobs of a single ingredient component, indexed by channel. + * The pending outputs of all running crafting jobs of a single ingredient component within a single channel. * * Instances are indexed independent of their quantity, * so that they can be looked up by the instances that are shown in the storage terminal. @@ -30,66 +28,72 @@ public class PendingCraftingJobOutputs { private final IngredientComponent ingredientComponent; - private final Int2ObjectMap>> channeledOutputs; + private final int channel; + private final Map> outputs; /** - * Collect the outputs that all running crafting jobs in the given network are still expected to produce. + * Collect the outputs that all running crafting jobs in the given channel are still expected to produce. * @param ingredientComponent The ingredient component to collect the outputs for. * @param network A network. + * @param channel The channel to collect the outputs for. * @param The instance type. * @param The matching condition parameter. * @return The pending crafting job outputs. */ public static PendingCraftingJobOutputs collectFromNetwork(IngredientComponent ingredientComponent, - INetwork network) { - PendingCraftingJobOutputs pendingCraftingJobOutputs = new PendingCraftingJobOutputs<>(ingredientComponent); + INetwork network, int channel) { + PendingCraftingJobOutputs pendingCraftingJobOutputs = new PendingCraftingJobOutputs<>(ingredientComponent, channel); for (ITerminalStorageTabIngredientCraftingHandler handler : TerminalStorageTabIngredientCraftingHandlers.REGISTRY.getHandlers()) { Set handledPlans = Sets.newHashSet(); - for (ITerminalCraftingPlan craftingJob : handler.getCraftingJobs(network, IPositionedAddonsNetwork.WILDCARD_CHANNEL)) { + for (ITerminalCraftingPlan craftingJob : handler.getCraftingJobs(network, channel)) { pendingCraftingJobOutputs.addCraftingPlan(craftingJob, handledPlans); } } return pendingCraftingJobOutputs; } - public PendingCraftingJobOutputs(IngredientComponent ingredientComponent) { + public PendingCraftingJobOutputs(IngredientComponent ingredientComponent, int channel) { this.ingredientComponent = ingredientComponent; - this.channeledOutputs = new Int2ObjectOpenHashMap<>(); + this.channel = channel; + this.outputs = new TreeMap<>(ingredientComponent.getMatcher()); } public IngredientComponent getIngredientComponent() { return ingredientComponent; } + /** + * @return The channel these outputs were collected for. + */ + public int getChannel() { + return channel; + } + /** * Add a pending crafting job output. * - * If the given instance is already pending in the given channel, + * If the given instance is already pending, * the quantities are summed, and the most relevant status is kept. * - * @param channel A channel id. * @param instance An instance, where the quantity indicates the pending quantity. * @param status The status of the crafting job that will produce the given instance. */ - public void add(int channel, T instance, TerminalCraftingJobStatus status) { + public void add(T instance, TerminalCraftingJobStatus status) { IIngredientMatcher matcher = this.ingredientComponent.getMatcher(); if (matcher.isEmpty(instance)) { return; } - Map> outputs = this.channeledOutputs - .computeIfAbsent(channel, (c) -> new TreeMap<>(matcher)); T key = matcher.withQuantity(instance, 1); - PendingCraftingJobOutput existingOutput = outputs.get(key); + PendingCraftingJobOutput existingOutput = this.outputs.get(key); if (existingOutput != null) { instance = matcher.withQuantity(instance, addQuantities(matcher, matcher.getQuantity(existingOutput.getInstance()), matcher.getQuantity(instance))); - if (PendingCraftingJobOutput.getStatusPriority(existingOutput.getStatus()) - >= PendingCraftingJobOutput.getStatusPriority(status)) { + if (existingOutput.getStatus().getPriority() >= status.getPriority()) { status = existingOutput.getStatus(); } } - outputs.put(key, new PendingCraftingJobOutput<>(instance, status)); + this.outputs.put(key, new PendingCraftingJobOutput<>(instance, status)); } /** @@ -110,7 +114,7 @@ public void addCraftingPlan(ITerminalCraftingPlan craftingPlan, Set h ? TerminalCraftingJobStatus.CRAFTING : craftingPlan.getStatus(); for (IPrototypedIngredient output : craftingPlan.getOutputs()) { if (output.getComponent() == this.ingredientComponent) { - add(craftingPlan.getChannel(), (T) output.getPrototype(), status); + add((T) output.getPrototype(), status); } } @@ -121,32 +125,27 @@ public void addCraftingPlan(ITerminalCraftingPlan craftingPlan, Set h /** * Get the pending output for the given instance, independent of the instance's quantity. - * @param channel A channel id. * @param instance An instance. * @return The pending output, or null if the given instance is not being crafted. */ @Nullable - public PendingCraftingJobOutput get(int channel, T instance) { - Map> outputs = this.channeledOutputs.get(channel); - if (outputs == null) { - return null; - } + public PendingCraftingJobOutput get(T instance) { IIngredientMatcher matcher = this.ingredientComponent.getMatcher(); - return matcher.isEmpty(instance) ? null : outputs.get(matcher.withQuantity(instance, 1)); + return matcher.isEmpty(instance) ? null : this.outputs.get(matcher.withQuantity(instance, 1)); } /** - * @return All pending outputs, indexed by channel. + * @return All pending outputs. */ - public Int2ObjectMap>> getChanneledOutputs() { - return channeledOutputs; + public Collection> getOutputs() { + return this.outputs.values(); } /** * @return If no crafting job outputs are pending. */ public boolean isEmpty() { - return this.channeledOutputs.isEmpty(); + return this.outputs.isEmpty(); } private static long addQuantities(IIngredientMatcher matcher, long quantity, long quantityToAdd) { diff --git a/src/main/java/org/cyclops/integratedterminals/gametest/GameTestCraftingJobIndication.java b/src/main/java/org/cyclops/integratedterminals/gametest/GameTestCraftingJobIndication.java index de97ee03e..7e2ab40cf 100644 --- a/src/main/java/org/cyclops/integratedterminals/gametest/GameTestCraftingJobIndication.java +++ b/src/main/java/org/cyclops/integratedterminals/gametest/GameTestCraftingJobIndication.java @@ -76,9 +76,11 @@ public void testCraftingIndicationWhileJobIsRunning(GameTestHelper helper) { } ticksRunning.increment(); - if (PendingCraftingJobOutputs - .collectFromNetwork(IngredientComponents.ITEMSTACK, network) - .get(IPositionedAddonsNetworkIngredients.DEFAULT_CHANNEL, new ItemStack(Items.CHEST)) != null) { + // Both when a single channel is shown, and when all channels are shown at once + if (PendingCraftingJobOutputs.collectFromNetwork(IngredientComponents.ITEMSTACK, network, + IPositionedAddonsNetworkIngredients.DEFAULT_CHANNEL).get(new ItemStack(Items.CHEST)) != null + && PendingCraftingJobOutputs.collectFromNetwork(IngredientComponents.ITEMSTACK, network, + IPositionedAddonsNetworkIngredients.WILDCARD_CHANNEL).get(new ItemStack(Items.CHEST)) != null) { ticksIndicated.increment(); } else { ticksMissingIndication.increment(); diff --git a/src/main/java/org/cyclops/integratedterminals/gametest/GameTestPendingCraftingJobOutputs.java b/src/main/java/org/cyclops/integratedterminals/gametest/GameTestPendingCraftingJobOutputs.java index 4aeb5a420..b603e4ee1 100644 --- a/src/main/java/org/cyclops/integratedterminals/gametest/GameTestPendingCraftingJobOutputs.java +++ b/src/main/java/org/cyclops/integratedterminals/gametest/GameTestPendingCraftingJobOutputs.java @@ -7,6 +7,7 @@ import net.neoforged.neoforge.gametest.GameTestHolder; import net.neoforged.neoforge.gametest.PrefixGameTestTemplate; import org.cyclops.commoncapabilities.IngredientComponents; +import org.cyclops.integrateddynamics.api.network.IPositionedAddonsNetwork; import org.cyclops.integratedterminals.Reference; import org.cyclops.integratedterminals.api.terminalstorage.crafting.TerminalCraftingJobStatus; import org.cyclops.integratedterminals.core.terminalstorage.crafting.PendingCraftingJobOutput; @@ -21,7 +22,8 @@ public class GameTestPendingCraftingJobOutputs { private static PendingCraftingJobOutputs createOutputs() { - return new PendingCraftingJobOutputs<>(IngredientComponents.ITEMSTACK); + return new PendingCraftingJobOutputs<>(IngredientComponents.ITEMSTACK, + IPositionedAddonsNetwork.WILDCARD_CHANNEL); } @GameTest(template = "empty", templateNamespace = "cyclopscore") @@ -29,7 +31,7 @@ public void testEmpty(GameTestHelper helper) { PendingCraftingJobOutputs outputs = createOutputs(); helper.assertTrue(outputs.isEmpty(), "No outputs should be pending"); - helper.assertTrue(outputs.get(0, new ItemStack(Items.STONE)) == null, + helper.assertTrue(outputs.get(new ItemStack(Items.STONE)) == null, "No output should be pending for stone"); helper.succeed(); @@ -38,18 +40,16 @@ public void testEmpty(GameTestHelper helper) { @GameTest(template = "empty", templateNamespace = "cyclopscore") public void testLookupIgnoresQuantity(GameTestHelper helper) { PendingCraftingJobOutputs outputs = createOutputs(); - outputs.add(0, new ItemStack(Items.STONE, 5), TerminalCraftingJobStatus.CRAFTING); + outputs.add(new ItemStack(Items.STONE, 5), TerminalCraftingJobStatus.CRAFTING); helper.assertTrue(!outputs.isEmpty(), "Outputs should be pending"); - PendingCraftingJobOutput output = outputs.get(0, new ItemStack(Items.STONE, 64)); + PendingCraftingJobOutput output = outputs.get(new ItemStack(Items.STONE, 64)); helper.assertTrue(output != null, "An output should be pending for stone of any quantity"); helper.assertTrue(output.getInstance().getCount() == 5, "5 stone should be pending"); helper.assertTrue(output.getStatus() == TerminalCraftingJobStatus.CRAFTING, "Stone should be crafting"); - helper.assertTrue(outputs.get(0, new ItemStack(Items.DIRT)) == null, + helper.assertTrue(outputs.get(new ItemStack(Items.DIRT)) == null, "No output should be pending for another item"); - helper.assertTrue(outputs.get(1, new ItemStack(Items.STONE)) == null, - "No output should be pending in another channel"); helper.succeed(); } @@ -57,10 +57,10 @@ public void testLookupIgnoresQuantity(GameTestHelper helper) { @GameTest(template = "empty", templateNamespace = "cyclopscore") public void testQuantitiesAreSummed(GameTestHelper helper) { PendingCraftingJobOutputs outputs = createOutputs(); - outputs.add(0, new ItemStack(Items.STONE, 5), TerminalCraftingJobStatus.CRAFTING); - outputs.add(0, new ItemStack(Items.STONE, 7), TerminalCraftingJobStatus.CRAFTING); + outputs.add(new ItemStack(Items.STONE, 5), TerminalCraftingJobStatus.CRAFTING); + outputs.add(new ItemStack(Items.STONE, 7), TerminalCraftingJobStatus.CRAFTING); - helper.assertTrue(outputs.get(0, new ItemStack(Items.STONE)).getInstance().getCount() == 12, + helper.assertTrue(outputs.get(new ItemStack(Items.STONE)).getInstance().getCount() == 12, "12 stone should be pending"); helper.succeed(); @@ -69,21 +69,21 @@ public void testQuantitiesAreSummed(GameTestHelper helper) { @GameTest(template = "empty", templateNamespace = "cyclopscore") public void testMostRelevantStatusIsKept(GameTestHelper helper) { PendingCraftingJobOutputs outputs = createOutputs(); - outputs.add(0, new ItemStack(Items.STONE), TerminalCraftingJobStatus.CRAFTING); - outputs.add(0, new ItemStack(Items.STONE), TerminalCraftingJobStatus.PENDING_INPUTS); - helper.assertTrue(outputs.get(0, new ItemStack(Items.STONE)).getStatus() == TerminalCraftingJobStatus.PENDING_INPUTS, + outputs.add(new ItemStack(Items.STONE), TerminalCraftingJobStatus.CRAFTING); + outputs.add(new ItemStack(Items.STONE), TerminalCraftingJobStatus.PENDING_INPUTS); + helper.assertTrue(outputs.get(new ItemStack(Items.STONE)).getStatus() == TerminalCraftingJobStatus.PENDING_INPUTS, "Missing inputs should take precedence over crafting"); PendingCraftingJobOutputs outputsReversed = createOutputs(); - outputsReversed.add(0, new ItemStack(Items.STONE), TerminalCraftingJobStatus.PENDING_INPUTS); - outputsReversed.add(0, new ItemStack(Items.STONE), TerminalCraftingJobStatus.CRAFTING); - helper.assertTrue(outputsReversed.get(0, new ItemStack(Items.STONE)).getStatus() == TerminalCraftingJobStatus.PENDING_INPUTS, + outputsReversed.add(new ItemStack(Items.STONE), TerminalCraftingJobStatus.PENDING_INPUTS); + outputsReversed.add(new ItemStack(Items.STONE), TerminalCraftingJobStatus.CRAFTING); + helper.assertTrue(outputsReversed.get(new ItemStack(Items.STONE)).getStatus() == TerminalCraftingJobStatus.PENDING_INPUTS, "Missing inputs should take precedence over crafting, independent of insertion order"); PendingCraftingJobOutputs outputsQueueing = createOutputs(); - outputsQueueing.add(0, new ItemStack(Items.STONE), TerminalCraftingJobStatus.QUEUEING); - outputsQueueing.add(0, new ItemStack(Items.STONE), TerminalCraftingJobStatus.CRAFTING); - helper.assertTrue(outputsQueueing.get(0, new ItemStack(Items.STONE)).getStatus() == TerminalCraftingJobStatus.CRAFTING, + outputsQueueing.add(new ItemStack(Items.STONE), TerminalCraftingJobStatus.QUEUEING); + outputsQueueing.add(new ItemStack(Items.STONE), TerminalCraftingJobStatus.CRAFTING); + helper.assertTrue(outputsQueueing.get(new ItemStack(Items.STONE)).getStatus() == TerminalCraftingJobStatus.CRAFTING, "Crafting should take precedence over queueing"); helper.succeed(); @@ -92,7 +92,7 @@ public void testMostRelevantStatusIsKept(GameTestHelper helper) { @GameTest(template = "empty", templateNamespace = "cyclopscore") public void testEmptyInstancesAreIgnored(GameTestHelper helper) { PendingCraftingJobOutputs outputs = createOutputs(); - outputs.add(0, ItemStack.EMPTY, TerminalCraftingJobStatus.CRAFTING); + outputs.add(ItemStack.EMPTY, TerminalCraftingJobStatus.CRAFTING); helper.assertTrue(outputs.isEmpty(), "Empty instances should not be pending"); @@ -100,16 +100,11 @@ public void testEmptyInstancesAreIgnored(GameTestHelper helper) { } @GameTest(template = "empty", templateNamespace = "cyclopscore") - public void testMultipleChannels(GameTestHelper helper) { - PendingCraftingJobOutputs outputs = createOutputs(); - outputs.add(0, new ItemStack(Items.STONE, 5), TerminalCraftingJobStatus.CRAFTING); - outputs.add(1, new ItemStack(Items.STONE, 3), TerminalCraftingJobStatus.QUEUEING); - - helper.assertTrue(outputs.getChanneledOutputs().size() == 2, "Two channels should have pending outputs"); - helper.assertTrue(outputs.get(0, new ItemStack(Items.STONE)).getInstance().getCount() == 5, - "5 stone should be pending in channel 0"); - helper.assertTrue(outputs.get(1, new ItemStack(Items.STONE)).getInstance().getCount() == 3, - "3 stone should be pending in channel 1"); + public void testChannelIsRemembered(GameTestHelper helper) { + helper.assertValueEqual(createOutputs().getChannel(), IPositionedAddonsNetwork.WILDCARD_CHANNEL, + "Channel of the wildcard outputs"); + helper.assertValueEqual(new PendingCraftingJobOutputs<>(IngredientComponents.ITEMSTACK, 3).getChannel(), 3, + "Channel of the channeled outputs"); helper.succeed(); } diff --git a/src/main/java/org/cyclops/integratedterminals/inventory/container/ContainerTerminalStorageBase.java b/src/main/java/org/cyclops/integratedterminals/inventory/container/ContainerTerminalStorageBase.java index 1ce32ea2f..3fb2f1610 100644 --- a/src/main/java/org/cyclops/integratedterminals/inventory/container/ContainerTerminalStorageBase.java +++ b/src/main/java/org/cyclops/integratedterminals/inventory/container/ContainerTerminalStorageBase.java @@ -241,7 +241,7 @@ public void broadcastChanges() { // Update active server tab ITerminalStorageTabServer activeServerTab = getTabServer(getSelectedTab()); if (activeServerTab != null) { - activeServerTab.updateActive(); + activeServerTab.updateActive(getSelectedChannel()); } } diff --git a/src/main/java/org/cyclops/integratedterminals/network/packet/TerminalStorageIngredientCraftingJobsPacket.java b/src/main/java/org/cyclops/integratedterminals/network/packet/TerminalStorageIngredientCraftingJobsPacket.java index 263624cee..137107e25 100644 --- a/src/main/java/org/cyclops/integratedterminals/network/packet/TerminalStorageIngredientCraftingJobsPacket.java +++ b/src/main/java/org/cyclops/integratedterminals/network/packet/TerminalStorageIngredientCraftingJobsPacket.java @@ -1,7 +1,6 @@ package org.cyclops.integratedterminals.network.packet; import com.google.common.collect.Lists; -import it.unimi.dsi.fastutil.ints.Int2ObjectMap; import net.minecraft.client.Minecraft; import net.minecraft.core.HolderLookup; import net.minecraft.nbt.CompoundTag; @@ -32,7 +31,6 @@ import org.cyclops.integratedterminals.inventory.container.ContainerTerminalStorageBase; import java.util.List; -import java.util.Map; /** * Packet for sending the pending outputs of all running crafting jobs from server to client. @@ -50,6 +48,8 @@ public class TerminalStorageIngredientCraftingJobsPacket extends PacketCodec TerminalStorageIngredientCraftingJobsPacket(HolderLookup.Provider PendingCraftingJobOutputs pendingCraftingJobOutputs) { super(ID); this.tabId = tabId; + this.channel = pendingCraftingJobOutputs.getChannel(); this.data = new CompoundTag(); IIngredientMatcher matcher = pendingCraftingJobOutputs.getIngredientComponent().getMatcher(); ListTag list = new ListTag(); - for (Int2ObjectMap.Entry>> channelEntry - : pendingCraftingJobOutputs.getChanneledOutputs().int2ObjectEntrySet()) { - for (PendingCraftingJobOutput output : channelEntry.getValue().values()) { - CompoundTag tag = new CompoundTag(); - tag.putInt("channel", channelEntry.getIntKey()); - tag.put("ingredient", IPrototypedIngredient.serialize(lookupProvider, - new PrototypedIngredient<>(pendingCraftingJobOutputs.getIngredientComponent(), - output.getInstance(), matcher.getExactMatchNoQuantityCondition()))); - tag.putInt("status", output.getStatus().ordinal()); - list.add(tag); - } + for (PendingCraftingJobOutput output : pendingCraftingJobOutputs.getOutputs()) { + CompoundTag tag = new CompoundTag(); + tag.put("ingredient", IPrototypedIngredient.serialize(lookupProvider, + new PrototypedIngredient<>(pendingCraftingJobOutputs.getIngredientComponent(), + output.getInstance(), matcher.getExactMatchNoQuantityCondition()))); + tag.putInt("status", output.getStatus().ordinal()); + list.add(tag); } this.data.put("craftingJobOutputs", list); } @@ -92,7 +89,6 @@ public void actionClient(Level world, Player player) { for (int i = 0; i < list.size(); i++) { CompoundTag tag = list.getCompound(i); outputs.add(new PendingCraftingJobOutputEntry( - tag.getInt("channel"), IPrototypedIngredient.deserialize(world.registryAccess(), tag.getCompound("ingredient")), TerminalCraftingJobStatus.values()[tag.getInt("status")])); } @@ -102,7 +98,7 @@ public void actionClient(Level world, Player player) { if (player.containerMenu instanceof ContainerTerminalStorageBase container) { TerminalStorageTabIngredientComponentClient tab = (TerminalStorageTabIngredientComponentClient) container.getTabClient(tabId); if (tab != null) { - tab.setPendingCraftingJobOutputs(outputs); + tab.setPendingCraftingJobOutputs(channel, outputs); } // Hard-coded crafting tab @@ -111,7 +107,7 @@ public void actionClient(Level world, Player player) { TerminalStorageTabIngredientComponentClient tabCrafting = (TerminalStorageTabIngredientComponentClient) container .getTabClient(TerminalStorageTabIngredientComponentItemStackCrafting.NAME.toString()); if (tabCrafting != null) { - tabCrafting.setPendingCraftingJobOutputs(outputs); + tabCrafting.setPendingCraftingJobOutputs(channel, outputs); } } }