Repository: AllayMC/Allay
Pull request #611: feat: double chest
Author: login: IWareQ
Created: 2025-03-22T19:09:24Z | Merged: 2025-03-25T07:48:02Z



Diff:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4cbe38ca0f..8ec7bdebfa 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -31,6 +31,10 @@ Unless otherwise specified, any version comparison below is the comparison of se
 - (API) Added `PlayerJoinEvent#joinMessage` and `PlayerQuitEvent#quitMessage` and correspond setters. Now these messages can be edited by plugins.
 - (API) Added `BlockBaseComponent#getDropXpAmount` method.
 - (API) Added option `tickDimensionInParallel` to control whether tick dimensions in the same world in parallel during world tick.
+- (API) Added `BlockFace#fromMinecraftCardinalDirection` and `BlockFace#isVertical` methods.
+- (API) Added `BlockStateWithPos#getBlockEntity` method.
+- (API) Implemented chest pairing, and several related interfaces & objects including `BlockEntityPairableComponent`,
+    `DoubleChestContainer`, are added to api module, see commit history for more details.
 - Implemented ores.
 - Added `InternalRegistries#TRIM_PATTERNS` and `InternalRegistries#TRIM_MATERIALS`.
 
@@ -43,6 +47,8 @@ Unless otherwise specified, any version comparison below is the comparison of se
 - (API) Moved player, ban/whitelist related methods from `Server` to the new `PlayerService` class.
 - (API) Moved `Server#getNetworkInterface` method from `Server` to the new `PlayerService` class. `NetworkInterface` is now held by `PlayerService` rather than `Server`.
 - (API) Moved `PlayerQuitEvent#reason` to the newly introduced event `ClientDisconnectEvent`. `PlayerQuitEvent` will only be called for already logged in player now.
