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..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; @@ -55,8 +57,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,10 +85,35 @@ 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; + } + + @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/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..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 @@ -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,13 @@ 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.getBlockEntity(pos) instanceof BlockEntityChestWall redstoneWall) { + redstoneWall.updateRedstoneSignal(); + } + } } 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..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 @@ -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 { @@ -59,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 @@ -79,6 +82,21 @@ 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(); + updateRedstoneSignal(); + } + + @Override + public int getMaxStackSize() { + return 1; + } + }; public BlockEntityChestWall(BlockEntityType type, BlockPos pos, BlockState state) { super(type, pos, state); @@ -119,10 +137,51 @@ 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 redstone power a Redstone wall emits to its neighbours, as last updated. + */ + public int getRedstoneSignal() { + return redstoneSignal; + } + + /** + * 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; + } + int signal = getComparatorSignal(); + if (signal != redstoneSignal) { + redstoneSignal = signal; + setChanged(); + Block block = getBlockState().getBlock(); + level.updateNeighborsAt(worldPosition, block); + level.updateNeighbourForOutputSignal(worldPosition, block); + } + } + /** * @return The formed chest this wall belongs to. */ @@ -270,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() { @@ -292,6 +355,10 @@ 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)); + } + tag.putInt("redstone_signal", redstoneSignal); } private void saveDisplay(CompoundTag tag, HolderLookup.Provider registries, boolean withStats) { @@ -345,6 +412,9 @@ 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); + redstoneSignal = tag.getInt("redstone_signal"); } @Override @@ -369,7 +439,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 a2496203..fec0af64 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 @@ -32,6 +32,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; @@ -62,6 +63,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; @@ -1971,6 +1973,72 @@ 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)); + // 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(() -> { + 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"); + 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")) + .thenSucceed(); + } + // Nested chests @GameTest(template = TEMPLATE_EMPTY) 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..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 @@ -12,7 +12,9 @@ "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_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": "Redstone Chest Wall", + "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", @@ -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": "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.", "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 00000000..d63fddb8 Binary files /dev/null and b/loader-common/src/main/resources/assets/colossalchests2/textures/block/chest_wall_redstone.png differ 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 00000000..67ab5d15 Binary files /dev/null and b/loader-common/src/main/resources/assets/colossalchests2/textures/block/chest_wall_redstone_icon.png differ 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)); } /**