diff --git a/loader-common/src/main/java/org/cyclops/colossalchests2/GeneralConfig.java b/loader-common/src/main/java/org/cyclops/colossalchests2/GeneralConfig.java index 7f13b45e..faf7d434 100644 --- a/loader-common/src/main/java/org/cyclops/colossalchests2/GeneralConfig.java +++ b/loader-common/src/main/java/org/cyclops/colossalchests2/GeneralConfig.java @@ -45,6 +45,9 @@ public class GeneralConfig extends DummyConfigCommon { @ConfigurablePropertyCommon(category = "chest", comment = "If items that do not stack, such as tools, can be stored.", isCommandable = true, configLocation = ModConfigLocation.SERVER) public static boolean acceptNonStackables = true; + @ConfigurablePropertyCommon(category = "display", comment = "If Display walls show blocks and other 3D items like an item frame, front-on, instead of like an inventory icon.", configLocation = ModConfigLocation.CLIENT) + public static boolean displayItemFrameStyle = false; + public GeneralConfig(M mod) { super(mod, "general"); } 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 59172791..7e400c10 100644 --- a/loader-common/src/main/java/org/cyclops/colossalchests2/RegistryEntries.java +++ b/loader-common/src/main/java/org/cyclops/colossalchests2/RegistryEntries.java @@ -9,8 +9,8 @@ import net.minecraft.world.level.block.entity.BlockEntityType; import org.cyclops.colossalchests2.blockentity.BlockEntityChestCore; import org.cyclops.colossalchests2.blockentity.BlockEntityChestWall; -import org.cyclops.colossalchests2.inventory.ChestSettings; import org.cyclops.colossalchests2.inventory.ContainerChest; +import org.cyclops.colossalchests2.inventory.ContainerDisplay; import org.cyclops.colossalchests2.inventory.ContainerInterface; import org.cyclops.colossalchests2.storage.ChestStorage; import org.cyclops.cyclopscore.config.DeferredHolderCommon; @@ -29,7 +29,7 @@ public class RegistryEntries { public static final DeferredHolderCommon, MenuType> MENU_INTERFACE = DeferredHolderCommon.create(Registries.MENU, ResourceLocation.parse("colossalchests2:interface")); - public static final DeferredHolderCommon, DataComponentType> COMPONENT_CHEST_SETTINGS = DeferredHolderCommon.create(Registries.DATA_COMPONENT_TYPE, ResourceLocation.parse("colossalchests2:chest_settings")); + 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 7d04f52e..fa807e16 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 @@ -3,11 +3,16 @@ import com.google.common.collect.Lists; import net.minecraft.core.BlockPos; 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.Level; import net.minecraft.world.level.block.EntityBlock; import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.entity.BlockEntityTicker; +import net.minecraft.world.level.block.entity.BlockEntityType; import net.minecraft.world.level.block.state.BlockState; import net.minecraft.world.phys.BlockHitResult; import org.cyclops.colossalchests2.RegistryEntries; @@ -50,13 +55,37 @@ public BlockEntity newBlockEntity(BlockPos pos, BlockState state) { @Override protected InteractionResult useWithoutItem(BlockState state, Level level, BlockPos pos, Player player, BlockHitResult hit) { - // Like other walls a click opens the chest, sneaking opens an interface's own settings. + // 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()) { if (player instanceof ServerPlayer serverPlayer && level.getBlockEntity(pos) instanceof BlockEntityChestWall wall) { IModHelpers.get().getMinecraftHelpers().openMenu(serverPlayer, wall, buf -> buf.writeBlockPos(pos)); } return InteractionResult.sidedSuccess(level.isClientSide); } + if (type == WallType.DISPLAY && level.getBlockEntity(pos) instanceof BlockEntityChestWall wall) { + InteractionResult result = DisplayWallInteractions.useWithoutItem(player, wall, hit.getDirection()); + if (result != null) { + return result; + } + } return super.useWithoutItem(state, level, pos, player, hit); } + + @Override + protected ItemInteractionResult useItemOn(ItemStack stack, BlockState state, Level level, BlockPos pos, Player player, InteractionHand hand, BlockHitResult hit) { + if (type == WallType.DISPLAY && level.getBlockEntity(pos) instanceof BlockEntityChestWall wall) { + ItemInteractionResult result = DisplayWallInteractions.useItemOn(stack, player, wall, hit.getDirection()); + if (result != null) { + return result; + } + } + return super.useItemOn(stack, state, level, pos, player, hand, hit); + } + + @Nullable + @Override + public BlockEntityTicker getTicker(Level level, BlockState state, BlockEntityType blockEntityType) { + return type == WallType.DISPLAY && !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/DisplayWallInteractions.java b/loader-common/src/main/java/org/cyclops/colossalchests2/block/DisplayWallInteractions.java new file mode 100644 index 00000000..1894d553 --- /dev/null +++ b/loader-common/src/main/java/org/cyclops/colossalchests2/block/DisplayWallInteractions.java @@ -0,0 +1,213 @@ +package org.cyclops.colossalchests2.block; + +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.InteractionResult; +import net.minecraft.world.ItemInteractionResult; +import net.minecraft.world.entity.player.Inventory; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.state.BlockState; +import org.cyclops.colossalchests2.ColossalChestsInstance; +import org.cyclops.colossalchests2.blockentity.BlockEntityChestCore; +import org.cyclops.colossalchests2.blockentity.BlockEntityChestWall; +import org.cyclops.colossalchests2.inventory.ContainerDisplay; +import org.cyclops.colossalchests2.network.packet.ServerboundDisplayTakePacket; +import org.cyclops.colossalchests2.storage.ChestStorage; +import org.cyclops.cyclopscore.helper.IModHelpers; +import org.jetbrains.annotations.Nullable; + +import java.util.Optional; + +/** + * Drawer style clicks on Display walls: left-click takes, right-click inserts, a double right-click inserts all. + * An empty right-click opens the settings. Each face shows its own type, or acts like a plain wall when hidden. + * @author rubensworks + */ +public final class DisplayWallInteractions { + + private static boolean attackReleased = true; + + private DisplayWallInteractions() { + } + + /** + * Called on the client at the end of each tick. + * @param attackDown If the attack button is held. + */ + public static void onClientTick(boolean attackDown) { + if (!attackDown) { + attackReleased = true; + } + } + + public static boolean isDisplayWall(BlockState state) { + return state.getBlock() instanceof BlockChestFunctionalWall wall && wall.getType() == WallType.DISPLAY; + } + + /** + * @param level The level. + * @param pos A position. + * @param face A face of it, null if unknown. + * @return The Display wall at the position, if the face is not hidden. + */ + public static Optional getShownWall(Level level, BlockPos pos, @Nullable Direction face) { + return isDisplayWall(level.getBlockState(pos)) && level.getBlockEntity(pos) instanceof BlockEntityChestWall wall + && (face == null || !wall.isFaceHidden(face)) ? Optional.of(wall) : Optional.empty(); + } + + /** + * A Display wall is only mined with a tool that can harvest it, so taking items never breaks the chest. + * Hidden faces act like a plain wall. + * @param level The level. + * @param pos A position. + * @param face The clicked face, null if unknown. + * @param player A player. + * @return If a left-click by the player must not mine the block. + */ + public static boolean isProtectedFromMining(Level level, BlockPos pos, @Nullable Direction face, Player player) { + return getShownWall(level, pos, face).isPresent() && !player.getMainHandItem().isCorrectToolForDrops(level.getBlockState(pos)); + } + + /** + * Handle a left-click on a block, called on both sides by the loaders before mining starts. + * The client asks the server to take items, once per click. + * @param player The player. + * @param level The level. + * @param pos The clicked position. + * @param face The clicked face, null if unknown. + * @return If mining must be cancelled. + */ + public static boolean onAttack(Player player, Level level, BlockPos pos, @Nullable Direction face) { + if (!isProtectedFromMining(level, pos, face, player)) { + return false; + } + // Holding the button repeats this, only a new click takes again. + if (level.isClientSide && attackReleased && face != null) { + attackReleased = false; + ColossalChestsInstance.MOD.getPacketHandlerCommon().sendToServer(new ServerboundDisplayTakePacket(pos, face, player.isShiftKeyDown())); + } + return true; + } + + private static Optional getStorage(BlockEntityChestWall wall) { + return wall.getCore().map(BlockEntityChestCore::getStorage); + } + + /** + * Take the type shown on a face into the player's held slot, then the rest of the inventory, dropping what does + * not fit. + * @param player The player. + * @param wall A Display wall. + * @param face The clicked face. + * @param single If one item is taken, otherwise a stack. + * @return How many were taken. + */ + public static long take(Player player, BlockEntityChestWall wall, Direction face, boolean single) { + ItemStack type = wall.getDisplayed(face); + Optional storage = getStorage(wall); + if (type.isEmpty() || storage.isEmpty()) { + return 0; + } + long taken = storage.get().extract(type, single ? 1 : type.getMaxStackSize(), false); + if (taken > 0) { + ItemStack stack = type.copyWithCount((int) taken); + // Fill the held slot first. + Inventory inventory = player.getInventory(); + ItemStack held = inventory.getSelected(); + if (held.isEmpty()) { + inventory.setItem(inventory.selected, stack.split(stack.getMaxStackSize())); + } else if (ItemStack.isSameItemSameComponents(held, stack)) { + held.grow(stack.split(Math.max(0, held.getMaxStackSize() - held.getCount())).getCount()); + } + if (!stack.isEmpty() && !inventory.add(stack)) { + player.drop(stack, false); + } + wall.updateDisplayStats(false); + } + return taken; + } + + /** + * A right-click with an item on a face: an empty face starts showing it, and the type it shows is inserted. + * @return The result, or null to fall back to the default wall behaviour. + */ + @Nullable + public static ItemInteractionResult useItemOn(ItemStack stack, Player player, BlockEntityChestWall wall, Direction face) { + Optional storage = getStorage(wall); + // An empty hand is handled by useWithoutItem. + if (stack.isEmpty() || storage.isEmpty() || wall.isFaceHidden(face)) { + return null; + } + if (!player.level().isClientSide) { + if (wall.getDisplayed(face).isEmpty()) { + wall.setDisplayed(face, stack); + } + if (ItemStack.isSameItemSameComponents(stack, wall.getDisplayed(face))) { + if (wall.recordInsertClick(player, face, player.level().getGameTime())) { + insertAll(player, storage.get(), wall.getDisplayed(face)); + } else { + stack.shrink((int) storage.get().insert(stack, stack.getCount(), false)); + } + wall.updateDisplayStats(false); + } + } + return ItemInteractionResult.sidedSuccess(player.level().isClientSide); + } + + /** + * A right-click with an empty hand on a face: right after an insert there it inserts all of the type it shows, + * otherwise it opens the settings. Sneaking does nothing, except on a hidden face, where it is the only way to the + * settings. + * @return The result, or null to fall back to the default wall behaviour. + */ + @Nullable + public static InteractionResult useWithoutItem(Player player, BlockEntityChestWall wall, Direction face) { + if (wall.isFaceHidden(face)) { + if (!player.isSecondaryUseActive()) { + return null; + } + if (player instanceof ServerPlayer serverPlayer) { + openSettings(serverPlayer, wall); + } + return InteractionResult.sidedSuccess(player.level().isClientSide); + } + if (player.isSecondaryUseActive()) { + return InteractionResult.PASS; + } + // Only the server knows about the previous click, the client lets it decide. + if (player instanceof ServerPlayer serverPlayer) { + Optional storage = getStorage(wall); + if (storage.isPresent() && !wall.getDisplayed(face).isEmpty() && wall.recordInsertClick(player, face, player.level().getGameTime())) { + insertAll(player, storage.get(), wall.getDisplayed(face)); + wall.updateDisplayStats(false); + } else { + openSettings(serverPlayer, wall); + } + } + return InteractionResult.sidedSuccess(player.level().isClientSide); + } + + private static void openSettings(ServerPlayer player, BlockEntityChestWall wall) { + IModHelpers.get().getMinecraftHelpers().openMenu(player, wall, buf -> ContainerDisplay.writeOpenData(buf, wall)); + } + + /** + * Insert every stack of the type from the player's inventory. + * @return How many were inserted. + */ + public static long insertAll(Player player, ChestStorage storage, ItemStack type) { + long inserted = 0; + for (ItemStack stack : player.getInventory().items) { + if (ItemStack.isSameItemSameComponents(stack, type)) { + long count = storage.insert(stack, stack.getCount(), false); + stack.shrink((int) count); + inserted += count; + } + } + player.getInventory().setChanged(); + return inserted; + } +} 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 a66828c3..0cbf2eef 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 @@ -14,7 +14,11 @@ public enum WallType { /** * Item I/O where inserts of held types that do not fit are destroyed. */ - VOID; + VOID, + /** + * Shows one item type and its count on its faces, and lets players take and insert it by clicking. + */ + DISPLAY; public static final WallType[] VALUES = values(); @@ -25,6 +29,13 @@ public String getName() { return name().toLowerCase(Locale.ROOT); } + /** + * @return If automation can move items through this wall. + */ + public boolean exposesItems() { + return this != DISPLAY; + } + public String getRegistryName() { return "chest_wall_" + getName(); } 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 67f0d40d..eb1fec1e 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 @@ -10,10 +10,10 @@ import net.minecraft.nbt.CompoundTag; import net.minecraft.nbt.NbtOps; import net.minecraft.nbt.Tag; +import net.minecraft.network.chat.Component; import net.minecraft.network.protocol.Packet; import net.minecraft.network.protocol.game.ClientGamePacketListener; import net.minecraft.network.protocol.game.ClientboundBlockEntityDataPacket; -import net.minecraft.network.chat.Component; import net.minecraft.resources.ResourceLocation; import net.minecraft.server.level.ServerPlayer; import net.minecraft.sounds.SoundEvent; @@ -41,14 +41,13 @@ import org.cyclops.colossalchests2.capability.ItemHandlerLogic; import org.cyclops.colossalchests2.capability.StorageSignals; import org.cyclops.colossalchests2.config.MaterialProperties; -import org.cyclops.colossalchests2.inventory.ChestSettings; import org.cyclops.colossalchests2.inventory.ContainerChest; import org.cyclops.colossalchests2.multiblock.ChestCoreIndex; -import org.cyclops.colossalchests2.network.ChestNetwork; import org.cyclops.colossalchests2.multiblock.ChestShape; import org.cyclops.colossalchests2.multiblock.ChestStructure; import org.cyclops.colossalchests2.multiblock.LevelStructureView; import org.cyclops.colossalchests2.multiblock.StructureDetector; +import org.cyclops.colossalchests2.network.ChestNetwork; import org.cyclops.colossalchests2.storage.ChestStorage; import org.cyclops.colossalchests2.storage.CompressionFamilies; import org.cyclops.colossalchests2.storage.CompressionFamiliesCache; @@ -92,7 +91,6 @@ public class BlockEntityChestCore extends BlockEntity implements MenuProvider, C private ChestStructure structure; private int lastSize; private List decoratedPositions = List.of(); - private ChestSettings settings = ChestSettings.DEFAULT; private boolean registered; private boolean validationRequested = true; private int validationCooldown; @@ -256,23 +254,6 @@ public int getComparatorSignal() { return isFormed() ? StorageSignals.getComparatorSignal(storage) : 0; } - public ChestSettings getSettings() { - return settings; - } - - /** - * Change the GUI settings, and show them to everyone viewing the chest. - */ - public void setSettings(ChestSettings settings) { - this.settings = settings; - setChanged(); - for (ServerPlayer viewer : viewers) { - if (viewer.containerMenu instanceof ContainerChest menu && menu.isFor(this)) { - menu.onSettingsChanged(settings); - } - } - } - /** * Open the chest GUI, which only works while formed. */ @@ -572,7 +553,6 @@ protected void saveAdditional(CompoundTag tag, HolderLookup.Provider registries) tag.putInt("data_version", DATA_VERSION); tag.put("storage", ChestStorage.CODEC.encodeStart(registries.createSerializationContext(NbtOps.INSTANCE), storage.toContents()).getOrThrow()); saveStructure(tag); - tag.put("settings", ChestSettings.CODEC.encodeStart(NbtOps.INSTANCE, settings).getOrThrow()); tag.put("upgrades", ItemContainerContents.CODEC.encodeStart(registries.createSerializationContext(NbtOps.INSTANCE), ItemContainerContents.fromItems(upgrades.getItems())).getOrThrow()); } @@ -620,9 +600,6 @@ protected void loadAdditional(CompoundTag tag, HolderLookup.Provider registries) ? BlockPos.CODEC.listOf().parse(NbtOps.INSTANCE, tag.get("decorated")).result().map(ImmutableList::copyOf).orElse(ImmutableList.of()) : List.of(); lastSize = tag.getInt("last_size"); - if (tag.contains("settings")) { - settings = ChestSettings.CODEC.parse(NbtOps.INSTANCE, tag.get("settings")).result().orElse(ChestSettings.DEFAULT); - } applyProfile(true); } @@ -633,9 +610,6 @@ protected void collectImplicitComponents(DataComponentMap.Builder components) { if (!contents.entries().isEmpty()) { components.set(RegistryEntries.COMPONENT_CHEST_CONTENTS.value(), contents); } - if (!settings.equals(ChestSettings.DEFAULT)) { - components.set(RegistryEntries.COMPONENT_CHEST_SETTINGS.value(), settings); - } if (!upgrades.isEmpty()) { components.set(RegistryEntries.COMPONENT_CHEST_UPGRADES.value(), ItemContainerContents.fromItems(upgrades.getItems())); } @@ -644,7 +618,6 @@ protected void collectImplicitComponents(DataComponentMap.Builder components) { @Override protected void applyImplicitComponents(DataComponentInput input) { super.applyImplicitComponents(input); - settings = input.getOrDefault(RegistryEntries.COMPONENT_CHEST_SETTINGS.value(), ChestSettings.DEFAULT); loadUpgrades(input.getOrDefault(RegistryEntries.COMPONENT_CHEST_UPGRADES.value(), ItemContainerContents.EMPTY)); ChestStorage.Contents contents = input.get(RegistryEntries.COMPONENT_CHEST_CONTENTS.value()); if (contents != null) { @@ -657,7 +630,6 @@ protected void applyImplicitComponents(DataComponentInput input) { public void removeComponentsFromTag(CompoundTag tag) { super.removeComponentsFromTag(tag); tag.remove("storage"); - tag.remove("settings"); tag.remove("upgrades"); } 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 b9384798..6cdd977e 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 @@ -1,10 +1,17 @@ package org.cyclops.colossalchests2.blockentity; +import com.google.common.collect.Maps; import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; import net.minecraft.core.HolderLookup; import net.minecraft.core.NonNullList; import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.ListTag; +import net.minecraft.nbt.Tag; import net.minecraft.network.chat.Component; +import net.minecraft.network.protocol.Packet; +import net.minecraft.network.protocol.game.ClientGamePacketListener; +import net.minecraft.network.protocol.game.ClientboundBlockEntityDataPacket; import net.minecraft.world.Container; import net.minecraft.world.ContainerHelper; import net.minecraft.world.MenuProvider; @@ -13,6 +20,8 @@ import net.minecraft.world.entity.player.Player; import net.minecraft.world.inventory.AbstractContainerMenu; import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.Block; import net.minecraft.world.level.block.entity.BlockEntity; import net.minecraft.world.level.block.entity.BlockEntityType; import net.minecraft.world.level.block.state.BlockState; @@ -22,21 +31,34 @@ import org.cyclops.colossalchests2.block.WallType; import org.cyclops.colossalchests2.capability.ItemHandlerLogic; import org.cyclops.colossalchests2.capability.WallAccess; +import org.cyclops.colossalchests2.inventory.ContainerDisplay; import org.cyclops.colossalchests2.inventory.ContainerInterface; import org.cyclops.colossalchests2.multiblock.ChestCoreIndex; +import org.cyclops.colossalchests2.multiblock.ChestShape; +import org.cyclops.colossalchests2.storage.DisplayStats; import org.jetbrains.annotations.Nullable; +import java.util.Arrays; import java.util.List; +import java.util.Map; import java.util.Optional; +import java.util.UUID; /** * A functional wall, giving automation access to its chest under the wall's rules. - * Only an Interface has settings: its filter and direction. + * 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. * @author rubensworks */ public class BlockEntityChestWall extends BlockEntity implements MenuProvider { public static final int FILTER_SLOTS = 9; + public static final int FACES = Direction.values().length; + /** + * The longest time between two clicks on a Display wall that make a double click. + */ + public static final int DOUBLE_CLICK_TICKS = 10; + private static final int DISPLAY_UPDATE_TICKS = 10; private final SimpleContainer settings = new SimpleContainer(FILTER_SLOTS) { @Override @@ -51,9 +73,17 @@ public int getMaxStackSize() { } }; private WallAccess.Mode mode = WallAccess.Mode.BOTH; + private final ItemStack[] displayed = new ItemStack[FACES]; + private final DisplayStats[] displayStats = new DisplayStats[FACES]; + // A bit per side and option, set when the option is off. + private int disabledOptions; + private final Map lastInserts = Maps.newHashMap(); + private final Container displayedContainer = new DisplayedContainer(); public BlockEntityChestWall(BlockEntityType type, BlockPos pos, BlockState state) { super(type, pos, state); + Arrays.fill(displayed, ItemStack.EMPTY); + Arrays.fill(displayStats, DisplayStats.EMPTY); } public BlockEntityChestWall(BlockPos pos, BlockState state) { @@ -89,6 +119,7 @@ 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; }; } @@ -102,11 +133,143 @@ public Optional getCore() { return ChestCoreIndex.findFormedCore(level, worldPosition); } + /** + * @return The formed chest this wall gives automation access to, empty for walls without item access. + */ + public Optional getExposedCore() { + return getWallType().exposesItems() ? getCore() : Optional.empty(); + } + /** * @return Item handler semantics for automation through this wall, while its chest is formed. */ public Optional getItemHandlerLogic() { - return getCore().map(core -> new ItemHandlerLogic(core.getStorage(), getAccess())); + return getExposedCore().map(core -> new ItemHandlerLogic(core.getStorage(), getAccess())); + } + + /** + * @return The faces of a Display wall on the outside of its formed chest, which can show an item. + */ + public List getDisplayFaces() { + return getCore().map(core -> ChestShape.getOuterFaces(core.getStructure(), worldPosition)).orElse(List.of()); + } + + /** + * @param face A face. + * @return The type a Display wall shows on the face, empty if none. + */ + public ItemStack getDisplayed(Direction face) { + return displayed[face.ordinal()]; + } + + public void setDisplayed(Direction face, ItemStack type) { + displayed[face.ordinal()] = type.isEmpty() ? ItemStack.EMPTY : type.copyWithCount(1); + lastInserts.clear(); + setChanged(); + updateDisplayStats(true); + } + + /** + * @param face A face. + * @param option An option. + * @return If the option is on for the face of a Display wall. + */ + public boolean isEnabled(Direction face, DisplayOption option) { + return (disabledOptions & getOptionBit(face, option)) == 0; + } + + /** + * Turn an option of a face of a Display wall on or off. + * @param face A face. + * @param option An option. + * @param enabled If it is on. + * @return If it changed. + */ + public boolean setEnabled(Direction face, DisplayOption option, boolean enabled) { + if (isEnabled(face, option) == enabled) { + return false; + } + disabledOptions ^= getOptionBit(face, option); + setChanged(); + updateDisplayStats(true); + return true; + } + + /** + * @param face A face. + * @return If a Display wall shows nothing on the face, and acts like a plain wall there. + */ + public boolean isFaceHidden(Direction face) { + return !isEnabled(face, DisplayOption.SHOWN); + } + + /** + * @param face A face. + * @return The options that are off for the face, a bit per option ordinal. + */ + public int getDisabledOptions(Direction face) { + return (disabledOptions >> (face.ordinal() * DisplayOption.values().length)) & ((1 << DisplayOption.values().length) - 1); + } + + private static int getOptionBit(Direction face, DisplayOption option) { + return 1 << (face.ordinal() * DisplayOption.values().length + option.ordinal()); + } + + /** + * @return A container with the type a Display wall shows on each face, by face ordinal, for its menu. + */ + public Container getDisplayedContainer() { + return displayedContainer; + } + + /** + * @param face A face. + * @return What a Display wall shows about the type on the face, as last synced on clients. + */ + public DisplayStats getDisplayStats(Direction face) { + return displayStats[face.ordinal()]; + } + + /** + * @param player A player. + * @param face The clicked face. + * @param gameTime The current game time. + * @return If the player's previous insert was on the same face and recent enough to make this click a double + * click. Records this click. + */ + public boolean recordInsertClick(Player player, Direction face, long gameTime) { + InsertClick previous = lastInserts.put(player.getUUID(), new InsertClick(face, gameTime)); + return previous != null && previous.face() == face && gameTime - previous.gameTime() <= DOUBLE_CLICK_TICKS; + } + + /** + * Recompute a Display wall's stats and sync them to clients when they changed. + * @param force If clients are updated even without changes. + */ + public void updateDisplayStats(boolean force) { + if (level == null || level.isClientSide) { + return; + } + Optional core = getCore(); + boolean changed = force; + for (Direction face : Direction.values()) { + ItemStack type = displayed[face.ordinal()]; + DisplayStats stats = core.map(c -> DisplayStats.of(c.getStorage(), type)).orElse(DisplayStats.EMPTY); + if (!stats.equals(displayStats[face.ordinal()])) { + displayStats[face.ordinal()] = stats; + changed = true; + } + } + if (changed) { + BlockState state = getBlockState(); + level.sendBlockUpdated(worldPosition, state, state, Block.UPDATE_CLIENTS); + } + } + + public static void serverTick(Level level, BlockPos pos, BlockState state, BlockEntityChestWall wall) { + if (level.getGameTime() % DISPLAY_UPDATE_TICKS == 0 && wall.getWallType() == WallType.DISPLAY) { + wall.updateDisplayStats(false); + } } private void onSettingsChanged() { @@ -128,6 +291,44 @@ protected void saveAdditional(CompoundTag tag, HolderLookup.Provider registries) super.saveAdditional(tag, registries); tag.putString("mode", mode.name()); ContainerHelper.saveAllItems(tag, settings.getItems(), registries); + saveDisplay(tag, registries, false); + } + + private void saveDisplay(CompoundTag tag, HolderLookup.Provider registries, boolean withStats) { + ListTag faces = new ListTag(); + for (Direction face : Direction.values()) { + ItemStack type = displayed[face.ordinal()]; + if (!type.isEmpty() || (withStats && !displayStats[face.ordinal()].equals(DisplayStats.EMPTY))) { + CompoundTag faceTag = new CompoundTag(); + faceTag.putString("face", face.getSerializedName()); + if (!type.isEmpty()) { + faceTag.put("item", type.save(registries)); + } + if (withStats) { + faceTag.put("stats", displayStats[face.ordinal()].toTag()); + } + faces.add(faceTag); + } + } + tag.put("display", faces); + tag.putInt("disabled_options", disabledOptions); + } + + private void loadDisplay(CompoundTag tag, HolderLookup.Provider registries) { + Arrays.fill(displayed, ItemStack.EMPTY); + Arrays.fill(displayStats, DisplayStats.EMPTY); + for (Tag entry : tag.getList("display", Tag.TAG_COMPOUND)) { + CompoundTag faceTag = (CompoundTag) entry; + Direction face = Direction.byName(faceTag.getString("face")); + if (face != null) { + displayed[face.ordinal()] = faceTag.contains("item") + ? ItemStack.parseOptional(registries, faceTag.getCompound("item")) : ItemStack.EMPTY; + if (faceTag.contains("stats")) { + displayStats[face.ordinal()] = DisplayStats.fromTag(faceTag.getCompound("stats")); + } + } + } + disabledOptions = tag.getInt("disabled_options"); } @Override @@ -143,6 +344,21 @@ protected void loadAdditional(CompoundTag tag, HolderLookup.Provider registries) for (int i = 0; i < items.size(); i++) { settings.getItems().set(i, items.get(i)); } + loadDisplay(tag, registries); + } + + @Override + public CompoundTag getUpdateTag(HolderLookup.Provider registries) { + // Clients only need what a Display wall shows. + CompoundTag tag = new CompoundTag(); + saveDisplay(tag, registries, true); + return tag; + } + + @Nullable + @Override + public Packet getUpdatePacket() { + return ClientboundBlockEntityDataPacket.create(this); } @Override @@ -153,6 +369,66 @@ public Component getDisplayName() { @Nullable @Override public AbstractContainerMenu createMenu(int id, Inventory inventory, Player player) { - return new ContainerInterface(id, inventory, this); + return getWallType() == WallType.DISPLAY ? new ContainerDisplay(id, inventory, this) : new ContainerInterface(id, inventory, this); + } + + private record InsertClick(Direction face, long gameTime) { + } + + private class DisplayedContainer implements Container { + + @Override + public int getContainerSize() { + return FACES; + } + + @Override + public boolean isEmpty() { + return Arrays.stream(displayed).allMatch(ItemStack::isEmpty); + } + + @Override + public ItemStack getItem(int slot) { + return displayed[slot]; + } + + @Override + public ItemStack removeItem(int slot, int amount) { + return removeItemNoUpdate(slot); + } + + @Override + public ItemStack removeItemNoUpdate(int slot) { + ItemStack previous = getItem(slot); + setItem(slot, ItemStack.EMPTY); + return previous; + } + + @Override + public void setItem(int slot, ItemStack stack) { + setDisplayed(Direction.from3DDataValue(slot), stack); + } + + @Override + public int getMaxStackSize() { + return 1; + } + + @Override + public void setChanged() { + BlockEntityChestWall.this.setChanged(); + } + + @Override + public boolean stillValid(Player player) { + return Container.stillValidBlockEntity(BlockEntityChestWall.this, player); + } + + @Override + public void clearContent() { + for (Direction face : Direction.values()) { + setDisplayed(face, ItemStack.EMPTY); + } + } } } diff --git a/loader-common/src/main/java/org/cyclops/colossalchests2/blockentity/BlockEntityChestWallConfig.java b/loader-common/src/main/java/org/cyclops/colossalchests2/blockentity/BlockEntityChestWallConfig.java index 9c37f9c7..65267fe7 100644 --- a/loader-common/src/main/java/org/cyclops/colossalchests2/blockentity/BlockEntityChestWallConfig.java +++ b/loader-common/src/main/java/org/cyclops/colossalchests2/blockentity/BlockEntityChestWallConfig.java @@ -2,7 +2,9 @@ import net.minecraft.world.level.block.entity.BlockEntityType; import org.cyclops.colossalchests2.block.BlockChestFunctionalWall; +import org.cyclops.colossalchests2.block.WallType; import org.cyclops.colossalchests2.client.render.ChestOverlays; +import org.cyclops.colossalchests2.client.render.DisplayWallOverlay; import org.cyclops.colossalchests2.client.render.FunctionalWallOverlay; import org.cyclops.cyclopscore.config.extendedconfig.BlockEntityConfigCommon; import org.cyclops.cyclopscore.init.IModBase; @@ -28,7 +30,7 @@ public void onForgeRegistered() { super.onForgeRegistered(); if (getMod().getModHelpers().getMinecraftHelpers().isClientSide()) { for (BlockChestFunctionalWall wall : BlockChestFunctionalWall.getFunctionalInstances()) { - ChestOverlays.register(wall, new FunctionalWallOverlay(wall.getType())); + ChestOverlays.register(wall, wall.getType() == WallType.DISPLAY ? new DisplayWallOverlay() : new FunctionalWallOverlay(wall.getType())); } } } diff --git a/loader-common/src/main/java/org/cyclops/colossalchests2/blockentity/DisplayOption.java b/loader-common/src/main/java/org/cyclops/colossalchests2/blockentity/DisplayOption.java new file mode 100644 index 00000000..77b055ad --- /dev/null +++ b/loader-common/src/main/java/org/cyclops/colossalchests2/blockentity/DisplayOption.java @@ -0,0 +1,23 @@ +package org.cyclops.colossalchests2.blockentity; + + + +import java.util.Locale; + +/** + * What a side of a Display wall shows, each toggled per side. + * @author rubensworks + */ +public enum DisplayOption { + /** + * The side itself. A hidden side looks and acts like a plain wall. + */ + SHOWN, + COUNT, + FILL_LEVEL, + UPGRADE_INDICATORS; + + public String getTranslationKey() { + return "gui.colossalchests2.display." + name().toLowerCase(Locale.ROOT); + } +} diff --git a/loader-common/src/main/java/org/cyclops/colossalchests2/client/gui/ContainerScreenChest.java b/loader-common/src/main/java/org/cyclops/colossalchests2/client/gui/ContainerScreenChest.java index ddfe5e42..db479406 100644 --- a/loader-common/src/main/java/org/cyclops/colossalchests2/client/gui/ContainerScreenChest.java +++ b/loader-common/src/main/java/org/cyclops/colossalchests2/client/gui/ContainerScreenChest.java @@ -10,19 +10,24 @@ import net.minecraft.client.gui.components.EditBox; import net.minecraft.client.gui.components.Tooltip; import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen; +import net.minecraft.client.gui.screens.inventory.tooltip.DefaultTooltipPositioner; +import net.minecraft.client.gui.screens.inventory.tooltip.TooltipRenderUtil; import net.minecraft.network.chat.Component; import net.minecraft.world.entity.player.Inventory; import net.minecraft.world.inventory.Slot; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; import org.cyclops.colossalchests2.ColossalChestsInstance; import org.cyclops.colossalchests2.inventory.ChestClickAction; import org.cyclops.colossalchests2.inventory.ChestClickLogic; import org.cyclops.colossalchests2.inventory.ChestLayout; import org.cyclops.colossalchests2.inventory.ChestSearch; -import org.cyclops.colossalchests2.inventory.ChestSettings; import org.cyclops.colossalchests2.inventory.ContainerChest; import org.cyclops.colossalchests2.network.packet.ServerboundChestClickPacket; import org.cyclops.colossalchests2.network.packet.ServerboundChestDragPacket; -import org.cyclops.colossalchests2.network.packet.ServerboundChestSettingsPacket; +import org.cyclops.colossalchests2.network.packet.ServerboundChestFormPacket; +import org.cyclops.colossalchests2.storage.CompressionFamiliesCache; +import org.cyclops.colossalchests2.storage.CompressionFamily; import org.cyclops.colossalchests2.storage.DeepSlot; import org.cyclops.colossalchests2.upgrade.ChestUpgrade; import org.cyclops.colossalchests2.upgrade.ChestUpgrades; @@ -30,21 +35,14 @@ import org.cyclops.colossalchests2.upgrade.UpgradeSet; import org.cyclops.cyclopscore.client.gui.image.Images; import org.cyclops.cyclopscore.helper.IModHelpers; -import net.minecraft.world.item.Item; -import net.minecraft.world.item.ItemStack; import org.jetbrains.annotations.Nullable; -import net.minecraft.client.gui.screens.inventory.tooltip.DefaultTooltipPositioner; -import net.minecraft.client.gui.screens.inventory.tooltip.TooltipRenderUtil; -import org.cyclops.colossalchests2.network.packet.ServerboundChestFormPacket; -import org.cyclops.colossalchests2.storage.CompressionFamiliesCache; -import org.cyclops.colossalchests2.storage.CompressionFamily; import org.joml.Vector2ic; import org.lwjgl.glfw.GLFW; import java.text.NumberFormat; import java.util.List; -import java.util.Map; import java.util.Locale; +import java.util.Map; import java.util.Optional; import java.util.Set; @@ -122,36 +120,24 @@ protected void init() { addRenderableWidget(new SettingsButton(leftPos + getGridLeft() + getGridWidth() - SETTINGS_WIDTH, topPos + 3, b -> setSettingsOpen(!settingsOpen))); - // Two columns of three rows, which fits the grid area of the smallest chest. + // Two columns of two rows, which fits the grid area of the smallest chest. int settingsX = leftPos + layout.getGridX() - 1; - // From the search row, so four rows fit above the info row of the smallest chest. + // From the search row. int settingsY = topPos + SEARCH_Y - 1; int width = (layout.columns() * ChestLayout.SLOT_SIZE) / 2 - 1; int height = 16; - settingsButtons.add(addRenderableWidget(Button.builder(Component.empty(), - b -> sendSettings(menu.getSettings().withShowFillLevels(!menu.getSettings().showFillLevels()))) - .bounds(settingsX, settingsY, width, height) - .tooltip(Tooltip.create(Component.translatable("gui.colossalchests2.show_fill_levels.info"))).build())); - settingsButtons.add(addRenderableWidget(Button.builder(Component.empty(), - b -> sendSettings(menu.getSettings().withShowCounts(!menu.getSettings().showCounts()))) - .bounds(settingsX + width + 2, settingsY, width, height) - .tooltip(Tooltip.create(Component.translatable("gui.colossalchests2.show_counts.info"))).build())); - settingsButtons.add(addRenderableWidget(Button.builder(Component.empty(), - b -> sendSettings(menu.getSettings().withShowUpgradeIndicators(!menu.getSettings().showUpgradeIndicators()))) - .bounds(settingsX, settingsY + height + 2, width, height) - .tooltip(Tooltip.create(Component.translatable("gui.colossalchests2.show_upgrade_indicators.info"))).build())); settingsButtons.add(addRenderableWidget(Button.builder(Component.translatable("gui.colossalchests2.lock_all"), b -> sendClick(0, ChestClickAction.LOCK_ALL)) - .bounds(settingsX, settingsY + 2 * (height + 2), width, height).build())); + .bounds(settingsX, settingsY, width, height).build())); settingsButtons.add(addRenderableWidget(Button.builder(Component.translatable("gui.colossalchests2.clear_locks"), b -> sendClick(0, ChestClickAction.CLEAR_LOCKS)) - .bounds(settingsX + width + 2, settingsY + 2 * (height + 2), width, height).build())); + .bounds(settingsX + width + 2, settingsY, width, height).build())); settingsButtons.add(addRenderableWidget(Button.builder(Component.translatable("gui.colossalchests2.void_all"), b -> sendClick(0, ChestClickAction.VOID_ALL)) - .bounds(settingsX, settingsY + 3 * (height + 2), width, height).build())); + .bounds(settingsX, settingsY + height + 2, width, height).build())); settingsButtons.add(addRenderableWidget(Button.builder(Component.translatable("gui.colossalchests2.clear_voids"), b -> sendClick(0, ChestClickAction.CLEAR_VOIDS)) - .bounds(settingsX + width + 2, settingsY + 3 * (height + 2), width, height).build())); + .bounds(settingsX + width + 2, settingsY + height + 2, width, height).build())); setSettingsOpen(settingsOpen); } @@ -163,22 +149,14 @@ private void setSettingsOpen(boolean open) { searchField.visible = !open; } - private void sendSettings(ChestSettings settings) { - ColossalChestsInstance.MOD.getPacketHandlerCommon().sendToServer(new ServerboundChestSettingsPacket(menu.containerId, settings)); - } - @Override protected void containerTick() { super.containerTick(); - ChestSettings settings = menu.getSettings(); - settingsButtons.get(0).setMessage(toggleLabel("show_fill_levels", settings.showFillLevels())); - settingsButtons.get(1).setMessage(toggleLabel("show_counts", settings.showCounts())); - settingsButtons.get(2).setMessage(toggleLabel("show_upgrade_indicators", settings.showUpgradeIndicators())); // Locks and void marks come with their upgrades. - updateUpgradeButton(settingsButtons.get(3), hasLockUpgrade(), "lock_all", "requires_lock_upgrade"); - updateUpgradeButton(settingsButtons.get(4), hasLockUpgrade(), "clear_locks", "requires_lock_upgrade"); - updateUpgradeButton(settingsButtons.get(5), hasVoidUpgrade(), "void_all", "requires_void_upgrade"); - updateUpgradeButton(settingsButtons.get(6), hasVoidUpgrade(), "clear_voids", "requires_void_upgrade"); + updateUpgradeButton(settingsButtons.get(0), hasLockUpgrade(), "lock_all", "requires_lock_upgrade"); + updateUpgradeButton(settingsButtons.get(1), hasLockUpgrade(), "clear_locks", "requires_lock_upgrade"); + updateUpgradeButton(settingsButtons.get(2), hasVoidUpgrade(), "void_all", "requires_void_upgrade"); + updateUpgradeButton(settingsButtons.get(3), hasVoidUpgrade(), "clear_voids", "requires_void_upgrade"); } private static void updateUpgradeButton(Button button, boolean enabled, String key, String disabledKey) { @@ -196,11 +174,6 @@ private boolean hasLockUpgrade() { return menu.getUpgradeSet().has(ChestUpgrades.LOCK); } - private static Component toggleLabel(String key, boolean value) { - return Component.translatable("gui.colossalchests2." + key, - Component.translatable(value ? "options.on" : "options.off")); - } - private record DragPreview(ItemStack cursor, ItemStack remainder, Map added, Set capped) { } diff --git a/loader-common/src/main/java/org/cyclops/colossalchests2/client/gui/ContainerScreenDisplay.java b/loader-common/src/main/java/org/cyclops/colossalchests2/client/gui/ContainerScreenDisplay.java new file mode 100644 index 00000000..00f9cb85 --- /dev/null +++ b/loader-common/src/main/java/org/cyclops/colossalchests2/client/gui/ContainerScreenDisplay.java @@ -0,0 +1,196 @@ +package org.cyclops.colossalchests2.client.gui; + +import com.google.common.collect.Lists; +import net.minecraft.ChatFormatting; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.components.Button; +import net.minecraft.client.gui.components.WidgetSprites; +import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen; +import net.minecraft.core.Direction; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.entity.player.Inventory; +import net.minecraft.world.inventory.Slot; +import net.minecraft.world.item.ItemStack; +import org.cyclops.colossalchests2.Reference; +import org.cyclops.colossalchests2.blockentity.DisplayOption; +import org.cyclops.colossalchests2.inventory.ContainerDisplay; +import org.cyclops.colossalchests2.upgrade.ChestUpgrades; + +import java.util.List; + +/** + * Settings of a Display wall, a column per face. + * @author rubensworks + */ +public class ContainerScreenDisplay extends AbstractContainerScreen { + + private static final int COLOR_LABEL = 0x404040; + + private static final List ICON_OPTIONS = List.of(DisplayOption.COUNT, DisplayOption.FILL_LEVEL, + DisplayOption.UPGRADE_INDICATORS); + private static final WidgetSprites BUTTON_SPRITES = new WidgetSprites(ResourceLocation.withDefaultNamespace("widget/button"), + ResourceLocation.withDefaultNamespace("widget/button_disabled"), ResourceLocation.withDefaultNamespace("widget/button_highlighted")); + private static final int COLOR_ICON_OFF = 0xA0000000; + private static final int COLOR_ICON_STRIKE = 0xFFD04030; + private static final int COLOR_BAR_BACKGROUND = 0xFF101010; + private static final int COLOR_BAR = 0xFF40C040; + + private final List