Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,9 @@ public class GeneralConfig<M extends IModBase> extends DummyConfigCommon<M> {
@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");
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -29,7 +29,7 @@ public class RegistryEntries {

public static final DeferredHolderCommon<MenuType<?>, MenuType<ContainerInterface>> MENU_INTERFACE = DeferredHolderCommon.create(Registries.MENU, ResourceLocation.parse("colossalchests2:interface"));

public static final DeferredHolderCommon<DataComponentType<?>, DataComponentType<ChestSettings>> COMPONENT_CHEST_SETTINGS = DeferredHolderCommon.create(Registries.DATA_COMPONENT_TYPE, ResourceLocation.parse("colossalchests2:chest_settings"));
public static final DeferredHolderCommon<MenuType<?>, MenuType<ContainerDisplay>> MENU_DISPLAY = DeferredHolderCommon.create(Registries.MENU, ResourceLocation.parse("colossalchests2:display"));

public static final DeferredHolderCommon<MenuType<?>, MenuType<ContainerChest>> MENU_CHEST = DeferredHolderCommon.create(Registries.MENU, ResourceLocation.parse("colossalchests2:chest"));

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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 <T extends BlockEntity> BlockEntityTicker<T> getTicker(Level level, BlockState state, BlockEntityType<T> 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;
}
}
Original file line number Diff line number Diff line change
@@ -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<BlockEntityChestWall> 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<ChestStorage> 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<ChestStorage> 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<ChestStorage> 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<ChestStorage> 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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand All @@ -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();
}
Expand Down
Loading
Loading