From f210365f64f4377375934b94a13f7b1db9eb15ac Mon Sep 17 00:00:00 2001 From: rubensworks Date: Tue, 1 Sep 2026 15:19:52 +0000 Subject: [PATCH 1/3] Add ender-upgradable storage terminals with an ender chest tab Storage terminals can be ender-upgraded by right-clicking them with an eye of ender, which unlocks an additional Ender Storage tab that exposes the player's ender chest inventory. Portable storage terminals can be ender-upgraded by combining them with an eye of ender in a crafting grid, which retains the network they were linked to. Ender-upgraded terminals emit ender particles, and give back the eye of ender when they are broken. The ender chest tab uses regular container slots, so it has no channel selector, search field or variable filter slots. Tabs can now hide those via ITerminalStorageTabClient, and can apply regular quick move semantics via ITerminalStorageTabCommon#handleQuickMove. Based on the work in #191. Closes #177 Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01Kzc5LxSaedCXCK4pynhJV3 --- .../integratedterminals/GeneralConfig.java | 2 + .../IntegratedTerminals.java | 8 + .../integratedterminals/RegistryEntries.java | 5 + .../TerminalStorageEnderUpgradedTrigger.java | 52 ++++ ...inalStorageEnderUpgradedTriggerConfig.java | 20 ++ .../ITerminalStorageTabClient.java | 21 ++ .../ITerminalStorageTabCommon.java | 17 ++ .../ITerminalStorageTabRegistry.java | 14 + .../ContainerScreenTerminalStorage.java | 59 ++++- .../DataComponentEnderUpgradedConfig.java | 18 ++ .../TerminalStorageTabEnderChest.java | 43 +++ .../TerminalStorageTabEnderChestClient.java | 185 +++++++++++++ .../TerminalStorageTabEnderChestCommon.java | 133 ++++++++++ .../TerminalStorageTabEnderChestServer.java | 41 +++ .../TerminalStorageTabRegistry.java | 6 + .../terminalstorage/TerminalStorageTabs.java | 5 + ...meTestAdvancementsIntegratedTerminals.java | 56 +++- .../GameTestTerminalStorageEnderChest.java | 248 ++++++++++++++++++ .../ContainerTerminalStorageBase.java | 23 ++ .../ContainerTerminalStorageItem.java | 9 + .../ContainerTerminalStoragePart.java | 15 ++ .../item/ItemTerminalStoragePortable.java | 13 + .../part/PartTypeTerminalStorage.java | 85 ++++++ ...inalStoragePortableEnderUpgradeConfig.java | 54 ++++ .../integratedterminals/lang/en_us.json | 16 ++ .../ender_upgrade_storage_terminal.json | 20 ++ .../info/terminals_info.xml | 9 + .../info/terminals_tutorials.xml | 16 ++ ...rminal_storage_portable_ender_upgrade.json | 12 + 29 files changed, 1188 insertions(+), 17 deletions(-) create mode 100644 src/main/java/org/cyclops/integratedterminals/advancement/criterion/TerminalStorageEnderUpgradedTrigger.java create mode 100644 src/main/java/org/cyclops/integratedterminals/advancement/criterion/TerminalStorageEnderUpgradedTriggerConfig.java create mode 100644 src/main/java/org/cyclops/integratedterminals/component/DataComponentEnderUpgradedConfig.java create mode 100644 src/main/java/org/cyclops/integratedterminals/core/terminalstorage/TerminalStorageTabEnderChest.java create mode 100644 src/main/java/org/cyclops/integratedterminals/core/terminalstorage/TerminalStorageTabEnderChestClient.java create mode 100644 src/main/java/org/cyclops/integratedterminals/core/terminalstorage/TerminalStorageTabEnderChestCommon.java create mode 100644 src/main/java/org/cyclops/integratedterminals/core/terminalstorage/TerminalStorageTabEnderChestServer.java create mode 100644 src/main/java/org/cyclops/integratedterminals/gametest/GameTestTerminalStorageEnderChest.java create mode 100644 src/main/java/org/cyclops/integratedterminals/recipe/RecipeTerminalStoragePortableEnderUpgradeConfig.java create mode 100644 src/main/resources/data/integratedterminals/advancement/storage_terminal_ender/ender_upgrade_storage_terminal.json create mode 100644 src/main/resources/data/integratedterminals/recipe/crafting/terminal_storage_portable_ender_upgrade.json diff --git a/src/main/java/org/cyclops/integratedterminals/GeneralConfig.java b/src/main/java/org/cyclops/integratedterminals/GeneralConfig.java index 70dd1cd2bf..febcbea66d 100644 --- a/src/main/java/org/cyclops/integratedterminals/GeneralConfig.java +++ b/src/main/java/org/cyclops/integratedterminals/GeneralConfig.java @@ -27,6 +27,8 @@ public class GeneralConfig extends DummyConfig { public static boolean terminalStorageDefaultToCraftingPlanTree = 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) + public static boolean terminalStorageTabEnderChestEnabled = true; @ConfigurableProperty(category = "machine", comment = "The number of items that should be selected when clicking on an item in the storage terminal.", isCommandable = true) public static int guiStorageItemInitialQuantity = 64; diff --git a/src/main/java/org/cyclops/integratedterminals/IntegratedTerminals.java b/src/main/java/org/cyclops/integratedterminals/IntegratedTerminals.java index fda5cbd11b..c22e0584b9 100644 --- a/src/main/java/org/cyclops/integratedterminals/IntegratedTerminals.java +++ b/src/main/java/org/cyclops/integratedterminals/IntegratedTerminals.java @@ -21,9 +21,11 @@ import org.cyclops.integratedterminals.api.terminalstorage.ITerminalStorageTabRegistry; import org.cyclops.integratedterminals.api.terminalstorage.crafting.ITerminalStorageTabIngredientCraftingHandlerRegistry; import org.cyclops.integratedterminals.api.terminalstorage.location.ITerminalStorageLocationRegistry; +import org.cyclops.integratedterminals.advancement.criterion.TerminalStorageEnderUpgradedTriggerConfig; import org.cyclops.integratedterminals.block.BlockChorusGlassConfig; import org.cyclops.integratedterminals.block.BlockMenrilGlassConfig; import org.cyclops.integratedterminals.capability.ingredient.TerminalIngredientComponentCapabilities; +import org.cyclops.integratedterminals.component.DataComponentEnderUpgradedConfig; import org.cyclops.integratedterminals.component.DataComponentTerminalStorageInventoriesConfig; import org.cyclops.integratedterminals.component.DataComponentTerminalStorageStateConfig; import org.cyclops.integratedterminals.core.terminalstorage.TerminalStorageTabRegistry; @@ -36,6 +38,7 @@ import org.cyclops.integratedterminals.item.ItemTerminalStoragePortableConfig; import org.cyclops.integratedterminals.modcompat.integratedcrafting.IntegratedCraftingModCompat; import org.cyclops.integratedterminals.part.PartTypes; +import org.cyclops.integratedterminals.recipe.RecipeTerminalStoragePortableEnderUpgradeConfig; import org.cyclops.integratedterminals.proxy.ClientProxy; import org.cyclops.integratedterminals.proxy.CommonProxy; @@ -122,6 +125,11 @@ public void onConfigsRegister(ConfigHandler configHandler) { configHandler.addConfigurable(new DataComponentTerminalStorageInventoriesConfig()); configHandler.addConfigurable(new DataComponentTerminalStorageStateConfig()); + configHandler.addConfigurable(new DataComponentEnderUpgradedConfig()); + + configHandler.addConfigurable(new RecipeTerminalStoragePortableEnderUpgradeConfig()); + + configHandler.addConfigurable(new TerminalStorageEnderUpgradedTriggerConfig()); } @Override diff --git a/src/main/java/org/cyclops/integratedterminals/RegistryEntries.java b/src/main/java/org/cyclops/integratedterminals/RegistryEntries.java index ef78a6bd98..b8f9692a5b 100644 --- a/src/main/java/org/cyclops/integratedterminals/RegistryEntries.java +++ b/src/main/java/org/cyclops/integratedterminals/RegistryEntries.java @@ -6,7 +6,9 @@ import net.minecraft.resources.ResourceLocation; import net.minecraft.world.inventory.MenuType; import net.minecraft.world.item.Item; +import net.minecraft.world.item.crafting.RecipeSerializer; import net.neoforged.neoforge.registries.DeferredHolder; +import org.cyclops.cyclopscore.recipe.type.RecipeCraftingShapelessCustomOutput; import org.cyclops.integrateddynamics.core.item.ItemBlockEnergyContainer; import org.cyclops.integratedterminals.inventory.container.ContainerTerminalCraftingJobs; import org.cyclops.integratedterminals.inventory.container.ContainerTerminalCraftingJobsPlan; @@ -38,5 +40,8 @@ public class RegistryEntries { public static final DeferredHolder, DataComponentType> COMPONENT_TERMINAL_STORAGE_INVENTORIES = DeferredHolder.create(Registries.DATA_COMPONENT_TYPE, ResourceLocation.parse("integratedterminals:terminal_storage_inventories")); public static final DeferredHolder, DataComponentType> COMPONENT_TERMINAL_STORAGE_STATE = DeferredHolder.create(Registries.DATA_COMPONENT_TYPE, ResourceLocation.parse("integratedterminals:terminal_storage_state")); + public static final DeferredHolder, DataComponentType> COMPONENT_ENDER_UPGRADED = DeferredHolder.create(Registries.DATA_COMPONENT_TYPE, ResourceLocation.parse("integratedterminals:ender_upgraded")); + + public static final DeferredHolder, RecipeSerializer> RECIPESERIALIZER_TERMINAL_STORAGE_PORTABLE_ENDER_UPGRADE = DeferredHolder.create(Registries.RECIPE_SERIALIZER, ResourceLocation.parse("integratedterminals:crafting_special_terminal_storage_portable_ender_upgrade")); } diff --git a/src/main/java/org/cyclops/integratedterminals/advancement/criterion/TerminalStorageEnderUpgradedTrigger.java b/src/main/java/org/cyclops/integratedterminals/advancement/criterion/TerminalStorageEnderUpgradedTrigger.java new file mode 100644 index 0000000000..af984a0ba7 --- /dev/null +++ b/src/main/java/org/cyclops/integratedterminals/advancement/criterion/TerminalStorageEnderUpgradedTrigger.java @@ -0,0 +1,52 @@ +package org.cyclops.integratedterminals.advancement.criterion; + +import com.mojang.serialization.Codec; +import com.mojang.serialization.codecs.RecordCodecBuilder; +import net.minecraft.advancements.critereon.ContextAwarePredicate; +import net.minecraft.advancements.critereon.EntityPredicate; +import net.minecraft.advancements.critereon.SimpleCriterionTrigger; +import net.minecraft.server.level.ServerPlayer; + +import javax.annotation.Nullable; +import java.util.Optional; + +/** + * Triggers when a storage terminal is ender-upgraded. + * @author rubensworks + */ +public class TerminalStorageEnderUpgradedTrigger extends SimpleCriterionTrigger { + + @Nullable + private static TerminalStorageEnderUpgradedTrigger INSTANCE = null; + + public static final Codec CODEC = RecordCodecBuilder.create( + builder -> builder.group( + EntityPredicate.ADVANCEMENT_CODEC.optionalFieldOf("player").forGetter(TerminalStorageEnderUpgradedTrigger.Instance::player) + ) + .apply(builder, TerminalStorageEnderUpgradedTrigger.Instance::new) + ); + + public TerminalStorageEnderUpgradedTrigger() { + INSTANCE = this; + } + + @Override + public Codec codec() { + return CODEC; + } + + /** + * Trigger this criterion for the given player. + * @param player The player that ender-upgraded a storage terminal. + */ + public static void onEnderUpgraded(ServerPlayer player) { + if (INSTANCE != null) { + INSTANCE.trigger(player, instance -> true); + } + } + + public static record Instance( + Optional player + ) implements SimpleCriterionTrigger.SimpleInstance { + } +} diff --git a/src/main/java/org/cyclops/integratedterminals/advancement/criterion/TerminalStorageEnderUpgradedTriggerConfig.java b/src/main/java/org/cyclops/integratedterminals/advancement/criterion/TerminalStorageEnderUpgradedTriggerConfig.java new file mode 100644 index 0000000000..f0c7d1c4c1 --- /dev/null +++ b/src/main/java/org/cyclops/integratedterminals/advancement/criterion/TerminalStorageEnderUpgradedTriggerConfig.java @@ -0,0 +1,20 @@ +package org.cyclops.integratedterminals.advancement.criterion; + +import org.cyclops.cyclopscore.config.extendedconfig.CriterionTriggerConfig; +import org.cyclops.integratedterminals.IntegratedTerminals; + +/** + * Config for {@link TerminalStorageEnderUpgradedTrigger}. + * @author rubensworks + */ +public class TerminalStorageEnderUpgradedTriggerConfig extends CriterionTriggerConfig { + + public TerminalStorageEnderUpgradedTriggerConfig() { + super( + IntegratedTerminals._instance, + "terminal_storage_ender_upgraded", + new TerminalStorageEnderUpgradedTrigger() + ); + } + +} diff --git a/src/main/java/org/cyclops/integratedterminals/api/terminalstorage/ITerminalStorageTabClient.java b/src/main/java/org/cyclops/integratedterminals/api/terminalstorage/ITerminalStorageTabClient.java index 6db5dae819..e6cf63d2b1 100644 --- a/src/main/java/org/cyclops/integratedterminals/api/terminalstorage/ITerminalStorageTabClient.java +++ b/src/main/java/org/cyclops/integratedterminals/api/terminalstorage/ITerminalStorageTabClient.java @@ -188,6 +188,27 @@ public boolean handleScroll(AbstractContainerMenu container, int channel, int ho */ public List> getButtons(); + /** + * @return If the search field should be shown for this tab. + */ + public default boolean hasSearchField() { + return true; + } + + /** + * @return If the channel field should be shown for this tab. + */ + public default boolean hasChannelField() { + return true; + } + + /** + * @return If the variable-based filter slots should be shown for this tab. + */ + public default boolean hasVariableFilterSlots() { + return true; + } + public default int getSlotOffsetX() { return DEFAULT_SLOT_OFFSET_X; } diff --git a/src/main/java/org/cyclops/integratedterminals/api/terminalstorage/ITerminalStorageTabCommon.java b/src/main/java/org/cyclops/integratedterminals/api/terminalstorage/ITerminalStorageTabCommon.java index cdf38d0692..cb449e33fa 100644 --- a/src/main/java/org/cyclops/integratedterminals/api/terminalstorage/ITerminalStorageTabCommon.java +++ b/src/main/java/org/cyclops/integratedterminals/api/terminalstorage/ITerminalStorageTabCommon.java @@ -10,6 +10,7 @@ import net.minecraft.world.item.ItemStack; import org.apache.commons.lang3.tuple.Pair; import org.cyclops.integrateddynamics.api.evaluate.variable.ValueDeseralizationContext; +import org.cyclops.integratedterminals.inventory.container.ContainerTerminalStorageBase; import javax.annotation.Nullable; import java.util.Collections; @@ -37,6 +38,22 @@ public default void onUpdate(AbstractContainerMenu container, Player player, } + /** + * Handle a quick move (shift-click) on the given container slot. + * + * This is only called for the tab that is currently selected, + * and allows tabs with regular container slots to apply regular quick move semantics. + * + * @param container The active container. + * @param player The player that is quick moving. + * @param slotIndex The index of the slot that is being quick moved. + * @return The moved stack following {@link AbstractContainerMenu#quickMoveStack} semantics, + * or {@link Optional#empty()} if this tab does not handle quick moves. + */ + public default Optional handleQuickMove(ContainerTerminalStorageBase container, Player player, int slotIndex) { + return Optional.empty(); + } + public static interface IVariableInventory { public default void loadNamedInventory(String name, Container inventory, HolderLookup.Provider holderLookupProvider) { NonNullList tabItems = this.getNamedInventory(name, holderLookupProvider); diff --git a/src/main/java/org/cyclops/integratedterminals/api/terminalstorage/ITerminalStorageTabRegistry.java b/src/main/java/org/cyclops/integratedterminals/api/terminalstorage/ITerminalStorageTabRegistry.java index 3d3718bb32..0fb1dd2410 100644 --- a/src/main/java/org/cyclops/integratedterminals/api/terminalstorage/ITerminalStorageTabRegistry.java +++ b/src/main/java/org/cyclops/integratedterminals/api/terminalstorage/ITerminalStorageTabRegistry.java @@ -20,6 +20,20 @@ public interface ITerminalStorageTabRegistry extends IRegistry { */ public T register(T tab); + /** + * Register a new tab, and make sure it is placed after all currently registered tabs. + * + * This is useful for tabs that are registered from an event that is fired multiple times, + * as those would otherwise end up in-between the tabs that are registered from later firings. + * + * @param tab The tab to register. + * @param The tab type. + * @return The registered tab. + */ + public default T registerLast(T tab) { + return register(tab); + } + /** * Get a tab by unique name. * @param name The tab name. 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 4e35ac42e7..126d98814a 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 @@ -114,7 +114,7 @@ public void init() { Component.translatable("gui.integratedterminals.channel"), true, getMenu().getChannelStrings()); fieldChannel.setMaxLength(15); - fieldChannel.setVisible(true); + fieldChannel.setVisible(hasChannelField()); fieldChannel.setTextColor(16777215); fieldChannel.setCanLoseFocus(true); fieldChannel.setEditable(true); @@ -148,7 +148,7 @@ public int getVisibleRows() { fieldSearch = new WidgetTextFieldExtended(Minecraft.getInstance().font, leftPos + SEARCH_X, topPos + SEARCH_Y, getSearchWidth() - 10, SEARCH_HEIGHT, Component.translatable("gui.cyclopscore.search")); fieldSearch.setMaxLength(50); - fieldSearch.setVisible(true); + fieldSearch.setVisible(hasSearchField()); fieldSearch.setTextColor(16777215); fieldSearch.setCanLoseFocus(true); fieldSearch.setEditable(true); @@ -215,7 +215,7 @@ public void repositionInventorySlots() { @Override public void containerTick() { super.containerTick(); - if (!initialized && getSelectedClientTab().isPresent()) { + if (!initialized && getSelectedClientTab().isPresent() && hasSearchField()) { initialized = true; String filter = getSelectedClientTab().get().getInstanceFilter(getMenu().getSelectedChannel()); if (filter != null && !"".equals(filter)) { @@ -268,6 +268,24 @@ protected int getPlayerInventoryOffsetY() { .orElse(0); } + public boolean hasSearchField() { + return getSelectedClientTab() + .map(ITerminalStorageTabClient::hasSearchField) + .orElse(true); + } + + public boolean hasChannelField() { + return getSelectedClientTab() + .map(ITerminalStorageTabClient::hasChannelField) + .orElse(true); + } + + public boolean hasVariableFilterSlots() { + return getSelectedClientTab() + .map(ITerminalStorageTabClient::hasVariableFilterSlots) + .orElse(true); + } + @Override protected void renderBg(GuiGraphics guiGraphics, float f, int mouseX, int mouseY) { //super.renderBg(matrixStack, f, mouseX, mouseY); @@ -275,8 +293,12 @@ protected void renderBg(GuiGraphics guiGraphics, float f, int mouseX, int mouseY this.renderBgTab(guiGraphics, f, mouseX, mouseY); this.renderBgPlayerInventory(guiGraphics, f, mouseX, mouseY); - fieldChannel.render(guiGraphics, mouseX, mouseY, f); - fieldSearch.render(guiGraphics, mouseX, mouseY, f); + if (hasChannelField()) { + fieldChannel.render(guiGraphics, mouseX, mouseY, f); + } + if (hasSearchField()) { + fieldSearch.render(guiGraphics, mouseX, mouseY, f); + } drawTabsBackground(guiGraphics); drawTabContents(guiGraphics, getMenu().getSelectedTab(), getMenu().getSelectedChannel(), DrawLayer.BACKGROUND, f, getGuiLeftTotal() + getSlotsOffsetX(), getGuiTopTotal() + getSlotsOffsetY(), mouseX, mouseY); @@ -352,9 +374,11 @@ protected void renderBgTab(GuiGraphics guiGraphics, float f, int mouseX, int mou guiGraphics.blit(this.texture, leftPos + getGridXSize() + 32, topPos + SCROLL_Y + getScrollHeight() - 2, 20, 101, 14, 1); // bottom // Textbox background - guiGraphics.blit(this.texture, leftPos + SEARCH_X - 1, topPos + SEARCH_Y - 2, 28, 0, 1, SEARCH_HEIGHT - 8); // left - blitRescalable(guiGraphics, this.texture, leftPos + SEARCH_X, topPos + SEARCH_Y - 2, blitOffset, 29, 0, 1, SEARCH_HEIGHT - 8, 256, 256, getSearchWidth(), SEARCH_HEIGHT - 8); // middle - guiGraphics.blit(this.texture, leftPos + SEARCH_X + getSearchWidth() - 1, topPos + SEARCH_Y - 2, 117, 0, 1, SEARCH_HEIGHT - 8); // right + if (hasSearchField()) { + guiGraphics.blit(this.texture, leftPos + SEARCH_X - 1, topPos + SEARCH_Y - 2, 28, 0, 1, SEARCH_HEIGHT - 8); // left + blitRescalable(guiGraphics, this.texture, leftPos + SEARCH_X, topPos + SEARCH_Y - 2, blitOffset, 29, 0, 1, SEARCH_HEIGHT - 8, 256, 256, getSearchWidth(), SEARCH_HEIGHT - 8); // middle + guiGraphics.blit(this.texture, leftPos + SEARCH_X + getSearchWidth() - 1, topPos + SEARCH_Y - 2, 117, 0, 1, SEARCH_HEIGHT - 8); // right + } // Render tab-specific things getSelectedClientTab().ifPresent(tab -> tab.onTabBackgroundRender(this, guiGraphics, f, mouseX, mouseY)); @@ -368,8 +392,10 @@ protected void renderBgPlayerInventory(GuiGraphics guiGraphics, float f, int mou // Render player inventory guiGraphics.blit(this.texture, leftPos + (getGridXSize() / 2) - (9 * GuiHelpers.SLOT_SIZE / 2) + getPlayerInventoryOffsetX() + 3, topPos + 52 + getGridYSize() + getPlayerInventoryOffsetY() , 34, 24, 216, 93); - // Auxiliary slots - guiGraphics.blit(this.texture, leftPos + (getGridXSize() / 2) + (9 * GuiHelpers.SLOT_SIZE / 2) + getPlayerInventoryOffsetX() + 57, topPos + 61 + getGridYSize() + getPlayerInventoryOffsetY(), 0, 12, 20, 57); + // Variable-based filter slots + if (hasVariableFilterSlots()) { + guiGraphics.blit(this.texture, leftPos + (getGridXSize() / 2) + (9 * GuiHelpers.SLOT_SIZE / 2) + getPlayerInventoryOffsetX() + 57, topPos + 61 + getGridYSize() + getPlayerInventoryOffsetY(), 0, 12, 20, 57); + } } @Override @@ -528,7 +554,7 @@ && mouseX > getGuiLeft() + TAB_OFFSET_X } // Update channel when changing channel field - if (this.fieldChannel.mouseClicked(mouseX, mouseY, mouseButton)) { + if (hasChannelField() && this.fieldChannel.mouseClicked(mouseX, mouseY, mouseButton)) { int channel; try { channel = Integer.parseInt(this.fieldChannel.getActiveElement()); @@ -578,7 +604,9 @@ && mouseX > getGuiLeft() + TAB_OFFSET_X } // Click in search field - fieldSearch.mouseClicked(mouseX, mouseY, mouseButton); + if (hasSearchField()) { + fieldSearch.mouseClicked(mouseX, mouseY, mouseButton); + } // Handle buttons clicks tabOptional.ifPresent(tab -> { @@ -735,6 +763,9 @@ public boolean mouseScrolled(double mouseX, double mouseY, double mouseZ, double protected boolean handleKeyCodeFirst(int keyCode, int scanCode) { InputConstants.Key inputCode = InputConstants.getKey(keyCode, scanCode); if (org.cyclops.integrateddynamics.proxy.ClientProxy.FOCUS_LP_SEARCH.isActiveAndMatches(inputCode)) { + if (!hasSearchField()) { + return false; + } fieldSearch.setFocused(true); swallowNextCharacter = true; return true; @@ -879,7 +910,9 @@ protected void drawTabsBackground(GuiGraphics guiGraphics) { int offsetX = TAB_OFFSET_X; // Draw channels label - guiGraphics.drawString(font, L10NHelpers.localize("gui.integratedterminals.terminal_storage.channel"), getGuiLeft() + 30, getGuiTop() + 26, 16777215); + if (hasChannelField()) { + guiGraphics.drawString(font, L10NHelpers.localize("gui.integratedterminals.terminal_storage.channel"), getGuiLeft() + 30, getGuiTop() + 26, 16777215); + } // Draw all tabs next to each other horizontally for (ITerminalStorageTabClient tab : getMenu().getTabsClient().values()) { diff --git a/src/main/java/org/cyclops/integratedterminals/component/DataComponentEnderUpgradedConfig.java b/src/main/java/org/cyclops/integratedterminals/component/DataComponentEnderUpgradedConfig.java new file mode 100644 index 0000000000..20a9a8c5d8 --- /dev/null +++ b/src/main/java/org/cyclops/integratedterminals/component/DataComponentEnderUpgradedConfig.java @@ -0,0 +1,18 @@ +package org.cyclops.integratedterminals.component; + +import com.mojang.serialization.Codec; +import net.minecraft.network.codec.ByteBufCodecs; +import org.cyclops.cyclopscore.config.extendedconfig.DataComponentConfig; +import org.cyclops.integratedterminals.IntegratedTerminals; + +/** + * @author rubensworks + */ +public class DataComponentEnderUpgradedConfig extends DataComponentConfig { + + public DataComponentEnderUpgradedConfig() { + super(IntegratedTerminals._instance, "ender_upgraded", builder -> builder + .persistent(Codec.BOOL) + .networkSynchronized(ByteBufCodecs.BOOL)); + } +} diff --git a/src/main/java/org/cyclops/integratedterminals/core/terminalstorage/TerminalStorageTabEnderChest.java b/src/main/java/org/cyclops/integratedterminals/core/terminalstorage/TerminalStorageTabEnderChest.java new file mode 100644 index 0000000000..d23b26f276 --- /dev/null +++ b/src/main/java/org/cyclops/integratedterminals/core/terminalstorage/TerminalStorageTabEnderChest.java @@ -0,0 +1,43 @@ +package org.cyclops.integratedterminals.core.terminalstorage; + +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.entity.player.Player; +import org.cyclops.integrateddynamics.api.network.INetwork; +import org.cyclops.integratedterminals.Reference; +import org.cyclops.integratedterminals.api.terminalstorage.ITerminalStorageTab; +import org.cyclops.integratedterminals.api.terminalstorage.ITerminalStorageTabClient; +import org.cyclops.integratedterminals.api.terminalstorage.ITerminalStorageTabCommon; +import org.cyclops.integratedterminals.api.terminalstorage.ITerminalStorageTabServer; +import org.cyclops.integratedterminals.inventory.container.ContainerTerminalStorageBase; + +/** + * A storage terminal tab that exposes the ender chest inventory of the player. + * + * This tab is only available in terminals that have been ender-upgraded. + * + * @author rubensworks + */ +public class TerminalStorageTabEnderChest implements ITerminalStorageTab { + + public static final ResourceLocation NAME = ResourceLocation.fromNamespaceAndPath(Reference.MOD_ID, "ender_chest"); + + @Override + public ResourceLocation getName() { + return NAME; + } + + @Override + public ITerminalStorageTabClient createClientTab(ContainerTerminalStorageBase container, Player player) { + return new TerminalStorageTabEnderChestClient(container, getName()); + } + + @Override + public ITerminalStorageTabServer createServerTab(ContainerTerminalStorageBase container, Player player, INetwork network) { + return new TerminalStorageTabEnderChestServer(getName()); + } + + @Override + public ITerminalStorageTabCommon createCommonTab(ContainerTerminalStorageBase container, Player player) { + return new TerminalStorageTabEnderChestCommon(container, getName()); + } +} diff --git a/src/main/java/org/cyclops/integratedterminals/core/terminalstorage/TerminalStorageTabEnderChestClient.java b/src/main/java/org/cyclops/integratedterminals/core/terminalstorage/TerminalStorageTabEnderChestClient.java new file mode 100644 index 0000000000..1dc11e165d --- /dev/null +++ b/src/main/java/org/cyclops/integratedterminals/core/terminalstorage/TerminalStorageTabEnderChestClient.java @@ -0,0 +1,185 @@ +package org.cyclops.integratedterminals.core.terminalstorage; + +import com.google.common.collect.Lists; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.inventory.AbstractContainerMenu; +import net.minecraft.world.inventory.Slot; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import org.cyclops.integratedterminals.GeneralConfig; +import org.cyclops.integratedterminals.api.terminalstorage.ITerminalButton; +import org.cyclops.integratedterminals.api.terminalstorage.ITerminalRowColumnProvider; +import org.cyclops.integratedterminals.api.terminalstorage.ITerminalStorageSlot; +import org.cyclops.integratedterminals.api.terminalstorage.ITerminalStorageTabClient; +import org.cyclops.integratedterminals.inventory.container.ContainerTerminalStorageBase; + +import java.util.Collections; +import java.util.List; +import java.util.Set; + +/** + * A client-side storage terminal tab that exposes the ender chest inventory of the player. + * + * The ender chest contents are shown through regular container slots, + * so this tab does not expose any storage slots of its own. + * + * @author rubensworks + */ +public class TerminalStorageTabEnderChestClient implements ITerminalStorageTabClient { + + private static final ITerminalRowColumnProvider ROW_COLUMN_PROVIDER = () -> new ITerminalRowColumnProvider + .RowsAndColumns(TerminalStorageTabEnderChestCommon.ROWS, TerminalStorageTabEnderChestCommon.COLUMNS); + + private final ContainerTerminalStorageBase container; + private final ResourceLocation name; + private final ItemStack icon; + + public TerminalStorageTabEnderChestClient(ContainerTerminalStorageBase container, ResourceLocation name) { + this.container = container; + this.name = name; + this.icon = new ItemStack(Items.ENDER_CHEST); + } + + /** + * @param container A storage terminal container. + * @return If the ender chest tab is available in the given terminal. + */ + public static boolean isAvailable(ContainerTerminalStorageBase container) { + return GeneralConfig.terminalStorageTabEnderChestEnabled && container.isEnderUpgraded(); + } + + @Override + public void onSelect(int channel) { + + } + + @Override + public void onDeselect(int channel) { + + } + + @Override + public ResourceLocation getName() { + return this.name; + } + + @Override + public ItemStack getIcon() { + return this.icon; + } + + @Override + public List getTooltip() { + return Lists.newArrayList(Component.translatable("gui.integratedterminals.terminal_storage.ender_chest")); + } + + @Override + public String getInstanceFilter(int channel) { + return ""; + } + + @Override + public void setInstanceFilter(int channel, String filter) { + + } + + @Override + public List getSlots(int channel, int offset, int limit) { + return Collections.emptyList(); + } + + @Override + public ITerminalRowColumnProvider getRowColumnProvider() { + return ROW_COLUMN_PROVIDER; + } + + @Override + public boolean isEnabled() { + return isAvailable(this.container); + } + + @Override + public int getSlotCount(int channel) { + return TerminalStorageTabEnderChestCommon.SIZE; + } + + @Override + public String getStatus(int channel) { + return ""; + } + + @Override + public int[] getChannels() { + return new int[0]; + } + + @Override + public void resetActiveSlot() { + + } + + @Override + public boolean handleClick(AbstractContainerMenu container, int channel, int hoveringStorageSlot, int mouseButton, + boolean hasClickedOutside, boolean hasClickedInStorage, int hoveredContainerSlot, + boolean isQuickMove) { + // The ender chest slots are regular container slots, so they are handled by the container itself + return false; + } + + @Override + public boolean handleScroll(AbstractContainerMenu container, int channel, int hoveringStorageSlot, double delta, + boolean hasClickedOutside, boolean hasClickedInStorage, int hoveredContainerSlot) { + return false; + } + + @Override + public int getActiveSlotId() { + return -1; + } + + @Override + public int getActiveSlotQuantity() { + return 0; + } + + @Override + public void setActiveSlotQuantity(int quantity) { + + } + + @Override + public List> getButtons() { + return Collections.emptyList(); + } + + @Override + public boolean hasSearchField() { + return false; + } + + @Override + public boolean hasChannelField() { + return false; + } + + @Override + public boolean hasVariableFilterSlots() { + return false; + } + + @Override + public boolean isSlotValidForDraggingInto(int channel, Slot slot) { + return false; + } + + @Override + public int computeDraggingQuantity(Set dragSlots, int dragMode, ItemStack stack, int quantity) { + return 0; + } + + @Override + public int dragIntoSlot(AbstractContainerMenu container, int channel, Slot slot, int quantity, boolean simulate) { + return 0; + } +} diff --git a/src/main/java/org/cyclops/integratedterminals/core/terminalstorage/TerminalStorageTabEnderChestCommon.java b/src/main/java/org/cyclops/integratedterminals/core/terminalstorage/TerminalStorageTabEnderChestCommon.java new file mode 100644 index 0000000000..60bc5a5495 --- /dev/null +++ b/src/main/java/org/cyclops/integratedterminals/core/terminalstorage/TerminalStorageTabEnderChestCommon.java @@ -0,0 +1,133 @@ +package org.cyclops.integratedterminals.core.terminalstorage; + +import com.google.common.collect.Lists; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.Container; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.inventory.AbstractContainerMenu; +import net.minecraft.world.inventory.Slot; +import net.minecraft.world.item.ItemStack; +import org.apache.commons.lang3.tuple.Pair; +import org.cyclops.cyclopscore.helper.GuiHelpers; +import org.cyclops.integrateddynamics.api.evaluate.variable.ValueDeseralizationContext; +import org.cyclops.integratedterminals.api.terminalstorage.ITerminalStorageTabClient; +import org.cyclops.integratedterminals.api.terminalstorage.ITerminalStorageTabCommon; +import org.cyclops.integratedterminals.inventory.container.ContainerTerminalStorageBase; + +import java.util.List; +import java.util.Optional; + +/** + * A common storage terminal tab that exposes the ender chest inventory of the player. + * @author rubensworks + */ +public class TerminalStorageTabEnderChestCommon implements ITerminalStorageTabCommon { + + public static final int ROWS = 3; + public static final int COLUMNS = 9; + public static final int SIZE = ROWS * COLUMNS; + + private static final int PLAYER_INVENTORY_SIZE = 36; + private static final int PLAYER_SLOTS_SIZE = PLAYER_INVENTORY_SIZE + 5; // Also includes the armor and offhand slots + + private final ContainerTerminalStorageBase container; + private final ResourceLocation name; + private final List slots; + + public TerminalStorageTabEnderChestCommon(ContainerTerminalStorageBase container, ResourceLocation name) { + this.container = container; + this.name = name; + this.slots = Lists.newArrayListWithCapacity(SIZE); + } + + @Override + public ResourceLocation getName() { + return this.name; + } + + /** + * @return If the ender chest tab can be interacted with in the terminal it is part of. + */ + public boolean isAvailable() { + return TerminalStorageTabEnderChestClient.isAvailable(this.container); + } + + @Override + public List> loadSlots(AbstractContainerMenu container, int startIndex, Player player, + Optional variableInventory, + ValueDeseralizationContext valueDeseralizationContext) { + List> slots = Lists.newArrayListWithCapacity(SIZE); + + Container enderChestInventory = player.getEnderChestInventory(); + for (int row = 0; row < ROWS; row++) { + for (int column = 0; column < COLUMNS; column++) { + int finalRow = row; + int finalColumn = column; + Slot slot = new Slot(enderChestInventory, column + row * COLUMNS, 0, 0) { + @Override + public boolean mayPlace(ItemStack stack) { + return isAvailable() && super.mayPlace(stack); + } + + @Override + public boolean mayPickup(Player player) { + return isAvailable() && super.mayPickup(player); + } + }; + this.slots.add(slot); + slots.add(Pair.of(slot, factors -> Pair.of( + factors.offsetX() + ITerminalStorageTabClient.DEFAULT_SLOT_OFFSET_X + finalColumn * GuiHelpers.SLOT_SIZE, + factors.offsetY() + ITerminalStorageTabClient.DEFAULT_SLOT_OFFSET_Y + finalRow * GuiHelpers.SLOT_SIZE + ))); + } + } + + return slots; + } + + /** + * @return The index of the first ender chest slot within the container, or -1 if the slots were not loaded. + */ + public int getFirstSlotIndex() { + return this.slots.isEmpty() ? -1 : this.slots.get(0).index; + } + + @Override + public Optional handleQuickMove(ContainerTerminalStorageBase container, Player player, int slotIndex) { + int firstSlotIndex = getFirstSlotIndex(); + if (!isAvailable() || firstSlotIndex < 0 || slotIndex < 0 || slotIndex >= container.slots.size()) { + return Optional.empty(); + } + + Slot slot = container.getSlot(slotIndex); + if (!slot.hasItem()) { + return Optional.of(ItemStack.EMPTY); + } + ItemStack slotStack = slot.getItem(); + ItemStack originalStack = slotStack.copy(); + + if (slotIndex >= firstSlotIndex && slotIndex < firstSlotIndex + SIZE) { + // Move from the ender chest into the player inventory + if (!container.moveStackTo(slotStack, 0, PLAYER_INVENTORY_SIZE, true)) { + return Optional.of(ItemStack.EMPTY); + } + } else if (slotIndex < PLAYER_SLOTS_SIZE) { + // Move from the player inventory into the ender chest + if (!container.moveStackTo(slotStack, firstSlotIndex, firstSlotIndex + SIZE, false)) { + return Optional.of(ItemStack.EMPTY); + } + } else { + // Slots of other tabs are not visible in this tab + return Optional.of(ItemStack.EMPTY); + } + + if (slotStack.isEmpty()) { + slot.setByPlayer(ItemStack.EMPTY); + } else { + slot.setChanged(); + } + slot.onTake(player, slotStack); + + return Optional.of(originalStack); + } +} diff --git a/src/main/java/org/cyclops/integratedterminals/core/terminalstorage/TerminalStorageTabEnderChestServer.java b/src/main/java/org/cyclops/integratedterminals/core/terminalstorage/TerminalStorageTabEnderChestServer.java new file mode 100644 index 0000000000..1c6e1ad1dc --- /dev/null +++ b/src/main/java/org/cyclops/integratedterminals/core/terminalstorage/TerminalStorageTabEnderChestServer.java @@ -0,0 +1,41 @@ +package org.cyclops.integratedterminals.core.terminalstorage; + +import net.minecraft.resources.ResourceLocation; +import org.cyclops.integratedterminals.api.terminalstorage.ITerminalStorageTabServer; + +/** + * A server-side storage terminal tab for the ender chest. + * + * The ender chest contents are exposed through regular container slots, + * so no server-side synchronization logic is needed here. + * + * @author rubensworks + */ +public class TerminalStorageTabEnderChestServer implements ITerminalStorageTabServer { + + private final ResourceLocation name; + + public TerminalStorageTabEnderChestServer(ResourceLocation name) { + this.name = name; + } + + @Override + public ResourceLocation getName() { + return this.name; + } + + @Override + public void init() { + + } + + @Override + public void deInit() { + + } + + @Override + public void updateActive(int channel) { + + } +} diff --git a/src/main/java/org/cyclops/integratedterminals/core/terminalstorage/TerminalStorageTabRegistry.java b/src/main/java/org/cyclops/integratedterminals/core/terminalstorage/TerminalStorageTabRegistry.java index ccbbce0589..886901382c 100644 --- a/src/main/java/org/cyclops/integratedterminals/core/terminalstorage/TerminalStorageTabRegistry.java +++ b/src/main/java/org/cyclops/integratedterminals/core/terminalstorage/TerminalStorageTabRegistry.java @@ -32,6 +32,12 @@ public T register(T tab) { return tab; } + @Override + public T registerLast(T tab) { + tabs.remove(tab.getName().toString()); + return register(tab); + } + @Nullable @Override public ITerminalStorageTab getTab(ResourceLocation name) { diff --git a/src/main/java/org/cyclops/integratedterminals/core/terminalstorage/TerminalStorageTabs.java b/src/main/java/org/cyclops/integratedterminals/core/terminalstorage/TerminalStorageTabs.java index 754b949959..591f3193e1 100644 --- a/src/main/java/org/cyclops/integratedterminals/core/terminalstorage/TerminalStorageTabs.java +++ b/src/main/java/org/cyclops/integratedterminals/core/terminalstorage/TerminalStorageTabs.java @@ -38,6 +38,11 @@ public static void afterIngredientComponentCapabilitiesRegistration(AttachCapabi && ingredientComponentItemStack.getCapability(Capabilities.PositionedAddonsNetworkIngredientsHandler.INGREDIENT).isPresent()) { TerminalStorageTabs.REGISTRY.register(new TerminalStorageTabIngredientComponentItemStackCrafting(ingredientComponentItemStack)); } + + // The ender chest tab is only shown in terminals that have been ender-upgraded. + // This event is fired once per ingredient component, + // so re-register the tab to keep it after all ingredient component tabs. + TerminalStorageTabs.REGISTRY.registerLast(new TerminalStorageTabEnderChest()); } } diff --git a/src/main/java/org/cyclops/integratedterminals/gametest/GameTestAdvancementsIntegratedTerminals.java b/src/main/java/org/cyclops/integratedterminals/gametest/GameTestAdvancementsIntegratedTerminals.java index c1d0c96c6e..84de52829c 100644 --- a/src/main/java/org/cyclops/integratedterminals/gametest/GameTestAdvancementsIntegratedTerminals.java +++ b/src/main/java/org/cyclops/integratedterminals/gametest/GameTestAdvancementsIntegratedTerminals.java @@ -17,6 +17,7 @@ import net.minecraft.server.level.ServerPlayer; import net.minecraft.world.SimpleContainer; import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; import net.neoforged.neoforge.common.NeoForge; import net.neoforged.neoforge.event.entity.player.PlayerContainerEvent; import net.neoforged.neoforge.event.entity.player.PlayerEvent; @@ -51,7 +52,7 @@ @PrefixGameTestTemplate(false) public class GameTestAdvancementsIntegratedTerminals { - private static boolean hasAdvancementUnlocked(ServerPlayer player, String advancementId) { + static boolean hasAdvancementUnlocked(ServerPlayer player, String advancementId) { ResourceLocation id = ResourceLocation.parse(advancementId); ServerAdvancementManager manager = player.server.getAdvancements(); AdvancementHolder holder = manager.get(id); @@ -77,13 +78,15 @@ private static void assertAdvancementNotUnlocked(GameTestHelper helper, ServerPl * {@code setValue} here also prevents the packet sends that happen inside the * {@link ContainerTerminalStoragePart} constructor (e.g. from {@code setSelectedChannel}). */ - private static ContainerTerminalStoragePart createSilentContainer(ServerPlayer player, PartTarget partTarget) { + static ContainerTerminalStoragePart createSilentContainer(ServerPlayer player, PartTarget partTarget) { return new ContainerTerminalStoragePart( 1, player.getInventory(), partTarget, PartTypes.TERMINAL_STORAGE, Optional.empty(), new TerminalStorageState(() -> {})) { @Override public void setValue(int valueId, CompoundTag value) { - // Suppress cyclopscore:value_notify packet sends in the game test environment. + // Store the value without sending a cyclopscore:value_notify packet, + // as those fail in the game test environment. + onUpdate(valueId, value); } }; } @@ -100,7 +103,7 @@ public void setValue(int valueId, CompoundTag value) { * been called (enabling the {@code containerListener} that fires advancement criteria), and the * player has been added to both the world and the player list. */ - private static ServerPlayer createMockServerPlayer(GameTestHelper helper) { + static ServerPlayer createMockServerPlayer(GameTestHelper helper) { CommonListenerCookie cookie = CommonListenerCookie.createInitial( new GameProfile(UUID.randomUUID(), "test-mock-player"), false); ServerPlayer player = new ServerPlayer( @@ -255,6 +258,51 @@ public void invalidate() {} "filter_enchantable advancement not unlocked")); } + /** + * Tests the ender_upgrade_storage_terminal advancement, + * triggered by right-clicking a storage terminal with an eye of ender. + */ + @GameTest(template = "empty", templateNamespace = "cyclopscore") + public void testAdvancementEnderUpgradeStorageTerminal(GameTestHelper helper) { + BlockPos pos = new BlockPos(1, 2, 1); + ServerPlayer player = createMockServerPlayer(helper); + + // Place cable block and add terminal_storage part + helper.setBlock(pos, RegistryEntries.BLOCK_CABLE.value()); + NetworkHelpers.initNetwork(helper.getLevel(), helper.absolutePos(pos), Direction.NORTH); + PartHelpers.addPart(helper.getLevel(), helper.absolutePos(pos), Direction.NORTH, + PartTypes.TERMINAL_STORAGE, new ItemStack(PartTypes.TERMINAL_STORAGE.getItem())); + + // Simulate right-clicking the part with an eye of ender + GameTestTerminalStorageEnderChest.enderUpgradePart(helper, pos, player, new ItemStack(Items.ENDER_EYE)); + + helper.succeedWhen(() -> helper.assertTrue( + hasAdvancementUnlocked(player, "integratedterminals:storage_terminal_ender/ender_upgrade_storage_terminal"), + "ender_upgrade_storage_terminal advancement not unlocked")); + } + + /** + * Negative test: right-clicking a storage terminal without an eye of ender + * should NOT trigger the ender_upgrade_storage_terminal advancement. + */ + @GameTest(template = "empty", templateNamespace = "cyclopscore") + public void testAdvancementEnderUpgradeStorageTerminalNegative(GameTestHelper helper) { + BlockPos pos = new BlockPos(1, 2, 1); + ServerPlayer player = createMockServerPlayer(helper); + + // Place cable block and add terminal_storage part + helper.setBlock(pos, RegistryEntries.BLOCK_CABLE.value()); + NetworkHelpers.initNetwork(helper.getLevel(), helper.absolutePos(pos), Direction.NORTH); + PartHelpers.addPart(helper.getLevel(), helper.absolutePos(pos), Direction.NORTH, + PartTypes.TERMINAL_STORAGE, new ItemStack(PartTypes.TERMINAL_STORAGE.getItem())); + + // Simulate right-clicking the part with an ender pearl instead of an eye of ender + GameTestTerminalStorageEnderChest.enderUpgradePart(helper, pos, player, new ItemStack(Items.ENDER_PEARL)); + + helper.succeedWhen(() -> assertAdvancementNotUnlocked(helper, player, + "integratedterminals:storage_terminal_ender/ender_upgrade_storage_terminal")); + } + /** * Negative test: giving a non-matching item should NOT trigger the root advancement. */ diff --git a/src/main/java/org/cyclops/integratedterminals/gametest/GameTestTerminalStorageEnderChest.java b/src/main/java/org/cyclops/integratedterminals/gametest/GameTestTerminalStorageEnderChest.java new file mode 100644 index 0000000000..704fea412f --- /dev/null +++ b/src/main/java/org/cyclops/integratedterminals/gametest/GameTestTerminalStorageEnderChest.java @@ -0,0 +1,248 @@ +package org.cyclops.integratedterminals.gametest; + +import com.google.common.collect.Lists; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.gametest.framework.GameTest; +import net.minecraft.gametest.framework.GameTestHelper; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.inventory.Slot; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraft.world.item.crafting.CraftingInput; +import net.minecraft.world.item.crafting.CraftingRecipe; +import net.minecraft.world.item.crafting.RecipeHolder; +import net.minecraft.world.phys.BlockHitResult; +import net.minecraft.world.phys.Vec3; +import net.neoforged.neoforge.gametest.GameTestHolder; +import net.neoforged.neoforge.gametest.PrefixGameTestTemplate; +import org.cyclops.integrateddynamics.RegistryEntries; +import org.cyclops.integrateddynamics.api.part.PartPos; +import org.cyclops.integrateddynamics.api.part.PartTarget; +import org.cyclops.integrateddynamics.core.helper.NetworkHelpers; +import org.cyclops.integrateddynamics.core.helper.PartHelpers; +import org.cyclops.integratedterminals.Reference; +import org.cyclops.integratedterminals.core.terminalstorage.TerminalStorageTabEnderChest; +import org.cyclops.integratedterminals.core.terminalstorage.TerminalStorageTabEnderChestCommon; +import org.cyclops.integratedterminals.inventory.container.ContainerTerminalStoragePart; +import org.cyclops.integratedterminals.item.ItemTerminalStoragePortable; +import org.cyclops.integratedterminals.part.PartTypeTerminalStorage; +import org.cyclops.integratedterminals.part.PartTypes; + +import java.util.List; +import java.util.Optional; + +/** + * Game tests for the ender-upgraded storage terminal and its ender chest tab. + * @author rubensworks + */ +@GameTestHolder(Reference.MOD_ID) +@PrefixGameTestTemplate(false) +public class GameTestTerminalStorageEnderChest { + + private static final BlockPos POS = new BlockPos(1, 2, 1); + + /** + * Places a cable with a storage terminal part on it. + */ + private static PartTarget placeTerminal(GameTestHelper helper) { + helper.setBlock(POS, RegistryEntries.BLOCK_CABLE.value()); + NetworkHelpers.initNetwork(helper.getLevel(), helper.absolutePos(POS), Direction.NORTH); + PartHelpers.addPart(helper.getLevel(), helper.absolutePos(POS), Direction.NORTH, + PartTypes.TERMINAL_STORAGE, new ItemStack(PartTypes.TERMINAL_STORAGE.getItem())); + return PartTarget.fromCenter(PartPos.of(helper.getLevel(), helper.absolutePos(POS), Direction.NORTH)); + } + + private static PartTypeTerminalStorage.State getPartState(GameTestHelper helper, BlockPos pos) { + return (PartTypeTerminalStorage.State) PartHelpers + .getPart(PartPos.of(helper.getLevel(), helper.absolutePos(pos), Direction.NORTH)) + .getState(); + } + + /** + * Simulates a player right-clicking the storage terminal part with the given item. + */ + static void enderUpgradePart(GameTestHelper helper, BlockPos pos, ServerPlayer player, ItemStack heldItem) { + BlockPos absolutePos = helper.absolutePos(pos); + PartTypeTerminalStorage.State state = getPartState(helper, pos); + BlockHitResult hitResult = new BlockHitResult( + Vec3.atCenterOf(absolutePos), Direction.NORTH, absolutePos, false); + try { + PartTypes.TERMINAL_STORAGE.onPartActivated(state, absolutePos, helper.getLevel(), player, + InteractionHand.MAIN_HAND, heldItem, hitResult); + } catch (UnsupportedOperationException e) { + // When the terminal is not ender-upgraded, the terminal gui is opened instead, + // which sends a cyclopscore:value_notify packet that fails in the game test environment, + // because the embedded channel does not have custom NeoForge channels negotiated. + } + } + + /** + * Tests that right-clicking a storage terminal with an eye of ender ender-upgrades it. + */ + @GameTest(template = "empty", templateNamespace = "cyclopscore") + public void testEnderUpgradeWithEyeOfEnder(GameTestHelper helper) { + ServerPlayer player = GameTestAdvancementsIntegratedTerminals.createMockServerPlayer(helper); + placeTerminal(helper); + + helper.assertTrue(!getPartState(helper, POS).isEnderUpgraded(), + "Terminal should not be ender-upgraded before right-clicking it with an eye of ender"); + + // Creative players don't consume the eye of ender, so make sure we're not in creative mode + player.getAbilities().instabuild = false; + + ItemStack heldItem = new ItemStack(Items.ENDER_EYE, 2); + enderUpgradePart(helper, POS, player, heldItem); + + helper.assertTrue(getPartState(helper, POS).isEnderUpgraded(), + "Terminal should be ender-upgraded after right-clicking it with an eye of ender"); + helper.assertTrue(heldItem.getCount() == 1, + "One eye of ender should have been consumed, but the stack size is " + heldItem.getCount()); + + helper.succeed(); + } + + /** + * Tests that right-clicking a storage terminal with another item does not ender-upgrade it. + */ + @GameTest(template = "empty", templateNamespace = "cyclopscore") + public void testEnderUpgradeWithOtherItem(GameTestHelper helper) { + ServerPlayer player = GameTestAdvancementsIntegratedTerminals.createMockServerPlayer(helper); + placeTerminal(helper); + + player.getAbilities().instabuild = false; + + ItemStack heldItem = new ItemStack(Items.ENDER_PEARL, 2); + enderUpgradePart(helper, POS, player, heldItem); + + helper.assertTrue(!getPartState(helper, POS).isEnderUpgraded(), + "Terminal should not be ender-upgraded by an ender pearl"); + helper.assertTrue(heldItem.getCount() == 2, "No ender pearl should have been consumed"); + + helper.succeed(); + } + + /** + * Tests that breaking an ender-upgraded storage terminal gives back the eye of ender. + */ + @GameTest(template = "empty", templateNamespace = "cyclopscore") + public void testEnderUpgradedTerminalDropsEyeOfEnder(GameTestHelper helper) { + ServerPlayer player = GameTestAdvancementsIntegratedTerminals.createMockServerPlayer(helper); + PartTarget partTarget = placeTerminal(helper); + enderUpgradePart(helper, POS, player, new ItemStack(Items.ENDER_EYE)); + + PartTypeTerminalStorage.State state = getPartState(helper, POS); + List drops = Lists.newArrayList(); + PartTypes.TERMINAL_STORAGE.addDrops(partTarget, state, drops, true, false); + + helper.assertTrue(drops.stream().anyMatch(drop -> drop.is(Items.ENDER_EYE)), + "An eye of ender should have been dropped"); + helper.assertTrue(!state.isEnderUpgraded(), + "The ender upgrade should have been removed after dropping the eye of ender"); + + helper.succeed(); + } + + /** + * Tests that the ender chest tab is only interactable after the terminal has been ender-upgraded. + */ + @GameTest(template = "empty", templateNamespace = "cyclopscore") + public void testEnderChestTabAvailability(GameTestHelper helper) { + ServerPlayer player = GameTestAdvancementsIntegratedTerminals.createMockServerPlayer(helper); + PartTarget partTarget = placeTerminal(helper); + + ContainerTerminalStoragePart container = GameTestAdvancementsIntegratedTerminals + .createSilentContainer(player, partTarget); + TerminalStorageTabEnderChestCommon tabCommon = (TerminalStorageTabEnderChestCommon) + container.getTabCommon(TerminalStorageTabEnderChest.NAME.toString()); + + helper.assertTrue(tabCommon != null, "The ender chest tab should always be present in the container"); + helper.assertTrue(!container.isEnderUpgraded(), "The terminal should not be ender-upgraded yet"); + helper.assertTrue(!tabCommon.isAvailable(), "The ender chest tab should not be available yet"); + + Slot enderSlot = container.getSlot(tabCommon.getFirstSlotIndex()); + helper.assertTrue(!enderSlot.mayPlace(new ItemStack(Items.STICK)), + "Ender chest slots should not accept items before the terminal is ender-upgraded"); + + enderUpgradePart(helper, POS, player, new ItemStack(Items.ENDER_EYE)); + + helper.assertTrue(container.isEnderUpgraded(), "The terminal should be ender-upgraded"); + helper.assertTrue(tabCommon.isAvailable(), "The ender chest tab should be available"); + helper.assertTrue(enderSlot.mayPlace(new ItemStack(Items.STICK)), + "Ender chest slots should accept items after the terminal is ender-upgraded"); + + helper.succeed(); + } + + /** + * Tests shift-clicking items from the player inventory into the ender chest, and back. + */ + @GameTest(template = "empty", templateNamespace = "cyclopscore") + public void testEnderChestQuickMove(GameTestHelper helper) { + ServerPlayer player = GameTestAdvancementsIntegratedTerminals.createMockServerPlayer(helper); + PartTarget partTarget = placeTerminal(helper); + + ContainerTerminalStoragePart container = GameTestAdvancementsIntegratedTerminals + .createSilentContainer(player, partTarget); + enderUpgradePart(helper, POS, player, new ItemStack(Items.ENDER_EYE)); + container.setSelectedTab(TerminalStorageTabEnderChest.NAME.toString()); + + TerminalStorageTabEnderChestCommon tabCommon = (TerminalStorageTabEnderChestCommon) + container.getTabCommon(TerminalStorageTabEnderChest.NAME.toString()); + int firstEnderSlot = tabCommon.getFirstSlotIndex(); + + // Shift-click a stack from the player inventory into the ender chest + container.getSlot(0).set(new ItemStack(Items.STICK, 16)); + container.quickMoveStack(player, 0); + + helper.assertTrue(container.getSlot(0).getItem().isEmpty(), + "The player inventory slot should be empty after shift-clicking into the ender chest"); + helper.assertTrue(player.getEnderChestInventory().getItem(0).is(Items.STICK) + && player.getEnderChestInventory().getItem(0).getCount() == 16, + "The ender chest should contain the shift-clicked items, but it contains " + + player.getEnderChestInventory().getItem(0)); + + // Shift-click the stack back out of the ender chest + container.quickMoveStack(player, firstEnderSlot); + + helper.assertTrue(player.getEnderChestInventory().getItem(0).isEmpty(), + "The ender chest slot should be empty after shift-clicking out of it"); + helper.assertTrue(player.getInventory().countItem(Items.STICK) == 16, + "The player inventory should contain the shift-clicked items again"); + + helper.succeed(); + } + + /** + * Tests that a portable storage terminal can be ender-upgraded via crafting, + * while retaining the network it was linked to. + */ + @GameTest(template = "empty", templateNamespace = "cyclopscore") + public void testPortableEnderUpgradeRecipe(GameTestHelper helper) { + Optional> recipeHolder = helper.getLevel().getRecipeManager() + .byKey(ResourceLocation.parse("integratedterminals:crafting/terminal_storage_portable_ender_upgrade")); + helper.assertTrue(recipeHolder.isPresent(), "The portable terminal ender upgrade recipe should be registered"); + CraftingRecipe recipe = (CraftingRecipe) recipeHolder.get().value(); + + ItemStack terminal = new ItemStack(org.cyclops.integratedterminals.RegistryEntries.ITEM_TERMINAL_STORAGE_PORTABLE.get()); + ItemTerminalStoragePortable.setGroupId(terminal, 42); + CraftingInput input = CraftingInput.of(2, 1, List.of(terminal, new ItemStack(Items.ENDER_EYE))); + + helper.assertTrue(recipe.matches(input, helper.getLevel()), "The ender upgrade recipe should match"); + ItemStack output = recipe.assemble(input, helper.getLevel().registryAccess()); + helper.assertTrue(ItemTerminalStoragePortable.isEnderUpgraded(output), + "The crafted portable terminal should be ender-upgraded"); + helper.assertTrue(ItemTerminalStoragePortable.getGroupId(output) == 42, + "The crafted portable terminal should retain its network link"); + + // Upgrading an already upgraded terminal should not be possible + CraftingInput upgradedInput = CraftingInput.of(2, 1, List.of(output, new ItemStack(Items.ENDER_EYE))); + helper.assertTrue(recipe.assemble(upgradedInput, helper.getLevel().registryAccess()).isEmpty(), + "An already ender-upgraded portable terminal should not be upgradable again"); + + 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 3fb2f1610b..8f3865aecc 100644 --- a/src/main/java/org/cyclops/integratedterminals/inventory/container/ContainerTerminalStorageBase.java +++ b/src/main/java/org/cyclops/integratedterminals/inventory/container/ContainerTerminalStorageBase.java @@ -199,6 +199,20 @@ public Optional getNetwork() { public abstract ITerminalStorageLocation getLocation(); public abstract L getLocationInstance(); + /** + * @return If this terminal has been ender-upgraded, which unlocks the ender storage tab. + */ + public boolean isEnderUpgraded() { + return false; + } + + /** + * Exposes {@link #moveItemStackTo(ItemStack, int, int, boolean)} to terminal storage tabs. + */ + public boolean moveStackTo(ItemStack stack, int startIndex, int endIndex, boolean reverseDirection) { + return moveItemStackTo(stack, startIndex, endIndex, reverseDirection); + } + @Override public void onDirty() { @@ -285,6 +299,15 @@ public List> getTabS @Override public ItemStack quickMoveStack(Player player, int slotID) { + // Let the selected tab handle the quick move with regular container semantics, if it wants to + ITerminalStorageTabCommon selectedTabCommon = getTabCommon(getSelectedTab()); + if (selectedTabCommon != null) { + Optional quickMoved = selectedTabCommon.handleQuickMove(this, player, slotID); + if (quickMoved.isPresent()) { + return quickMoved.get(); + } + } + // Handle any (modded) client-side quick move controls if(player.level().isClientSide) { Optional> tabOptional = this.screen.getSelectedClientTab(); diff --git a/src/main/java/org/cyclops/integratedterminals/inventory/container/ContainerTerminalStorageItem.java b/src/main/java/org/cyclops/integratedterminals/inventory/container/ContainerTerminalStorageItem.java index 6579a1e5a1..a58616e9f7 100644 --- a/src/main/java/org/cyclops/integratedterminals/inventory/container/ContainerTerminalStorageItem.java +++ b/src/main/java/org/cyclops/integratedterminals/inventory/container/ContainerTerminalStorageItem.java @@ -82,6 +82,15 @@ public ItemStack getItemStack(Player player) { return this.itemLocation.getItemStack(player); } + @Override + public boolean isEnderUpgraded() { + if (this.itemLocation == null) { + // This can be called from the super constructor, before this field is assigned + return false; + } + return ItemTerminalStoragePortable.isEnderUpgraded(getItemStack(player)); + } + @Override public boolean stillValid(Player playerIn) { ItemStack item = getItemStack(player); diff --git a/src/main/java/org/cyclops/integratedterminals/inventory/container/ContainerTerminalStoragePart.java b/src/main/java/org/cyclops/integratedterminals/inventory/container/ContainerTerminalStoragePart.java index 3222dacf9b..e220c46c06 100644 --- a/src/main/java/org/cyclops/integratedterminals/inventory/container/ContainerTerminalStoragePart.java +++ b/src/main/java/org/cyclops/integratedterminals/inventory/container/ContainerTerminalStoragePart.java @@ -6,6 +6,7 @@ import net.minecraft.world.entity.player.Player; import net.minecraft.world.inventory.MenuType; import net.neoforged.neoforge.common.NeoForge; +import org.cyclops.integrateddynamics.api.PartStateException; import org.cyclops.integrateddynamics.api.evaluate.EvaluationException; import org.cyclops.integrateddynamics.api.evaluate.variable.IVariable; import org.cyclops.integrateddynamics.api.network.INetwork; @@ -86,6 +87,20 @@ public Optional getPartContainer() { return partContainer; } + @Override + public boolean isEnderUpgraded() { + if (this.partContainer == null) { + // This can be called from the super constructor, before this field is assigned + return false; + } + try { + return getPartState().map(PartTypeTerminalStorage.State::isEnderUpgraded).orElse(false); + } catch (PartStateException e) { + // The part was removed while its gui was open + return false; + } + } + @Override public boolean stillValid(Player playerIn) { return PartHelpers.canInteractWith(getPartTarget(), player, this.partContainer.get()); diff --git a/src/main/java/org/cyclops/integratedterminals/item/ItemTerminalStoragePortable.java b/src/main/java/org/cyclops/integratedterminals/item/ItemTerminalStoragePortable.java index 8410cc1a96..e262c1c33f 100644 --- a/src/main/java/org/cyclops/integratedterminals/item/ItemTerminalStoragePortable.java +++ b/src/main/java/org/cyclops/integratedterminals/item/ItemTerminalStoragePortable.java @@ -1,5 +1,6 @@ package org.cyclops.integratedterminals.item; +import net.minecraft.ChatFormatting; import net.minecraft.core.Direction; import net.minecraft.core.HolderLookup; import net.minecraft.core.NonNullList; @@ -128,6 +129,10 @@ public void appendHoverText(ItemStack stack, Item.TooltipContext context, List= 0) { tooltip.add(Component.translatable(L10NValues.PART_TOOLTIP_MONODIRECTIONALCONNECTOR_GROUP, groupId)); } + if (isEnderUpgraded(stack)) { + tooltip.add(Component.translatable("item.integratedterminals.terminal_storage_portable.ender_upgraded") + .withStyle(ChatFormatting.DARK_PURPLE)); + } } public static int getGroupId(ItemStack itemStack) { @@ -138,6 +143,14 @@ public static void setGroupId(ItemStack itemStack, int groupId) { itemStack.set(RegistryEntries.DATACOMPONENT_OMNIDIRECTIONAL_GROUP, groupId); } + public static boolean isEnderUpgraded(ItemStack itemStack) { + return Objects.requireNonNullElse(itemStack.get(org.cyclops.integratedterminals.RegistryEntries.COMPONENT_ENDER_UPGRADED), false); + } + + public static void setEnderUpgraded(ItemStack itemStack, boolean enderUpgraded) { + itemStack.set(org.cyclops.integratedterminals.RegistryEntries.COMPONENT_ENDER_UPGRADED, enderUpgraded); + } + public static ITerminalStorageTabCommon.IVariableInventory getVariableInventory(ItemStack itemStack) { // Navigate to relevant tag in item CompoundTag tagInventories = itemStack.get(org.cyclops.integratedterminals.RegistryEntries.COMPONENT_TERMINAL_STORAGE_INVENTORIES); diff --git a/src/main/java/org/cyclops/integratedterminals/part/PartTypeTerminalStorage.java b/src/main/java/org/cyclops/integratedterminals/part/PartTypeTerminalStorage.java index e5ddc22f9e..c60c490ffd 100644 --- a/src/main/java/org/cyclops/integratedterminals/part/PartTypeTerminalStorage.java +++ b/src/main/java/org/cyclops/integratedterminals/part/PartTypeTerminalStorage.java @@ -1,23 +1,36 @@ package org.cyclops.integratedterminals.part; import com.google.common.collect.Maps; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; import net.minecraft.core.HolderLookup; import net.minecraft.core.NonNullList; +import net.minecraft.core.particles.ParticleTypes; import net.minecraft.nbt.CompoundTag; import net.minecraft.nbt.ListTag; import net.minecraft.nbt.Tag; import net.minecraft.network.RegistryFriendlyByteBuf; import net.minecraft.network.chat.Component; +import net.minecraft.server.level.ServerLevel; import net.minecraft.server.level.ServerPlayer; +import net.minecraft.sounds.SoundEvents; +import net.minecraft.sounds.SoundSource; import net.minecraft.world.ContainerHelper; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.InteractionResult; import net.minecraft.world.MenuProvider; import net.minecraft.world.entity.player.Inventory; import net.minecraft.world.entity.player.Player; import net.minecraft.world.inventory.AbstractContainerMenu; import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraft.world.level.Level; +import net.minecraft.world.phys.BlockHitResult; import org.apache.commons.lang3.tuple.Triple; import org.cyclops.cyclopscore.network.PacketCodec; import org.cyclops.integrateddynamics.api.evaluate.variable.ValueDeseralizationContext; +import org.cyclops.integrateddynamics.api.network.INetwork; +import org.cyclops.integrateddynamics.api.network.IPartNetwork; import org.cyclops.integrateddynamics.api.part.IPartContainer; import org.cyclops.integrateddynamics.api.part.PartPos; import org.cyclops.integrateddynamics.api.part.PartTarget; @@ -25,6 +38,7 @@ import org.cyclops.integrateddynamics.core.part.PartStateEmpty; import org.cyclops.integrateddynamics.core.part.PartTypeBase; import org.cyclops.integratedterminals.GeneralConfig; +import org.cyclops.integratedterminals.advancement.criterion.TerminalStorageEnderUpgradedTrigger; import org.cyclops.integratedterminals.api.terminalstorage.ITerminalStorageTabCommon; import org.cyclops.integratedterminals.core.part.PartTypeTerminal; import org.cyclops.integratedterminals.core.terminalstorage.TerminalStorageTabIngredientComponentItemStackCrafting; @@ -42,6 +56,8 @@ */ public class PartTypeTerminalStorage extends PartTypeTerminal { + private static final int PARTICLE_INTERVAL = 5; + public PartTypeTerminalStorage(String name) { super(name); } @@ -56,6 +72,53 @@ protected PartTypeTerminalStorage.State constructDefaultState() { return new PartTypeTerminalStorage.State(); } + @Override + public InteractionResult onPartActivated(State partState, BlockPos pos, Level world, Player player, InteractionHand hand, ItemStack heldItem, BlockHitResult hit) { + // Ender-upgrade this terminal when it is right-clicked with an eye of ender + if (GeneralConfig.terminalStorageTabEnderChestEnabled + && !partState.isEnderUpgraded() && heldItem.is(Items.ENDER_EYE)) { + if (!world.isClientSide()) { + partState.setEnderUpgraded(true); + if (!player.getAbilities().instabuild) { + heldItem.shrink(1); + } + world.playSound(null, pos, SoundEvents.END_PORTAL_FRAME_FILL, SoundSource.BLOCKS, 1.0F, 1.0F); + TerminalStorageEnderUpgradedTrigger.onEnderUpgraded((ServerPlayer) player); + } + return InteractionResult.sidedSuccess(world.isClientSide()); + } + + return super.onPartActivated(partState, pos, world, player, hand, heldItem, hit); + } + + @Override + public boolean isUpdate(State state) { + // Ender-upgraded terminals need updates for spawning particles, even if energy consumption is disabled + return super.isUpdate(state) || state.isEnderUpgraded(); + } + + @Override + public void update(INetwork network, IPartNetwork partNetwork, PartTarget target, State state) { + super.update(network, partNetwork, target, state); + + if (state.isEnderUpgraded()) { + spawnEnderParticles(target); + } + } + + protected void spawnEnderParticles(PartTarget target) { + Level level = target.getCenter().getPos().getLevel(false); + if (level instanceof ServerLevel serverLevel && serverLevel.getGameTime() % PARTICLE_INTERVAL == 0) { + BlockPos pos = target.getCenter().getPos().getBlockPos(); + Direction side = target.getCenter().getSide(); + serverLevel.sendParticles(ParticleTypes.PORTAL, + pos.getX() + 0.5D + side.getStepX() * 0.4D, + pos.getY() + 0.5D + side.getStepY() * 0.4D, + pos.getZ() + 0.5D + side.getStepZ() * 0.4D, + 2, 0.2D, 0.2D, 0.2D, 0.05D); + } + } + @Override public Optional getContainerProvider(PartPos pos) { return Optional.of(new MenuProvider() { @@ -101,6 +164,12 @@ public void writeExtraGuiData(RegistryFriendlyByteBuf packetBuffer, PartPos pos, @Override public void addDrops(PartTarget target, State state, List itemStacks, boolean dropMainElement, boolean saveState) { + // Give back the eye of ender that was used to ender-upgrade this terminal + if (!saveState && state.isEnderUpgraded()) { + itemStacks.add(new ItemStack(Items.ENDER_EYE)); + state.setEnderUpgraded(false); + } + for (Map.Entry> entry : state.getNamedInventories().entrySet()) { // TODO: for now hardcoded on crafting tab if (entry.getKey().equals(TerminalStorageTabIngredientComponentItemStackCrafting.NAME.toString())) { @@ -122,10 +191,22 @@ public static class State extends PartStateEmpty private final Map> namedInventories; private final Map playerStorageStates; + private boolean enderUpgraded; public State() { this.namedInventories = Maps.newHashMap(); this.playerStorageStates = Maps.newHashMap(); + this.enderUpgraded = false; + } + + public boolean isEnderUpgraded() { + return this.enderUpgraded; + } + + public void setEnderUpgraded(boolean enderUpgraded) { + this.enderUpgraded = enderUpgraded; + this.onDirty(); + this.sendUpdate(); } @Override @@ -167,6 +248,8 @@ public TerminalStorageState getPlayerStorageState(Player player) { public void writeToNBT(ValueDeseralizationContext valueDeseralizationContext, CompoundTag tag) { super.writeToNBT(valueDeseralizationContext, tag); + tag.putBoolean("enderUpgraded", this.enderUpgraded); + // Write namedInventories ListTag namedInventoriesList = new ListTag(); for (Map.Entry> entry : this.namedInventories.entrySet()) { @@ -193,6 +276,8 @@ public void writeToNBT(ValueDeseralizationContext valueDeseralizationContext, Co public void readFromNBT(ValueDeseralizationContext valueDeseralizationContext, CompoundTag tag) { super.readFromNBT(valueDeseralizationContext, tag); + this.enderUpgraded = tag.getBoolean("enderUpgraded"); + // Read namedInventories for (Tag listEntry : tag.getList("namedInventories", Tag.TAG_COMPOUND)) { NonNullList list = NonNullList.withSize(((CompoundTag) listEntry).getInt("itemCount"), ItemStack.EMPTY); diff --git a/src/main/java/org/cyclops/integratedterminals/recipe/RecipeTerminalStoragePortableEnderUpgradeConfig.java b/src/main/java/org/cyclops/integratedterminals/recipe/RecipeTerminalStoragePortableEnderUpgradeConfig.java new file mode 100644 index 0000000000..83e49ba28e --- /dev/null +++ b/src/main/java/org/cyclops/integratedterminals/recipe/RecipeTerminalStoragePortableEnderUpgradeConfig.java @@ -0,0 +1,54 @@ +package org.cyclops.integratedterminals.recipe; + +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.CraftingInput; +import org.cyclops.cyclopscore.config.extendedconfig.RecipeConfig; +import org.cyclops.cyclopscore.recipe.type.RecipeCraftingShapelessCustomOutput; +import org.cyclops.integratedterminals.IntegratedTerminals; +import org.cyclops.integratedterminals.RegistryEntries; +import org.cyclops.integratedterminals.item.ItemTerminalStoragePortable; + +/** + * Config for the recipe that ender-upgrades a portable storage terminal with an eye of ender. + * @author rubensworks + */ +public class RecipeTerminalStoragePortableEnderUpgradeConfig extends RecipeConfig { + + public RecipeTerminalStoragePortableEnderUpgradeConfig() { + super(IntegratedTerminals._instance, + "crafting_special_terminal_storage_portable_ender_upgrade", + eConfig -> new RecipeCraftingShapelessCustomOutput.Serializer( + RecipeTerminalStoragePortableEnderUpgradeConfig::createStaticOutput, + RecipeTerminalStoragePortableEnderUpgradeConfig::transformCraftingOutput) + ); + } + + /** + * @return The output that is shown in recipe listings, such as JEI and the info book. + */ + protected static ItemStack createStaticOutput() { + ItemStack itemStack = new ItemStack(RegistryEntries.ITEM_TERMINAL_STORAGE_PORTABLE.get()); + ItemTerminalStoragePortable.setEnderUpgraded(itemStack, true); + return itemStack; + } + + /** + * Copy the input terminal, so that its network link and stored settings are retained. + */ + protected static ItemStack transformCraftingOutput(CraftingInput inventory, ItemStack staticOutput) { + for (int i = 0; i < inventory.size(); i++) { + ItemStack itemStack = inventory.getItem(i); + if (itemStack.getItem() instanceof ItemTerminalStoragePortable) { + if (ItemTerminalStoragePortable.isEnderUpgraded(itemStack)) { + // Don't allow upgrading an already upgraded terminal + return ItemStack.EMPTY; + } + ItemStack output = itemStack.copyWithCount(1); + ItemTerminalStoragePortable.setEnderUpgraded(output, true); + return output; + } + } + return ItemStack.EMPTY; + } + +} diff --git a/src/main/resources/assets/integratedterminals/lang/en_us.json b/src/main/resources/assets/integratedterminals/lang/en_us.json index 3b8c7f4ae0..3a22be46ff 100644 --- a/src/main/resources/assets/integratedterminals/lang/en_us.json +++ b/src/main/resources/assets/integratedterminals/lang/en_us.json @@ -12,6 +12,7 @@ "gui.integratedterminals.terminal_storage.storage_name": "%s Storage", "gui.integratedterminals.terminal_storage.crafting_name": "%s Crafting", "gui.integratedterminals.terminal_storage.channel": "Chan:", + "gui.integratedterminals.terminal_storage.ender_chest": "Ender Storage", "gui.integratedterminals.terminal_storage.craft": "craft", "gui.integratedterminals.terminal_storage.tooltip.requirements": "Crafting Requirements:", "gui.integratedterminals.terminal_storage.tooltip.crafting": "Being crafted: %s", @@ -121,6 +122,8 @@ "advancement.integratedterminals.gui_storage_terminal.desc": "Open the gui of a Storage Terminal", "advancement.integratedterminals.filter_enchantable": "Dynamic Storage Terminal Filtering", "advancement.integratedterminals.filter_enchantable.desc": "Dynamically filter enchantable items in the Storage Terminal using a Variable", + "advancement.integratedterminals.ender_upgrade_storage_terminal": "Storage from the End", + "advancement.integratedterminals.ender_upgrade_storage_terminal.desc": "Ender-upgrade a Storage Terminal by right-clicking it with an Eye of Ender", "_comment": "Blocks", "block.integratedterminals.menril_glass": "Menril Glass", @@ -134,6 +137,7 @@ "item.integratedterminals.terminal_storage_portable.status.no_network": "No network has been linked. Right click on Omni-Directional Connector to link.", "item.integratedterminals.terminal_storage_portable.status.invalid_network": "The linked network could not be found", "item.integratedterminals.terminal_storage_portable.status.linked": "Linked to network", + "item.integratedterminals.terminal_storage_portable.ender_upgraded": "Ender-upgraded", "_comment": "Part types", "parttype.integratedterminals.terminal_storage": "Storage Terminal", @@ -184,6 +188,13 @@ "info_book.integratedterminals.storage_terminal.power_usage.text2": "When you have the JEI mod installed, you will be able to exploit JEI's recipe shift-clicking ability to quickly fill the crafting grid. The configured source of the auto-refill mode determines where the items come from. Furthermore, you can enable synchronization between the JEI and &lStorage Terminal&r search fields.", "info_book.integratedterminals.storage_terminal.power_usage.text3": "If you are not satisfied with the provided text-based filtering options, you can insert your own &lPredicates&r via &lVariable Cards&r. These should accept the ingredient type as input, and a &9Boolean&0 as ouput.", + "info_book.integratedterminals.storage_terminal.ender_storage": "Ender Storage", + "info_book.integratedterminals.storage_terminal.ender_storage.text1": "&lStorage Terminals&r can be &oender-upgraded&r by right-clicking them with an &lEye of Ender&r. Upgraded terminals emit &oender particles&r, so you can tell them apart at a glance.", + "info_book.integratedterminals.storage_terminal.ender_storage.text2": "An ender-upgraded terminal gains an additional &lEnder Storage&r tab, which shows the contents of your personal &lEnder Chest&r. Just like with a regular &lEnder Chest&r, you can insert and extract items there, including by shift-clicking.", + "info_book.integratedterminals.storage_terminal.ender_storage.text3": "Because the &lEnder Chest&r is bound to you instead of to the network, this tab has no channel selector, search field or &lVariable&r filter slots.", + "info_book.integratedterminals.storage_terminal.ender_storage.text4": "Breaking an ender-upgraded &lStorage Terminal&r will give you back the &lEye of Ender&r that was used to upgrade it.", + "info_book.integratedterminals.storage_terminal.ender_storage.text5": "&lPortable Storage Terminals&r can be ender-upgraded as well, by combining them with an &lEye of Ender&r in a crafting grid. This retains the network the terminal was linked to.", + "info_book.integratedterminals.storage_terminal.portable": "Portable Terminal", "info_book.integratedterminals.storage_terminal.portable.text1": "By attaching some &lCrystalized Menril Chunks&r to your &lStorage Terminal&r, you can create a portable variant of the terminal.", "info_book.integratedterminals.storage_terminal.portable.text2": "In order to link your &lPortable Storage Terminal&r to a certain network, your network will need an &lOmni-Directional Connector&r. Linking to it can be done by just right-clicking with your &lPortable Storage Terminal&r on the &lOmni-Directional Connector&r.", @@ -204,6 +215,11 @@ "info_book.integratedterminals.tutorials.storage_terminal.text1": "The &lStorage Terminal&r allows you to show the storage contents of the attached network and interact with its contents. Different ingredient types are available in separate tabs, and each tab can be filtered by channel, search queries and &lVariable Cards&r. They can also be sorted in various ways. Refer to the manual for further details.", "info_book.integratedterminals.tutorials.storage_terminal.text2": "This tutorial is simple, craft &lMenril Glass&r, a &lStorage Terminal&r, and open its gui after attaching it to a network.", + "info_book.integratedterminals.tutorials.storage_terminal_ender": "Ender-upgraded Storage Terminal", + "info_book.integratedterminals.tutorials.storage_terminal_ender.text1": "Next to the storage of your network, a &lStorage Terminal&r can also give you access to your personal &lEnder Chest&r. For that, the terminal first needs to be &oender-upgraded&r.", + "info_book.integratedterminals.tutorials.storage_terminal_ender.text2": "In order to complete this tutorial, place a &lStorage Terminal&r on a &lCable&r, and right-click it with an &lEye of Ender&r. The &lEye of Ender&r will be consumed, the terminal will start emitting &oender particles&r, and a new &lEnder Storage&r tab will show up in its gui. You will get the &lEye of Ender&r back when you break the terminal again.", + "info_book.integratedterminals.tutorials.storage_terminal_ender.text3": "The same can be done with a &lPortable Storage Terminal&r, by combining it with an &lEye of Ender&r in a crafting grid.", + "info_book.integratedterminals.tutorials.storage_terminal_filtering": "Storage Terminal Filtering", "info_book.integratedterminals.tutorials.storage_terminal_filtering.text1": "One of the many useful features of the &lStorage Terminal&r is its ability to &ofilter&r through ingredients. In this tutorial, we will explore two different filtering methods: text-based filtering and variable-based filtering (only the second one will give you an advancement). This tutorial will use item-based storage as provided by Item Interfaces from Integrated Tunnels.", "info_book.integratedterminals.tutorials.storage_terminal_filtering.text2": "For the first part of this tutorial, we will filter through items by typing in a certain string in the search field. Note that you can use a hotkey to select this search field more quickly. And if you have JEI installed, you can synchronize its contents with the JEI search field. By default, you will search within the ingredient name. By prefixing the string with &o'@'&r, you can search by mod. With &o'#'&r, tooltips can be searched. Finally, &o'$'&r queries item tags. In order to complete the first part of this tutorial, type &o'$plank @minecraft'&r into the search field to show all planks added by Minecraft.", diff --git a/src/main/resources/data/integratedterminals/advancement/storage_terminal_ender/ender_upgrade_storage_terminal.json b/src/main/resources/data/integratedterminals/advancement/storage_terminal_ender/ender_upgrade_storage_terminal.json new file mode 100644 index 0000000000..aa3b3d34c4 --- /dev/null +++ b/src/main/resources/data/integratedterminals/advancement/storage_terminal_ender/ender_upgrade_storage_terminal.json @@ -0,0 +1,20 @@ +{ + "display": { + "icon": { + "id": "minecraft:ender_eye" + }, + "title": { + "translate": "advancement.integratedterminals.ender_upgrade_storage_terminal" + }, + "description": { + "translate": "advancement.integratedterminals.ender_upgrade_storage_terminal.desc" + }, + "frame": "goal" + }, + "parent": "integratedterminals:storage_terminal/gui_storage_terminal", + "criteria": { + "criteria_0": { + "trigger": "integratedterminals:terminal_storage_ender_upgraded" + } + } +} diff --git a/src/main/resources/data/integratedterminals/info/terminals_info.xml b/src/main/resources/data/integratedterminals/info/terminals_info.xml index 3e9f2b0bc9..c64c1f1635 100644 --- a/src/main/resources/data/integratedterminals/info/terminals_info.xml +++ b/src/main/resources/data/integratedterminals/info/terminals_info.xml @@ -55,6 +55,15 @@ key.integratedterminals.terminal.craftinggrid.balance +
+ info_book.integratedterminals.storage_terminal.ender_storage.text1 + info_book.integratedterminals.storage_terminal.ender_storage.text2 + info_book.integratedterminals.storage_terminal.ender_storage.text3 + info_book.integratedterminals.storage_terminal.ender_storage.text4 + info_book.integratedterminals.storage_terminal.ender_storage.text5 + integratedterminals:crafting/terminal_storage_portable_ender_upgrade +
+
info_book.integratedterminals.storage_terminal.portable.text1 info_book.integratedterminals.storage_terminal.portable.text2 diff --git a/src/main/resources/data/integratedterminals/info/terminals_tutorials.xml b/src/main/resources/data/integratedterminals/info/terminals_tutorials.xml index c0b6261385..5fbcdf6330 100644 --- a/src/main/resources/data/integratedterminals/info/terminals_tutorials.xml +++ b/src/main/resources/data/integratedterminals/info/terminals_tutorials.xml @@ -22,6 +22,22 @@
+
+ info_book.integratedterminals.tutorials.storage_terminal_ender.text1 + info_book.integratedterminals.tutorials.storage_terminal_ender.text2 + info_book.integratedterminals.tutorials.storage_terminal_ender.text3 + integratedterminals:crafting/terminal_storage_portable_ender_upgrade + + + + + + minecraft:ender_eye + integratedterminals:terminal_storage_portable + + +
+
info_book.integratedterminals.tutorials.storage_terminal_filtering.text1 info_book.integratedterminals.tutorials.storage_terminal_filtering.text2 diff --git a/src/main/resources/data/integratedterminals/recipe/crafting/terminal_storage_portable_ender_upgrade.json b/src/main/resources/data/integratedterminals/recipe/crafting/terminal_storage_portable_ender_upgrade.json new file mode 100644 index 0000000000..4437d02d35 --- /dev/null +++ b/src/main/resources/data/integratedterminals/recipe/crafting/terminal_storage_portable_ender_upgrade.json @@ -0,0 +1,12 @@ +{ + "type": "integratedterminals:crafting_special_terminal_storage_portable_ender_upgrade", + "category": "misc", + "ingredients": [ + { + "item": "integratedterminals:terminal_storage_portable" + }, + { + "item": "minecraft:ender_eye" + } + ] +} From ffe4c58e369fe159f0074767191ad2fd36baa8b4 Mon Sep 17 00:00:00 2001 From: rubensworks Date: Thu, 3 Sep 2026 17:11:41 +0000 Subject: [PATCH 2/3] Make ender particles optional, and fix them not starting on upgrade * Add a terminalStorageEnderParticles config option, disabled by default. * Hide the scrollbar on the ender chest tab, as all its slots always fit on screen. Tabs can opt out of it via ITerminalStorageTabClient. * Spawn the particles with the same motion as EnderChestBlock#animateTick, with only the spawn area shifted onto the face the terminal is on. They previously used gaussian offsets and velocities, which spread them out much further than the particles of an ender chest. * Make the network re-check if the terminal needs update ticks after it is ender-upgraded. A network only calls addNetworkElementUpdateable when an element is added to it, and terminals don't need update ticks by default, as energyConsumptionMultiplier is 0 by default. So the particles only started showing up after the network was reloaded. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01Kzc5LxSaedCXCK4pynhJV3 --- .../integratedterminals/GeneralConfig.java | 2 + .../ITerminalStorageTabClient.java | 7 ++ .../ContainerScreenTerminalStorage.java | 26 +++++-- .../TerminalStorageTabEnderChestClient.java | 6 ++ .../part/PartTypeTerminalStorage.java | 74 ++++++++++++++++--- .../integratedterminals/lang/en_us.json | 4 +- 6 files changed, 101 insertions(+), 18 deletions(-) diff --git a/src/main/java/org/cyclops/integratedterminals/GeneralConfig.java b/src/main/java/org/cyclops/integratedterminals/GeneralConfig.java index febcbea66d..729a9c214a 100644 --- a/src/main/java/org/cyclops/integratedterminals/GeneralConfig.java +++ b/src/main/java/org/cyclops/integratedterminals/GeneralConfig.java @@ -29,6 +29,8 @@ public class GeneralConfig extends DummyConfig { 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) public static boolean terminalStorageTabEnderChestEnabled = true; + @ConfigurableProperty(category = "core", comment = "If ender-upgraded storage terminals should emit ender particles.", isCommandable = true, configLocation = ModConfig.Type.SERVER) + public static boolean terminalStorageEnderParticles = false; @ConfigurableProperty(category = "machine", comment = "The number of items that should be selected when clicking on an item in the storage terminal.", isCommandable = true) public static int guiStorageItemInitialQuantity = 64; diff --git a/src/main/java/org/cyclops/integratedterminals/api/terminalstorage/ITerminalStorageTabClient.java b/src/main/java/org/cyclops/integratedterminals/api/terminalstorage/ITerminalStorageTabClient.java index e6cf63d2b1..75975f155a 100644 --- a/src/main/java/org/cyclops/integratedterminals/api/terminalstorage/ITerminalStorageTabClient.java +++ b/src/main/java/org/cyclops/integratedterminals/api/terminalstorage/ITerminalStorageTabClient.java @@ -209,6 +209,13 @@ public default boolean hasVariableFilterSlots() { return true; } + /** + * @return If the scrollbar next to the storage grid should be shown for this tab. + */ + public default boolean hasScrollbar() { + return true; + } + public default int getSlotOffsetX() { return DEFAULT_SLOT_OFFSET_X; } 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 126d98814a..00544b9598 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 @@ -143,7 +143,9 @@ public int getVisibleRows() { return getSlotVisibleRows(); } }; - addWidget(this.scrollBar); + if (hasScrollbar()) { + addWidget(this.scrollBar); + } fieldSearch = new WidgetTextFieldExtended(Minecraft.getInstance().font, leftPos + SEARCH_X, topPos + SEARCH_Y, getSearchWidth() - 10, SEARCH_HEIGHT, Component.translatable("gui.cyclopscore.search")); @@ -286,6 +288,12 @@ public boolean hasVariableFilterSlots() { .orElse(true); } + public boolean hasScrollbar() { + return getSelectedClientTab() + .map(ITerminalStorageTabClient::hasScrollbar) + .orElse(true); + } + @Override protected void renderBg(GuiGraphics guiGraphics, float f, int mouseX, int mouseY) { //super.renderBg(matrixStack, f, mouseX, mouseY); @@ -302,7 +310,9 @@ protected void renderBg(GuiGraphics guiGraphics, float f, int mouseX, int mouseY drawTabsBackground(guiGraphics); drawTabContents(guiGraphics, getMenu().getSelectedTab(), getMenu().getSelectedChannel(), DrawLayer.BACKGROUND, f, getGuiLeftTotal() + getSlotsOffsetX(), getGuiTopTotal() + getSlotsOffsetY(), mouseX, mouseY); - scrollBar.render(guiGraphics, mouseX, mouseY, f); + if (hasScrollbar()) { + scrollBar.render(guiGraphics, mouseX, mouseY, f); + } Optional> tabOptional = getSelectedClientTab(); tabOptional.ifPresent(tab -> { @@ -369,9 +379,11 @@ protected void renderBgTab(GuiGraphics guiGraphics, float f, int mouseX, int mou } // Scrollbar background - guiGraphics.blit(this.texture, leftPos + getGridXSize() + 32, topPos + SCROLL_Y - 1, 20, 12, 14, 1); // top - blitRescalable(guiGraphics, this.texture, leftPos + getGridXSize() + 32, topPos + SCROLL_Y, blitOffset, 20, 13, 14, 1, 256, 256, 14, getScrollHeight() - 2); // middle - guiGraphics.blit(this.texture, leftPos + getGridXSize() + 32, topPos + SCROLL_Y + getScrollHeight() - 2, 20, 101, 14, 1); // bottom + if (hasScrollbar()) { + guiGraphics.blit(this.texture, leftPos + getGridXSize() + 32, topPos + SCROLL_Y - 1, 20, 12, 14, 1); // top + blitRescalable(guiGraphics, this.texture, leftPos + getGridXSize() + 32, topPos + SCROLL_Y, blitOffset, 20, 13, 14, 1, 256, 256, 14, getScrollHeight() - 2); // middle + guiGraphics.blit(this.texture, leftPos + getGridXSize() + 32, topPos + SCROLL_Y + getScrollHeight() - 2, 20, 101, 14, 1); // bottom + } // Textbox background if (hasSearchField()) { @@ -452,7 +464,9 @@ protected void renderLabels(GuiGraphics guiGraphics, int mouseX, int mouseY) { @Override protected void drawCurrentScreen(GuiGraphics guiGraphics, int mouseX, int mouseY, float partialTicks) { - scrollBar.render(guiGraphics, mouseX, mouseY, partialTicks); + if (hasScrollbar()) { + scrollBar.render(guiGraphics, mouseX, mouseY, partialTicks); + } ResourceLocation oldTexture = this.texture; getSelectedClientTab().ifPresent(tab -> { diff --git a/src/main/java/org/cyclops/integratedterminals/core/terminalstorage/TerminalStorageTabEnderChestClient.java b/src/main/java/org/cyclops/integratedterminals/core/terminalstorage/TerminalStorageTabEnderChestClient.java index 1dc11e165d..ddc6f2419b 100644 --- a/src/main/java/org/cyclops/integratedterminals/core/terminalstorage/TerminalStorageTabEnderChestClient.java +++ b/src/main/java/org/cyclops/integratedterminals/core/terminalstorage/TerminalStorageTabEnderChestClient.java @@ -168,6 +168,12 @@ public boolean hasVariableFilterSlots() { return false; } + @Override + public boolean hasScrollbar() { + // All ender chest slots fit on screen at once + return false; + } + @Override public boolean isSlotValidForDraggingInto(int channel, Slot slot) { return false; diff --git a/src/main/java/org/cyclops/integratedterminals/part/PartTypeTerminalStorage.java b/src/main/java/org/cyclops/integratedterminals/part/PartTypeTerminalStorage.java index c60c490ffd..f025063148 100644 --- a/src/main/java/org/cyclops/integratedterminals/part/PartTypeTerminalStorage.java +++ b/src/main/java/org/cyclops/integratedterminals/part/PartTypeTerminalStorage.java @@ -15,6 +15,7 @@ import net.minecraft.server.level.ServerPlayer; import net.minecraft.sounds.SoundEvents; import net.minecraft.sounds.SoundSource; +import net.minecraft.util.RandomSource; import net.minecraft.world.ContainerHelper; import net.minecraft.world.InteractionHand; import net.minecraft.world.InteractionResult; @@ -28,12 +29,16 @@ import net.minecraft.world.phys.BlockHitResult; import org.apache.commons.lang3.tuple.Triple; import org.cyclops.cyclopscore.network.PacketCodec; +import org.cyclops.integrateddynamics.api.PartStateException; import org.cyclops.integrateddynamics.api.evaluate.variable.ValueDeseralizationContext; import org.cyclops.integrateddynamics.api.network.INetwork; +import org.cyclops.integrateddynamics.api.network.INetworkElement; import org.cyclops.integrateddynamics.api.network.IPartNetwork; +import org.cyclops.integrateddynamics.api.network.IPartNetworkElement; import org.cyclops.integrateddynamics.api.part.IPartContainer; import org.cyclops.integrateddynamics.api.part.PartPos; import org.cyclops.integrateddynamics.api.part.PartTarget; +import org.cyclops.integrateddynamics.core.helper.NetworkHelpers; import org.cyclops.integrateddynamics.core.helper.PartHelpers; import org.cyclops.integrateddynamics.core.part.PartStateEmpty; import org.cyclops.integrateddynamics.core.part.PartTypeBase; @@ -57,6 +62,7 @@ public class PartTypeTerminalStorage extends PartTypeTerminal { private static final int PARTICLE_INTERVAL = 5; + private static final double FACE_OFFSET = 0.4D; public PartTypeTerminalStorage(String name) { super(name); @@ -84,6 +90,7 @@ public InteractionResult onPartActivated(State partState, BlockPos pos, Level wo } world.playSound(null, pos, SoundEvents.END_PORTAL_FRAME_FILL, SoundSource.BLOCKS, 1.0F, 1.0F); TerminalStorageEnderUpgradedTrigger.onEnderUpgraded((ServerPlayer) player); + refreshNetworkElementUpdateable(world, pos, partState); } return InteractionResult.sidedSuccess(world.isClientSide()); } @@ -91,31 +98,78 @@ public InteractionResult onPartActivated(State partState, BlockPos pos, Level wo return super.onPartActivated(partState, pos, world, player, hand, heldItem, hit); } + /** + * Make the network re-check if the part at the given position needs update ticks. + * + * A network only calls {@link INetwork#addNetworkElementUpdateable} when an element is added to it, + * so a part that only starts needing update ticks later on + * would otherwise not receive any until its network is reloaded. + */ + protected void refreshNetworkElementUpdateable(Level world, BlockPos pos, State partState) { + NetworkHelpers.getNetwork(world, pos, null).ifPresent(network -> { + for (INetworkElement element : network.getElements()) { + if (element instanceof IPartNetworkElement partNetworkElement) { + try { + if (partNetworkElement.getPartState() == partState) { + network.addNetworkElementUpdateable(element); + return; + } + } catch (PartStateException e) { + // This element's part was removed in the meantime + } + } + } + }); + } + @Override public boolean isUpdate(State state) { // Ender-upgraded terminals need updates for spawning particles, even if energy consumption is disabled - return super.isUpdate(state) || state.isEnderUpgraded(); + return super.isUpdate(state) || (GeneralConfig.terminalStorageEnderParticles && state.isEnderUpgraded()); } @Override public void update(INetwork network, IPartNetwork partNetwork, PartTarget target, State state) { super.update(network, partNetwork, target, state); - if (state.isEnderUpgraded()) { + if (GeneralConfig.terminalStorageEnderParticles && state.isEnderUpgraded()) { spawnEnderParticles(target); } } + /** + * Spawn ender particles in front of the terminal. + * + * This uses the same particle motion as {@link net.minecraft.world.level.block.EnderChestBlock#animateTick}, + * with the spawn area shifted from the center of the block onto the face that the terminal is placed on. + * + * Unlike an ender chest, a terminal is not a block of its own, so its particles can not be spawned + * from a client-side {@code animateTick}. They are sent from here instead, + * which is why they are spawned once every {@link #PARTICLE_INTERVAL} ticks rather than on every tick. + */ protected void spawnEnderParticles(PartTarget target) { Level level = target.getCenter().getPos().getLevel(false); - if (level instanceof ServerLevel serverLevel && serverLevel.getGameTime() % PARTICLE_INTERVAL == 0) { - BlockPos pos = target.getCenter().getPos().getBlockPos(); - Direction side = target.getCenter().getSide(); - serverLevel.sendParticles(ParticleTypes.PORTAL, - pos.getX() + 0.5D + side.getStepX() * 0.4D, - pos.getY() + 0.5D + side.getStepY() * 0.4D, - pos.getZ() + 0.5D + side.getStepZ() * 0.4D, - 2, 0.2D, 0.2D, 0.2D, 0.05D); + if (!(level instanceof ServerLevel serverLevel) || serverLevel.getGameTime() % PARTICLE_INTERVAL != 0) { + return; + } + + BlockPos pos = target.getCenter().getPos().getBlockPos(); + Direction side = target.getCenter().getSide(); + RandomSource random = serverLevel.getRandom(); + + for (int i = 0; i < 3; i++) { + int j = random.nextInt(2) * 2 - 1; + int k = random.nextInt(2) * 2 - 1; + double x = pos.getX() + 0.5D + 0.25D * j + side.getStepX() * FACE_OFFSET; + double y = pos.getY() + random.nextFloat() + side.getStepY() * FACE_OFFSET; + double z = pos.getZ() + 0.5D + 0.25D * k + side.getStepZ() * FACE_OFFSET; + double motionX = random.nextFloat() * j; + double motionY = (random.nextFloat() - 0.5D) * 0.125D; + double motionZ = random.nextFloat() * k; + + // A count of 0 makes the offsets be interpreted as the exact motion of a single particle, + // just like the client-side particles of an ender chest + serverLevel.sendParticles(ParticleTypes.PORTAL, x, y, z, 0, motionX, motionY, motionZ, 1.0D); } } diff --git a/src/main/resources/assets/integratedterminals/lang/en_us.json b/src/main/resources/assets/integratedterminals/lang/en_us.json index 3a22be46ff..bf789fb31b 100644 --- a/src/main/resources/assets/integratedterminals/lang/en_us.json +++ b/src/main/resources/assets/integratedterminals/lang/en_us.json @@ -189,7 +189,7 @@ "info_book.integratedterminals.storage_terminal.power_usage.text3": "If you are not satisfied with the provided text-based filtering options, you can insert your own &lPredicates&r via &lVariable Cards&r. These should accept the ingredient type as input, and a &9Boolean&0 as ouput.", "info_book.integratedterminals.storage_terminal.ender_storage": "Ender Storage", - "info_book.integratedterminals.storage_terminal.ender_storage.text1": "&lStorage Terminals&r can be &oender-upgraded&r by right-clicking them with an &lEye of Ender&r. Upgraded terminals emit &oender particles&r, so you can tell them apart at a glance.", + "info_book.integratedterminals.storage_terminal.ender_storage.text1": "&lStorage Terminals&r can be &oender-upgraded&r by right-clicking them with an &lEye of Ender&r. Optionally, upgraded terminals can emit &oender particles&r so that you can tell them apart at a glance. This can be enabled in the server-side config file, where it is disabled by default.", "info_book.integratedterminals.storage_terminal.ender_storage.text2": "An ender-upgraded terminal gains an additional &lEnder Storage&r tab, which shows the contents of your personal &lEnder Chest&r. Just like with a regular &lEnder Chest&r, you can insert and extract items there, including by shift-clicking.", "info_book.integratedterminals.storage_terminal.ender_storage.text3": "Because the &lEnder Chest&r is bound to you instead of to the network, this tab has no channel selector, search field or &lVariable&r filter slots.", "info_book.integratedterminals.storage_terminal.ender_storage.text4": "Breaking an ender-upgraded &lStorage Terminal&r will give you back the &lEye of Ender&r that was used to upgrade it.", @@ -217,7 +217,7 @@ "info_book.integratedterminals.tutorials.storage_terminal_ender": "Ender-upgraded Storage Terminal", "info_book.integratedterminals.tutorials.storage_terminal_ender.text1": "Next to the storage of your network, a &lStorage Terminal&r can also give you access to your personal &lEnder Chest&r. For that, the terminal first needs to be &oender-upgraded&r.", - "info_book.integratedterminals.tutorials.storage_terminal_ender.text2": "In order to complete this tutorial, place a &lStorage Terminal&r on a &lCable&r, and right-click it with an &lEye of Ender&r. The &lEye of Ender&r will be consumed, the terminal will start emitting &oender particles&r, and a new &lEnder Storage&r tab will show up in its gui. You will get the &lEye of Ender&r back when you break the terminal again.", + "info_book.integratedterminals.tutorials.storage_terminal_ender.text2": "In order to complete this tutorial, place a &lStorage Terminal&r on a &lCable&r, and right-click it with an &lEye of Ender&r. The &lEye of Ender&r will be consumed, and a new &lEnder Storage&r tab will show up in its gui. You will get the &lEye of Ender&r back when you break the terminal again.", "info_book.integratedterminals.tutorials.storage_terminal_ender.text3": "The same can be done with a &lPortable Storage Terminal&r, by combining it with an &lEye of Ender&r in a crafting grid.", "info_book.integratedterminals.tutorials.storage_terminal_filtering": "Storage Terminal Filtering", From 88abed3524c655556efa284ea18b3efc64199807 Mon Sep 17 00:00:00 2001 From: rubensworks Date: Thu, 3 Sep 2026 17:33:59 +0000 Subject: [PATCH 3/3] Match the ender chest tab padding to a vanilla container gui The tab background reserved space for the scrollbar on its right side, which was left as a gap on the ender chest tab, where the scrollbar is hidden. The background now hugs the storage grid with the same 8 pixel padding that a vanilla container gui has, for tabs without a scrollbar. The slot backgrounds are now positioned relative to the slot offsets instead of to the tab background, so that they stay in place when the tab background moves. This is equivalent for all existing tabs, as none of them override the default slot offsets. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01Kzc5LxSaedCXCK4pynhJV3 --- .../ContainerScreenTerminalStorage.java | 49 ++++++++++++++----- 1 file changed, 36 insertions(+), 13 deletions(-) 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 00544b9598..b8905e5712 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 @@ -80,6 +80,9 @@ public class ContainerScreenTerminalStorage