diff --git a/gradle.properties b/gradle.properties index fa8a05232..16071f518 100644 --- a/gradle.properties +++ b/gradle.properties @@ -34,6 +34,6 @@ org.gradle.caching=true cyclopscore_version=1.30.0-1139 integrateddynamics_version=1.37.1-2122 integratedterminalscompat_version=1.0.0-209 -integratedcrafting_version=1.7.1-765 +integratedcrafting_version=1.7.2-852 integratedtunnels_version=1.12.0-772 commoncapabilities_version=2.11.6-376 diff --git a/src/main/java/org/cyclops/integratedterminals/GeneralConfig.java b/src/main/java/org/cyclops/integratedterminals/GeneralConfig.java index d670d5e96..d5efe17b4 100644 --- a/src/main/java/org/cyclops/integratedterminals/GeneralConfig.java +++ b/src/main/java/org/cyclops/integratedterminals/GeneralConfig.java @@ -23,8 +23,10 @@ public class GeneralConfig extends DummyConfig { public static int terminalStoragePacketMaxInstances = 512; @ConfigurableProperty(category = "core", comment = "The maximum number of terminal storage crafting recipes that can be sent in a single packet. Reduce this when you have packet overflows.", isCommandable = true, configLocation = ModConfig.Type.SERVER) public static int terminalStoragePacketMaxRecipes = 128; - @ConfigurableProperty(category = "core", comment = "If crafting plans should default to the tree-based view. If false, it will default to the flattened view.", isCommandable = true, configLocation = ModConfig.Type.COMMON) - public static boolean terminalStorageDefaultToCraftingPlanTree = false; + @ConfigurableProperty(category = "core", comment = "If crafting plans should default to the graph-based view. If false, it will default to the flattened view.", isCommandable = true, configLocation = ModConfig.Type.COMMON) + public static boolean terminalStorageDefaultToCraftingPlanGraph = true; + @ConfigurableProperty(category = "core", comment = "If crafting plans can also be shown as a tree list, next to the graph and compact views.", isCommandable = true, configLocation = ModConfig.Type.CLIENT) + public static boolean terminalStorageCraftingPlanTreeListEnabled = false; @ConfigurableProperty(category = "core", comment = "The limit for the number of leaves in a tree-based crafting plan after which it won't be sent to the client anymore.", isCommandable = true, configLocation = ModConfig.Type.SERVER) public static int terminalStorageMaxTreePlanSize = 64; @ConfigurableProperty(category = "core", comment = "If storage terminals can be ender-upgraded with an eye of ender to gain a tab with the player's ender chest contents.", isCommandable = true, configLocation = ModConfig.Type.SERVER) 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 e4f8f6dac..55ad0869b 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 @@ -35,9 +35,10 @@ public enum TerminalCraftingJobStatus { */ PENDING_INPUTS(Helpers.RGBAToInt(245, 172, 3, 150), true, 4), /** - * The recipe inputs could not be inserted into the crafting handler. + * The recipe inputs could not be inserted into the crafting handler, for example because it is busy. + * They will be inserted again later. */ - INVALID_INPUTS(Helpers.RGBAToInt(250, 10, 13, 150), true, 5), + INVALID_INPUTS(Helpers.RGBAToInt(245, 120, 3, 150), true, 5), /** * The output is actively being crafted. */ diff --git a/src/main/java/org/cyclops/integratedterminals/api/terminalstorage/crafting/TerminalCraftingPlanStatic.java b/src/main/java/org/cyclops/integratedterminals/api/terminalstorage/crafting/TerminalCraftingPlanStatic.java index b3733480d..c771922ae 100644 --- a/src/main/java/org/cyclops/integratedterminals/api/terminalstorage/crafting/TerminalCraftingPlanStatic.java +++ b/src/main/java/org/cyclops/integratedterminals/api/terminalstorage/crafting/TerminalCraftingPlanStatic.java @@ -315,22 +315,24 @@ protected static void groupDependenciesByPrototype(IndexedEntries indexedEnt long quantity = IndexedEntries.getQuantity(outputs); if (plan.getStatus() == TerminalCraftingJobStatus.ERROR - || plan.getStatus() == TerminalCraftingJobStatus.INVALID - || plan.getStatus() == TerminalCraftingJobStatus.INVALID_INPUTS) { + || plan.getStatus() == TerminalCraftingJobStatus.INVALID) { if (plan.getDependencies().isEmpty()) { entry.setQuantityMissing(entry.getQuantityMissing() + quantity); } else { entry.setQuantityToCraft(entry.getQuantityToCraft() + quantity); } } + // Inputs that the target refused are inserted again later if (plan.getStatus() == TerminalCraftingJobStatus.QUEUEING || plan.getStatus() == TerminalCraftingJobStatus.PENDING_DEPENDENCIES || plan.getStatus() == TerminalCraftingJobStatus.PENDING_INPUTS + || plan.getStatus() == TerminalCraftingJobStatus.INVALID_INPUTS || plan.getStatus() == TerminalCraftingJobStatus.UNSTARTED) { entry.setQuantityToCraft(entry.getQuantityToCraft() + quantity); } + // Finished jobs with nothing left to craft are done, not crafting if (plan.getStatus() == TerminalCraftingJobStatus.CRAFTING || - plan.getStatus() == TerminalCraftingJobStatus.FINISHED) { + (plan.getStatus() == TerminalCraftingJobStatus.FINISHED && plan.getCraftingQuantity() > 0)) { entry.setQuantityCrafting(entry.getQuantityCrafting() + quantity); } } @@ -559,6 +561,36 @@ public static TerminalCraftingPlanStatic deserialize(HolderLookup.Provide return plan; } + /** + * @param craftingPlan A crafting plan. + * @param The plan identifier type. + * @return A copy of the plan in which the plan and all of its dependencies are finished, + * with nothing left to craft and no ingredients left in their buffers. + */ + public static TerminalCraftingPlanStatic asCompleted(ITerminalCraftingPlan craftingPlan) { + return asCompleted(craftingPlan, Collections.emptyMap()); + } + + /** + * @param craftingPlan A running crafting plan. + * @param knownOutputs Outputs per job id, used instead of the remaining outputs, which are empty once crafted. + * @param The id type. + * @return A copy of the plan in which everything has been crafted. + */ + public static TerminalCraftingPlanStatic asCompleted(ITerminalCraftingPlan craftingPlan, + Map>> knownOutputs) { + List> dependencies = Lists.newArrayList(); + for (ITerminalCraftingPlan dependency : craftingPlan.getDependencies()) { + dependencies.add(asCompleted(dependency, knownOutputs)); + } + return new TerminalCraftingPlanStatic<>(craftingPlan.getId(), dependencies, + knownOutputs.getOrDefault(craftingPlan.getId(), craftingPlan.getOutputs()), + TerminalCraftingJobStatus.FINISHED, 0, + Math.max(craftingPlan.getCraftingQuantityTotal(), craftingPlan.getCraftingQuantity()), + Collections.emptyList(), Collections.emptyList(), Label.RUNNING, craftingPlan.getTickDuration(), + craftingPlan.getEstimatedTickDurationTotal(), 0, craftingPlan.getChannel(), craftingPlan.getInitiatorName()); + } + public static enum Label { RUNNING("gui.integratedterminals.terminal_storage.craftingplan.label.running"), VALID("gui.integratedterminals.terminal_storage.craftingplan.label.valid"), diff --git a/src/main/java/org/cyclops/integratedterminals/client/gui/container/ContainerScreenTerminalCraftingJobs.java b/src/main/java/org/cyclops/integratedterminals/client/gui/container/ContainerScreenTerminalCraftingJobs.java index d810f1890..c20d091af 100644 --- a/src/main/java/org/cyclops/integratedterminals/client/gui/container/ContainerScreenTerminalCraftingJobs.java +++ b/src/main/java/org/cyclops/integratedterminals/client/gui/container/ContainerScreenTerminalCraftingJobs.java @@ -17,7 +17,6 @@ import org.cyclops.cyclopscore.client.gui.component.button.ButtonText; import org.cyclops.cyclopscore.client.gui.container.ContainerScreenExtended; import org.cyclops.cyclopscore.helper.GuiHelpers; -import org.cyclops.cyclopscore.helper.Helpers; import org.cyclops.cyclopscore.helper.L10NHelpers; import org.cyclops.cyclopscore.helper.RenderHelpers; import org.cyclops.integrateddynamics.api.part.PartPos; @@ -25,8 +24,8 @@ import org.cyclops.integratedterminals.IntegratedTerminals; import org.cyclops.integratedterminals.Reference; import org.cyclops.integratedterminals.api.terminalstorage.crafting.ITerminalCraftingPlanFlat; -import org.cyclops.integratedterminals.api.terminalstorage.crafting.TerminalCraftingJobStatus; import org.cyclops.integratedterminals.client.gui.container.component.GuiCraftingPlan; +import org.cyclops.integratedterminals.client.gui.container.component.GuiCraftingPlanProgressBar; import org.cyclops.integratedterminals.core.client.gui.CraftingJobGuiData; import org.cyclops.integratedterminals.core.terminalstorage.crafting.HandlerWrappedTerminalCraftingPlan; import org.cyclops.integratedterminals.inventory.container.ContainerTerminalCraftingJobs; @@ -52,11 +51,6 @@ public class ContainerScreenTerminalCraftingJobs extends ContainerScreenExtended private static final int COLUMN_INFO = 132; private static final int COLUMN_INFO_SPACING = 6; private static final int PROGRESS_BAR_OFFSET_Y = 8; - private static final int PROGRESS_BAR_HEIGHT = 8; - - private static final int BORDER_COLOR = Helpers.RGBAToInt(0, 0, 0, 180); - private static final int TRACK_COLOR = Helpers.RGBAToInt(0, 0, 0, 100); - private static final int FILL_COLOR = TerminalCraftingJobStatus.CRAFTING.getColor() | 0xFF000000; private final Player player; @@ -196,44 +190,15 @@ protected boolean isHoveringOutputs(HandlerWrappedTerminalCraftingPlan plan, dou && mouseX < OUTPUT_SLOT_X + 4 + GuiHelpers.SLOT_SIZE_INNER * outputs; } - /** - * Draw how far a job has come as a bar, with the time it has been running on the left, - * the completed percentage in the middle, and the estimated time until it is done on the right. - */ protected void drawProgressBar(GuiGraphics guiGraphics, ITerminalCraftingPlanFlat plan, int x, int y, int width) { - int progress = GuiCraftingPlan.getProgress(plan); - int filled = progress > 0 ? width * progress / 100 : 0; - - // A sunken track, so that the numbers inside the bar stay readable over both the track and the fill - guiGraphics.fill(x, y, x + width, y + PROGRESS_BAR_HEIGHT, BORDER_COLOR); - guiGraphics.fill(x + 1, y + 1, x + width - 1, y + PROGRESS_BAR_HEIGHT - 1, TRACK_COLOR); - if (filled > 1) { - guiGraphics.fill(x + 1, y + 1, x + filled - 1, y + PROGRESS_BAR_HEIGHT - 1, FILL_COLOR); - } - - long tickDuration = plan.getTickDuration(); - if (tickDuration >= 0) { - drawProgressBarString(guiGraphics, GuiCraftingPlan.getDurationValue(tickDuration), x + 2, y); - } - if (progress >= 0) { - String progressString = L10NHelpers.localize("gui.integratedterminals.terminal_crafting_job.craftingplan.progress.short", progress); - drawProgressBarString(guiGraphics, progressString, x + (width - scaledWidth(progressString)) / 2, y); - } - if (plan.getStatus().isValid()) { - String remainingString = GuiCraftingPlan.getDurationValue(plan.getEstimatedTickDurationRemaining()); - drawProgressBarString(guiGraphics, remainingString, x + width - 2 - scaledWidth(remainingString), y); - } + GuiCraftingPlanProgressBar.draw(guiGraphics, GuiCraftingPlan.getProgress(plan), plan.getTickDuration(), + plan.getEstimatedTickDurationRemaining(), plan.getStatus().isValid(), x, y, width); } protected int scaledWidth(String string) { return font.width(string) / 2; } - protected void drawProgressBarString(GuiGraphics guiGraphics, String string, int x, int y) { - RenderHelpers.drawScaledString(guiGraphics.pose(), guiGraphics.bufferSource(), font, string, - x, y + 2, 0.5f, 16777215, true, Font.DisplayMode.NORMAL); - } - protected List getPlanTooltipLines(ITerminalCraftingPlanFlat plan) { List lines = Lists.newArrayList(); diff --git a/src/main/java/org/cyclops/integratedterminals/client/gui/container/ContainerScreenTerminalCraftingJobsPlan.java b/src/main/java/org/cyclops/integratedterminals/client/gui/container/ContainerScreenTerminalCraftingJobsPlan.java index b56f66cc6..4ea350325 100644 --- a/src/main/java/org/cyclops/integratedterminals/client/gui/container/ContainerScreenTerminalCraftingJobsPlan.java +++ b/src/main/java/org/cyclops/integratedterminals/client/gui/container/ContainerScreenTerminalCraftingJobsPlan.java @@ -1,37 +1,44 @@ package org.cyclops.integratedterminals.client.gui.container; -import net.minecraft.ChatFormatting; +import com.google.common.collect.Maps; import net.minecraft.client.gui.GuiGraphics; import net.minecraft.nbt.CompoundTag; import net.minecraft.network.chat.Component; -import net.minecraft.resources.ResourceLocation; import net.minecraft.world.entity.player.Inventory; -import net.minecraft.world.entity.player.Player; +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.client.gui.component.button.ButtonText; -import org.cyclops.cyclopscore.client.gui.container.ContainerScreenExtended; -import org.cyclops.cyclopscore.helper.L10NHelpers; import org.cyclops.integrateddynamics.api.part.PartPos; import org.cyclops.integratedterminals.IntegratedTerminals; -import org.cyclops.integratedterminals.Reference; +import org.cyclops.integratedterminals.api.terminalstorage.crafting.ITerminalCraftingPlan; +import org.cyclops.integratedterminals.api.terminalstorage.crafting.ITerminalCraftingPlanFlat; +import org.cyclops.integratedterminals.api.terminalstorage.crafting.TerminalCraftingPlanStatic; import org.cyclops.integratedterminals.client.gui.container.component.GuiCraftingPlan; import org.cyclops.integratedterminals.client.gui.container.component.GuiCraftingPlanFlat; -import org.cyclops.integratedterminals.client.gui.container.component.GuiCraftingPlanToggler; +import org.cyclops.integratedterminals.client.gui.container.component.GuiCraftingPlanGraph; +import org.cyclops.integratedterminals.client.gui.container.component.GuiCraftingPlanProgressBar; +import org.cyclops.integratedterminals.client.gui.container.component.IGuiCraftingPlanView; import org.cyclops.integratedterminals.inventory.container.ContainerTerminalCraftingJobsPlan; import org.cyclops.integratedterminals.network.packet.CancelCraftingJobPacket; import org.cyclops.integratedterminals.network.packet.OpenCraftingJobsGuiPacket; -import org.lwjgl.glfw.GLFW; import javax.annotation.Nullable; +import java.util.List; +import java.util.Map; /** * A gui for visualizing a live crafting plan. * @author rubensworks */ -public class ContainerScreenTerminalCraftingJobsPlan extends ContainerScreenExtended { +public class ContainerScreenTerminalCraftingJobsPlan extends ContainerScreenTerminalCraftingPlanBase { - private final Player player; - private GuiCraftingPlanToggler guiCraftingPlanToggler; + private static final int PROGRESS_BAR_SPACING = 6; + @Nullable + private ButtonText cancelButton; + + // The last view of each type is kept, so that its visualization state can be inherited after an update @Nullable private GuiCraftingPlan guiCraftingPlan; @Nullable @@ -40,92 +47,62 @@ public class ContainerScreenTerminalCraftingJobsPlan extends ContainerScreenExte private boolean craftingPlanInitialized = false; private boolean craftingPlanFlatInitialized = false; + @Nullable + private ITerminalCraftingPlan lastCraftingPlan; + @Nullable + private ITerminalCraftingPlan completedCraftingPlan; + @Nullable + private ITerminalCraftingPlanFlat completedCraftingPlanFlat; + // Outputs are only what is left to craft, so the last non-empty outputs per job id are kept for the completed plan + private final Map>> knownOutputs = Maps.newHashMap(); + public ContainerScreenTerminalCraftingJobsPlan(ContainerTerminalCraftingJobsPlan container, Inventory inventory, Component title) { super(container, inventory, title); + } - this.player = inventory.player; - this.guiCraftingPlanToggler = new GuiCraftingPlanToggler( - () -> this.getMenu().getCraftingPlan().orElse(null), - () -> this.getMenu().getCraftingPlanFlat().orElse(null), - () -> { - GuiCraftingPlan previousGuiCraftingPlan = this.guiCraftingPlan; - this.guiCraftingPlan = new GuiCraftingPlan(this, this.getMenu().getCraftingPlan().get(), leftPos, topPos, 9, 18, 10); - if (previousGuiCraftingPlan != null) { - this.guiCraftingPlan.inheritVisualizationState(previousGuiCraftingPlan); - } - addRenderableWidget(this.guiCraftingPlan); - this.guiCraftingPlanFlat = null; - - if (this.getMenu().getCraftingPlanFlat().isPresent()) { - addRenderableWidget(new ButtonText(leftPos + 8, topPos + 198, 80, 20, - Component.translatable("gui.integratedterminals.craftingplan.view.flat"), - Component.translatable("gui.integratedterminals.craftingplan.view.flat").withStyle(ChatFormatting.ITALIC), - (b) -> { - this.guiCraftingPlanToggler.setCraftingPlanDisplayMode(GuiCraftingPlanToggler.CraftingPlanDisplayMode.FLAT); - this.init(); - }, - true)); - } - }, - () -> { - GuiCraftingPlanFlat previousGuiCraftingPlan = this.guiCraftingPlanFlat; - this.guiCraftingPlanFlat = new GuiCraftingPlanFlat(this, this.getMenu().getCraftingPlanFlat().get(), leftPos, topPos, 9, 18, 10); - if (previousGuiCraftingPlan != null) { - this.guiCraftingPlanFlat.inheritVisualizationState(previousGuiCraftingPlan); - } - addRenderableWidget(this.guiCraftingPlanFlat); - this.guiCraftingPlan = null; - - if (this.getMenu().getCraftingPlan().isPresent()) { - addRenderableWidget(new ButtonText(leftPos + 8, topPos + 198, 80, 20, - Component.translatable("gui.integratedterminals.craftingplan.view.tree"), - Component.translatable("gui.integratedterminals.craftingplan.view.tree").withStyle(ChatFormatting.ITALIC), - (b) -> { - this.guiCraftingPlanToggler.setCraftingPlanDisplayMode(GuiCraftingPlanToggler.CraftingPlanDisplayMode.TREE); - this.init(); - }, - true)); - } - }, - () -> { - this.guiCraftingPlan = null; - this.guiCraftingPlanFlat = null; - } - ); + @Override + protected IGuiCraftingPlanView createTreeView() { + GuiCraftingPlan previous = this.guiCraftingPlan; + this.guiCraftingPlan = new GuiCraftingPlan(this, getCraftingPlan(), leftPos, topPos, 9, 18, 10); + if (previous != null) { + this.guiCraftingPlan.inheritVisualizationState(previous); + } + return this.guiCraftingPlan; } @Override - protected ResourceLocation constructGuiTexture() { - return ResourceLocation.fromNamespaceAndPath(Reference.MOD_ID, "textures/gui/crafting_plan.png"); + protected IGuiCraftingPlanView createFlatView() { + GuiCraftingPlanFlat previous = this.guiCraftingPlanFlat; + this.guiCraftingPlanFlat = new GuiCraftingPlanFlat(this, getCraftingPlanFlat(), leftPos, topPos, 9, 18, 10); + if (previous != null) { + this.guiCraftingPlanFlat.inheritVisualizationState(previous); + } + return this.guiCraftingPlanFlat; } @Override - public ResourceLocation getGuiTexture() { - return this.guiCraftingPlanToggler.getCraftingPlanDisplayMode() == GuiCraftingPlanToggler.CraftingPlanDisplayMode.FLAT ? ResourceLocation.fromNamespaceAndPath(Reference.MOD_ID, "textures/gui/crafting_plan_flat.png") : super.getGuiTexture(); + protected boolean isLive() { + return true; } @Override - public int getBaseXSize() { - return 256; + protected String getEmptyMessage() { + return "gui.integratedterminals.terminal_crafting_job.craftingplan.empty"; } @Override - public int getBaseYSize() { - return 222; + protected void onEscape() { + returnToOverview(); } @Override public void init() { super.init(); + this.cancelButton = null; - // Reset states - this.renderables.clear(); - this.children().clear(); - - this.guiCraftingPlanToggler.init(); - - if (this.guiCraftingPlan != null || this.guiCraftingPlanFlat != null) { - addRenderableWidget(new ButtonText(leftPos + 221 + 10 - 100, topPos + 198, 100, 20, + // Plans that are too large are only sent in their flattened form + if (this.activeView != null && (getMenu().getCraftingPlan().isPresent() || getMenu().getCraftingPlanFlat().isPresent())) { + addRenderableWidget(this.cancelButton = new ButtonText(leftPos + imageWidth - 25 - 100, topPos + imageHeight - 24, 100, 20, Component.translatable("gui.integratedterminals.terminal_crafting_job.craftingplan.cancel"), Component.translatable("gui.integratedterminals.terminal_crafting_job.craftingplan.cancel"), (b) -> cancelCraftingJob(), @@ -134,81 +111,126 @@ public void init() { } } + /** + * @return The plan of the running job, or the completed version of its last plan once it has finished. + */ + @Nullable @Override - public boolean keyPressed(int typedChar, int keyCode, int modifiers) { - if (typedChar == GLFW.GLFW_KEY_ESCAPE) { - returnToOverview(); - return true; - } else { - return super.keyPressed(typedChar, keyCode, modifiers); + protected ITerminalCraftingPlan getCraftingPlan() { + if (getMenu().getCraftingPlan().isPresent()) { + ITerminalCraftingPlan craftingPlan = getMenu().getCraftingPlan().get(); + if (craftingPlan != this.lastCraftingPlan) { + this.lastCraftingPlan = craftingPlan; + rememberOutputs(craftingPlan); + } + this.completedCraftingPlan = null; + this.completedCraftingPlanFlat = null; + return this.lastCraftingPlan; + } + // Finished jobs are removed from the network, so keep showing the result instead of an empty job. + // Only do this if everything was crafted, as the job may also have been cancelled elsewhere. + // The server does not always send a last plan with nothing left to craft before removing the job. + if (this.lastCraftingPlan != null && this.lastCraftingPlan.getCraftingQuantity() == 0) { + if (this.completedCraftingPlan == null) { + this.completedCraftingPlan = TerminalCraftingPlanStatic.asCompleted(this.lastCraftingPlan, this.knownOutputs); + } + return this.completedCraftingPlan; } + return null; } - private void returnToOverview() { - PartPos center = getMenu().getTarget().get().getCenter(); - OpenCraftingJobsGuiPacket.send(center.getPos().getBlockPos(), center.getSide()); + protected void rememberOutputs(ITerminalCraftingPlan craftingPlan) { + List> outputs = craftingPlan.getOutputs(); + long remaining = craftingPlan.getCraftingQuantity(); + if (remaining > 0 && outputs.stream().anyMatch(output -> !GuiCraftingPlanGraph.isEmpty(output))) { + // Outputs are for the remaining crafts, so scale them to all crafts of the job + long total = Math.max(craftingPlan.getCraftingQuantityTotal(), remaining); + this.knownOutputs.put(craftingPlan.getId(), outputs.stream() + .>map(output -> scaleQuantity(output, total, remaining)) + .toList()); + } + for (ITerminalCraftingPlan dependency : craftingPlan.getDependencies()) { + rememberOutputs(dependency); + } } - private void cancelCraftingJob() { - // Send packet to cancel crafting job - IntegratedTerminals._instance.getPacketHandler().sendToServer( - new CancelCraftingJobPacket(getMenu().getCraftingJobGuiData())); - - // Return to overview - returnToOverview(); + protected static IPrototypedIngredient scaleQuantity(IPrototypedIngredient ingredient, long multiplier, long divisor) { + IIngredientMatcher matcher = ingredient.getComponent().getMatcher(); + long quantity = matcher.getQuantity(ingredient.getPrototype()) * multiplier / divisor; + return new PrototypedIngredient(ingredient.getComponent(), + matcher.withQuantity(ingredient.getPrototype(), quantity), ingredient.getCondition()); } + @Nullable @Override - protected void renderBg(GuiGraphics guiGraphics, float partialTicks, int mouseX, int mouseY) { - super.renderBg(guiGraphics, partialTicks, mouseX, mouseY); - if (this.guiCraftingPlan != null) { - guiCraftingPlan.drawGuiContainerBackgroundLayer(guiGraphics, partialTicks, mouseX, mouseY); - } else if (this.guiCraftingPlanFlat != null) { - guiCraftingPlanFlat.drawGuiContainerBackgroundLayer(guiGraphics, partialTicks, mouseX, mouseY); - } else { - guiGraphics.drawCenteredString(font, L10NHelpers.localize("gui.integratedterminals.terminal_crafting_job.craftingplan.empty"), - leftPos + getBaseXSize() / 2, topPos + 23, 16777215); + protected ITerminalCraftingPlanFlat getCraftingPlanFlat() { + if (getMenu().getCraftingPlanFlat().isPresent()) { + return getMenu().getCraftingPlanFlat().get(); } + ITerminalCraftingPlan craftingPlan = getCraftingPlan(); + if (craftingPlan == null || craftingPlan != this.completedCraftingPlan) { + return null; + } + // Flatten only once, as this is called multiple times on each init + if (this.completedCraftingPlanFlat == null) { + this.completedCraftingPlanFlat = craftingPlan.flatten(); + } + return this.completedCraftingPlanFlat; } @Override - protected void drawCurrentScreen(GuiGraphics guiGraphics, int mouseX, int mouseY, float partialTicks) { - super.drawCurrentScreen(guiGraphics, mouseX, mouseY, partialTicks); - if (this.guiCraftingPlan != null) { - guiCraftingPlan.render(guiGraphics, mouseX, mouseY, partialTicks); - } else if (this.guiCraftingPlanFlat != null) { - guiCraftingPlanFlat.render(guiGraphics, mouseX, mouseY, partialTicks); + protected void renderBg(GuiGraphics guiGraphics, float partialTicks, int mouseX, int mouseY) { + super.renderBg(guiGraphics, partialTicks, mouseX, mouseY); + ITerminalCraftingPlanFlat craftingPlanFlat = getCraftingPlanFlat(); + if (this.activeView != null && craftingPlanFlat != null) { + // The graph also counts the jobs that are already done, which the flattened plan no longer contains + int progress = this.activeView instanceof GuiCraftingPlanGraph graph + ? graph.getProgress() : GuiCraftingPlan.getProgress(craftingPlanFlat); + int left = getProgressBarLeft(); + GuiCraftingPlanProgressBar.draw(guiGraphics, progress, craftingPlanFlat.getTickDuration(), + craftingPlanFlat.getEstimatedTickDurationRemaining(), craftingPlanFlat.getStatus().isValid(), + left, getProgressBarTop(), getProgressBarRight() - left); } } @Override protected void renderLabels(GuiGraphics guiGraphics, int mouseX, int mouseY) { - // super.drawGuiContainerForegroundLayer(matrixStack, mouseX, mouseY); - if (this.guiCraftingPlan != null) { - guiCraftingPlan.drawGuiContainerForegroundLayer(guiGraphics, mouseX, mouseY); - } else if (this.guiCraftingPlanFlat != null) { - guiCraftingPlanFlat.drawGuiContainerForegroundLayer(guiGraphics, mouseX, mouseY); + super.renderLabels(guiGraphics, mouseX, mouseY); + ITerminalCraftingPlanFlat craftingPlanFlat = getCraftingPlanFlat(); + if (this.activeView != null && craftingPlanFlat != null && craftingPlanFlat.getStatus().isValid()) { + // Explain an unknown remaining duration when hovering it, as the plan header no longer shows it for running jobs + long remaining = craftingPlanFlat.getEstimatedTickDurationRemaining(); + String remainingString = GuiCraftingPlan.getDurationValue(remaining); + int width = font.width(remainingString) / 2; + GuiCraftingPlan.drawUnknownDurationTooltip(this, guiGraphics, remainingString, remaining, + getProgressBarRight() - 2 - width - leftPos, getProgressBarTop() + 2 - topPos, mouseX, mouseY); } } - @Override - public boolean mouseScrolled(double mouseX, double mouseY, double mouseZ, double delta) { - if (this.guiCraftingPlan != null) { - return guiCraftingPlan.mouseScrolled(mouseX, mouseY, mouseZ, delta); - } else if (this.guiCraftingPlanFlat != null) { - return guiCraftingPlanFlat.mouseScrolled(mouseX, mouseY, mouseZ, delta); - } - return super.mouseScrolled(mouseX, mouseY, mouseZ, delta); + protected int getProgressBarLeft() { + return this.viewButtonsRight + PROGRESS_BAR_SPACING; } - @Override - public boolean mouseDragged(double mouseX, double mouseY, int mouseButton, double mouseXPrev, double mouseYPrev) { - if (this.guiCraftingPlan != null) { - return guiCraftingPlan.mouseDragged(mouseX, mouseY, mouseButton, mouseXPrev, mouseYPrev); - } else if (this.guiCraftingPlanFlat != null) { - return guiCraftingPlanFlat.mouseDragged(mouseX, mouseY, mouseButton, mouseXPrev, mouseYPrev); - } - return super.mouseDragged(mouseX, mouseY, mouseButton, mouseXPrev, mouseYPrev); + protected int getProgressBarRight() { + return this.cancelButton != null ? this.cancelButton.getX() - PROGRESS_BAR_SPACING : leftPos + imageWidth - 25; + } + + protected int getProgressBarTop() { + return topPos + imageHeight - 23 + (18 - GuiCraftingPlanProgressBar.HEIGHT) / 2; + } + + private void returnToOverview() { + PartPos center = getMenu().getTarget().get().getCenter(); + OpenCraftingJobsGuiPacket.send(center.getPos().getBlockPos(), center.getSide()); + } + + private void cancelCraftingJob() { + // Send packet to cancel crafting job + IntegratedTerminals._instance.getPacketHandler().sendToServer( + new CancelCraftingJobPacket(getMenu().getCraftingJobGuiData())); + + // Return to overview + returnToOverview(); } @Override diff --git a/src/main/java/org/cyclops/integratedterminals/client/gui/container/ContainerScreenTerminalCraftingPlanBase.java b/src/main/java/org/cyclops/integratedterminals/client/gui/container/ContainerScreenTerminalCraftingPlanBase.java new file mode 100644 index 000000000..a1d6d58bc --- /dev/null +++ b/src/main/java/org/cyclops/integratedterminals/client/gui/container/ContainerScreenTerminalCraftingPlanBase.java @@ -0,0 +1,231 @@ +package org.cyclops.integratedterminals.client.gui.container; + +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.components.Button; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.entity.player.Inventory; +import org.cyclops.cyclopscore.client.gui.container.ContainerScreenExtended; +import org.cyclops.cyclopscore.helper.L10NHelpers; +import org.cyclops.cyclopscore.inventory.container.ContainerExtended; +import org.cyclops.integratedterminals.Reference; +import org.cyclops.integratedterminals.api.terminalstorage.crafting.ITerminalCraftingPlan; +import org.cyclops.integratedterminals.api.terminalstorage.crafting.ITerminalCraftingPlanFlat; +import org.cyclops.integratedterminals.client.gui.container.component.GuiCraftingPlanButtons; +import org.cyclops.integratedterminals.client.gui.container.component.GuiCraftingPlanGraph; +import org.cyclops.integratedterminals.client.gui.container.component.GuiCraftingPlanToggler; +import org.cyclops.integratedterminals.client.gui.container.component.IGuiCraftingPlanView; +import org.lwjgl.glfw.GLFW; + +import javax.annotation.Nullable; + +/** + * Base gui for showing a crafting plan in one of the crafting plan views. + * @author rubensworks + */ +public abstract class ContainerScreenTerminalCraftingPlanBase extends ContainerScreenExtended { + + private static final ResourceLocation TEXTURE_PANEL = ResourceLocation.fromNamespaceAndPath(Reference.MOD_ID, "textures/gui/part_terminal_storage.png"); + + protected final GuiCraftingPlanToggler guiCraftingPlanToggler; + + @Nullable + protected IGuiCraftingPlanView activeView; + // The X position after the view buttons in the bottom row + protected int viewButtonsRight; + // Kept so that its visualization state can be inherited after an update + @Nullable + private GuiCraftingPlanGraph guiCraftingPlanGraph; + + public ContainerScreenTerminalCraftingPlanBase(C container, Inventory inventory, Component title) { + super(container, inventory, title); + + this.guiCraftingPlanToggler = new GuiCraftingPlanToggler( + this::getCraftingPlan, + this::getCraftingPlanFlat, + () -> setActiveView(createTreeView()), + this::initGraphView, + () -> setActiveView(createFlatView()), + () -> this.activeView = null + ); + } + + /** + * @return The plan to show in the tree and graph views. + */ + @Nullable + protected abstract ITerminalCraftingPlan getCraftingPlan(); + + /** + * @return The plan to show in the flat view. + */ + @Nullable + protected abstract ITerminalCraftingPlanFlat getCraftingPlanFlat(); + + protected abstract IGuiCraftingPlanView createTreeView(); + + protected abstract IGuiCraftingPlanView createFlatView(); + + /** + * @return If the plan is of a running job. + */ + protected abstract boolean isLive(); + + /** + * @return The unlocalized message shown when there is no plan to show. + */ + protected abstract String getEmptyMessage(); + + /** + * Called when escape is pressed. + */ + protected abstract void onEscape(); + + /** + * Handle a key press that was not handled by the active view. + * @param keyCode The key code. + * @param scanCode The scan code. + * @param modifiers The modifiers. + * @return If the key press was handled. + */ + protected boolean onKeyPressed(int keyCode, int scanCode, int modifiers) { + return false; + } + + protected void initGraphView() { + GuiCraftingPlanGraph previous = this.guiCraftingPlanGraph; + ITerminalCraftingPlan craftingPlan = getCraftingPlan(); + // Live plans change on each update, previews only keep their state for the same plan + if (previous != null && !isLive() && previous.getCraftingPlan() != craftingPlan) { + previous = null; + } + this.guiCraftingPlanGraph = new GuiCraftingPlanGraph(this, craftingPlan, leftPos, topPos, 9, 18, + imageWidth - 18, imageHeight - 44, isLive(), previous); + // Overlay buttons must be added before the graph, so that they receive clicks first + for (Button button : GuiCraftingPlanButtons.createGraphButtons(this.guiCraftingPlanGraph, this::init)) { + addRenderableWidget(button); + } + setActiveView(this.guiCraftingPlanGraph); + } + + protected void setActiveView(IGuiCraftingPlanView view) { + this.activeView = view; + addRenderableWidget(view); + } + + @Override + protected ResourceLocation constructGuiTexture() { + return ResourceLocation.fromNamespaceAndPath(Reference.MOD_ID, "textures/gui/crafting_plan.png"); + } + + @Override + public ResourceLocation getGuiTexture() { + return this.guiCraftingPlanToggler.getCraftingPlanDisplayMode() == GuiCraftingPlanToggler.CraftingPlanDisplayMode.FLAT ? ResourceLocation.fromNamespaceAndPath(Reference.MOD_ID, "textures/gui/crafting_plan_flat.png") : super.getGuiTexture(); + } + + @Override + public int getBaseXSize() { + return this.guiCraftingPlanToggler.getWidth(this.width); + } + + @Override + public int getBaseYSize() { + return this.guiCraftingPlanToggler.getHeight(this.height); + } + + @Override + public void init() { + // The display mode determines the gui size + this.guiCraftingPlanToggler.resolveDisplayMode(); + super.init(); + + // Reset states + this.renderables.clear(); + this.children().clear(); + this.activeView = null; + + this.viewButtonsRight = leftPos + 8; + for (Button button : GuiCraftingPlanButtons.createViewButtons(this.guiCraftingPlanToggler, leftPos + 8, topPos + imageHeight - 23, this::init)) { + addRenderableWidget(button); + this.viewButtonsRight = Math.max(this.viewButtonsRight, button.getX() + button.getWidth()); + } + + this.guiCraftingPlanToggler.init(); + } + + @Override + public boolean keyPressed(int keyCode, int scanCode, int modifiers) { + if (keyCode == GLFW.GLFW_KEY_ESCAPE) { + onEscape(); + return true; + } + if (this.activeView != null && this.activeView.keyPressed(keyCode, scanCode, modifiers)) { + return true; + } + if (onKeyPressed(keyCode, scanCode, modifiers)) { + return true; + } + return super.keyPressed(keyCode, scanCode, modifiers); + } + + @Override + protected void renderBg(GuiGraphics guiGraphics, float partialTicks, int mouseX, int mouseY) { + if (this.guiCraftingPlanToggler.getCraftingPlanDisplayMode() == GuiCraftingPlanToggler.CraftingPlanDisplayMode.GRAPH) { + ContainerScreenTerminalStorage.drawPanel(guiGraphics, TEXTURE_PANEL, leftPos, topPos, imageWidth, imageHeight); + } else { + super.renderBg(guiGraphics, partialTicks, mouseX, mouseY); + } + if (this.activeView != null) { + activeView.drawGuiContainerBackgroundLayer(guiGraphics, partialTicks, mouseX, mouseY); + } else { + guiGraphics.drawCenteredString(font, L10NHelpers.localize(getEmptyMessage()), + leftPos + imageWidth / 2, topPos + 23, 16777215); + } + } + + @Override + protected void renderLabels(GuiGraphics guiGraphics, int mouseX, int mouseY) { + if (this.activeView != null) { + activeView.drawGuiContainerForegroundLayer(guiGraphics, mouseX, mouseY); + } + } + + @Override + protected void drawCurrentScreen(GuiGraphics guiGraphics, int mouseX, int mouseY, float partialTicks) { + super.drawCurrentScreen(guiGraphics, mouseX, mouseY, partialTicks); + if (this.activeView != null) { + activeView.render(guiGraphics, mouseX, mouseY, partialTicks); + } + } + + @Override + public boolean charTyped(char character, int modifiers) { + if (this.activeView != null && this.activeView.charTyped(character, modifiers)) { + return true; + } + return super.charTyped(character, modifiers); + } + + @Override + public boolean mouseScrolled(double mouseX, double mouseY, double mouseZ, double delta) { + if (this.activeView != null) { + return activeView.mouseScrolled(mouseX, mouseY, mouseZ, delta); + } + return super.mouseScrolled(mouseX, mouseY, mouseZ, delta); + } + + @Override + public boolean mouseDragged(double mouseX, double mouseY, int mouseButton, double mouseXPrev, double mouseYPrev) { + if (this.activeView != null) { + return activeView.mouseDragged(mouseX, mouseY, mouseButton, mouseXPrev, mouseYPrev); + } + return super.mouseDragged(mouseX, mouseY, mouseButton, mouseXPrev, mouseYPrev); + } + + @Override + public boolean mouseReleased(double mouseX, double mouseY, int mouseButton) { + // The views are not hit-tested by the default event routing, so they need to be notified explicitly + boolean handled = this.activeView != null && activeView.mouseReleased(mouseX, mouseY, mouseButton); + return super.mouseReleased(mouseX, mouseY, mouseButton) || handled; + } +} diff --git a/src/main/java/org/cyclops/integratedterminals/client/gui/container/ContainerScreenTerminalStorage.java b/src/main/java/org/cyclops/integratedterminals/client/gui/container/ContainerScreenTerminalStorage.java index 1f9dce871..06bed8fa9 100644 --- a/src/main/java/org/cyclops/integratedterminals/client/gui/container/ContainerScreenTerminalStorage.java +++ b/src/main/java/org/cyclops/integratedterminals/client/gui/container/ContainerScreenTerminalStorage.java @@ -373,25 +373,10 @@ protected void renderBgTab(GuiGraphics guiGraphics, float f, int mouseX, int mou int offsetX = getTabBackgroundOffsetX(); int offsetY = TAB_BACKGROUND_OFFSET; int blitOffset = 0; - int cornerSize = 7; int columns = getSlotRowLength(); int rows = getSlotVisibleRows(); - // Corners - //blit(matrixStack, leftPos + offsetXX, topPos + offsetYY, 0, 0, imageWidth - 2 * offsetX, imageHeight - 2 * offsetY); // top-left - guiGraphics.blit(this.texture, leftPos + offsetX, topPos + offsetY, cornerSize, 0, cornerSize, cornerSize); // top-left - guiGraphics.blit(this.texture, leftPos + offsetX + tabWidth - cornerSize, topPos + offsetY, 0, 0, cornerSize, cornerSize); // top-right - guiGraphics.blit(this.texture, leftPos + offsetX + tabWidth - cornerSize, topPos + offsetY + tabHeight - cornerSize, cornerSize * 2, 0, cornerSize, cornerSize); // bottom-right - guiGraphics.blit(this.texture, leftPos + offsetX, topPos + offsetY + tabHeight - cornerSize, cornerSize * 3, 0, cornerSize, cornerSize); // bottom-left - - // Sides - blitRescalable(guiGraphics, this.texture, leftPos + offsetX + cornerSize, topPos + offsetY, blitOffset, cornerSize + 4, 0, 1, cornerSize, 256, 256, tabWidth - cornerSize * 2, cornerSize); // top - blitRescalable(guiGraphics, this.texture, leftPos + offsetX + tabWidth - cornerSize, topPos + offsetY + cornerSize, blitOffset, 0, 4, cornerSize, 1, 256, 256, cornerSize, tabHeight - cornerSize * 2); // right - blitRescalable(guiGraphics, this.texture, leftPos + offsetX + cornerSize, topPos + offsetY + tabHeight - cornerSize, blitOffset, 25, 0, 1, cornerSize, 256, 256, tabWidth - cornerSize * 2, cornerSize); // bottom - blitRescalable(guiGraphics, this.texture, leftPos + offsetX, topPos + offsetY + cornerSize, blitOffset, cornerSize, 4, cornerSize, 1, 256, 256, cornerSize, tabHeight - cornerSize * 2); // left - - // Background - blitRescalable(guiGraphics, this.texture, leftPos + offsetX + cornerSize, topPos + offsetY + cornerSize, blitOffset, 0, 3, 1, 1, 256, 256, tabWidth - cornerSize * 2, tabHeight - cornerSize * 2); + drawPanel(guiGraphics, this.texture, leftPos + offsetX, topPos + offsetY, tabWidth, tabHeight); // Slots for (int j = 0; j < rows; j++) { @@ -422,6 +407,35 @@ protected void renderBgTab(GuiGraphics guiGraphics, float f, int mouseX, int mou getSelectedClientTab().ifPresent(tab -> tab.onTabBackgroundRender(this, guiGraphics, f, mouseX, mouseY)); } + /** + * Draw a panel with a bevelled border of any size, by 9-slicing the given texture. + * @param guiGraphics The gui graphics. + * @param texture A texture with the same layout as part_terminal_storage.png. + * @param x The X position. + * @param y The Y position. + * @param width The width. + * @param height The height. + */ + public static void drawPanel(GuiGraphics guiGraphics, ResourceLocation texture, int x, int y, int width, int height) { + int blitOffset = 0; + int cornerSize = 7; + + // Corners + guiGraphics.blit(texture, x, y, cornerSize, 0, cornerSize, cornerSize); // top-left + guiGraphics.blit(texture, x + width - cornerSize, y, 0, 0, cornerSize, cornerSize); // top-right + guiGraphics.blit(texture, x + width - cornerSize, y + height - cornerSize, cornerSize * 2, 0, cornerSize, cornerSize); // bottom-right + guiGraphics.blit(texture, x, y + height - cornerSize, cornerSize * 3, 0, cornerSize, cornerSize); // bottom-left + + // Sides + blitRescalable(guiGraphics, texture, x + cornerSize, y, blitOffset, cornerSize + 4, 0, 1, cornerSize, 256, 256, width - cornerSize * 2, cornerSize); // top + blitRescalable(guiGraphics, texture, x + width - cornerSize, y + cornerSize, blitOffset, 0, 4, cornerSize, 1, 256, 256, cornerSize, height - cornerSize * 2); // right + blitRescalable(guiGraphics, texture, x + cornerSize, y + height - cornerSize, blitOffset, 25, 0, 1, cornerSize, 256, 256, width - cornerSize * 2, cornerSize); // bottom + blitRescalable(guiGraphics, texture, x, y + cornerSize, blitOffset, cornerSize, 4, cornerSize, 1, 256, 256, cornerSize, height - cornerSize * 2); // left + + // Background + blitRescalable(guiGraphics, texture, x + cornerSize, y + cornerSize, blitOffset, 0, 3, 1, 1, 256, 256, width - cornerSize * 2, height - cornerSize * 2); + } + public static void blitRescalable(GuiGraphics guiGraphics, ResourceLocation texture, int p_93145_, int p_93146_, int p_93147_, float p_93148_, float p_93149_, int p_93150_, int p_93151_, int p_93152_, int p_93153_, int realWidth, int realHeight) { guiGraphics.blit(texture, p_93145_, p_93145_ + realWidth, p_93146_, p_93146_ + realHeight, p_93147_, p_93150_, p_93151_, p_93148_, p_93149_, p_93152_, p_93153_); } diff --git a/src/main/java/org/cyclops/integratedterminals/client/gui/container/ContainerScreenTerminalStorageCraftingPlan.java b/src/main/java/org/cyclops/integratedterminals/client/gui/container/ContainerScreenTerminalStorageCraftingPlan.java index f02941760..498c97f7c 100644 --- a/src/main/java/org/cyclops/integratedterminals/client/gui/container/ContainerScreenTerminalStorageCraftingPlan.java +++ b/src/main/java/org/cyclops/integratedterminals/client/gui/container/ContainerScreenTerminalStorageCraftingPlan.java @@ -1,23 +1,18 @@ package org.cyclops.integratedterminals.client.gui.container; import net.minecraft.ChatFormatting; -import net.minecraft.client.gui.GuiGraphics; import net.minecraft.client.gui.components.Checkbox; import net.minecraft.client.gui.components.Tooltip; import net.minecraft.nbt.CompoundTag; import net.minecraft.network.chat.Component; -import net.minecraft.resources.ResourceLocation; import net.minecraft.world.entity.player.Inventory; import org.cyclops.cyclopscore.client.gui.component.button.ButtonText; -import org.cyclops.cyclopscore.client.gui.container.ContainerScreenExtended; -import org.cyclops.cyclopscore.helper.L10NHelpers; import org.cyclops.integratedterminals.IntegratedTerminals; -import org.cyclops.integratedterminals.Reference; import org.cyclops.integratedterminals.api.terminalstorage.crafting.ITerminalCraftingPlan; import org.cyclops.integratedterminals.api.terminalstorage.crafting.ITerminalCraftingPlanFlat; import org.cyclops.integratedterminals.client.gui.container.component.GuiCraftingPlan; import org.cyclops.integratedterminals.client.gui.container.component.GuiCraftingPlanFlat; -import org.cyclops.integratedterminals.client.gui.container.component.GuiCraftingPlanToggler; +import org.cyclops.integratedterminals.client.gui.container.component.IGuiCraftingPlanView; import org.cyclops.integratedterminals.core.client.gui.CraftingOptionGuiData; import org.cyclops.integratedterminals.inventory.container.ContainerTerminalStorageCraftingPlanBase; import org.cyclops.integratedterminals.network.packet.TerminalStorageIngredientOpenCraftingJobAmountGuiPacket; @@ -29,14 +24,7 @@ * A gui for previewing a crafting plan. * @author rubensworks */ -public class ContainerScreenTerminalStorageCraftingPlan> extends ContainerScreenExtended { - - private GuiCraftingPlanToggler guiCraftingPlanToggler; - - @Nullable - private GuiCraftingPlan guiCraftingPlan; - @Nullable - private GuiCraftingPlanFlat guiCraftingPlanFlat; +public class ContainerScreenTerminalStorageCraftingPlan> extends ContainerScreenTerminalCraftingPlanBase { private ITerminalCraftingPlan craftingPlan; private ITerminalCraftingPlanFlat craftingPlanFlat; @@ -44,112 +32,78 @@ public class ContainerScreenTerminalStorageCraftingPlan this.craftingPlan, - () -> this.craftingPlanFlat, - () -> { - this.guiCraftingPlan = new GuiCraftingPlan(this, this.craftingPlan, leftPos, topPos, 9, 18, 10); - addRenderableWidget(this.guiCraftingPlan); - - if (this.craftingPlanFlat != null) { - addRenderableWidget(new ButtonText(leftPos + 8, topPos + 198, 62, 20, - Component.translatable("gui.integratedterminals.craftingplan.view.flat"), - Component.translatable("gui.integratedterminals.craftingplan.view.flat").withStyle(ChatFormatting.ITALIC), - (b) -> { - this.guiCraftingPlanToggler.setCraftingPlanDisplayMode(GuiCraftingPlanToggler.CraftingPlanDisplayMode.FLAT); - this.init(); - }, - true)); - } - }, - () -> { - this.guiCraftingPlanFlat = new GuiCraftingPlanFlat(this, this.craftingPlanFlat, leftPos, topPos, 9, 18, 10); - addRenderableWidget(this.guiCraftingPlanFlat); - - if (this.craftingPlan != null) { - addRenderableWidget(new ButtonText(leftPos + 8, topPos + 198, 62, 20, - Component.translatable("gui.integratedterminals.craftingplan.view.tree"), - Component.translatable("gui.integratedterminals.craftingplan.view.tree").withStyle(ChatFormatting.ITALIC), - (b) -> { - this.guiCraftingPlanToggler.setCraftingPlanDisplayMode(GuiCraftingPlanToggler.CraftingPlanDisplayMode.TREE); - this.init(); - }, - true)); - } - }, - () -> { - this.guiCraftingPlan = null; - this.guiCraftingPlanFlat = null; - } - ); + @Nullable + @Override + protected ITerminalCraftingPlan getCraftingPlan() { + return this.craftingPlan; } + @Nullable @Override - protected ResourceLocation constructGuiTexture() { - return ResourceLocation.fromNamespaceAndPath(Reference.MOD_ID, "textures/gui/crafting_plan.png"); + protected ITerminalCraftingPlanFlat getCraftingPlanFlat() { + return this.craftingPlanFlat; } @Override - public ResourceLocation getGuiTexture() { - return this.guiCraftingPlanToggler.getCraftingPlanDisplayMode() == GuiCraftingPlanToggler.CraftingPlanDisplayMode.FLAT ? ResourceLocation.fromNamespaceAndPath(Reference.MOD_ID, "textures/gui/crafting_plan_flat.png") : super.getGuiTexture(); + protected IGuiCraftingPlanView createTreeView() { + return new GuiCraftingPlan(this, this.craftingPlan, leftPos, topPos, 9, 18, 10); } @Override - public int getBaseXSize() { - return 256; + protected IGuiCraftingPlanView createFlatView() { + return new GuiCraftingPlanFlat(this, this.craftingPlanFlat, leftPos, topPos, 9, 18, 10); } @Override - public int getBaseYSize() { - return 222; + protected boolean isLive() { + return false; } @Override - public void init() { - super.init(); + protected String getEmptyMessage() { + return "gui.integratedterminals.terminal_storage.step.crafting_plan_calculating"; + } - // Reset states - this.renderables.clear(); - this.children().clear(); - this.guiCraftingPlan = null; - this.guiCraftingPlanFlat = null; + @Override + protected void onEscape() { + returnToCraftingOptionAmount(); + } - this.guiCraftingPlanToggler.init(); + @Override + public void init() { + super.init(); - addRenderableWidget(new ButtonText(leftPos + 221 + 10 - 50 - 55, topPos + 198, 50, 20, + addRenderableWidget(new ButtonText(leftPos + imageWidth - 25 - 50 - 55, topPos + imageHeight - 24, 50, 20, Component.translatable("gui.integratedterminals.terminal_storage.step.back"), Component.translatable("gui.integratedterminals.terminal_storage.step.back"), (b) -> returnToCraftingOptionAmount(), true)); addRenderableWidget(Checkbox.builder(Component.translatable("gui.integratedterminals.terminal_storage.step.craft.notify"), font) - .pos(leftPos + 72, topPos + 200) + .pos(leftPos + 72, topPos + imageHeight - 22) .selected(getMenu().isNotifyOnCompletion()) .tooltip(Tooltip.create(Component.translatable("gui.integratedterminals.terminal_storage.step.craft.notify.info"))) .onValueChange((widget, selected) -> getMenu().setNotifyOnCompletion(selected)) .build()); - addRenderableWidget(buttonConfirm = new ButtonText(leftPos + 221 + 10 - 50, topPos + 198, 50, 20, + addRenderableWidget(buttonConfirm = new ButtonText(leftPos + imageWidth - 25 - 50, topPos + imageHeight - 24, 50, 20, Component.translatable("gui.integratedterminals.terminal_storage.step.craft"), Component.translatable("gui.integratedterminals.terminal_storage.step.craft").withStyle(ChatFormatting.YELLOW), createServerPressable(ContainerTerminalStorageCraftingPlanBase.BUTTON_START, (b) -> {}), true)); - buttonConfirm.active = (this.guiCraftingPlan != null && this.guiCraftingPlan.isValid()) || (this.guiCraftingPlanFlat != null && this.guiCraftingPlanFlat.isValid()); + buttonConfirm.active = this.activeView != null && this.activeView.isValid(); } @Override - public boolean keyPressed(int typedChar, int keyCode, int modifiers) { - if (typedChar == GLFW.GLFW_KEY_ESCAPE) { - returnToCraftingOptionAmount(); - return true; - } - if (this.guiCraftingPlan != null && this.guiCraftingPlan.isValid() - && (typedChar == GLFW.GLFW_KEY_ENTER || typedChar == GLFW.GLFW_KEY_KP_ENTER)) { + protected boolean onKeyPressed(int keyCode, int scanCode, int modifiers) { + if (this.activeView != null && this.activeView.isValid() + && (keyCode == GLFW.GLFW_KEY_ENTER || keyCode == GLFW.GLFW_KEY_KP_ENTER)) { buttonConfirm.onPress(); return true; } - return super.keyPressed(typedChar, keyCode, modifiers); + return false; } /** @@ -171,59 +125,6 @@ private void returnToTerminalStorage() { data.getLocation().openContainerFromClient(data); } - @Override - protected void renderBg(GuiGraphics guiGraphics, float partialTicks, int mouseX, int mouseY) { - super.renderBg(guiGraphics, partialTicks, mouseX, mouseY); - if (this.guiCraftingPlan != null) { - guiCraftingPlan.drawGuiContainerBackgroundLayer(guiGraphics, partialTicks, mouseX, mouseY); - } else if (this.guiCraftingPlanFlat != null) { - guiCraftingPlanFlat.drawGuiContainerBackgroundLayer(guiGraphics, partialTicks, mouseX, mouseY); - } else { - guiGraphics.drawCenteredString(font, L10NHelpers.localize("gui.integratedterminals.terminal_storage.step.crafting_plan_calculating"), - leftPos + getBaseXSize() / 2, topPos + 23, 16777215); - } - } - - @Override - protected void renderLabels(GuiGraphics guiGraphics, int mouseX, int mouseY) { - // super.drawGuiContainerForegroundLayer(matrixStack, mouseX, mouseY); - if (this.guiCraftingPlan != null) { - guiCraftingPlan.drawGuiContainerForegroundLayer(guiGraphics, mouseX, mouseY); - } else if (this.guiCraftingPlanFlat != null) { - guiCraftingPlanFlat.drawGuiContainerForegroundLayer(guiGraphics, mouseX, mouseY); - } - } - - @Override - protected void drawCurrentScreen(GuiGraphics guiGraphics, int mouseX, int mouseY, float partialTicks) { - super.drawCurrentScreen(guiGraphics, mouseX, mouseY, partialTicks); - if (this.guiCraftingPlan != null) { - guiCraftingPlan.render(guiGraphics, mouseX, mouseY, partialTicks); - } else if (this.guiCraftingPlanFlat != null) { - guiCraftingPlanFlat.render(guiGraphics, mouseX, mouseY, partialTicks); - } - } - - @Override - public boolean mouseScrolled(double mouseX, double mouseY, double mouseZ, double delta) { - if (this.guiCraftingPlan != null) { - return guiCraftingPlan.mouseScrolled(mouseX, mouseY, mouseZ, delta); - } else if (this.guiCraftingPlanFlat != null) { - return guiCraftingPlanFlat.mouseScrolled(mouseX, mouseY, mouseZ, delta); - } - return super.mouseScrolled(mouseX, mouseY, mouseZ, delta); - } - - @Override - public boolean mouseDragged(double mouseX, double mouseY, int mouseButton, double mouseXPrev, double mouseYPrev) { - if (this.guiCraftingPlan != null) { - return guiCraftingPlan.mouseDragged(mouseX, mouseY, mouseButton, mouseXPrev, mouseYPrev); - } else if (this.guiCraftingPlanFlat != null) { - return guiCraftingPlanFlat.mouseDragged(mouseX, mouseY, mouseButton, mouseXPrev, mouseYPrev); - } - return super.mouseDragged(mouseX, mouseY, mouseButton, mouseXPrev, mouseYPrev); - } - @Override public void onUpdate(int valueId, CompoundTag value) { super.onUpdate(valueId, value); diff --git a/src/main/java/org/cyclops/integratedterminals/client/gui/container/component/CraftingPlanGraphCamera.java b/src/main/java/org/cyclops/integratedterminals/client/gui/container/component/CraftingPlanGraphCamera.java new file mode 100644 index 000000000..77d57ff61 --- /dev/null +++ b/src/main/java/org/cyclops/integratedterminals/client/gui/container/component/CraftingPlanGraphCamera.java @@ -0,0 +1,227 @@ +package org.cyclops.integratedterminals.client.gui.container.component; + +/** + * The pan and zoom state of a graph inside a viewport. + * + * The camera stores the viewport-relative position of the graph origin, + * so the view stays steady when the graph around the origin changes. + * + * This class is independent of Minecraft, so it can be unit-tested and ported easily. + * + * @author rubensworks + */ +public class CraftingPlanGraphCamera { + + public static final float DEFAULT_ZOOM = 1F; + public static final float INITIAL_MAX_ZOOM = 1.5F; + public static final float MAX_ZOOM = 3F; + /** + * Graphs can always be zoomed out to this level, and further if that is needed to see them as a whole. + */ + public static final float MIN_ZOOM = 0.5F; + /** + * Below this level, nodes are too small to recognize, even for the largest graphs. + */ + public static final float MIN_ZOOM_LARGE_GRAPH = 0.25F; + /** + * The factor by which the zoom changes per step. + */ + public static final float ZOOM_STEP = 1.2F; + /** + * Zoom levels this close to the default level are snapped to it. + */ + public static final float DEFAULT_ZOOM_SNAP = 0.05F; + public static final int MARGIN = 8; + private static final float EPSILON = 0.001F; + + private double originX; + private double originY; + private float zoom = DEFAULT_ZOOM; + private float minZoom = MIN_ZOOM; + + public double getOriginX() { + return originX; + } + + public double getOriginY() { + return originY; + } + + public float getZoom() { + return zoom; + } + + public float getMinZoom() { + return minZoom; + } + + public boolean canZoomIn() { + return zoom < MAX_ZOOM - EPSILON; + } + + public boolean canZoomOut() { + return zoom > minZoom + EPSILON; + } + + /** + * Allow zooming out until the whole graph fits. + * @param bounds The graph bounds. + * @param viewWidth The viewport width. + * @param viewHeight The viewport height. + */ + public void updateMinZoom(CraftingPlanGraphLayout.Bounds bounds, int viewWidth, int viewHeight) { + this.minZoom = Math.max(MIN_ZOOM_LARGE_GRAPH, Math.min(MIN_ZOOM, getFitZoom(bounds, viewWidth, viewHeight))); + } + + public double toGraphX(double viewX) { + return (viewX - originX) / getZoom(); + } + + public double toGraphY(double viewY) { + return (viewY - originY) / getZoom(); + } + + public double toViewX(double graphX) { + return originX + graphX * getZoom(); + } + + public double toViewY(double graphY) { + return originY + graphY * getZoom(); + } + + public void pan(double dx, double dy) { + this.originX += dx; + this.originY += dy; + } + + /** + * Change the zoom level while keeping the graph point under the given view position in place. + * Zooming past or close to the default level stops at it, as that is where graphs look sharpest. + * @param viewX The anchor X position in the viewport. + * @param viewY The anchor Y position in the viewport. + * @param direction Positive to zoom in, negative to zoom out. + * @return If the zoom level changed. + */ + public boolean zoomAt(double viewX, double viewY, int direction) { + float newZoom = zoom * (float) Math.pow(ZOOM_STEP, Integer.signum(direction)); + if ((zoom < DEFAULT_ZOOM - EPSILON && newZoom > DEFAULT_ZOOM) || (zoom > DEFAULT_ZOOM + EPSILON && newZoom < DEFAULT_ZOOM) + || Math.abs(newZoom - DEFAULT_ZOOM) < DEFAULT_ZOOM_SNAP) { + newZoom = DEFAULT_ZOOM; + } + newZoom = Math.max(minZoom, Math.min(MAX_ZOOM, newZoom)); + if (Math.abs(newZoom - zoom) < EPSILON) { + return false; + } + double graphX = toGraphX(viewX); + double graphY = toGraphY(viewY); + this.zoom = newZoom; + this.originX = viewX - graphX * zoom; + this.originY = viewY - graphY * zoom; + return true; + } + + /** + * Zoom in as far as possible while the whole graph fits, and center it. + * If it does not even fit at the smallest zoom level, the root is aligned to the left center. + * @param bounds The graph bounds. + * @param viewWidth The viewport width. + * @param viewHeight The viewport height. + */ + public void fit(CraftingPlanGraphLayout.Bounds bounds, int viewWidth, int viewHeight) { + fit(bounds, viewWidth, viewHeight, MAX_ZOOM); + } + + /** + * Like {@link #fit(CraftingPlanGraphLayout.Bounds, int, int)}, but without zooming in as far, + * so that small graphs do not look oversized when they are first shown. + * @param bounds The graph bounds. + * @param viewWidth The viewport width. + * @param viewHeight The viewport height. + */ + public void reset(CraftingPlanGraphLayout.Bounds bounds, int viewWidth, int viewHeight) { + reset(bounds, viewWidth, viewHeight, INITIAL_MAX_ZOOM); + } + + /** + * Like {@link #reset(CraftingPlanGraphLayout.Bounds, int, int)}, with a custom zoom limit. + * @param bounds The graph bounds. + * @param viewWidth The viewport width. + * @param viewHeight The viewport height. + * @param maxZoom The largest zoom level to use. + */ + public void reset(CraftingPlanGraphLayout.Bounds bounds, int viewWidth, int viewHeight, float maxZoom) { + fit(bounds, viewWidth, viewHeight, maxZoom); + } + + protected void fit(CraftingPlanGraphLayout.Bounds bounds, int viewWidth, int viewHeight, float maxZoom) { + updateMinZoom(bounds, viewWidth, viewHeight); + this.zoom = Math.max(minZoom, Math.min(maxZoom, getFitZoom(bounds, viewWidth, viewHeight))); + if (fits(bounds, viewWidth, viewHeight, zoom)) { + this.originX = (viewWidth - bounds.getWidth() * zoom) / 2 - bounds.getMinX() * zoom; + } else { + this.originX = MARGIN - bounds.getMinX() * zoom; + } + centerVertically(bounds, viewHeight); + } + + /** + * @param bounds The graph bounds. + * @param viewWidth The viewport width. + * @param viewHeight The viewport height. + * @return The largest zoom level at which the whole graph fits in the viewport, including margins. + */ + public static float getFitZoom(CraftingPlanGraphLayout.Bounds bounds, int viewWidth, int viewHeight) { + float zoomX = bounds.getWidth() > 0 ? (viewWidth - 2F * MARGIN) / bounds.getWidth() : MAX_ZOOM; + float zoomY = bounds.getHeight() > 0 ? (viewHeight - 2F * MARGIN) / bounds.getHeight() : MAX_ZOOM; + return Math.max(0, Math.min(zoomX, zoomY)); + } + + /** + * @param bounds The graph bounds. + * @param viewWidth The viewport width. + * @param viewHeight The viewport height. + * @param zoom A zoom level. + * @return If the whole graph fits in the viewport at the given zoom level, including margins. + */ + public static boolean fits(CraftingPlanGraphLayout.Bounds bounds, int viewWidth, int viewHeight, float zoom) { + return bounds.getWidth() * zoom + 2 * MARGIN <= viewWidth && bounds.getHeight() * zoom + 2 * MARGIN <= viewHeight; + } + + protected void centerVertically(CraftingPlanGraphLayout.Bounds bounds, int viewHeight) { + if (bounds.getHeight() * zoom + 2 * MARGIN <= viewHeight) { + this.originY = (viewHeight - bounds.getHeight() * zoom) / 2 - bounds.getMinY() * zoom; + } else { + // Center the root, which is at the origin + this.originY = viewHeight / 2D - CraftingPlanGraphLayout.ROOT_NODE_SIZE / 2D * zoom; + } + } + + /** + * Restrict panning so that at least one node size of the graph stays visible, + * and update how far the graph can be zoomed out. + * @param bounds The graph bounds. + * @param viewWidth The viewport width. + * @param viewHeight The viewport height. + */ + public void clamp(CraftingPlanGraphLayout.Bounds bounds, int viewWidth, int viewHeight) { + updateMinZoom(bounds, viewWidth, viewHeight); + double visible = CraftingPlanGraphLayout.NODE_SIZE * zoom; + this.originX = clamp(originX, visible - bounds.getMaxX() * zoom, viewWidth - visible - bounds.getMinX() * zoom); + this.originY = clamp(originY, visible - bounds.getMaxY() * zoom, viewHeight - visible - bounds.getMinY() * zoom); + } + + protected static double clamp(double value, double min, double max) { + if (min > max) { + return (min + max) / 2; + } + return Math.max(min, Math.min(max, value)); + } + + public void copyFrom(CraftingPlanGraphCamera other) { + this.originX = other.originX; + this.originY = other.originY; + this.zoom = other.zoom; + this.minZoom = other.minZoom; + } + +} diff --git a/src/main/java/org/cyclops/integratedterminals/client/gui/container/component/CraftingPlanGraphLayout.java b/src/main/java/org/cyclops/integratedterminals/client/gui/container/component/CraftingPlanGraphLayout.java new file mode 100644 index 000000000..b6712e014 --- /dev/null +++ b/src/main/java/org/cyclops/integratedterminals/client/gui/container/component/CraftingPlanGraphLayout.java @@ -0,0 +1,301 @@ +package org.cyclops.integratedterminals.client.gui.container.component; + +import com.google.common.collect.Lists; + +import java.util.Collections; +import java.util.IdentityHashMap; +import java.util.List; +import java.util.Map; + +/** + * Places the nodes of a tree in columns from left to right, with the root at the graph origin. + * + * This class is independent of Minecraft, so it can be unit-tested and ported easily. + * + * @param The node type. + * @author rubensworks + */ +public class CraftingPlanGraphLayout> { + + public static final int NODE_SIZE = 26; + // The root is larger, so that it stands out as the goal of the plan + public static final int ROOT_NODE_SIZE = 34; + public static final int ROOT_GROWTH = ROOT_NODE_SIZE - NODE_SIZE; + public static final int COLUMN_GAP = 22; + public static final int ROW_GAP = 6; + public static final int COLUMN_PITCH = NODE_SIZE + COLUMN_GAP; + public static final int ROW_PITCH = NODE_SIZE + ROW_GAP; + + private final List> placements; + private final Map> placementsByNode; + private final List> segments; + private final Bounds bounds; + + private CraftingPlanGraphLayout(List> placements, List> segments, Bounds bounds) { + this.placements = placements; + this.placementsByNode = new IdentityHashMap<>(); + for (Placement placement : placements) { + this.placementsByNode.put(placement.getNode(), placement); + } + this.segments = segments; + this.bounds = bounds; + } + + /** + * @param root The root node. + * @param The node type. + * @return The layout of all nodes that are not collapsed into one of their ancestors. + */ + public static > CraftingPlanGraphLayout create(N root) { + List> placements = Lists.newArrayList(); + List> segments = Lists.newArrayList(); + Placement rootPlacement = place(root, 0, new int[]{0}, placements, segments); + + // Shift everything vertically so that the root ends up at the origin + int shiftY = -rootPlacement.getY(); + int minX = 0; + int minY = Integer.MAX_VALUE; + int maxX = 0; + int maxY = Integer.MIN_VALUE; + for (Placement placement : placements) { + placement.y += shiftY; + minY = Math.min(minY, placement.y); + maxX = Math.max(maxX, placement.x + placement.size); + maxY = Math.max(maxY, placement.y + placement.size); + } + for (Segment segment : segments) { + segment.y1 += shiftY; + segment.y2 += shiftY; + } + return new CraftingPlanGraphLayout<>(Collections.unmodifiableList(placements), + Collections.unmodifiableList(segments), new Bounds(minX, minY, maxX, maxY)); + } + + private static > Placement place(N node, int depth, int[] nextRow, + List> placements, List> segments) { + Placement placement = depth == 0 + ? new Placement<>(node, 0, ROOT_NODE_SIZE) + : new Placement<>(node, depth * COLUMN_PITCH + ROOT_GROWTH, NODE_SIZE); + placements.add(placement); + + List> childPlacements = Lists.newArrayList(); + int hidden = 0; + for (N child : node.getChildren()) { + if (child.isVisible()) { + childPlacements.add(place(child, depth + 1, nextRow, placements, segments)); + } else { + hidden += countNodes(child); + } + } + placement.hiddenDescendants = hidden; + + if (childPlacements.isEmpty()) { + // Leaves (and collapsed nodes) take the next free row + placement.y = nextRow[0]++ * ROW_PITCH; + } else { + int firstY = childPlacements.get(0).y; + int lastY = childPlacements.get(childPlacements.size() - 1).y; + // Align the center of the parent with the center of its children + placement.y = (firstY + lastY) / 2 - (placement.size - NODE_SIZE) / 2; + + // Elbow connectors: a stub out of the parent, and for each child a vertical part of the trunk and a stub into it. + // The trunk is split per child, so that the path to each child can be highlighted on its own. + int parentRight = placement.x + placement.size; + int trunkX = parentRight + COLUMN_GAP / 2; + int parentCenterY = placement.y + placement.size / 2; + List children = Lists.newArrayList(); + for (Placement childPlacement : childPlacements) { + children.add(childPlacement.getNode()); + } + segments.add(new Segment<>(parentRight, parentCenterY, trunkX, parentCenterY, children)); + for (Placement childPlacement : childPlacements) { + int childCenterY = childPlacement.y + childPlacement.size / 2; + List child = Collections.singletonList(childPlacement.getNode()); + if (childCenterY != parentCenterY) { + segments.add(new Segment<>(trunkX, Math.min(parentCenterY, childCenterY), trunkX, + Math.max(parentCenterY, childCenterY), child)); + } + segments.add(new Segment<>(trunkX, childCenterY, childPlacement.x - 1, childCenterY, child)); + } + } + return placement; + } + + protected static > int countNodes(N node) { + int count = 1; + for (N child : node.getChildren()) { + count += countNodes(child); + } + return count; + } + + /** + * @return All visible nodes, in depth-first order, starting with the root. + */ + public List> getPlacements() { + return placements; + } + + /** + * @return The line segments that connect the nodes. + */ + public List> getSegments() { + return segments; + } + + public Bounds getBounds() { + return bounds; + } + + /** + * @param node A node. + * @return The placement of the node, or null if it is not visible. + */ + public Placement getPlacement(N node) { + return placementsByNode.get(node); + } + + /** + * A node in a tree that can be laid out. + * @param The node type. + */ + public static interface INode> { + /** + * @return All children, including invisible ones. + */ + public List getChildren(); + + /** + * @return If this node is shown, otherwise it is collapsed into its parent. + */ + public boolean isVisible(); + } + + /** + * The position of a node in graph space. + * @param The node type. + */ + public static class Placement { + private final N node; + private final int x; + private final int size; + private int y; + private int hiddenDescendants; + + public Placement(N node, int x, int size) { + this.node = node; + this.x = x; + this.size = size; + } + + public N getNode() { + return node; + } + + public int getX() { + return x; + } + + public int getY() { + return y; + } + + /** + * @return The width and height of the node. + */ + public int getSize() { + return size; + } + + /** + * @return The number of descendants that are hidden because they are collapsed into this node. + */ + public int getHiddenDescendants() { + return hiddenDescendants; + } + } + + /** + * An axis-aligned line in graph space, from (x1, y1) to (x2, y2), both inclusive. + */ + public static class Segment { + private final int x1; + private int y1; + private final int x2; + private int y2; + private final List targets; + + public Segment(int x1, int y1, int x2, int y2, List targets) { + this.x1 = x1; + this.y1 = y1; + this.x2 = x2; + this.y2 = y2; + this.targets = targets; + } + + public int getX1() { + return x1; + } + + public int getY1() { + return y1; + } + + public int getX2() { + return x2; + } + + public int getY2() { + return y2; + } + + /** + * @return The child nodes this segment leads to. + */ + public List getTargets() { + return targets; + } + } + + /** + * A rectangle in graph space, with exclusive maximum. + */ + public static class Bounds { + private final int minX; + private final int minY; + private final int maxX; + private final int maxY; + + public Bounds(int minX, int minY, int maxX, int maxY) { + this.minX = minX; + this.minY = minY; + this.maxX = maxX; + this.maxY = maxY; + } + + public int getMinX() { + return minX; + } + + public int getMinY() { + return minY; + } + + public int getMaxX() { + return maxX; + } + + public int getMaxY() { + return maxY; + } + + public int getWidth() { + return maxX - minX; + } + + public int getHeight() { + return maxY - minY; + } + } + +} diff --git a/src/main/java/org/cyclops/integratedterminals/client/gui/container/component/GuiCraftingPlan.java b/src/main/java/org/cyclops/integratedterminals/client/gui/container/component/GuiCraftingPlan.java index aeaf3b565..1a0b04558 100644 --- a/src/main/java/org/cyclops/integratedterminals/client/gui/container/component/GuiCraftingPlan.java +++ b/src/main/java/org/cyclops/integratedterminals/client/gui/container/component/GuiCraftingPlan.java @@ -56,9 +56,13 @@ * * @author rubensworks */ -public class GuiCraftingPlan extends AbstractWidget { +public class GuiCraftingPlan extends AbstractWidget implements IGuiCraftingPlanView { public static final int ELEMENT_WIDTH = 221; + /** + * The width of the list views, including their scrollbar. + */ + public static final int VIEW_WIDTH = 238; private static final int ELEMENT_HEIGHT = 16; private static final int ELEMENT_HEIGHT_TOTAL = 18; @@ -75,14 +79,7 @@ public class GuiCraftingPlan extends AbstractWidget { private final List visibleElements; private final boolean valid; private final WidgetScrollBar scrollBar; - private final String label; - private final long tickDuration; - private final long estimatedTickDuration; - private final boolean estimatedTickDurationRemaining; - private final boolean showEstimatedTickDuration; - private final int channel; - @Nullable - private final String initiatorName; + private final GuiCraftingPlanHeader header; private int firstRow; @@ -96,16 +93,7 @@ public GuiCraftingPlan(AbstractContainerScreen parentGui, ITerminalCraftingPlan< this.valid = craftingPlan.getStatus().isValid(); this.scrollBar = new WidgetScrollBar(guiLeft + x + 227, guiTop + y + 0, 178, Component.translatable("gui.cyclopscore.scrollbar"), this::setFirstRow, visibleRows); this.scrollBar.setTotalRows(visibleElements.size()); - this.label = L10NHelpers.localize(craftingPlan.getUnlocalizedLabel()); - this.tickDuration = craftingPlan.getTickDuration(); - // Jobs that have not started yet can only show a total estimation, running jobs show what is left of it - this.estimatedTickDurationRemaining = craftingPlan.getStatus() != TerminalCraftingJobStatus.UNSTARTED; - this.estimatedTickDuration = this.estimatedTickDurationRemaining - ? craftingPlan.getEstimatedTickDurationRemaining() : craftingPlan.getEstimatedTickDurationTotal(); - // Jobs that can not be crafted have nothing to estimate, other jobs show a placeholder until they are measured - this.showEstimatedTickDuration = craftingPlan.getStatus().isValid(); - this.channel = craftingPlan.getChannel(); - this.initiatorName = craftingPlan.getInitiatorName(); + this.header = GuiCraftingPlanHeader.forPlan(craftingPlan); } public void inheritVisualizationState(GuiCraftingPlan guiCraftingPlan) { @@ -320,33 +308,7 @@ public static int getProgress(ITerminalCraftingPlanFlat craftingPlan) { } public void drawGuiContainerBackgroundLayer(GuiGraphics guiGraphics, float partialTicks, int mouseX, int mouseY) { - Font fontRenderer = Minecraft.getInstance().font; - - // Draw plan label - guiGraphics.drawCenteredString(Minecraft.getInstance().font, this.label, guiLeft + getX() + ELEMENT_WIDTH / 2 + 8, guiTop + getY() - 13, 16777215); - - // Draw duration - if (tickDuration >= 0) { - String durationString = getDurationString(tickDuration); - RenderHelpers.drawScaledString(guiGraphics.pose(), guiGraphics.bufferSource(), fontRenderer, durationString, guiLeft + getX() + 200, guiTop + getY() - 14, 0.5f, 16777215, true, Font.DisplayMode.NORMAL); - } - - // Draw channel - if (channel != -1) { - String channelString = L10NHelpers.localize("gui.integratedterminals.terminal_crafting_job.craftingplan.crafting_channel", channel); - RenderHelpers.drawScaledString(guiGraphics.pose(), guiGraphics.bufferSource(), fontRenderer, channelString, guiLeft + getX() + 200, guiTop + getY() - 8, 0.5f, 16777215, true, Font.DisplayMode.NORMAL); - } - - // Draw initiator - if (initiatorName != null) { - String initiatorString = L10NHelpers.localize("gui.integratedterminals.terminal_crafting_job.craftingplan.owner", initiatorName); - RenderHelpers.drawScaledString(guiGraphics.pose(), guiGraphics.bufferSource(), fontRenderer, initiatorString, guiLeft + getX() - 4, guiTop + getY() - 8, 0.5f, 16777215, true, Font.DisplayMode.NORMAL); - } - - // Draw estimated duration - if (showEstimatedTickDuration) { - RenderHelpers.drawScaledString(guiGraphics.pose(), guiGraphics.bufferSource(), fontRenderer, getEstimatedDurationString(), guiLeft + getX() - 4, guiTop + getY() - 14, 0.5f, 16777215, true, Font.DisplayMode.NORMAL); - } + header.drawBackground(guiGraphics, guiLeft + getX(), guiTop + getY(), VIEW_WIDTH); drawGuiContainerLayer(guiGraphics, guiLeft, guiTop, ContainerScreenTerminalStorage.DrawLayer.BACKGROUND, partialTicks, mouseX, mouseY); scrollBar.render(guiGraphics, mouseX, mouseY, partialTicks); @@ -354,16 +316,7 @@ public void drawGuiContainerBackgroundLayer(GuiGraphics guiGraphics, float parti public void drawGuiContainerForegroundLayer(GuiGraphics guiGraphics, int mouseX, int mouseY) { drawGuiContainerLayer(guiGraphics, 0, 0, ContainerScreenTerminalStorage.DrawLayer.FOREGROUND, 0, mouseX, mouseY); - if (showEstimatedTickDuration) { - drawUnknownDurationTooltip(this.parentGui, guiGraphics, getEstimatedDurationString(), estimatedTickDuration, - getX() - 4, getY() - 14, mouseX, mouseY); - } - } - - protected String getEstimatedDurationString() { - return getDurationString(estimatedTickDurationRemaining - ? "gui.integratedterminals.terminal_crafting_job.craftingplan.duration.remaining" - : "gui.integratedterminals.terminal_crafting_job.craftingplan.duration.estimate", estimatedTickDuration); + header.drawForeground(this.parentGui, guiGraphics, getX(), getY(), mouseX, mouseY); } /** @@ -442,9 +395,11 @@ protected static void addElements(@Nullable Element parent, int indent, ITermina .collect(Collectors.toList()), 0, valid ? craftingPlan.getCraftingQuantity() : 0, + valid ? craftingPlan.getCraftingQuantityTotal() : 0, valid ? 0 : craftingPlan.getCraftingQuantity(), craftingPlan.getStatus().getColor(), craftingPlan.getStatus()); + currentElement.markCraftingJob(); if (parent != null) { parent.addChild(currentElement); } @@ -468,7 +423,7 @@ protected static void addElements(@Nullable Element parent, int indent, ITermina for (IPrototypedIngredient bufferedIngredient : craftingPlan.getBufferedIngredients()) { elements.add(currentElement.addChild(new Element(elementId++, indent + 1, Collections.singletonList(Collections.singletonList(bufferedIngredient)), bufferedIngredient.getComponent().getMatcher().getQuantity(bufferedIngredient.getPrototype()), - 0, 0, TerminalCraftingJobStatus.FINISHED.getColor(), TerminalCraftingJobStatus.FINISHED))); + 0, 0, TerminalCraftingJobStatus.FINISHED.getColor(), TerminalCraftingJobStatus.FINISHED).markStoredIngredient())); } } for (ITerminalCraftingPlan dependency : craftingPlan.getDependencies()) { @@ -485,27 +440,37 @@ protected void updateWidgetNarration(NarrationElementOutput p_259858_) { } - public static class Element { + public static class Element implements CraftingPlanGraphLayout.INode { private final int id; private final int indent; - private final List>> outputs; + private List>> outputs; private final long storageQuantity; private final long craftQuantity; + private final long craftQuantityTotal; private final long missingQuantity; private final int color; private final List children; private final TerminalCraftingJobStatus status; private boolean enabled; + private boolean storedIngredient; + private boolean craftingJob; + private boolean completed; public Element(int id, int indent, List> outputs, long storageQuantity, long craftQuantity, long missingQuantity, int color, TerminalCraftingJobStatus status) { + this(id, indent, outputs, storageQuantity, craftQuantity, craftQuantity, missingQuantity, color, status); + } + + public Element(int id, int indent, List> outputs, long storageQuantity, long craftQuantity, + long craftQuantityTotal, long missingQuantity, int color, TerminalCraftingJobStatus status) { this.id = id; this.indent = indent; this.outputs = (List) outputs; this.storageQuantity = storageQuantity; this.craftQuantity = craftQuantity; + this.craftQuantityTotal = craftQuantityTotal; this.missingQuantity = missingQuantity; this.color = color; this.status = status; @@ -534,6 +499,13 @@ public long getCraftQuantity() { return craftQuantity; } + /** + * @return The number of instances this job crafts in total, including the ones that were crafted already. + */ + public long getCraftQuantityTotal() { + return craftQuantityTotal; + } + public long getMissingQuantity() { return missingQuantity; } @@ -547,10 +519,16 @@ public Element addChild(Element element) { return element; } + @Override public List getChildren() { return children; } + @Override + public boolean isVisible() { + return isEnabled(); + } + public boolean isEnabled() { return enabled; } @@ -562,6 +540,49 @@ public void setEnabled(boolean enabled) { public TerminalCraftingJobStatus getStatus() { return status; } + + /** + * Mark this element as an ingredient that is taken from storage, instead of a crafting job. + * @return This element. + */ + public Element markStoredIngredient() { + this.storedIngredient = true; + return this; + } + + public boolean isStoredIngredient() { + return storedIngredient; + } + + /** + * Mark this element as a crafting job, instead of an ingredient that is stored or missing. + * @return This element. + */ + public Element markCraftingJob() { + this.craftingJob = true; + return this; + } + + public boolean isCraftingJob() { + return craftingJob; + } + + /** + * Mark this element as a crafting job that has been completed, and that is no longer part of the plan. + * @return This element. + */ + public Element markCompleted() { + this.completed = true; + return this; + } + + public boolean isCompleted() { + return completed; + } + + public void setOutputs(List>> outputs) { + this.outputs = outputs; + } } } diff --git a/src/main/java/org/cyclops/integratedterminals/client/gui/container/component/GuiCraftingPlanButtons.java b/src/main/java/org/cyclops/integratedterminals/client/gui/container/component/GuiCraftingPlanButtons.java new file mode 100644 index 000000000..47948131c --- /dev/null +++ b/src/main/java/org/cyclops/integratedterminals/client/gui/container/component/GuiCraftingPlanButtons.java @@ -0,0 +1,169 @@ +package org.cyclops.integratedterminals.client.gui.container.component; + +import com.google.common.collect.Lists; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.components.Button; +import net.minecraft.client.gui.components.Tooltip; +import net.minecraft.network.chat.Component; +import org.cyclops.cyclopscore.client.gui.component.button.ButtonImage; +import org.cyclops.cyclopscore.client.gui.image.IImage; +import org.cyclops.cyclopscore.helper.Helpers; +import org.cyclops.integratedterminals.client.gui.image.Images; + +import java.util.List; +import java.util.function.BooleanSupplier; + +/** + * Buttons that are shared by the crafting plan guis. + * @author rubensworks + */ +public class GuiCraftingPlanButtons { + + /** + * Create buttons for switching to each of the available display modes. + * @param toggler The display mode toggler. + * @param x The X position of the first button. + * @param y The Y position of the buttons. + * @param onChange Called after the display mode was changed. + * @return The buttons. + */ + public static List