+- (API) Removed `BlockEntityBaseComponent#clearCustomName` method, use `BlockEntityBaseComponent#setCustomName(null)` to clear custom name.
+- Removed `BlockEntityContainerHolderComponentImpl#constructor(Supplier<Container>, Consumer<ContainerViewer>, Consumer<ContainerViewer>)`.
 - Optimized `setItemStack` handling in `ItemStackRequestPacket` to no longer require sending an `InventorySlotPacket` (Issue #66).
 - Use `AtomicReference<ServerState>` instead of `AtomicBoolean`'s fields for state management
 
diff --git a/api/src/main/java/org/allaymc/api/block/data/BlockFace.java b/api/src/main/java/org/allaymc/api/block/data/BlockFace.java
index 572e106e0e..dd77b8f93d 100644
--- a/api/src/main/java/org/allaymc/api/block/data/BlockFace.java
+++ b/api/src/main/java/org/allaymc/api/block/data/BlockFace.java
@@ -35,13 +35,6 @@ public enum BlockFace {
 
     private final Vector3ic offset;
 
-    /**
-     * Get the block face by index.
-     *
-     * @param value the index.
-     *
-     * @return the block face.
-     */
     public static BlockFace fromId(int value) {
         return switch (value) {
             case 0 -> BlockFace.DOWN;
@@ -55,7 +48,7 @@ public static BlockFace fromId(int value) {
     }
 
     /**
-     * Get horizontal block faces.
+     * Retrieves the horizontal block faces.
      *
      * @return the horizontal block faces.
      */
@@ -64,7 +57,7 @@ public static BlockFace[] getHorizontalBlockFaces() {
     }
 
     /**
-     * Get vertical block faces.
+     * Retrieves the vertical block faces.
      *
      * @return the vertical block faces.
      */
@@ -73,35 +66,52 @@ public static BlockFace[] getVerticalBlockFaces() {
     }
 
     /**
-     * Add current block face offset to the given pos.
+     * Converts a {@link MinecraftCardinalDirection} to a corresponding {@link BlockFace}.
+     *
+     * @param direction the Minecraft cardinal direction to convert.
+     *
+     * @return the corresponding {@link BlockFace}.
+     */
+    @ApiStatus.Experimental
+    public static BlockFace fromMinecraftCardinalDirection(MinecraftCardinalDirection direction) {
+        return switch (direction) {
+            case NORTH -> NORTH;
+            case SOUTH -> SOUTH;
+            case EAST -> EAST;
+            case WEST -> WEST;
+        };
+    }
+
+    /**
+     * Adds the current block face offset to the specified coordinates.
      *
-     * @param x the x coordinate.
-     * @param y the y coordinate.
-     * @param z the z coordinate.
+     * @param x the x coordinate
+     * @param y the y coordinate
+     * @param z the z coordinate
      *
-     * @return the result pos.
+     * @return a new Vector3ic representing the resulting position
      */
     public Vector3ic offsetPos(int x, int y, int z) {
         return offset.add(x, y, z, new Vector3i());
     }
 
     /**
-     * Add current block face offset to the given pos.
+     * Adds the current block face offset to the specified position.
      *
-     * @param pos the pos.
+     * @param pos the position to offset
      *
-     * @return the result pos.
+     * @return a new Vector3ic representing the resulting position
      */
     public Vector3ic offsetPos(Vector3ic pos) {
         return offset.add(pos, new Vector3i());
     }
 
     /**
-     * Rotate the given AABB.
+     * Rotates the given AABB based on this block face.
      *
-     * @param aabb the AABB.
+     * @param aabb the axis-aligned bounding box to rotate
      *
-     * @return the rotated AABB.
+     * @return a new AABBd representing the rotated bounding box
      */
     public AABBd rotateAABB(AABBdc aabb) {
         var c1 = new Vector3d(aabb.minX(), aabb.minY(), aabb.minZ());
@@ -119,11 +129,11 @@ public AABBd rotateAABB(AABBdc aabb) {
     }
 
     /**
-     * Rotate the given vector.
+     * Rotates the given vector based on this block face.
      *
-     * @param vec the vector.
+     * @param vec the vector to rotate
      *
-     * @return the rotated vector.
+     * @return a new Vector3d representing the rotated vector
      */
     @SuppressWarnings("SuspiciousNameCombination")
     public Vector3d rotateVector(Vector3dc vec) {
@@ -163,16 +173,9 @@ public Vector3d rotateVector(Vector3dc vec) {
         }
 
         // Translate back to original point
-        result.add(0.5, 0.5, 0.5);
-
-        return result;
+        return result.add(0.5, 0.5, 0.5);
     }
 
-    /**
-     * Get the opposite block face.
-     *
-     * @return the opposite block face.
-     */
     public BlockFace opposite() {
         return switch (this) {
             case DOWN -> UP;
@@ -185,9 +188,11 @@ public BlockFace opposite() {
     }
 
     /**
-     * Rotate this BlockFace around the Y axis clockwise (NORTH =&gt; EAST =&gt; SOUTH =&gt; WEST =&gt; NORTH).
+     * Rotates this block face clockwise around the Y axis (NORTH => EAST => SOUTH => WEST => NORTH).
+     *
+     * @return the rotated {@link BlockFace}
      *
-     * @return block face.
+     * @throws IllegalStateException if this face is {@link #UP} or {@link #DOWN}
      */
     public BlockFace rotateY() {
         return switch (this) {
@@ -200,9 +205,11 @@ public BlockFace rotateY() {
     }
 
     /**
-     * Rotate this BlockFace around the Y axis counter-clockwise (NORTH =&gt; WEST =&gt; SOUTH =&gt; EAST =&gt; NORTH).
+     * Rotates this block face counter-clockwise around the Y axis (NORTH => WEST => SOUTH => EAST => NORTH).
+     *
+     * @return the rotated {@link BlockFace}
      *
-     * @return block face.
+     * @throws IllegalStateException if this face is {@link #UP} or {@link #DOWN}
      */
     public BlockFace rotateYCCW() {
         return switch (this) {
@@ -215,18 +222,27 @@ public BlockFace rotateYCCW() {
     }
 
     /**
-     * Check if this block face is vertical.
+     * Checks if this block face is horizontal.
      *
-     * @return true if vertical, false if not.
+     * @return {@code true} if this face is {@link #NORTH}, {@link #EAST}, {@link #SOUTH}, or {@link #WEST}, {@code false} otherwise
      */
     public boolean isHorizontal() {
         return this == NORTH || this == EAST || this == SOUTH || this == WEST;
     }
 
     /**
-     * Get the Minecraft cardinal direction which represents this block face.
+     * Checks if this block face is vertical.
+     *
+     * @return {@code true} if this face is {@link #UP} or {@link #DOWN}, {@code false} otherwise
+     */
+    public boolean isVertical() {
+        return this == UP || this == DOWN;
+    }
+
+    /**
+     * Converts this block face to its corresponding Minecraft cardinal direction.
      *
-     * @return the Minecraft cardinal direction.
+     * @return the corresponding {@link MinecraftCardinalDirection}, or {@code null} if not applicable
      */
     @ApiStatus.Experimental
     public MinecraftCardinalDirection toMinecraftCardinalDirection() {
diff --git a/api/src/main/java/org/allaymc/api/block/dto/BlockStateWithPos.java b/api/src/main/java/org/allaymc/api/block/dto/BlockStateWithPos.java
index c9c0ea7d29..d57768ead8 100644
--- a/api/src/main/java/org/allaymc/api/block/dto/BlockStateWithPos.java
+++ b/api/src/main/java/org/allaymc/api/block/dto/BlockStateWithPos.java
@@ -2,16 +2,15 @@
 
 import org.allaymc.api.block.data.BlockFace;
 import org.allaymc.api.block.type.BlockState;
+import org.allaymc.api.blockentity.BlockEntity;
 import org.allaymc.api.math.position.Position3i;
 import org.allaymc.api.math.position.Position3ic;
 import org.allaymc.api.world.Dimension;
 
 /**
- * Represents a block state in the world.
- *
  * @param blockState the block state.
- * @param pos        the position.
- * @param layer      the layer.
+ * @param pos        the position of the block in the world.
+ * @param layer      the layer where the block is located (default is 0).
  *
  * @author daoge_cmd
  */
@@ -22,21 +21,21 @@ public record BlockStateWithPos(
 ) {
 
     /**
-     * Construct a new block state with the given block state and position. The layer is default to 0.
+     * Constructs a new {@link BlockStateWithPos} with the given block state and position. The layer is defaulted to 0.
      *
      * @param blockState the block state.
-     * @param pos        the position.
+     * @param pos        the position of the block.
      */
     public BlockStateWithPos(BlockState blockState, Position3ic pos) {
         this(blockState, pos, 0);
     }
 
     /**
-     * Offset the pos with the given {@link BlockFace}, and the other properties remain the same.
+     * Offsets the position with the given {@link BlockFace}, while leaving the other properties (block state and layer) unchanged.
      *
-     * @param blockFace the block face.
+     * @param blockFace the {@link BlockFace} to offset the position by.
      *
-     * @return the new block state with the offset pos.
+     * @return a new {@link BlockStateWithPos} with the updated position.
      */
     public BlockStateWithPos offsetPos(BlockFace blockFace) {
         var offsetPos = blockFace.offsetPos(pos);
@@ -48,11 +47,22 @@ public BlockStateWithPos offsetPos(BlockFace blockFace) {
     }
 
     /**
-     * Get the dimension of the block state.
+     * Retrieves the dimension in which this block state is located.
      *
-     * @return the dimension.
+     * @return the {@link Dimension} of the block state.
      */
     public Dimension dimension() {
         return pos.dimension();
     }
+
+    /**
+     * Retrieves the block entity at the current position.
+     *
+     * @param <T> the type of the block entity.
+     *
+     * @return the block entity at the current position.
+     */
+    public <T extends BlockEntity> T getBlockEntity() {
+        return (T) dimension().getBlockEntity(pos);
+    }
 }
diff --git a/api/src/main/java/org/allaymc/api/blockentity/component/BlockEntityBaseComponent.java b/api/src/main/java/org/allaymc/api/blockentity/component/BlockEntityBaseComponent.java
index ca14dced82..27342c3b04 100644
--- a/api/src/main/java/org/allaymc/api/blockentity/component/BlockEntityBaseComponent.java
+++ b/api/src/main/java/org/allaymc/api/blockentity/component/BlockEntityBaseComponent.java
@@ -126,7 +126,7 @@ default boolean sendToClient() {
      */
     default BlockState getBlockState() {
         var pos = getPosition();
-        return pos.dimension().getBlockState(pos.x(), pos.y(), pos.z());
+        return pos.dimension().getBlockState(pos);
     }
 
     /**
@@ -151,11 +151,4 @@ default BlockState getBlockState() {
     default boolean hasCustomName() {
         return getCustomName() != null;
     }
-
-    /**
-     * Clears the custom name of the block entity.
-     */
-    default void clearCustomName() {
-        setCustomName(null);
-    }
 }
diff --git a/api/src/main/java/org/allaymc/api/blockentity/component/BlockEntityPairableComponent.java b/api/src/main/java/org/allaymc/api/blockentity/component/BlockEntityPairableComponent.java
new file mode 100644
index 0000000000..e6e7b10281
--- /dev/null
+++ b/api/src/main/java/org/allaymc/api/blockentity/component/BlockEntityPairableComponent.java
@@ -0,0 +1,43 @@
+package org.allaymc.api.blockentity.component;
+
+import org.allaymc.api.blockentity.BlockEntity;
+
+/**
+ * @author IWareQ
+ */
+public interface BlockEntityPairableComponent extends BlockEntityComponent {
+    /**
+     * Attempts to pair this block entity with another block entity.
+     *
+     * @param other the {@link BlockEntity} to attempt pairing with
+     *
+     * @return {@code true} if pairing was successful, {@code false} otherwise
+     */
+    boolean tryPairWith(BlockEntity other);
+
+    /**
+     * Removes the pairing relationship between this block entity and its paired entity, if any.
+     */
+    void unpair();
+
+    /**
+     * Retrieves the block entity paired with this one, if it exists.
+     *
+     * @return the paired {@link BlockEntity}, or {@code null} if no pairing exists
+     */
+    BlockEntity getPair();
+
+    /**
+     * Checks if this block entity is currently paired with another.
+     *
+     * @return {@code true} if this block entity is paired, {@code false} otherwise
+     */
+    boolean isPaired();
+
+    /**
+     * Determines if this block entity is the lead in its pairing relationship.
+     *
+     * @return {@code true} if this block entity is the lead, {@code false} otherwise
+     */
+    boolean isLead();
+}
diff --git a/api/src/main/java/org/allaymc/api/blockentity/interfaces/BlockEntityChest.java b/api/src/main/java/org/allaymc/api/blockentity/interfaces/BlockEntityChest.java
index b2ce7ef491..48cf2a0435 100644
--- a/api/src/main/java/org/allaymc/api/blockentity/interfaces/BlockEntityChest.java
+++ b/api/src/main/java/org/allaymc/api/blockentity/interfaces/BlockEntityChest.java
@@ -2,9 +2,10 @@
 
 import org.allaymc.api.blockentity.BlockEntity;
 import org.allaymc.api.blockentity.component.BlockEntityContainerHolderComponent;
+import org.allaymc.api.blockentity.component.BlockEntityPairableComponent;
 
 /**
  * @author daoge_cmd
  */
-public interface BlockEntityChest extends BlockEntity, BlockEntityContainerHolderComponent {
+public interface BlockEntityChest extends BlockEntity, BlockEntityContainerHolderComponent, BlockEntityPairableComponent {
 }
diff --git a/api/src/main/java/org/allaymc/api/container/Container.java b/api/src/main/java/org/allaymc/api/container/Container.java
index acc0b28f49..d1b1e84eab 100644
--- a/api/src/main/java/org/allaymc/api/container/Container.java
+++ b/api/src/main/java/org/allaymc/api/container/Container.java
@@ -161,7 +161,6 @@ default boolean isEmpty() {
      */
     List<ItemData> toNetworkItemData();
 
-
     /**
      * @see #setItemStack(int, ItemStack, boolean)
      */
diff --git a/api/src/main/java/org/allaymc/api/container/FullContainerType.java b/api/src/main/java/org/allaymc/api/container/FullContainerType.java
index af90650e77..739fc08cec 100644
--- a/api/src/main/java/org/allaymc/api/container/FullContainerType.java
+++ b/api/src/main/java/org/allaymc/api/container/FullContainerType.java
@@ -198,6 +198,12 @@ public record FullContainerType<T extends Container>(
             .mapRangedNetworkSlotIndex(51, 54, 0)
             .build();
 
+    public static final FullContainerType<DoubleChestContainer> DOUBLE_CHEST = builder()
+            .id(ContainerType.CONTAINER)
+            .size(54)
+            .mapAllSlotToType(ContainerSlotType.LEVEL_ENTITY)
+            .build();
+
     public FullContainerType(int id, ContainerSlotType[] slotTypeTable, Set<ContainerSlotType> heldSlotTypes, BiMap<Integer, Integer> networkSlotIndexMapper) {
         this.id = id;
         this.slotTypeTable = slotTypeTable;
diff --git a/api/src/main/java/org/allaymc/api/container/impl/DoubleChestContainer.java b/api/src/main/java/org/allaymc/api/container/impl/DoubleChestContainer.java
new file mode 100644
index 0000000000..82f17f7029
--- /dev/null
+++ b/api/src/main/java/org/allaymc/api/container/impl/DoubleChestContainer.java
@@ -0,0 +1,52 @@
+package org.allaymc.api.container.impl;
+
+import lombok.Setter;
+import org.allaymc.api.container.Container;
+import org.allaymc.api.container.FullContainerType;
+import org.allaymc.api.item.ItemStack;
+import org.cloudburstmc.protocol.bedrock.data.inventory.ItemData;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * @author IWareQ
+ */
+@Setter
+public class DoubleChestContainer extends BlockContainer {
+    private static final int CHEST_SIZE = 27;
+
+    private Container left;
+    private Container right;
+
+    public DoubleChestContainer() {
+        super(FullContainerType.DOUBLE_CHEST);
+    }
+
+    @Override
+    public ItemStack getItemStack(int slot) {
+        if (slot < CHEST_SIZE) {
+            return left.getItemStack(slot);
+        }
+
+        return right.getItemStack(slot - CHEST_SIZE);
+    }
+
+    @Override
+    public void setItemStack(int slot, ItemStack itemStack, boolean send) {
+        if (slot < CHEST_SIZE) {
+            left.setItemStack(slot, itemStack, send);
+            return;
+        }
+
+        right.setItemStack(slot - CHEST_SIZE, itemStack, send);
+    }
+
+    @Override
+    public List<ItemData> toNetworkItemData() {
+        List<ItemData> networkItems = new ArrayList<>();
+        networkItems.addAll(left.toNetworkItemData());
+        networkItems.addAll(right.toNetworkItemData());
+        return networkItems;
+    }
+}
diff --git a/server/src/main/java/org/allaymc/server/block/component/BlockChestBaseComponentImpl.java b/server/src/main/java/org/allaymc/server/block/component/BlockChestBaseComponentImpl.java
new file mode 100644
index 0000000000..4fb1bba919
--- /dev/null
+++ b/server/src/main/java/org/allaymc/server/block/component/BlockChestBaseComponentImpl.java
@@ -0,0 +1,46 @@
+package org.allaymc.server.block.component;
+
+import org.allaymc.api.block.BlockBehavior;
+import org.allaymc.api.block.component.BlockEntityHolderComponent;
+import org.allaymc.api.block.data.BlockFace;
+import org.allaymc.api.block.dto.BlockStateWithPos;
+import org.allaymc.api.block.dto.PlayerInteractInfo;
+import org.allaymc.api.block.property.type.BlockPropertyTypes;
+import org.allaymc.api.block.type.BlockState;
+import org.allaymc.api.block.type.BlockType;
+import org.allaymc.api.blockentity.interfaces.BlockEntityChest;
+import org.allaymc.server.component.annotation.Dependency;
+
+/**
+ * @author IWareQ
+ */
+public class BlockChestBaseComponentImpl extends BlockBaseComponentImpl {
+    @Dependency
+    private BlockEntityHolderComponent<BlockEntityChest> blockEntityHolderComponent;
+
+    public BlockChestBaseComponentImpl(BlockType<? extends BlockBehavior> blockType) {
+        super(blockType);
+    }
+
+    @Override
+    public void afterPlaced(BlockStateWithPos oldBlockState, BlockState newBlockState, PlayerInteractInfo placementInfo) {
+        super.afterPlaced(oldBlockState, newBlockState, placementInfo);
+
+        var direction = newBlockState.getPropertyValue(BlockPropertyTypes.MINECRAFT_CARDINAL_DIRECTION);
+        var blockFace = BlockFace.fromMinecraftCardinalDirection(direction);
+        var thisChest = blockEntityHolderComponent.getBlockEntity(oldBlockState.pos());
+
+        for (var face : new BlockFace[]{blockFace.rotateY(), blockFace.rotateYCCW()}) {
+            var other = oldBlockState.offsetPos(face).getBlockEntity();
+            if (other instanceof BlockEntityChest otherChest && !otherChest.isPaired()) {
+                var otherState = otherChest.getBlockState();
+                if (direction == otherState.getPropertyValue(BlockPropertyTypes.MINECRAFT_CARDINAL_DIRECTION)) {
+                    if (otherChest.tryPairWith(thisChest)) {
+                        thisChest.tryPairWith(otherChest);
+                        break;
+                    }
+                }
+            }
+        }
+    }
+}
diff --git a/server/src/main/java/org/allaymc/server/block/type/BlockTypeInitializer.java b/server/src/main/java/org/allaymc/server/block/type/BlockTypeInitializer.java
index cdee1ed675..f038de1e31 100644
--- a/server/src/main/java/org/allaymc/server/block/type/BlockTypeInitializer.java
+++ b/server/src/main/java/org/allaymc/server/block/type/BlockTypeInitializer.java
@@ -400,6 +400,7 @@ public static void initChest() {
                 .vanillaBlock(BlockId.CHEST)
                 .setProperties(BlockPropertyTypes.MINECRAFT_CARDINAL_DIRECTION)
                 .bindBlockEntity(BlockEntityTypes.CHEST)
+                .setBaseComponentSupplier(BlockChestBaseComponentImpl::new)
                 .build();
     }
 
diff --git a/server/src/main/java/org/allaymc/server/blockentity/component/BlockEntityChestBaseComponentImpl.java b/server/src/main/java/org/allaymc/server/blockentity/component/BlockEntityChestBaseComponentImpl.java
index d26a642cda..8a13dc500e 100644
--- a/server/src/main/java/org/allaymc/server/blockentity/component/BlockEntityChestBaseComponentImpl.java
+++ b/server/src/main/java/org/allaymc/server/blockentity/component/BlockEntityChestBaseComponentImpl.java
@@ -1,8 +1,10 @@
 package org.allaymc.server.blockentity.component;
 
 import org.allaymc.api.blockentity.component.BlockEntityContainerHolderComponent;
+import org.allaymc.api.blockentity.component.BlockEntityPairableComponent;
 import org.allaymc.api.blockentity.initinfo.BlockEntityInitInfo;
-import org.allaymc.api.container.impl.ChestContainer;
+import org.allaymc.api.math.MathUtils;
+import org.allaymc.api.math.position.Position3ic;
 import org.allaymc.api.world.Sound;
 import org.allaymc.server.component.annotation.Dependency;
 import org.allaymc.server.component.annotation.OnInitFinish;
@@ -14,6 +16,8 @@
 public class BlockEntityChestBaseComponentImpl extends BlockEntityBaseComponentImpl {
     @Dependency
     private BlockEntityContainerHolderComponent containerHolderComponent;
+    @Dependency
+    private BlockEntityPairableComponent pairableComponent;
 
     public BlockEntityChestBaseComponentImpl(BlockEntityInitInfo info) {
         super(info);
@@ -23,38 +27,43 @@ public BlockEntityChestBaseComponentImpl(BlockEntityInitInfo info) {
     @Override
     public void onInitFinish(BlockEntityInitInfo initInfo) {
         super.onInitFinish(initInfo);
-        ChestContainer container = containerHolderComponent.getContainer();
+
+        if (containerHolderComponent instanceof BlockEntityChestContainerHolderComponentImpl containerHolder) {
+            var doubleChestContainer = containerHolder.getDoubleChestContainer();
+            doubleChestContainer.addOnOpenListener(viewer -> {
+                if (doubleChestContainer.getViewers().size() == 1) {
+                    playSoundAndSendPacket(getPosition(), Sound.RANDOM_CHESTOPEN, 2);
+                    playSoundAndSendPacket(pairableComponent.getPair().getPosition(), Sound.RANDOM_CHESTOPEN, 2);
+                }
+            });
+            doubleChestContainer.addOnCloseListener(viewer -> {
+                if (doubleChestContainer.getViewers().isEmpty()) {
+                    playSoundAndSendPacket(getPosition(), Sound.RANDOM_CHESTCLOSED, 0);
+                    playSoundAndSendPacket(pairableComponent.getPair().getPosition(), Sound.RANDOM_CHESTCLOSED, 0);
+                }
+            });
+        }
+
+        var container = containerHolderComponent.getContainer();
         container.addOnOpenListener(viewer -> {
             if (container.getViewers().size() == 1) {
-                BlockEventPacket pk = new BlockEventPacket();
-                pk.setBlockPosition(position.toNetwork());
-                pk.setEventType(1);
-                pk.setEventData(2);
-
-                position.dimension().addSound(
-                        position.x() + 0.5f,
-                        position.y() + 0.5f,
-                        position.z() + 0.5f,
-                        Sound.RANDOM_CHESTOPEN
-                );
-                sendPacketToViewers(pk);
+                playSoundAndSendPacket(getPosition(), Sound.RANDOM_CHESTOPEN, 2);
             }
         });
         container.addOnCloseListener(viewer -> {
             if (container.getViewers().isEmpty()) {
-                BlockEventPacket pk = new BlockEventPacket();
-                pk.setBlockPosition(position.toNetwork());
-                pk.setEventType(1);
-                pk.setEventData(0);
-
-                position.dimension().addSound(
-                        position.x() + 0.5f,
-                        position.y() + 0.5f,
-                        position.z() + 0.5f,
-                        Sound.RANDOM_CHESTCLOSED
-                );
-                sendPacketToViewers(pk);
+                playSoundAndSendPacket(getPosition(), Sound.RANDOM_CHESTCLOSED, 0);
             }
         });
     }
+
+    private void playSoundAndSendPacket(Position3ic pos, String sound, int eventData) {
+        var packet = new BlockEventPacket();
+        packet.setBlockPosition(pos.toNetwork());
+        packet.setEventType(1);
+        packet.setEventData(eventData);
+
+        pos.dimension().addSound(MathUtils.center(pos), sound);
+        sendPacketToViewers(packet);
+    }
 }
diff --git a/server/src/main/java/org/allaymc/server/blockentity/component/BlockEntityChestContainerHolderComponentImpl.java b/server/src/main/java/org/allaymc/server/blockentity/component/BlockEntityChestContainerHolderComponentImpl.java
new file mode 100644
index 0000000000..8963acfa2c
--- /dev/null
+++ b/server/src/main/java/org/allaymc/server/blockentity/component/BlockEntityChestContainerHolderComponentImpl.java
@@ -0,0 +1,71 @@
+package org.allaymc.server.blockentity.component;
+
+import lombok.Getter;
+import org.allaymc.api.block.data.BlockFace;
+import org.allaymc.api.blockentity.component.BlockEntityContainerHolderComponent;
+import org.allaymc.api.blockentity.component.BlockEntityPairableComponent;
+import org.allaymc.api.container.impl.ChestContainer;
+import org.allaymc.api.container.impl.DoubleChestContainer;
+import org.allaymc.api.eventbus.EventHandler;
+import org.allaymc.api.math.position.Position3i;
+import org.allaymc.server.block.component.event.CBlockOnInteractEvent;
+import org.allaymc.server.component.annotation.Dependency;
+
+/**
+ * @author IWareQ
+ */
+public class BlockEntityChestContainerHolderComponentImpl extends BlockEntityContainerHolderComponentImpl {
+    @Getter
+    protected final DoubleChestContainer doubleChestContainer;
+
+    @Dependency
+    protected BlockEntityPairableComponent pairableComponent;
+
+    public BlockEntityChestContainerHolderComponentImpl() {
+        super(ChestContainer::new);
+        this.doubleChestContainer = new DoubleChestContainer();
+    }
+
+    @EventHandler
+    @Override
+    protected void onInteract(CBlockOnInteractEvent event) {
+        var interactInfo = event.getInteractInfo();
+        var player = interactInfo.player();
+        if (player == null || player.isSneaking()) {
+            return;
+        }
+
+        var pos = new Position3i(interactInfo.clickedBlockPos(), event.getDimension());
+        if (!isSpaceAboveClear(pos)) {
+            return;
+        }
+
+        if (pairableComponent.isPaired()) {
+            var pair = pairableComponent.getPair();
+            if (!isSpaceAboveClear((Position3i) pair.getPosition())) {
+                return;
+            }
+
+            var left = getContainer();
+            var right = ((BlockEntityContainerHolderComponent) pairableComponent.getPair()).getContainer();
+            if (!pairableComponent.isLead()) {
+                var temp = left;
+                left = right;
+                right = temp;
+            }
+
+            doubleChestContainer.setLeft(left);
+            doubleChestContainer.setRight(right);
+            doubleChestContainer.setBlockPos(new Position3i(interactInfo.clickedBlockPos(), event.getDimension()));
+            doubleChestContainer.addViewer(player);
+        } else {
+            container.addViewer(player);
+        }
+
+        event.setSuccess(true);
+    }
+
+    protected boolean isSpaceAboveClear(Position3i pos) {
+        return pos.dimension().getBlockState(BlockFace.UP.offsetPos(pos)).getBlockStateData().isTransparent();
+    }
+}
diff --git a/server/src/main/java/org/allaymc/server/blockentity/component/BlockEntityContainerHolderComponentImpl.java b/server/src/main/java/org/allaymc/server/blockentity/component/BlockEntityContainerHolderComponentImpl.java
index 1cd19e9d3d..29dabd95e3 100644
--- a/server/src/main/java/org/allaymc/server/blockentity/component/BlockEntityContainerHolderComponentImpl.java
+++ b/server/src/main/java/org/allaymc/server/blockentity/component/BlockEntityContainerHolderComponentImpl.java
@@ -4,7 +4,6 @@
 import org.allaymc.api.blockentity.component.BlockEntityBaseComponent;
 import org.allaymc.api.blockentity.component.BlockEntityContainerHolderComponent;
 import org.allaymc.api.container.Container;
-import org.allaymc.api.container.ContainerViewer;
 import org.allaymc.api.container.FullContainerType;
 import org.allaymc.api.container.impl.BlockContainer;
 import org.allaymc.api.eventbus.EventHandler;
@@ -20,7 +19,6 @@
 import org.joml.Vector3d;
 
 import java.util.concurrent.ThreadLocalRandom;
-import java.util.function.Consumer;
 import java.util.function.Supplier;
 
 /**
@@ -40,16 +38,6 @@ public BlockEntityContainerHolderComponentImpl(Supplier<Container> containerSupp
         this.container = containerSupplier.get();
     }
 
-    public BlockEntityContainerHolderComponentImpl(
-            Supplier<Container> containerSupplier,
-            Consumer<ContainerViewer> onOpenListener,
-            Consumer<ContainerViewer> onCloseListener
-    ) {
-        this.container = containerSupplier.get();
-        this.container.addOnOpenListener(onOpenListener);
-        this.container.addOnCloseListener(onCloseListener);
-    }
-
     @Override
     public <T extends Container> T getContainer() {
         return (T) container;
@@ -117,8 +105,10 @@ public boolean hasContainer(FullContainerType<?> type) {
     public <T extends Container> T getContainerBySlotType(ContainerSlotType slotType) {
         // BlockEntityContainerHolder can only hold one container in its lifetime
         // So we only need to check the slotType which caller provided
-        if (!container.getContainerType().heldSlotTypes().contains(slotType))
+        if (!container.getContainerType().heldSlotTypes().contains(slotType)) {
             throw new IllegalArgumentException("The container " + container.getContainerType() + " does not have the slot type " + slotType);
+        }
+
         return (T) container;
     }
 }
diff --git a/server/src/main/java/org/allaymc/server/blockentity/component/BlockEntityPairableComponentImpl.java b/server/src/main/java/org/allaymc/server/blockentity/component/BlockEntityPairableComponentImpl.java
new file mode 100644
index 0000000000..4c3c9c0203
--- /dev/null
+++ b/server/src/main/java/org/allaymc/server/blockentity/component/BlockEntityPairableComponentImpl.java
@@ -0,0 +1,96 @@
+package org.allaymc.server.blockentity.component;
+
+import lombok.Getter;
+import org.allaymc.api.blockentity.BlockEntity;
+import org.allaymc.api.blockentity.component.BlockEntityBaseComponent;
+import org.allaymc.api.blockentity.component.BlockEntityPairableComponent;
+import org.allaymc.api.eventbus.EventHandler;
+import org.allaymc.server.block.component.event.CBlockOnReplaceEvent;
+import org.allaymc.server.blockentity.component.event.CBlockEntityLoadNBTEvent;
+import org.allaymc.server.blockentity.component.event.CBlockEntitySaveNBTEvent;
+import org.allaymc.server.component.annotation.Dependency;
+
+/**
+ * @author IWareQ
+ */
+public class BlockEntityPairableComponentImpl implements BlockEntityPairableComponent {
+    protected static final String TAG_PAIR_LEAD = "allay:pairlead";
+    protected static final String TAG_PAIR_X = "pairx";
+    protected static final String TAG_PAIR_Z = "pairz";
+
+    @Dependency
+    protected BlockEntityBaseComponent baseComponent;
+
+    @Getter
+    protected boolean lead;
+    protected Integer pairX;
+    protected Integer pairZ;
+
+    @Override
+    public boolean tryPairWith(BlockEntity other) {
+        if (isPaired()) {
+            return false;
+        }
+
+        pairX = other.getPosition().x();
+        pairZ = other.getPosition().z();
+
+        baseComponent.sendBlockEntityDataPacketToViewers();
+
+        var pair = (BlockEntityPairableComponent) getPair();
+        if (!pair.isPaired()) {
+            lead = true;
+        }
+
+        return true;
+    }
+
+    @Override
+    public void unpair() {
+        lead = false;
+
+        pairX = null;
+        pairZ = null;
+
+        baseComponent.sendBlockEntityDataPacketToViewers();
+    }
+
+    @EventHandler
+    protected void onLoadNBT(CBlockEntityLoadNBTEvent event) {
+        var nbt = event.getNbt();
+        nbt.listenForBoolean(TAG_PAIR_LEAD, value -> lead = value);
+        nbt.listenForInt(TAG_PAIR_X, value -> pairX = value);
+        nbt.listenForInt(TAG_PAIR_Z, value -> pairZ = value);
+    }
+
+    @EventHandler
+    protected void onSaveNBT(CBlockEntitySaveNBTEvent event) {
+        var builder = event.getNbt();
+        if (isPaired()) {
+            builder.putBoolean(TAG_PAIR_LEAD, lead)
+                    .putInt(TAG_PAIR_X, pairX)
+                    .putInt(TAG_PAIR_Z, pairZ);
+        }
+    }
+
+    @EventHandler
+    protected void onReplace(CBlockOnReplaceEvent event) {
+        var blockEntity = event.getCurrentBlockState().getBlockEntity();
+        if (blockEntity instanceof BlockEntityPairableComponent pairableComponent) {
+            if (pairableComponent.isPaired()) {
+                ((BlockEntityPairableComponent) pairableComponent.getPair()).unpair();
+                pairableComponent.unpair();
+            }
+        }
+    }
+
+    @Override
+    public BlockEntity getPair() {
+        return baseComponent.getDimension().getBlockEntity(pairX, baseComponent.getPosition().y(), pairZ);
+    }
+
+    @Override
+    public boolean isPaired() {
+        return pairX != null && pairZ != null;
+    }
+}
diff --git a/server/src/main/java/org/allaymc/server/blockentity/impl/BlockEntityChestImpl.java b/server/src/main/java/org/allaymc/server/blockentity/impl/BlockEntityChestImpl.java
index faa05659cf..75044c7270 100644
--- a/server/src/main/java/org/allaymc/server/blockentity/impl/BlockEntityChestImpl.java
+++ b/server/src/main/java/org/allaymc/server/blockentity/impl/BlockEntityChestImpl.java
@@ -2,6 +2,7 @@
 
 import lombok.experimental.Delegate;
 import org.allaymc.api.blockentity.component.BlockEntityContainerHolderComponent;
+import org.allaymc.api.blockentity.component.BlockEntityPairableComponent;
 import org.allaymc.api.blockentity.initinfo.BlockEntityInitInfo;
 import org.allaymc.api.blockentity.interfaces.BlockEntityChest;
 import org.allaymc.api.component.interfaces.Component;
@@ -15,6 +16,8 @@
 public class BlockEntityChestImpl extends BlockEntityImpl implements BlockEntityChest {
     @Delegate
     protected BlockEntityContainerHolderComponent containerHolderComponent;
+    @Delegate
+    protected BlockEntityPairableComponent pairableComponent;
 
     public BlockEntityChestImpl(BlockEntityInitInfo initInfo, List<ComponentProvider<? extends Component>> componentProviders) {
         super(initInfo, componentProviders);
diff --git a/server/src/main/java/org/allaymc/server/blockentity/type/BlockEntityTypeInitializer.java b/server/src/main/java/org/allaymc/server/blockentity/type/BlockEntityTypeInitializer.java
index 86010e06c9..5e31998a8a 100644
--- a/server/src/main/java/org/allaymc/server/blockentity/type/BlockEntityTypeInitializer.java
+++ b/server/src/main/java/org/allaymc/server/blockentity/type/BlockEntityTypeInitializer.java
@@ -40,7 +40,8 @@ public static void initChest() {
                 .builder(BlockEntityChestImpl.class)
                 .name(BlockEntityId.CHEST)
                 .addComponent(BlockEntityChestBaseComponentImpl::new, BlockEntityChestBaseComponentImpl.class)
-                .addComponent(() -> new BlockEntityContainerHolderComponentImpl(ChestContainer::new), BlockEntityContainerHolderComponentImpl.class)
+                .addComponent(BlockEntityChestContainerHolderComponentImpl::new, BlockEntityChestContainerHolderComponentImpl.class)
+                .addComponent(BlockEntityPairableComponentImpl::new, BlockEntityPairableComponentImpl.class)
                 .build();
     }
 


Reviews:
user: login: smartcmd
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-23T16:43:21Z
user: login: IWareQ
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-23T17:05:11Z

Review comments:
diff_hunk: @@ -0,0 +1,96 @@
+package org.allaymc.server.blockentity.component;
+
+import lombok.Getter;
+import org.allaymc.api.blockentity.BlockEntity;
+import org.allaymc.api.blockentity.component.BlockEntityBaseComponent;
+import org.allaymc.api.blockentity.component.BlockEntityPairableComponent;
+import org.allaymc.api.eventbus.EventHandler;
+import org.allaymc.server.block.component.event.CBlockOnReplaceEvent;
+import org.allaymc.server.blockentity.component.event.CBlockEntityLoadNBTEvent;
+import org.allaymc.server.blockentity.component.event.CBlockEntitySaveNBTEvent;
+import org.allaymc.server.component.annotation.Dependency;
+
+/**
+ * @author IWareQ
+ */
+public class BlockEntityPairableComponentImpl implements BlockEntityPairableComponent {
+    protected static final String TAG_ALLAY_PAIR_LEAD = "allay:pairlead";
path: server/src/main/java/org/allaymc/server/blockentity/component/BlockEntityPairableComponentImpl.java
user: login: smartcmd
body: Is it possible to remove this field?
created_at: 2025-03-23T16:43:07Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,96 @@
+package org.allaymc.server.blockentity.component;
+
+import lombok.Getter;
+import org.allaymc.api.blockentity.BlockEntity;
+import org.allaymc.api.blockentity.component.BlockEntityBaseComponent;
+import org.allaymc.api.blockentity.component.BlockEntityPairableComponent;
+import org.allaymc.api.eventbus.EventHandler;
+import org.allaymc.server.block.component.event.CBlockOnReplaceEvent;
+import org.allaymc.server.blockentity.component.event.CBlockEntityLoadNBTEvent;
+import org.allaymc.server.blockentity.component.event.CBlockEntitySaveNBTEvent;
+import org.allaymc.server.component.annotation.Dependency;
+
+/**
+ * @author IWareQ
+ */
+public class BlockEntityPairableComponentImpl implements BlockEntityPairableComponent {
+    protected static final String TAG_ALLAY_PAIR_LEAD = "allay:pairlead";
path: server/src/main/java/org/allaymc/server/blockentity/component/BlockEntityPairableComponentImpl.java
user: login: IWareQ
body: No
created_at: 2025-03-23T17:05:11Z
author_association: MEMBER

PR comments:
user: login: codecov[bot]
created_at: 2025-03-22T19:11:19Z
body: ## [Codecov](https://app.codecov.io/gh/AllayMC/Allay/pull/611?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=AllayMC) Report
Attention: Patch coverage is `8.33333%` with `121 lines` in your changes missing coverage. Please review.

| [Files with missing lines](https://app.codecov.io/gh/AllayMC/Allay/pull/611?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=AllayMC) | Patch % | Lines |
|---|---|---|
| [...ty/component/BlockEntityPairableComponentImpl.java](https://app.codecov.io/gh/AllayMC/Allay/pull/611?src=pr&el=tree&filepath=server%2Fsrc%2Fmain%2Fjava%2Forg%2Fallaymc%2Fserver%2Fblockentity%2Fcomponent%2FBlockEntityPairableComponentImpl.java&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=AllayMC#diff-c2VydmVyL3NyYy9tYWluL2phdmEvb3JnL2FsbGF5bWMvc2VydmVyL2Jsb2NrZW50aXR5L2NvbXBvbmVudC9CbG9ja0VudGl0eVBhaXJhYmxlQ29tcG9uZW50SW1wbC5qYXZh) | 0.00% | [34 Missing :warning: ](https://app.codecov.io/gh/AllayMC/Allay/pull/611?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=AllayMC) |
| [.../BlockEntityChestContainerHolderComponentImpl.java](https://app.codecov.io/gh/AllayMC/Allay/pull/611?src=pr&el=tree&filepath=server%2Fsrc%2Fmain%2Fjava%2Forg%2Fallaymc%2Fserver%2Fblockentity%2Fcomponent%2FBlockEntityChestContainerHolderComponentImpl.java&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=AllayMC#diff-c2VydmVyL3NyYy9tYWluL2phdmEvb3JnL2FsbGF5bWMvc2VydmVyL2Jsb2NrZW50aXR5L2NvbXBvbmVudC9CbG9ja0VudGl0eUNoZXN0Q29udGFpbmVySG9sZGVyQ29tcG9uZW50SW1wbC5qYXZh) | 0.00% | [29 Missing :warning: ](https://app.codecov.io/gh/AllayMC/Allay/pull/611?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=AllayMC) |
| [...y/component/BlockEntityChestBaseComponentImpl.java](https://app.codecov.io/gh/AllayMC/Allay/pull/611?src=pr&el=tree&filepath=server%2Fsrc%2Fmain%2Fjava%2Forg%2Fallaymc%2Fserver%2Fblockentity%2Fcomponent%2FBlockEntityChestBaseComponentImpl.java&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=AllayMC#diff-c2VydmVyL3NyYy9tYWluL2phdmEvb3JnL2FsbGF5bWMvc2VydmVyL2Jsb2NrZW50aXR5L2NvbXBvbmVudC9CbG9ja0VudGl0eUNoZXN0QmFzZUNvbXBvbmVudEltcGwuamF2YQ==) | 0.00% | [22 Missing :warning: ](https://app.codecov.io/gh/AllayMC/Allay/pull/611?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=AllayMC) |
| [...laymc/api/container/impl/DoubleChestContainer.java](https://app.codecov.io/gh/AllayMC/Allay/pull/611?src=pr&el=tree&filepath=api%2Fsrc%2Fmain%2Fjava%2Forg%2Fallaymc%2Fapi%2Fcontainer%2Fimpl%2FDoubleChestContainer.java&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=AllayMC#diff-YXBpL3NyYy9tYWluL2phdmEvb3JnL2FsbGF5bWMvYXBpL2NvbnRhaW5lci9pbXBsL0RvdWJsZUNoZXN0Q29udGFpbmVyLmphdmE=) | 0.00% | [14 Missing :warning: ](https://app.codecov.io/gh/AllayMC/Allay/pull/611?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=AllayMC) |
| [...r/block/component/BlockChestBaseComponentImpl.java](https://app.codecov.io/gh/AllayMC/Allay/pull/611?src=pr&el=tree&filepath=server%2Fsrc%2Fmain%2Fjava%2Forg%2Fallaymc%2Fserver%2Fblock%2Fcomponent%2FBlockChestBaseComponentImpl.java&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=AllayMC#diff-c2VydmVyL3NyYy9tYWluL2phdmEvb3JnL2FsbGF5bWMvc2VydmVyL2Jsb2NrL2NvbXBvbmVudC9CbG9ja0NoZXN0QmFzZUNvbXBvbmVudEltcGwuamF2YQ==) | 13.33% | [13 Missing :warning: ](https://app.codecov.io/gh/AllayMC/Allay/pull/611?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=AllayMC) |
| [...ain/java/org/allaymc/api/block/data/BlockFace.java](https://app.codecov.io/gh/AllayMC/Allay/pull/611?src=pr&el=tree&filepath=api%2Fsrc%2Fmain%2Fjava%2Forg%2Fallaymc%2Fapi%2Fblock%2Fdata%2FBlockFace.java&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=AllayMC#diff-YXBpL3NyYy9tYWluL2phdmEvb3JnL2FsbGF5bWMvYXBpL2Jsb2NrL2RhdGEvQmxvY2tGYWNlLmphdmE=) | 14.28% | [6 Missing :warning: ](https://app.codecov.io/gh/AllayMC/Allay/pull/611?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=AllayMC) |
| [...a/org/allaymc/api/block/dto/BlockStateWithPos.java](https://app.codecov.io/gh/AllayMC/Allay/pull/611?src=pr&el=tree&filepath=api%2Fsrc%2Fmain%2Fjava%2Forg%2Fallaymc%2Fapi%2Fblock%2Fdto%2FBlockStateWithPos.java&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=AllayMC#diff-YXBpL3NyYy9tYWluL2phdmEvb3JnL2FsbGF5bWMvYXBpL2Jsb2NrL2R0by9CbG9ja1N0YXRlV2l0aFBvcy5qYXZh) | 0.00% | [1 Missing :warning: ](https://app.codecov.io/gh/AllayMC/Allay/pull/611?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=AllayMC) |
| [...lockentity/component/BlockEntityBaseComponent.java](https://app.codecov.io/gh/AllayMC/Allay/pull/611?src=pr&el=tree&filepath=api%2Fsrc%2Fmain%2Fjava%2Forg%2Fallaymc%2Fapi%2Fblockentity%2Fcomponent%2FBlockEntityBaseComponent.java&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=AllayMC#diff-YXBpL3NyYy9tYWluL2phdmEvb3JnL2FsbGF5bWMvYXBpL2Jsb2NrZW50aXR5L2NvbXBvbmVudC9CbG9ja0VudGl0eUJhc2VDb21wb25lbnQuamF2YQ==) | 0.00% | [1 Missing :warning: ](https://app.codecov.io/gh/AllayMC/Allay/pull/611?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=AllayMC) |
| [...onent/BlockEntityContainerHolderComponentImpl.java](https://app.codecov.io/gh/AllayMC/Allay/pull/611?src=pr&el=tree&filepath=server%2Fsrc%2Fmain%2Fjava%2Forg%2Fallaymc%2Fserver%2Fblockentity%2Fcomponent%2FBlockEntityContainerHolderComponentImpl.java&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=AllayMC#diff-c2VydmVyL3NyYy9tYWluL2phdmEvb3JnL2FsbGF5bWMvc2VydmVyL2Jsb2NrZW50aXR5L2NvbXBvbmVudC9CbG9ja0VudGl0eUNvbnRhaW5lckhvbGRlckNvbXBvbmVudEltcGwuamF2YQ==) | 0.00% | [1 Missing :warning: ](https://app.codecov.io/gh/AllayMC/Allay/pull/611?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=AllayMC) |

| [Files with missing lines](https://app.codecov.io/gh/AllayMC/Allay/pull/611?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=AllayMC) | Coverage Δ | Complexity Δ | |
|---|---|---|---|
| [...main/java/org/allaymc/api/container/Container.java](https://app.codecov.io/gh/AllayMC/Allay/pull/611?src=pr&el=tree&filepath=api%2Fsrc%2Fmain%2Fjava%2Forg%2Fallaymc%2Fapi%2Fcontainer%2FContainer.java&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=AllayMC#diff-YXBpL3NyYy9tYWluL2phdmEvb3JnL2FsbGF5bWMvYXBpL2NvbnRhaW5lci9Db250YWluZXIuamF2YQ==) | `3.22% <ø> (ø)` | `1.00 <0.00> (ø)` | |
| [...a/org/allaymc/api/container/FullContainerType.java](https://app.codecov.io/gh/AllayMC/Allay/pull/611?src=pr&el=tree&filepath=api%2Fsrc%2Fmain%2Fjava%2Forg%2Fallaymc%2Fapi%2Fcontainer%2FFullContainerType.java&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=AllayMC#diff-YXBpL3NyYy9tYWluL2phdmEvb3JnL2FsbGF5bWMvYXBpL2NvbnRhaW5lci9GdWxsQ29udGFpbmVyVHlwZS5qYXZh) | `91.34% <100.00%> (+0.21%)` | `5.00 <0.00> (ø)` | |
| [...llaymc/server/block/type/BlockTypeInitializer.java](https://app.codecov.io/gh/AllayMC/Allay/pull/611?src=pr&el=tree&filepath=server%2Fsrc%2Fmain%2Fjava%2Forg%2Fallaymc%2Fserver%2Fblock%2Ftype%2FBlockTypeInitializer.java&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=AllayMC#diff-c2VydmVyL3NyYy9tYWluL2phdmEvb3JnL2FsbGF5bWMvc2VydmVyL2Jsb2NrL3R5cGUvQmxvY2tUeXBlSW5pdGlhbGl6ZXIuamF2YQ==) | `96.56% <100.00%> (+<0.01%)` | `98.00 <0.00> (ø)` | |
| [.../server/blockentity/impl/BlockEntityChestImpl.java](https://app.codecov.io/gh/AllayMC/Allay/pull/611?src=pr&el=tree&filepath=server%2Fsrc%2Fmain%2Fjava%2Forg%2Fallaymc%2Fserver%2Fblockentity%2Fimpl%2FBlockEntityChestImpl.java&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=AllayMC#diff-c2VydmVyL3NyYy9tYWluL2phdmEvb3JnL2FsbGF5bWMvc2VydmVyL2Jsb2NrZW50aXR5L2ltcGwvQmxvY2tFbnRpdHlDaGVzdEltcGwuamF2YQ==) | `0.00% <ø> (ø)` | `0.00 <0.00> (ø)` | |
| [...r/blockentity/type/BlockEntityTypeInitializer.java](https://app.codecov.io/gh/AllayMC/Allay/pull/611?src=pr&el=tree&filepath=server%2Fsrc%2Fmain%2Fjava%2Forg%2Fallaymc%2Fserver%2Fblockentity%2Ftype%2FBlockEntityTypeInitializer.java&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=AllayMC#diff-c2VydmVyL3NyYy9tYWluL2phdmEvb3JnL2FsbGF5bWMvc2VydmVyL2Jsb2NrZW50aXR5L3R5cGUvQmxvY2tFbnRpdHlUeXBlSW5pdGlhbGl6ZXIuamF2YQ==) | `100.00% <100.00%> (ø)` | `12.00 <0.00> (ø)` | |
| [...a/org/allaymc/api/block/dto/BlockStateWithPos.java](https://app.codecov.io/gh/AllayMC/Allay/pull/611?src=pr&el=tree&filepath=api%2Fsrc%2Fmain%2Fjava%2Forg%2Fallaymc%2Fapi%2Fblock%2Fdto%2FBlockStateWithPos.java&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=AllayMC#diff-YXBpL3NyYy9tYWluL2phdmEvb3JnL2FsbGF5bWMvYXBpL2Jsb2NrL2R0by9CbG9ja1N0YXRlV2l0aFBvcy5qYXZh) | `11.11% <0.00%> (-1.39%)` | `1.00 <0.00> (ø)` | |
| [...lockentity/component/BlockEntityBaseComponent.java](https://app.codecov.io/gh/AllayMC/Allay/pull/611?src=pr&el=tree&filepath=api%2Fsrc%2Fmain%2Fjava%2Forg%2Fallaymc%2Fapi%2Fblockentity%2Fcomponent%2FBlockEntityBaseComponent.java&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=AllayMC#diff-YXBpL3NyYy9tYWluL2phdmEvb3JnL2FsbGF5bWMvYXBpL2Jsb2NrZW50aXR5L2NvbXBvbmVudC9CbG9ja0VudGl0eUJhc2VDb21wb25lbnQuamF2YQ==) | `0.00% <0.00%> (ø)` | `0.00 <0.00> (ø)` | |
| [...onent/BlockEntityContainerHolderComponentImpl.java](https://app.codecov.io/gh/AllayMC/Allay/pull/611?src=pr&el=tree&filepath=server%2Fsrc%2Fmain%2Fjava%2Forg%2Fallaymc%2Fserver%2Fblockentity%2Fcomponent%2FBlockEntityContainerHolderComponentImpl.java&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=AllayMC#diff-c2VydmVyL3NyYy9tYWluL2phdmEvb3JnL2FsbGF5bWMvc2VydmVyL2Jsb2NrZW50aXR5L2NvbXBvbmVudC9CbG9ja0VudGl0eUNvbnRhaW5lckhvbGRlckNvbXBvbmVudEltcGwuamF2YQ==) | `23.68% <0.00%> (+2.75%)` | `4.00 <0.00> (ø)` | |
| [...ain/java/org/allaymc/api/block/data/BlockFace.java](https://app.codecov.io/gh/AllayMC/Allay/pull/611?src=pr&el=tree&filepath=api%2Fsrc%2Fmain%2Fjava%2Forg%2Fallaymc%2Fapi%2Fblock%2Fdata%2FBlockFace.java&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=AllayMC#diff-YXBpL3NyYy9tYWluL2phdmEvb3JnL2FsbGF5bWMvYXBpL2Jsb2NrL2RhdGEvQmxvY2tGYWNlLmphdmE=) | `51.72% <14.28%> (-4.38%)` | `11.00 <0.00> (ø)` | |
| [...r/block/component/BlockChestBaseComponentImpl.java](https://app.codecov.io/gh/AllayMC/Allay/pull/611?src=pr&el=tree&filepath=server%2Fsrc%2Fmain%2Fjava%2Forg%2Fallaymc%2Fserver%2Fblock%2Fcomponent%2FBlockChestBaseComponentImpl.java&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=AllayMC#diff-c2VydmVyL3NyYy9tYWluL2phdmEvb3JnL2FsbGF5bWMvc2VydmVyL2Jsb2NrL2NvbXBvbmVudC9CbG9ja0NoZXN0QmFzZUNvbXBvbmVudEltcGwuamF2YQ==) | `13.33% <13.33%> (ø)` | `1.00 <1.00> (?)` | |
| ... and [4 more](https://app.codecov.io/gh/AllayMC/Allay/pull/611?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=AllayMC) | |

... and [2 files with indirect coverage changes](https://app.codecov.io/gh/AllayMC/Allay/pull/611/indirect-changes?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=AllayMC)

<details><summary> :rocket: New features to boost your workflow: </summary>

- :snowflake: [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems.
</details>
author_association: NONE
user: login: smartcmd
created_at: 2025-03-23T16:43:32Z
body: LGTM!!!❤️
author_association: MEMBER