From 98fbd5f30f84863f12d7d17b9faea1e637b07b25 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 5 Oct 2026 17:52:38 +0000 Subject: [PATCH 1/4] Add Redstone wall A functional wall whose comparator output follows the whole chest, like the core, or the slots holding one item type. The item is set through a ghost slot in its settings, opened by sneak-right-click with an empty hand, which also shows the current signal. The core refreshes Redstone walls when its contents change. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01RSz5r6eowhjNuANBjZE2v6 --- .../colossalchests2/RegistryEntries.java | 3 + .../block/BlockChestFunctionalWall.java | 15 +++- .../colossalchests2/block/WallType.java | 9 ++- .../blockentity/BlockEntityChestCore.java | 8 +++ .../blockentity/BlockEntityChestWall.java | 50 ++++++++++++-- .../capability/StorageSignals.java | 14 ++++ .../client/gui/ContainerScreenRedstone.java | 65 ++++++++++++++++++ .../gametest/GameTestsCommon.java | 55 +++++++++++++++ .../inventory/ContainerRedstone.java | 63 +++++++++++++++++ .../inventory/ContainerRedstoneConfig.java | 23 +++++++ ...erRedstoneConfigScreenFactoryProvider.java | 19 +++++ .../blockstates/chest_wall_redstone.json | 10 +++ .../assets/colossalchests2/lang/en_us.json | 5 ++ .../models/block/chest_wall_redstone.json | 6 ++ .../block/chest_wall_redstone_formed.json | 6 ++ .../models/item/chest_wall_redstone.json | 3 + .../textures/block/chest_wall_redstone.png | Bin 0 -> 364 bytes .../block/chest_wall_redstone_icon.png | Bin 0 -> 166 bytes .../blocks/chest_wall_redstone.json | 19 +++++ .../tags/block/mineable/pickaxe.json | 3 +- .../capability/TestStorageSignals.java | 23 +++++++ .../colossalchests2/ColossalChestsFabric.java | 2 + .../colossalchests2/ColossalChestsForge.java | 2 + .../colossalchests2/ColossalChests.java | 2 + 24 files changed, 396 insertions(+), 9 deletions(-) create mode 100644 loader-common/src/main/java/org/cyclops/colossalchests2/client/gui/ContainerScreenRedstone.java create mode 100644 loader-common/src/main/java/org/cyclops/colossalchests2/inventory/ContainerRedstone.java create mode 100644 loader-common/src/main/java/org/cyclops/colossalchests2/inventory/ContainerRedstoneConfig.java create mode 100644 loader-common/src/main/java/org/cyclops/colossalchests2/inventory/ContainerRedstoneConfigScreenFactoryProvider.java create mode 100644 loader-common/src/main/resources/assets/colossalchests2/blockstates/chest_wall_redstone.json create mode 100644 loader-common/src/main/resources/assets/colossalchests2/models/block/chest_wall_redstone.json create mode 100644 loader-common/src/main/resources/assets/colossalchests2/models/block/chest_wall_redstone_formed.json create mode 100644 loader-common/src/main/resources/assets/colossalchests2/models/item/chest_wall_redstone.json create mode 100644 loader-common/src/main/resources/assets/colossalchests2/textures/block/chest_wall_redstone.png create mode 100644 loader-common/src/main/resources/assets/colossalchests2/textures/block/chest_wall_redstone_icon.png create mode 100644 loader-common/src/main/resources/data/colossalchests2/loot_table/blocks/chest_wall_redstone.json diff --git a/loader-common/src/main/java/org/cyclops/colossalchests2/RegistryEntries.java b/loader-common/src/main/java/org/cyclops/colossalchests2/RegistryEntries.java index 7e400c10..e8f60e64 100644 --- a/loader-common/src/main/java/org/cyclops/colossalchests2/RegistryEntries.java +++ b/loader-common/src/main/java/org/cyclops/colossalchests2/RegistryEntries.java @@ -12,6 +12,7 @@ import org.cyclops.colossalchests2.inventory.ContainerChest; import org.cyclops.colossalchests2.inventory.ContainerDisplay; import org.cyclops.colossalchests2.inventory.ContainerInterface; +import org.cyclops.colossalchests2.inventory.ContainerRedstone; import org.cyclops.colossalchests2.storage.ChestStorage; import org.cyclops.cyclopscore.config.DeferredHolderCommon; @@ -29,6 +30,8 @@ public class RegistryEntries { public static final DeferredHolderCommon, MenuType> MENU_INTERFACE = DeferredHolderCommon.create(Registries.MENU, ResourceLocation.parse("colossalchests2:interface")); + public static final DeferredHolderCommon, MenuType> MENU_REDSTONE = DeferredHolderCommon.create(Registries.MENU, ResourceLocation.parse("colossalchests2:redstone")); + public static final DeferredHolderCommon, MenuType> MENU_DISPLAY = DeferredHolderCommon.create(Registries.MENU, ResourceLocation.parse("colossalchests2:display")); public static final DeferredHolderCommon, MenuType> MENU_CHEST = DeferredHolderCommon.create(Registries.MENU, ResourceLocation.parse("colossalchests2:chest")); diff --git a/loader-common/src/main/java/org/cyclops/colossalchests2/block/BlockChestFunctionalWall.java b/loader-common/src/main/java/org/cyclops/colossalchests2/block/BlockChestFunctionalWall.java index fa807e16..39b14782 100644 --- a/loader-common/src/main/java/org/cyclops/colossalchests2/block/BlockChestFunctionalWall.java +++ b/loader-common/src/main/java/org/cyclops/colossalchests2/block/BlockChestFunctionalWall.java @@ -55,8 +55,9 @@ public BlockEntity newBlockEntity(BlockPos pos, BlockState state) { @Override protected InteractionResult useWithoutItem(BlockState state, Level level, BlockPos pos, Player player, BlockHitResult hit) { - // A right-click opens the chest, while sneaking opens an interface's settings. Display walls handle their own clicks. - if (type == WallType.INTERFACE && player.isSecondaryUseActive()) { + // A right-click opens the chest, while sneaking opens the settings of an interface or redstone wall. Display walls + // handle their own clicks. + if ((type == WallType.INTERFACE || type == WallType.REDSTONE) && player.isSecondaryUseActive()) { if (player instanceof ServerPlayer serverPlayer && level.getBlockEntity(pos) instanceof BlockEntityChestWall wall) { IModHelpers.get().getMinecraftHelpers().openMenu(serverPlayer, wall, buf -> buf.writeBlockPos(pos)); } @@ -82,6 +83,16 @@ protected ItemInteractionResult useItemOn(ItemStack stack, BlockState state, Lev return super.useItemOn(stack, state, level, pos, player, hand, hit); } + @Override + protected boolean hasAnalogOutputSignal(BlockState state) { + return type == WallType.REDSTONE; + } + + @Override + protected int getAnalogOutputSignal(BlockState state, Level level, BlockPos pos) { + return level.getBlockEntity(pos) instanceof BlockEntityChestWall wall ? wall.getComparatorSignal() : 0; + } + @Nullable @Override public BlockEntityTicker getTicker(Level level, BlockState state, BlockEntityType blockEntityType) { diff --git a/loader-common/src/main/java/org/cyclops/colossalchests2/block/WallType.java b/loader-common/src/main/java/org/cyclops/colossalchests2/block/WallType.java index 0cbf2eef..d57d8a79 100644 --- a/loader-common/src/main/java/org/cyclops/colossalchests2/block/WallType.java +++ b/loader-common/src/main/java/org/cyclops/colossalchests2/block/WallType.java @@ -1,5 +1,6 @@ package org.cyclops.colossalchests2.block; + import java.util.Locale; /** @@ -18,7 +19,11 @@ public enum WallType { /** * Shows one item type and its count on its faces, and lets players take and insert it by clicking. */ - DISPLAY; + DISPLAY, + /** + * A comparator signal for the whole chest, or for the slots holding one item type set in its own GUI. + */ + REDSTONE; public static final WallType[] VALUES = values(); @@ -33,7 +38,7 @@ public String getName() { * @return If automation can move items through this wall. */ public boolean exposesItems() { - return this != DISPLAY; + return this == INTERFACE || this == VOID; } public String getRegistryName() { diff --git a/loader-common/src/main/java/org/cyclops/colossalchests2/blockentity/BlockEntityChestCore.java b/loader-common/src/main/java/org/cyclops/colossalchests2/blockentity/BlockEntityChestCore.java index eb1fec1e..e7ed35eb 100644 --- a/loader-common/src/main/java/org/cyclops/colossalchests2/blockentity/BlockEntityChestCore.java +++ b/loader-common/src/main/java/org/cyclops/colossalchests2/blockentity/BlockEntityChestCore.java @@ -36,8 +36,10 @@ import org.cyclops.colossalchests2.GeneralConfig; import org.cyclops.colossalchests2.RegistryEntries; import org.cyclops.colossalchests2.block.BlockChestCore; +import org.cyclops.colossalchests2.block.BlockChestFunctionalWall; import org.cyclops.colossalchests2.block.BlockChestWall; import org.cyclops.colossalchests2.block.ChestMaterial; +import org.cyclops.colossalchests2.block.WallType; import org.cyclops.colossalchests2.capability.ItemHandlerLogic; import org.cyclops.colossalchests2.capability.StorageSignals; import org.cyclops.colossalchests2.config.MaterialProperties; @@ -354,6 +356,12 @@ protected void tick() { contentsChanged = false; setChanged(); level.updateNeighbourForOutputSignal(worldPosition, getBlockState().getBlock()); + for (BlockPos pos : decoratedPositions) { + BlockState state = level.getBlockState(pos); + if (state.getBlock() instanceof BlockChestFunctionalWall wall && wall.getType() == WallType.REDSTONE) { + level.updateNeighbourForOutputSignal(pos, wall); + } + } } if (!viewers.isEmpty() && storage.hasDirtySlots()) { onDirtySlots(storage.drainDirtySlots()); diff --git a/loader-common/src/main/java/org/cyclops/colossalchests2/blockentity/BlockEntityChestWall.java b/loader-common/src/main/java/org/cyclops/colossalchests2/blockentity/BlockEntityChestWall.java index 6cdd977e..431a573d 100644 --- a/loader-common/src/main/java/org/cyclops/colossalchests2/blockentity/BlockEntityChestWall.java +++ b/loader-common/src/main/java/org/cyclops/colossalchests2/blockentity/BlockEntityChestWall.java @@ -30,9 +30,11 @@ import org.cyclops.colossalchests2.block.BlockChestWall; import org.cyclops.colossalchests2.block.WallType; import org.cyclops.colossalchests2.capability.ItemHandlerLogic; +import org.cyclops.colossalchests2.capability.StorageSignals; import org.cyclops.colossalchests2.capability.WallAccess; import org.cyclops.colossalchests2.inventory.ContainerDisplay; import org.cyclops.colossalchests2.inventory.ContainerInterface; +import org.cyclops.colossalchests2.inventory.ContainerRedstone; import org.cyclops.colossalchests2.multiblock.ChestCoreIndex; import org.cyclops.colossalchests2.multiblock.ChestShape; import org.cyclops.colossalchests2.storage.DisplayStats; @@ -46,8 +48,8 @@ /** * A functional wall, giving automation access to its chest under the wall's rules. - * Only an Interface has settings: its filter and direction. A Display wall keeps the type it shows on each face, - * what each face shows, and its stats for clients. + * An Interface has a filter and direction. A Display wall keeps the type it shows on each face, what each face shows, + * and its stats for clients. A Redstone wall keeps the item type it signals for. * @author rubensworks */ public class BlockEntityChestWall extends BlockEntity implements MenuProvider { @@ -79,6 +81,21 @@ public int getMaxStackSize() { private int disabledOptions; private final Map lastInserts = Maps.newHashMap(); private final Container displayedContainer = new DisplayedContainer(); + private final SimpleContainer redstoneTarget = new SimpleContainer(1) { + @Override + public void setChanged() { + super.setChanged(); + BlockEntityChestWall.this.setChanged(); + if (level != null && !level.isClientSide) { + level.updateNeighbourForOutputSignal(worldPosition, getBlockState().getBlock()); + } + } + + @Override + public int getMaxStackSize() { + return 1; + } + }; public BlockEntityChestWall(BlockEntityType type, BlockPos pos, BlockState state) { super(type, pos, state); @@ -119,10 +136,26 @@ public WallAccess getAccess() { return switch (getWallType()) { case INTERFACE -> new WallAccess(mode, settings.getItems(), false); case VOID -> new WallAccess(WallAccess.Mode.BOTH, List.of(), true); - case DISPLAY -> WallAccess.OPEN; + case DISPLAY, REDSTONE -> WallAccess.OPEN; }; } + /** + * @return The item type a Redstone wall signals for, empty for the whole chest. + */ + public Container getRedstoneTarget() { + return redstoneTarget; + } + + /** + * @return The comparator signal of a Redstone wall, 0 while its chest is not formed. + */ + public int getComparatorSignal() { + ItemStack target = redstoneTarget.getItem(0); + return getCore().map(core -> target.isEmpty() ? StorageSignals.getComparatorSignal(core.getStorage()) + : StorageSignals.getComparatorSignal(core.getStorage(), target)).orElse(0); + } + /** * @return The formed chest this wall belongs to. */ @@ -292,6 +325,9 @@ protected void saveAdditional(CompoundTag tag, HolderLookup.Provider registries) tag.putString("mode", mode.name()); ContainerHelper.saveAllItems(tag, settings.getItems(), registries); saveDisplay(tag, registries, false); + if (!redstoneTarget.getItem(0).isEmpty()) { + tag.put("redstone_target", redstoneTarget.getItem(0).save(registries)); + } } private void saveDisplay(CompoundTag tag, HolderLookup.Provider registries, boolean withStats) { @@ -345,6 +381,8 @@ protected void loadAdditional(CompoundTag tag, HolderLookup.Provider registries) settings.getItems().set(i, items.get(i)); } loadDisplay(tag, registries); + redstoneTarget.getItems().set(0, tag.contains("redstone_target") + ? ItemStack.parseOptional(registries, tag.getCompound("redstone_target")) : ItemStack.EMPTY); } @Override @@ -369,7 +407,11 @@ public Component getDisplayName() { @Nullable @Override public AbstractContainerMenu createMenu(int id, Inventory inventory, Player player) { - return getWallType() == WallType.DISPLAY ? new ContainerDisplay(id, inventory, this) : new ContainerInterface(id, inventory, this); + return switch (getWallType()) { + case DISPLAY -> new ContainerDisplay(id, inventory, this); + case REDSTONE -> new ContainerRedstone(id, inventory, this); + default -> new ContainerInterface(id, inventory, this); + }; } private record InsertClick(Direction face, long gameTime) { diff --git a/loader-common/src/main/java/org/cyclops/colossalchests2/capability/StorageSignals.java b/loader-common/src/main/java/org/cyclops/colossalchests2/capability/StorageSignals.java index 2a3fc53b..31a73fda 100644 --- a/loader-common/src/main/java/org/cyclops/colossalchests2/capability/StorageSignals.java +++ b/loader-common/src/main/java/org/cyclops/colossalchests2/capability/StorageSignals.java @@ -1,7 +1,9 @@ package org.cyclops.colossalchests2.capability; +import net.minecraft.world.item.ItemStack; import org.cyclops.colossalchests2.storage.ChestStorage; import org.cyclops.colossalchests2.storage.DeepSlot; +import org.cyclops.colossalchests2.storage.DisplayStats; /** * Redstone signals derived from a {@link ChestStorage}. @@ -37,4 +39,16 @@ public static int getComparatorSignal(ChestStorage storage) { return (int) Math.floor(fill / slots * 14.0) + 1; } + /** + * Comparator signal for one item type: 0 when the chest holds none, otherwise 1 to 15 by how full the slots + * holding it are. + * @param storage A storage. + * @param type An item type. + * @return A signal from 0 to 15. + */ + public static int getComparatorSignal(ChestStorage storage, ItemStack type) { + DisplayStats stats = DisplayStats.of(storage, type); + return stats.count() <= 0 ? 0 : (int) Math.floor(stats.getFillLevel() * 14.0) + 1; + } + } diff --git a/loader-common/src/main/java/org/cyclops/colossalchests2/client/gui/ContainerScreenRedstone.java b/loader-common/src/main/java/org/cyclops/colossalchests2/client/gui/ContainerScreenRedstone.java new file mode 100644 index 00000000..731bc6fb --- /dev/null +++ b/loader-common/src/main/java/org/cyclops/colossalchests2/client/gui/ContainerScreenRedstone.java @@ -0,0 +1,65 @@ +package org.cyclops.colossalchests2.client.gui; + +import net.minecraft.ChatFormatting; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen; +import net.minecraft.network.chat.Component; +import net.minecraft.world.entity.player.Inventory; +import net.minecraft.world.inventory.Slot; +import org.cyclops.colossalchests2.inventory.ContainerRedstone; + +import java.util.List; + +/** + * Settings of a Redstone wall. + * @author rubensworks + */ +public class ContainerScreenRedstone extends AbstractContainerScreen { + + private static final int COLOR_LABEL = 0x404040; + private static final int COLOR_SIGNAL = 0xC02010; + + public ContainerScreenRedstone(ContainerRedstone menu, Inventory inventory, Component title) { + super(menu, inventory, title); + this.imageWidth = ContainerRedstone.WIDTH; + this.imageHeight = ContainerRedstone.HEIGHT; + this.inventoryLabelY = ContainerRedstone.INVENTORY_Y - 11; + } + + @Override + protected void renderBg(GuiGraphics guiGraphics, float partialTick, int mouseX, int mouseY) { + GuiPanels.drawPanel(guiGraphics, leftPos, topPos, imageWidth, imageHeight); + for (Slot slot : menu.slots) { + GuiPanels.drawSlot(guiGraphics, leftPos + slot.x, topPos + slot.y); + } + } + + @Override + protected void renderLabels(GuiGraphics guiGraphics, int mouseX, int mouseY) { + super.renderLabels(guiGraphics, mouseX, mouseY); + // What the slot means when empty, and the signal it gives now. + Component target = menu.getSlot(0).hasItem() ? menu.getSlot(0).getItem().getHoverName() + : Component.translatable("gui.colossalchests2.redstone.whole_chest"); + int textX = ContainerRedstone.TARGET_X + 22; + guiGraphics.drawString(font, target, textX, ContainerRedstone.TARGET_Y - 1, COLOR_LABEL, false); + Component signal = Component.translatable("gui.colossalchests2.redstone.signal", menu.getSignal()); + guiGraphics.drawString(font, signal, textX, ContainerRedstone.TARGET_Y + 9, COLOR_SIGNAL, false); + } + + @Override + public void render(GuiGraphics guiGraphics, int mouseX, int mouseY, float partialTick) { + super.render(guiGraphics, mouseX, mouseY, partialTick); + renderTooltip(guiGraphics, mouseX, mouseY); + } + + @Override + protected void renderTooltip(GuiGraphics guiGraphics, int mouseX, int mouseY) { + if (menu.getCarried().isEmpty() && hoveredSlot != null && menu.isGhostSlot(hoveredSlot.index) && !hoveredSlot.hasItem()) { + String key = "gui.colossalchests2.redstone.whole_chest"; + guiGraphics.renderComponentTooltip(font, List.of(Component.translatable(key), + Component.translatable(key + ".info").withStyle(ChatFormatting.GRAY)), mouseX, mouseY); + return; + } + super.renderTooltip(guiGraphics, mouseX, mouseY); + } +} diff --git a/loader-common/src/main/java/org/cyclops/colossalchests2/gametest/GameTestsCommon.java b/loader-common/src/main/java/org/cyclops/colossalchests2/gametest/GameTestsCommon.java index b0d1a3cf..3744890a 100644 --- a/loader-common/src/main/java/org/cyclops/colossalchests2/gametest/GameTestsCommon.java +++ b/loader-common/src/main/java/org/cyclops/colossalchests2/gametest/GameTestsCommon.java @@ -58,6 +58,7 @@ import org.cyclops.colossalchests2.inventory.ContainerChest; import org.cyclops.colossalchests2.inventory.ContainerDisplay; import org.cyclops.colossalchests2.inventory.ContainerInterface; +import org.cyclops.colossalchests2.inventory.ContainerRedstone; import org.cyclops.colossalchests2.multiblock.ChestCoreIndex; import org.cyclops.colossalchests2.multiblock.ChestStructure; import org.cyclops.colossalchests2.multiblock.StructureDiagnosis; @@ -1966,4 +1967,58 @@ public void testDisplayWallOnlyMinedWithAPickaxe(GameTestHelper helper) { .thenSucceed(); } + // Redstone walls + + private static int getComparatorOutput(GameTestHelper helper, BlockPos comparatorPos) { + return ((ComparatorBlockEntity) helper.getBlockEntity(comparatorPos)).getOutputSignal(); + } + + @GameTest(template = TEMPLATE_EMPTY) + public void testRedstoneWallSignal(GameTestHelper helper) { + BlockPos corePos = buildChest(helper, MIN_A, 3, ChestMaterial.WOOD); + BlockPos wallPos = placeWall(helper, MIN_A.offset(1, 0, 0), WallType.REDSTONE); + BlockPos comparatorPos = wallPos.north(); + helper.setBlock(comparatorPos.below(), Blocks.STONE); + helper.setBlock(comparatorPos, Blocks.COMPARATOR.defaultBlockState().setValue(ComparatorBlock.FACING, Direction.SOUTH)); + ItemStack dirt = new ItemStack(Items.DIRT); + helper.startSequence() + .thenWaitUntil(() -> assertFormed(helper, corePos, MIN_A, 3)) + .thenExecute(() -> { + helper.assertValueEqual(getComparatorOutput(helper, comparatorPos), 0, "signal of an empty chest"); + helper.assertTrue(getWall(helper, wallPos).getItemHandlerLogic().isEmpty(), "Expected no item access through a Redstone wall"); + getCore(helper, corePos).getStorage().insert(STONE, 1024, false); + }) + // Without a target, like the core: one full slot of 27. + .thenWaitUntil(() -> helper.assertValueEqual(getComparatorOutput(helper, comparatorPos), 1, "whole chest signal")) + .thenExecute(() -> getWall(helper, wallPos).getRedstoneTarget().setItem(0, dirt.copy())) + .thenWaitUntil(() -> helper.assertValueEqual(getComparatorOutput(helper, comparatorPos), 0, "signal without dirt")) + .thenExecute(() -> { + ChestStorage storage = getCore(helper, corePos).getStorage(); + storage.insert(dirt, storage.getCapacity(dirt), false); + }) + // Only the dirt slot counts, and it is full. + .thenWaitUntil(() -> helper.assertValueEqual(getComparatorOutput(helper, comparatorPos), 15, "signal of a full dirt slot")) + .thenExecute(() -> { + BlockEntityChestWall wall = getWall(helper, wallPos); + // The target survives saving and loading. + BlockEntityChestWall loaded = new BlockEntityChestWall(wall.getBlockPos(), wall.getBlockState()); + loaded.loadWithComponents(wall.saveWithoutMetadata(helper.getLevel().registryAccess()), helper.getLevel().registryAccess()); + helper.assertTrue(loaded.getRedstoneTarget().getItem(0).is(Items.DIRT), "Expected the target after loading"); + // The settings set the target through a ghost slot, and show the signal. + ServerPlayer player = makeViewer(helper); + ContainerRedstone menu = new ContainerRedstone(104, player.getInventory(), wall); + player.containerMenu = menu; + helper.assertValueEqual(menu.getSignal(), 15, "signal in the settings"); + menu.setCarried(ItemStack.EMPTY); + menu.clicked(0, 0, ClickType.PICKUP, player); + helper.assertTrue(wall.getRedstoneTarget().getItem(0).isEmpty(), "Expected the whole chest again"); + // Only Redstone walls give a signal. + BlockState interfaceState = functionalWall(WallType.INTERFACE).defaultBlockState(); + helper.assertFalse(interfaceState.hasAnalogOutputSignal(), "Expected no signal from an Interface"); + }) + // Two full slots of 27. + .thenWaitUntil(() -> helper.assertValueEqual(getComparatorOutput(helper, comparatorPos), 2, "whole chest signal again")) + .thenSucceed(); + } + } diff --git a/loader-common/src/main/java/org/cyclops/colossalchests2/inventory/ContainerRedstone.java b/loader-common/src/main/java/org/cyclops/colossalchests2/inventory/ContainerRedstone.java new file mode 100644 index 00000000..cb72f0fd --- /dev/null +++ b/loader-common/src/main/java/org/cyclops/colossalchests2/inventory/ContainerRedstone.java @@ -0,0 +1,63 @@ +package org.cyclops.colossalchests2.inventory; + +import net.minecraft.core.BlockPos; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.world.Container; +import net.minecraft.world.SimpleContainer; +import net.minecraft.world.entity.player.Inventory; +import net.minecraft.world.inventory.DataSlot; +import org.cyclops.colossalchests2.RegistryEntries; +import org.cyclops.colossalchests2.blockentity.BlockEntityChestWall; +import org.jetbrains.annotations.Nullable; + +/** + * The settings of a Redstone wall: the item type it signals for, empty for the whole chest, and its current signal. + * @author rubensworks + */ +public class ContainerRedstone extends ContainerGhostSettings { + + public static final int WIDTH = 176; + public static final int HEIGHT = 136; + public static final int TARGET_X = 8; + public static final int TARGET_Y = 22; + public static final int INVENTORY_Y = 54; + + private final DataSlot signal; + + /** + * Client-side constructor. + */ + public ContainerRedstone(int id, Inventory inventory, FriendlyByteBuf data) { + this(id, inventory, data.readBlockPos(), new SimpleContainer(1), null); + } + + /** + * Server-side constructor. + */ + public ContainerRedstone(int id, Inventory inventory, BlockEntityChestWall wall) { + this(id, inventory, wall.getBlockPos(), wall.getRedstoneTarget(), wall); + } + + private ContainerRedstone(int id, Inventory inventory, BlockPos pos, Container target, @Nullable BlockEntityChestWall wall) { + super(RegistryEntries.MENU_REDSTONE.value(), id, wall); + addGhostSlot(target, 0, TARGET_X, TARGET_Y); + addPlayerInventory(inventory, INVENTORY_Y); + this.signal = addDataSlot(wall == null ? DataSlot.standalone() : new DataSlot() { + @Override + public int get() { + return wall.getComparatorSignal(); + } + + @Override + public void set(int value) { + } + }); + } + + /** + * @return The wall's comparator signal, from 0 to 15. + */ + public int getSignal() { + return signal.get(); + } +} diff --git a/loader-common/src/main/java/org/cyclops/colossalchests2/inventory/ContainerRedstoneConfig.java b/loader-common/src/main/java/org/cyclops/colossalchests2/inventory/ContainerRedstoneConfig.java new file mode 100644 index 00000000..d1de7883 --- /dev/null +++ b/loader-common/src/main/java/org/cyclops/colossalchests2/inventory/ContainerRedstoneConfig.java @@ -0,0 +1,23 @@ +package org.cyclops.colossalchests2.inventory; + +import net.minecraft.world.flag.FeatureFlags; +import org.cyclops.cyclopscore.config.extendedconfig.GuiConfigCommon; +import org.cyclops.cyclopscore.config.extendedconfig.GuiConfigScreenFactoryProvider; +import org.cyclops.cyclopscore.init.IModBase; +import org.cyclops.cyclopscore.inventory.container.ContainerTypeDataCommon; + +/** + * Config for {@link ContainerRedstone}. + * @author rubensworks + */ +public class ContainerRedstoneConfig extends GuiConfigCommon { + + public ContainerRedstoneConfig(M mod) { + super(mod, "redstone", eConfig -> new ContainerTypeDataCommon<>(ContainerRedstone::new, FeatureFlags.VANILLA_SET)); + } + + @Override + public GuiConfigScreenFactoryProvider getScreenFactoryProvider() { + return new ContainerRedstoneConfigScreenFactoryProvider(); + } +} diff --git a/loader-common/src/main/java/org/cyclops/colossalchests2/inventory/ContainerRedstoneConfigScreenFactoryProvider.java b/loader-common/src/main/java/org/cyclops/colossalchests2/inventory/ContainerRedstoneConfigScreenFactoryProvider.java new file mode 100644 index 00000000..701f8bc2 --- /dev/null +++ b/loader-common/src/main/java/org/cyclops/colossalchests2/inventory/ContainerRedstoneConfigScreenFactoryProvider.java @@ -0,0 +1,19 @@ +package org.cyclops.colossalchests2.inventory; + +import net.minecraft.client.gui.screens.MenuScreens; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.client.gui.screens.inventory.MenuAccess; +import org.cyclops.colossalchests2.client.gui.ContainerScreenRedstone; +import org.cyclops.cyclopscore.client.gui.ScreenFactorySafe; +import org.cyclops.cyclopscore.config.extendedconfig.GuiConfigScreenFactoryProvider; + +/** + * Only loaded on the client. + * @author rubensworks + */ +public class ContainerRedstoneConfigScreenFactoryProvider extends GuiConfigScreenFactoryProvider { + @Override + public > MenuScreens.ScreenConstructor getScreenFactory() { + return new ScreenFactorySafe<>(ContainerScreenRedstone::new); + } +} diff --git a/loader-common/src/main/resources/assets/colossalchests2/blockstates/chest_wall_redstone.json b/loader-common/src/main/resources/assets/colossalchests2/blockstates/chest_wall_redstone.json new file mode 100644 index 00000000..f634f8a3 --- /dev/null +++ b/loader-common/src/main/resources/assets/colossalchests2/blockstates/chest_wall_redstone.json @@ -0,0 +1,10 @@ +{ + "variants": { + "formed=false": { + "model": "colossalchests2:block/chest_wall_redstone" + }, + "formed=true": { + "model": "colossalchests2:block/chest_wall_redstone_formed" + } + } +} diff --git a/loader-common/src/main/resources/assets/colossalchests2/lang/en_us.json b/loader-common/src/main/resources/assets/colossalchests2/lang/en_us.json index abdf405c..2fbb08c0 100644 --- a/loader-common/src/main/resources/assets/colossalchests2/lang/en_us.json +++ b/loader-common/src/main/resources/assets/colossalchests2/lang/en_us.json @@ -12,6 +12,8 @@ "block.colossalchests2.chest_wall_void": "Void Chest Interface", "block.colossalchests2.chest_wall_void.info": "An interface that destroys overflowing items.", "block.colossalchests2.chest_wall_display": "Chest Display", + "block.colossalchests2.chest_wall_redstone": "Redstone Chest Wall", + "block.colossalchests2.chest_wall_redstone.info": "Gives a comparator signal for the whole chest or one item. Sneak-right-click with empty hand to choose the item.", "block.colossalchests2.chest_wall_display.info": "Shows an item and its count. Take items with (sneak-)left-click and insert with (double) right-click. Right-click with empty hand to configure.", "block.colossalchests2.chest_wall_gold": "Golden Chest Wall", "block.colossalchests2.chest_core_gold": "Golden Chest Core", @@ -64,6 +66,9 @@ "gui.colossalchests2.lock_all": "Lock all", "gui.colossalchests2.wall.filter": "Filter", "gui.colossalchests2.wall.filter.info": "Click with item to configure.", + "gui.colossalchests2.redstone.whole_chest": "Whole chest", + "gui.colossalchests2.redstone.whole_chest.info": "The signal follows how full the chest is. Click with an item to only follow the slots holding it.", + "gui.colossalchests2.redstone.signal": "Signal: %s", "gui.colossalchests2.wall.displayed": "Displayed item", "gui.colossalchests2.wall.displayed.info": "Click with item to choose, click with empty hand to clear.", "gui.colossalchests2.display.side.front": "Front", diff --git a/loader-common/src/main/resources/assets/colossalchests2/models/block/chest_wall_redstone.json b/loader-common/src/main/resources/assets/colossalchests2/models/block/chest_wall_redstone.json new file mode 100644 index 00000000..415ec508 --- /dev/null +++ b/loader-common/src/main/resources/assets/colossalchests2/models/block/chest_wall_redstone.json @@ -0,0 +1,6 @@ +{ + "parent": "minecraft:block/cube_all", + "textures": { + "all": "colossalchests2:block/chest_wall_redstone" + } +} diff --git a/loader-common/src/main/resources/assets/colossalchests2/models/block/chest_wall_redstone_formed.json b/loader-common/src/main/resources/assets/colossalchests2/models/block/chest_wall_redstone_formed.json new file mode 100644 index 00000000..e077f5c6 --- /dev/null +++ b/loader-common/src/main/resources/assets/colossalchests2/models/block/chest_wall_redstone_formed.json @@ -0,0 +1,6 @@ +{ + "parent": "colossalchests2:block/formed_member", + "textures": { + "particle": "colossalchests2:block/chest_wall_redstone" + } +} diff --git a/loader-common/src/main/resources/assets/colossalchests2/models/item/chest_wall_redstone.json b/loader-common/src/main/resources/assets/colossalchests2/models/item/chest_wall_redstone.json new file mode 100644 index 00000000..304365c5 --- /dev/null +++ b/loader-common/src/main/resources/assets/colossalchests2/models/item/chest_wall_redstone.json @@ -0,0 +1,3 @@ +{ + "parent": "colossalchests2:block/chest_wall_redstone" +} diff --git a/loader-common/src/main/resources/assets/colossalchests2/textures/block/chest_wall_redstone.png b/loader-common/src/main/resources/assets/colossalchests2/textures/block/chest_wall_redstone.png new file mode 100644 index 0000000000000000000000000000000000000000..d63fddb80ae2eecf0eef6cf06b7976c67870d3b8 GIT binary patch literal 364 zcmV-y0h9iTP)#1pBF5u!D`B0{XvAPJ zzyf&hpNz-ju_5>@t{gs^6&mk-BS0I{G$qe-jB}3Zbo$cqv$+2GAICAyIZQR*cQI+2 z5<+O5xuR;_#Bt1IGWmCA=N$chA1fkcS@vX1l7s`0z|;F1RmI$Hx3{MdLck7(LuRuX zm&=9ueBMf2t#Yr|!>a0c3-ujui!IBN-EP+~s4A-3IPLm%iU?ocScm1QaeiX}0000< KMNUMnLSTYUKBU6{ literal 0 HcmV?d00001 diff --git a/loader-common/src/main/resources/assets/colossalchests2/textures/block/chest_wall_redstone_icon.png b/loader-common/src/main/resources/assets/colossalchests2/textures/block/chest_wall_redstone_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..67ab5d15c98f7c5fa88303a19dc005c0476a6b50 GIT binary patch literal 166 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`xt=bLAr*6y6BY;<9Fiz1D*Eqv z@yCmc%5FOtL7e2|WY0~NA0Hj%>6f!jN>TC@7dz0Tw;}bk*dD2xnwo$gF1r)np8pQ_>)JK4o`9ghOH4A_)eDjW=YN%+7Dw0koLG M)78&qol`;+06Qc-XaE2J literal 0 HcmV?d00001 diff --git a/loader-common/src/main/resources/data/colossalchests2/loot_table/blocks/chest_wall_redstone.json b/loader-common/src/main/resources/data/colossalchests2/loot_table/blocks/chest_wall_redstone.json new file mode 100644 index 00000000..ca284dff --- /dev/null +++ b/loader-common/src/main/resources/data/colossalchests2/loot_table/blocks/chest_wall_redstone.json @@ -0,0 +1,19 @@ +{ + "type": "minecraft:block", + "pools": [ + { + "rolls": 1, + "entries": [ + { + "type": "minecraft:item", + "name": "colossalchests2:chest_wall_redstone" + } + ], + "conditions": [ + { + "condition": "minecraft:survives_explosion" + } + ] + } + ] +} diff --git a/loader-common/src/main/resources/data/minecraft/tags/block/mineable/pickaxe.json b/loader-common/src/main/resources/data/minecraft/tags/block/mineable/pickaxe.json index bc5686f6..40108605 100644 --- a/loader-common/src/main/resources/data/minecraft/tags/block/mineable/pickaxe.json +++ b/loader-common/src/main/resources/data/minecraft/tags/block/mineable/pickaxe.json @@ -14,6 +14,7 @@ "colossalchests2:chest_core_netherite", "colossalchests2:chest_wall_interface", "colossalchests2:chest_wall_void", - "colossalchests2:chest_wall_display" + "colossalchests2:chest_wall_display", + "colossalchests2:chest_wall_redstone" ] } diff --git a/loader-common/src/test/java/org/cyclops/colossalchests2/capability/TestStorageSignals.java b/loader-common/src/test/java/org/cyclops/colossalchests2/capability/TestStorageSignals.java index 1ca20780..520df88e 100644 --- a/loader-common/src/test/java/org/cyclops/colossalchests2/capability/TestStorageSignals.java +++ b/loader-common/src/test/java/org/cyclops/colossalchests2/capability/TestStorageSignals.java @@ -72,4 +72,27 @@ public void testHalf() { assertEquals(8, StorageSignals.getComparatorSignal(storage)); } + @Test + public void testTypeAbsentIsZero() { + ChestStorage storage = new ChestStorage(3, CapacityProfile.ofDepth(4)); + storage.insert(new ItemStack(Items.STONE), 256, false); + assertEquals(0, StorageSignals.getComparatorSignal(storage, new ItemStack(Items.DIRT))); + } + + @Test + public void testTypeOnlyCountsItsSlots() { + // A quarter full dirt slot, next to a full stone slot and an empty one. + ChestStorage storage = new ChestStorage(3, CapacityProfile.ofDepth(4)); + storage.insert(new ItemStack(Items.STONE), 256, false); + storage.insert(new ItemStack(Items.DIRT), 64, false); + assertEquals(4, StorageSignals.getComparatorSignal(storage, new ItemStack(Items.DIRT))); + assertEquals(15, StorageSignals.getComparatorSignal(storage, new ItemStack(Items.STONE))); + } + + @Test + public void testTypeSingleItemGivesOne() { + ChestStorage storage = new ChestStorage(3, CapacityProfile.ofDepth(4)); + storage.insert(new ItemStack(Items.DIRT), 1, false); + assertEquals(1, StorageSignals.getComparatorSignal(storage, new ItemStack(Items.DIRT))); + } } diff --git a/loader-fabric/src/main/java/org/cyclops/colossalchests2/ColossalChestsFabric.java b/loader-fabric/src/main/java/org/cyclops/colossalchests2/ColossalChestsFabric.java index 22e8f94d..de2a9746 100644 --- a/loader-fabric/src/main/java/org/cyclops/colossalchests2/ColossalChestsFabric.java +++ b/loader-fabric/src/main/java/org/cyclops/colossalchests2/ColossalChestsFabric.java @@ -21,6 +21,7 @@ import org.cyclops.colossalchests2.inventory.ContainerChestConfig; import org.cyclops.colossalchests2.inventory.ContainerDisplayConfig; import org.cyclops.colossalchests2.inventory.ContainerInterfaceConfig; +import org.cyclops.colossalchests2.inventory.ContainerRedstoneConfig; import org.cyclops.colossalchests2.proxy.ClientProxyFabric; import org.cyclops.colossalchests2.proxy.CommonProxyFabric; import org.cyclops.colossalchests2.upgrade.ChestUpgrade; @@ -96,5 +97,6 @@ protected void onConfigsRegister(ConfigHandlerCommon configHandler) { configHandler.addConfigurable(new BlockEntityChestWallConfigFabric<>(this)); configHandler.addConfigurable(new ContainerInterfaceConfig<>(this)); configHandler.addConfigurable(new ContainerDisplayConfig<>(this)); + configHandler.addConfigurable(new ContainerRedstoneConfig<>(this)); } } diff --git a/loader-forge/src/main/java/org/cyclops/colossalchests2/ColossalChestsForge.java b/loader-forge/src/main/java/org/cyclops/colossalchests2/ColossalChestsForge.java index 53b1b14a..959c4380 100644 --- a/loader-forge/src/main/java/org/cyclops/colossalchests2/ColossalChestsForge.java +++ b/loader-forge/src/main/java/org/cyclops/colossalchests2/ColossalChestsForge.java @@ -20,6 +20,7 @@ import org.cyclops.colossalchests2.inventory.ContainerChestConfig; import org.cyclops.colossalchests2.inventory.ContainerDisplayConfig; import org.cyclops.colossalchests2.inventory.ContainerInterfaceConfig; +import org.cyclops.colossalchests2.inventory.ContainerRedstoneConfig; import org.cyclops.colossalchests2.proxy.ClientProxyForge; import org.cyclops.colossalchests2.proxy.CommonProxyForge; import org.cyclops.colossalchests2.upgrade.ChestUpgrade; @@ -100,5 +101,6 @@ protected void onConfigsRegister(ConfigHandlerCommon configHandler) { configHandler.addConfigurable(new BlockEntityChestWallConfigForge<>(this)); configHandler.addConfigurable(new ContainerInterfaceConfig<>(this)); configHandler.addConfigurable(new ContainerDisplayConfig<>(this)); + configHandler.addConfigurable(new ContainerRedstoneConfig<>(this)); } } diff --git a/loader-neoforge/src/main/java/org/cyclops/colossalchests2/ColossalChests.java b/loader-neoforge/src/main/java/org/cyclops/colossalchests2/ColossalChests.java index 223a19c5..45de7e4d 100644 --- a/loader-neoforge/src/main/java/org/cyclops/colossalchests2/ColossalChests.java +++ b/loader-neoforge/src/main/java/org/cyclops/colossalchests2/ColossalChests.java @@ -24,6 +24,7 @@ import org.cyclops.colossalchests2.inventory.ContainerChestConfig; import org.cyclops.colossalchests2.inventory.ContainerDisplayConfig; import org.cyclops.colossalchests2.inventory.ContainerInterfaceConfig; +import org.cyclops.colossalchests2.inventory.ContainerRedstoneConfig; import org.cyclops.colossalchests2.modcompat.CommonCapabilitiesModCompat; import org.cyclops.colossalchests2.proxy.ClientProxy; import org.cyclops.colossalchests2.proxy.CommonProxy; @@ -107,6 +108,7 @@ protected void onConfigsRegister(ConfigHandlerCommon configHandler) { configHandler.addConfigurable(new BlockEntityChestWallConfigNeoForge<>(this)); configHandler.addConfigurable(new ContainerInterfaceConfig<>(this)); configHandler.addConfigurable(new ContainerDisplayConfig<>(this)); + configHandler.addConfigurable(new ContainerRedstoneConfig<>(this)); } /** From 9240e17e248370b26bfcbd34097c00286b5d77ef Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 5 Oct 2026 18:15:58 +0000 Subject: [PATCH 2/4] Redstone wall: emit weak redstone power too Besides the comparator output, the wall now emits the same signal as weak power to every side, like a trapped chest, so dust and lamps work without a comparator. The emitted value is kept on the block entity and refreshed when the chest's contents or the target change, and every 10 ticks to catch the chest forming or breaking; neighbours are only updated when it changes. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01RSz5r6eowhjNuANBjZE2v6 --- .../block/BlockChestFunctionalWall.java | 19 +++++++++- .../blockentity/BlockEntityChestCore.java | 5 ++- .../blockentity/BlockEntityChestWall.java | 38 +++++++++++++++++-- .../gametest/GameTestsCommon.java | 13 +++++++ .../assets/colossalchests2/lang/en_us.json | 2 +- 5 files changed, 70 insertions(+), 7 deletions(-) diff --git a/loader-common/src/main/java/org/cyclops/colossalchests2/block/BlockChestFunctionalWall.java b/loader-common/src/main/java/org/cyclops/colossalchests2/block/BlockChestFunctionalWall.java index 39b14782..562b8a6e 100644 --- a/loader-common/src/main/java/org/cyclops/colossalchests2/block/BlockChestFunctionalWall.java +++ b/loader-common/src/main/java/org/cyclops/colossalchests2/block/BlockChestFunctionalWall.java @@ -2,12 +2,14 @@ import com.google.common.collect.Lists; import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; import net.minecraft.server.level.ServerPlayer; import net.minecraft.world.InteractionHand; import net.minecraft.world.InteractionResult; import net.minecraft.world.ItemInteractionResult; import net.minecraft.world.entity.player.Player; import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.BlockGetter; import net.minecraft.world.level.Level; import net.minecraft.world.level.block.EntityBlock; import net.minecraft.world.level.block.entity.BlockEntity; @@ -93,10 +95,25 @@ protected int getAnalogOutputSignal(BlockState state, Level level, BlockPos pos) return level.getBlockEntity(pos) instanceof BlockEntityChestWall wall ? wall.getComparatorSignal() : 0; } + @Override + protected boolean isSignalSource(BlockState state) { + return type == WallType.REDSTONE; + } + + /** + * Weak power to every side, like a trapped chest, so dust and lamps work without a comparator. + */ + @Override + protected int getSignal(BlockState state, BlockGetter level, BlockPos pos, Direction direction) { + return type == WallType.REDSTONE && state.getValue(FORMED) && level.getBlockEntity(pos) instanceof BlockEntityChestWall wall + ? wall.getRedstoneSignal() : 0; + } + @Nullable @Override public BlockEntityTicker getTicker(Level level, BlockState state, BlockEntityType blockEntityType) { - return type == WallType.DISPLAY && !level.isClientSide && blockEntityType == RegistryEntries.BLOCK_ENTITY_CHEST_WALL.value() + return (type == WallType.DISPLAY || type == WallType.REDSTONE) && !level.isClientSide + && blockEntityType == RegistryEntries.BLOCK_ENTITY_CHEST_WALL.value() ? (l, p, s, be) -> BlockEntityChestWall.serverTick(l, p, s, (BlockEntityChestWall) be) : null; } } diff --git a/loader-common/src/main/java/org/cyclops/colossalchests2/blockentity/BlockEntityChestCore.java b/loader-common/src/main/java/org/cyclops/colossalchests2/blockentity/BlockEntityChestCore.java index e7ed35eb..d129edca 100644 --- a/loader-common/src/main/java/org/cyclops/colossalchests2/blockentity/BlockEntityChestCore.java +++ b/loader-common/src/main/java/org/cyclops/colossalchests2/blockentity/BlockEntityChestCore.java @@ -358,8 +358,9 @@ protected void tick() { level.updateNeighbourForOutputSignal(worldPosition, getBlockState().getBlock()); for (BlockPos pos : decoratedPositions) { BlockState state = level.getBlockState(pos); - if (state.getBlock() instanceof BlockChestFunctionalWall wall && wall.getType() == WallType.REDSTONE) { - level.updateNeighbourForOutputSignal(pos, wall); + if (state.getBlock() instanceof BlockChestFunctionalWall wall && wall.getType() == WallType.REDSTONE + && level.getBlockEntity(pos) instanceof BlockEntityChestWall redstoneWall) { + redstoneWall.updateRedstoneSignal(); } } } diff --git a/loader-common/src/main/java/org/cyclops/colossalchests2/blockentity/BlockEntityChestWall.java b/loader-common/src/main/java/org/cyclops/colossalchests2/blockentity/BlockEntityChestWall.java index 431a573d..b458c272 100644 --- a/loader-common/src/main/java/org/cyclops/colossalchests2/blockentity/BlockEntityChestWall.java +++ b/loader-common/src/main/java/org/cyclops/colossalchests2/blockentity/BlockEntityChestWall.java @@ -61,6 +61,7 @@ public class BlockEntityChestWall extends BlockEntity implements MenuProvider { */ public static final int DOUBLE_CLICK_TICKS = 10; private static final int DISPLAY_UPDATE_TICKS = 10; + private static final int SIGNAL_UPDATE_TICKS = 10; private final SimpleContainer settings = new SimpleContainer(FILTER_SLOTS) { @Override @@ -81,14 +82,14 @@ public int getMaxStackSize() { private int disabledOptions; private final Map lastInserts = Maps.newHashMap(); private final Container displayedContainer = new DisplayedContainer(); + // The last signal a Redstone wall emitted, so redstone does not recompute it on every query. + private int redstoneSignal; private final SimpleContainer redstoneTarget = new SimpleContainer(1) { @Override public void setChanged() { super.setChanged(); BlockEntityChestWall.this.setChanged(); - if (level != null && !level.isClientSide) { - level.updateNeighbourForOutputSignal(worldPosition, getBlockState().getBlock()); - } + updateRedstoneSignal(); } @Override @@ -156,6 +157,31 @@ public int getComparatorSignal() { : StorageSignals.getComparatorSignal(core.getStorage(), target)).orElse(0); } + /** + * @return The redstone power a Redstone wall emits to its neighbours, as last updated. + */ + public int getRedstoneSignal() { + return redstoneSignal; + } + + /** + * Refresh the signal of a Redstone wall: comparators read it again, and neighbours are updated when its power + * changed. + */ + public void updateRedstoneSignal() { + if (level == null || level.isClientSide) { + return; + } + Block block = getBlockState().getBlock(); + int signal = getComparatorSignal(); + if (signal != redstoneSignal) { + redstoneSignal = signal; + setChanged(); + level.updateNeighborsAt(worldPosition, block); + } + level.updateNeighbourForOutputSignal(worldPosition, block); + } + /** * @return The formed chest this wall belongs to. */ @@ -303,6 +329,10 @@ public static void serverTick(Level level, BlockPos pos, BlockState state, Block if (level.getGameTime() % DISPLAY_UPDATE_TICKS == 0 && wall.getWallType() == WallType.DISPLAY) { wall.updateDisplayStats(false); } + // Contents changes update the signal right away, this catches the chest forming or breaking. + if (level.getGameTime() % SIGNAL_UPDATE_TICKS == 0 && wall.getWallType() == WallType.REDSTONE) { + wall.updateRedstoneSignal(); + } } private void onSettingsChanged() { @@ -328,6 +358,7 @@ protected void saveAdditional(CompoundTag tag, HolderLookup.Provider registries) if (!redstoneTarget.getItem(0).isEmpty()) { tag.put("redstone_target", redstoneTarget.getItem(0).save(registries)); } + tag.putInt("redstone_signal", redstoneSignal); } private void saveDisplay(CompoundTag tag, HolderLookup.Provider registries, boolean withStats) { @@ -383,6 +414,7 @@ protected void loadAdditional(CompoundTag tag, HolderLookup.Provider registries) loadDisplay(tag, registries); redstoneTarget.getItems().set(0, tag.contains("redstone_target") ? ItemStack.parseOptional(registries, tag.getCompound("redstone_target")) : ItemStack.EMPTY); + redstoneSignal = tag.getInt("redstone_signal"); } @Override diff --git a/loader-common/src/main/java/org/cyclops/colossalchests2/gametest/GameTestsCommon.java b/loader-common/src/main/java/org/cyclops/colossalchests2/gametest/GameTestsCommon.java index 3744890a..28b93ef4 100644 --- a/loader-common/src/main/java/org/cyclops/colossalchests2/gametest/GameTestsCommon.java +++ b/loader-common/src/main/java/org/cyclops/colossalchests2/gametest/GameTestsCommon.java @@ -29,6 +29,7 @@ import net.minecraft.world.level.block.Blocks; import net.minecraft.world.level.block.ComparatorBlock; import net.minecraft.world.level.block.HopperBlock; +import net.minecraft.world.level.block.RedstoneLampBlock; import net.minecraft.world.level.block.RenderShape; import net.minecraft.world.level.block.entity.BlockEntity; import net.minecraft.world.level.block.entity.ComparatorBlockEntity; @@ -1980,16 +1981,27 @@ public void testRedstoneWallSignal(GameTestHelper helper) { BlockPos comparatorPos = wallPos.north(); helper.setBlock(comparatorPos.below(), Blocks.STONE); helper.setBlock(comparatorPos, Blocks.COMPARATOR.defaultBlockState().setValue(ComparatorBlock.FACING, Direction.SOUTH)); + // A second wall powers a lamp directly, without a comparator. + BlockPos lampWallPos = placeWall(helper, MIN_A.offset(1, 0, 2), WallType.REDSTONE); + BlockPos lampPos = lampWallPos.south(); + helper.setBlock(lampPos, Blocks.REDSTONE_LAMP); ItemStack dirt = new ItemStack(Items.DIRT); helper.startSequence() .thenWaitUntil(() -> assertFormed(helper, corePos, MIN_A, 3)) .thenExecute(() -> { helper.assertValueEqual(getComparatorOutput(helper, comparatorPos), 0, "signal of an empty chest"); + helper.assertBlockProperty(lampPos, RedstoneLampBlock.LIT, false); helper.assertTrue(getWall(helper, wallPos).getItemHandlerLogic().isEmpty(), "Expected no item access through a Redstone wall"); getCore(helper, corePos).getStorage().insert(STONE, 1024, false); }) // Without a target, like the core: one full slot of 27. .thenWaitUntil(() -> helper.assertValueEqual(getComparatorOutput(helper, comparatorPos), 1, "whole chest signal")) + .thenWaitUntil(() -> { + helper.assertBlockProperty(lampPos, RedstoneLampBlock.LIT, true); + helper.assertValueEqual(helper.getLevel().getSignal(helper.absolutePos(lampWallPos), Direction.NORTH), 1, "weak power"); + // Weak power only, like a trapped chest's sides. + helper.assertValueEqual(helper.getLevel().getDirectSignal(helper.absolutePos(lampWallPos), Direction.NORTH), 0, "strong power"); + }) .thenExecute(() -> getWall(helper, wallPos).getRedstoneTarget().setItem(0, dirt.copy())) .thenWaitUntil(() -> helper.assertValueEqual(getComparatorOutput(helper, comparatorPos), 0, "signal without dirt")) .thenExecute(() -> { @@ -2015,6 +2027,7 @@ public void testRedstoneWallSignal(GameTestHelper helper) { // Only Redstone walls give a signal. BlockState interfaceState = functionalWall(WallType.INTERFACE).defaultBlockState(); helper.assertFalse(interfaceState.hasAnalogOutputSignal(), "Expected no signal from an Interface"); + helper.assertFalse(interfaceState.isSignalSource(), "Expected no power from an Interface"); }) // Two full slots of 27. .thenWaitUntil(() -> helper.assertValueEqual(getComparatorOutput(helper, comparatorPos), 2, "whole chest signal again")) diff --git a/loader-common/src/main/resources/assets/colossalchests2/lang/en_us.json b/loader-common/src/main/resources/assets/colossalchests2/lang/en_us.json index 2fbb08c0..9d536bde 100644 --- a/loader-common/src/main/resources/assets/colossalchests2/lang/en_us.json +++ b/loader-common/src/main/resources/assets/colossalchests2/lang/en_us.json @@ -13,7 +13,7 @@ "block.colossalchests2.chest_wall_void.info": "An interface that destroys overflowing items.", "block.colossalchests2.chest_wall_display": "Chest Display", "block.colossalchests2.chest_wall_redstone": "Redstone Chest Wall", - "block.colossalchests2.chest_wall_redstone.info": "Gives a comparator signal for the whole chest or one item. Sneak-right-click with empty hand to choose the item.", + "block.colossalchests2.chest_wall_redstone.info": "Emits a redstone signal, also to comparators, by how full the chest or one item is. Sneak-right-click with empty hand to choose the item.", "block.colossalchests2.chest_wall_display.info": "Shows an item and its count. Take items with (sneak-)left-click and insert with (double) right-click. Right-click with empty hand to configure.", "block.colossalchests2.chest_wall_gold": "Golden Chest Wall", "block.colossalchests2.chest_core_gold": "Golden Chest Core", From 55b16e2cef2610c5c20e303ad173f5be91655010 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 5 Oct 2026 18:21:14 +0000 Subject: [PATCH 3/4] Redstone wall: only notify comparators when the signal changes Comparators read the same value the wall emits, so like the neighbour update, the comparator update is skipped while the value stays the same. This keeps hopper traffic and the periodic check free of updates. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01RSz5r6eowhjNuANBjZE2v6 --- .../colossalchests2/blockentity/BlockEntityChestWall.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/loader-common/src/main/java/org/cyclops/colossalchests2/blockentity/BlockEntityChestWall.java b/loader-common/src/main/java/org/cyclops/colossalchests2/blockentity/BlockEntityChestWall.java index b458c272..cd5f9426 100644 --- a/loader-common/src/main/java/org/cyclops/colossalchests2/blockentity/BlockEntityChestWall.java +++ b/loader-common/src/main/java/org/cyclops/colossalchests2/blockentity/BlockEntityChestWall.java @@ -165,21 +165,21 @@ public int getRedstoneSignal() { } /** - * Refresh the signal of a Redstone wall: comparators read it again, and neighbours are updated when its power - * changed. + * Refresh the signal of a Redstone wall. Only when it changed are neighbours and comparators updated, as both + * read the same value. */ public void updateRedstoneSignal() { if (level == null || level.isClientSide) { return; } - Block block = getBlockState().getBlock(); int signal = getComparatorSignal(); if (signal != redstoneSignal) { redstoneSignal = signal; setChanged(); + Block block = getBlockState().getBlock(); level.updateNeighborsAt(worldPosition, block); + level.updateNeighbourForOutputSignal(worldPosition, block); } - level.updateNeighbourForOutputSignal(worldPosition, block); } /** From 5b9a83ce84dc8d01275e3bbe5d89bb6f2edf39fe Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 6 Oct 2026 17:36:20 +0000 Subject: [PATCH 4/4] Apply review wording for the Redstone and Display walls Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01RSz5r6eowhjNuANBjZE2v6 --- .../main/resources/assets/colossalchests2/lang/en_us.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/loader-common/src/main/resources/assets/colossalchests2/lang/en_us.json b/loader-common/src/main/resources/assets/colossalchests2/lang/en_us.json index 9d536bde..b8fb8e9d 100644 --- a/loader-common/src/main/resources/assets/colossalchests2/lang/en_us.json +++ b/loader-common/src/main/resources/assets/colossalchests2/lang/en_us.json @@ -13,8 +13,8 @@ "block.colossalchests2.chest_wall_void.info": "An interface that destroys overflowing items.", "block.colossalchests2.chest_wall_display": "Chest Display", "block.colossalchests2.chest_wall_redstone": "Redstone Chest Wall", - "block.colossalchests2.chest_wall_redstone.info": "Emits a redstone signal, also to comparators, by how full the chest or one item is. Sneak-right-click with empty hand to choose the item.", - "block.colossalchests2.chest_wall_display.info": "Shows an item and its count. Take items with (sneak-)left-click and insert with (double) right-click. Right-click with empty hand to configure.", + "block.colossalchests2.chest_wall_redstone.info": "Emits a redstone signal based on how full the chest is.", + "block.colossalchests2.chest_wall_display.info": "Shows an item and its count. Take items with (sneak-)left-click and insert with (double) right-click.", "block.colossalchests2.chest_wall_gold": "Golden Chest Wall", "block.colossalchests2.chest_core_gold": "Golden Chest Core", "block.colossalchests2.chest_wall_diamond": "Diamond Chest Wall", @@ -67,7 +67,7 @@ "gui.colossalchests2.wall.filter": "Filter", "gui.colossalchests2.wall.filter.info": "Click with item to configure.", "gui.colossalchests2.redstone.whole_chest": "Whole chest", - "gui.colossalchests2.redstone.whole_chest.info": "The signal follows how full the chest is. Click with an item to only follow the slots holding it.", + "gui.colossalchests2.redstone.whole_chest.info": "Redstone signal is based on the overall chest contents. Click with an item to consider only the slots holding this item.", "gui.colossalchests2.redstone.signal": "Signal: %s", "gui.colossalchests2.wall.displayed": "Displayed item", "gui.colossalchests2.wall.displayed.info": "Click with item to choose, click with empty hand to clear.",