diff --git a/paper-api/src/main/java/io/papermc/paper/block/LidMode.java b/paper-api/src/main/java/io/papermc/paper/block/LidMode.java
new file mode 100644
index 0000000000..a4f85cf32a
--- /dev/null
+++ b/paper-api/src/main/java/io/papermc/paper/block/LidMode.java
@@ -0,0 +1,50 @@
+package io.papermc.paper.block;
+
+import org.jspecify.annotations.NullMarked;
+
+/**
+ * Represents how the lid of a block behaves.
+ */
+@NullMarked
+public enum LidMode {
+ /**
+ * The default lid mode, the lid will open and close based on player interaction.
+ *
+ * the state used for this is provided with {@link Lidded#getTrueLidState()}
+ */
+ DEFAULT,
+
+ /**
+ * The lid will be forced open, regardless of player interaction.
+ *
+ * This needs to be manually unset with another call to {@link Lidded#setLidMode(LidMode)}.
+ */
+ FORCED_OPEN,
+
+ /**
+ * The lid will be forced closed, regardless of player interaction.
+ *
+ * This needs to be manually unset with another call to {@link Lidded#setLidMode(LidMode)}.
+ */
+ FORCED_CLOSED,
+
+ /**
+ * The lid will be forced open until at least one player has opened it.
+ *
+ * It will then revert to {@link #DEFAULT}.
+ *
+ * If at least one player is viewing it when this is set, it will immediately revert to
+ * {@link #DEFAULT}.
+ */
+ OPEN_UNTIL_VIEWED,
+
+ /**
+ * The lid will be forced closed until all players currently viewing it have closed it.
+ *
+ * It will then revert to {@link #DEFAULT}.
+ *
+ * If no players are viewing it when this is set, it will immediately revert to
+ * {@link #DEFAULT}.
+ */
+ CLOSED_UNTIL_NOT_VIEWED
+}
diff --git a/paper-api/src/main/java/io/papermc/paper/block/LidState.java b/paper-api/src/main/java/io/papermc/paper/block/LidState.java
new file mode 100644
index 0000000000..b6e86005ce
--- /dev/null
+++ b/paper-api/src/main/java/io/papermc/paper/block/LidState.java
@@ -0,0 +1,9 @@
+package io.papermc.paper.block;
+
+import org.jspecify.annotations.NullMarked;
+
+@NullMarked
+public enum LidState {
+ OPEN,
+ CLOSED
+}
diff --git a/paper-api/src/main/java/io/papermc/paper/block/Lidded.java b/paper-api/src/main/java/io/papermc/paper/block/Lidded.java
new file mode 100644
index 0000000000..23c97fd9b8
--- /dev/null
+++ b/paper-api/src/main/java/io/papermc/paper/block/Lidded.java
@@ -0,0 +1,37 @@
+package io.papermc.paper.block;
+
+import org.bukkit.block.TileState;
+import org.jspecify.annotations.NullMarked;
+
+@NullMarked
+public interface Lidded extends TileState {
+
+ /**
+ * Gets the current state of the block, respecting the lidded mode.
+ *
+ * @return the effective lid state
+ */
+ LidState getEffectiveLidState();
+
+ /**
+ * Gets how the lid would be without any lidded mode, based on players interacting with the block.
+ * @return the true lid state
+ */
+ LidState getTrueLidState();
+
+ /**
+ * Gets the current lid mode of the block.
+ *
+ * @return the lid mode
+ */
+ LidMode getLidMode();
+
+ /**
+ * Sets the lid mode of the block.
+ *
+ * @param mode the new lid mode
+ * @return the actually set lid mode
+ */
+ LidMode setLidMode(LidMode mode);
+
+}
diff --git a/paper-api/src/main/java/org/bukkit/block/Barrel.java b/paper-api/src/main/java/org/bukkit/block/Barrel.java
index d3789b2b7d..c714361ba5 100644
--- a/paper-api/src/main/java/org/bukkit/block/Barrel.java
+++ b/paper-api/src/main/java/org/bukkit/block/Barrel.java
@@ -5,4 +5,4 @@ import org.bukkit.loot.Lootable;
/**
* Represents a captured state of a Barrel.
*/
-public interface Barrel extends Container, com.destroystokyo.paper.loottable.LootableBlockInventory, Lidded { } // Paper
+public interface Barrel extends Container, com.destroystokyo.paper.loottable.LootableBlockInventory, Lidded, io.papermc.paper.block.Lidded { } // Paper
diff --git a/paper-api/src/main/java/org/bukkit/block/Chest.java b/paper-api/src/main/java/org/bukkit/block/Chest.java
index 5d02f9c938..85fb81c77f 100644
--- a/paper-api/src/main/java/org/bukkit/block/Chest.java
+++ b/paper-api/src/main/java/org/bukkit/block/Chest.java
@@ -9,7 +9,7 @@ import org.jetbrains.annotations.NotNull;
/**
* Represents a captured state of a chest.
*/
-public interface Chest extends Container, LootableBlockInventory, Lidded { // Paper
+public interface Chest extends Container, LootableBlockInventory, Lidded, io.papermc.paper.block.Lidded { // Paper
/**
* Gets the inventory of the chest block represented by this block state.
diff --git a/paper-api/src/main/java/org/bukkit/block/EnderChest.java b/paper-api/src/main/java/org/bukkit/block/EnderChest.java
index 6b66f38e55..9ec76ad38d 100644
--- a/paper-api/src/main/java/org/bukkit/block/EnderChest.java
+++ b/paper-api/src/main/java/org/bukkit/block/EnderChest.java
@@ -3,7 +3,7 @@ package org.bukkit.block;
/**
* Represents a captured state of an ender chest.
*/
-public interface EnderChest extends Lidded, TileState {
+public interface EnderChest extends Lidded, TileState, io.papermc.paper.block.Lidded {
// Paper start - More Chest Block API
/**
* Checks whether this ender chest is blocked by a block above
diff --git a/paper-api/src/main/java/org/bukkit/block/Lidded.java b/paper-api/src/main/java/org/bukkit/block/Lidded.java
index 30c7df0021..72cb0bffbe 100644
--- a/paper-api/src/main/java/org/bukkit/block/Lidded.java
+++ b/paper-api/src/main/java/org/bukkit/block/Lidded.java
@@ -1,25 +1,34 @@
package org.bukkit.block;
+/**
+ * @deprecated Incomplete api. Use {@link io.papermc.paper.block.Lidded} instead.
+ */
+@Deprecated // Paper - Deprecate Bukkit's Lidded API
public interface Lidded {
/**
* Sets the block's animated state to open and prevents it from being closed
* until {@link #close()} is called.
+ * @deprecated Use {@link io.papermc.paper.block.Lidded#setLidMode(io.papermc.paper.block.LidMode)}
*/
+ @Deprecated
void open();
/**
- * Sets the block's animated state to closed even if a player is currently
- * viewing this block.
+ * Unsets a corresponding call to {@link #open()}.
+ * @deprecated Misleading name. Use {@link io.papermc.paper.block.Lidded#setLidMode(io.papermc.paper.block.LidMode)}
*/
+ @Deprecated
void close();
// Paper start - More Lidded Block API
/**
- * Checks if the block's animation state.
+ * Checks is the Lid is currently forced open.
*
- * @return true if the block's animation state is set to open.
+ * @return true if the block's animation state is force open.
+ * @deprecated Misleading name. Use {@link io.papermc.paper.block.Lidded#getLidMode()} for the direct replacement, or {@link io.papermc.paper.block.Lidded#getEffectiveLidState()} to tell if the lid is visibly open to the player instead.
*/
+ @Deprecated
boolean isOpen();
// Paper end - More Lidded Block API
}
diff --git a/paper-api/src/main/java/org/bukkit/block/ShulkerBox.java b/paper-api/src/main/java/org/bukkit/block/ShulkerBox.java
index 5dc5318b0a..23995c48f2 100644
--- a/paper-api/src/main/java/org/bukkit/block/ShulkerBox.java
+++ b/paper-api/src/main/java/org/bukkit/block/ShulkerBox.java
@@ -8,7 +8,7 @@ import org.jetbrains.annotations.Nullable;
/**
* Represents a captured state of a ShulkerBox.
*/
-public interface ShulkerBox extends Container, LootableBlockInventory, Lidded { // Paper
+public interface ShulkerBox extends Container, LootableBlockInventory, Lidded, io.papermc.paper.block.Lidded { // Paper
/**
* Get the {@link DyeColor} corresponding to this ShulkerBox
diff --git a/paper-server/patches/sources/net/minecraft/world/level/block/entity/ContainerOpenersCounter.java.patch b/paper-server/patches/sources/net/minecraft/world/level/block/entity/ContainerOpenersCounter.java.patch
index 6d0edb5d1d..55c2d9d02a 100644
--- a/paper-server/patches/sources/net/minecraft/world/level/block/entity/ContainerOpenersCounter.java.patch
+++ b/paper-server/patches/sources/net/minecraft/world/level/block/entity/ContainerOpenersCounter.java.patch
@@ -4,33 +4,92 @@
private static final int CHECK_TICK_DELAY = 5;
private int openCount;
private double maxInteractionRange;
-+ public boolean opened; // CraftBukkit
++ // public boolean opened; // CraftBukkit // Paper - Replace with new Lidded API
protected abstract void onOpen(Level level, BlockPos pos, BlockState state);
-@@ -20,10 +_,36 @@
+@@ -20,10 +_,94 @@
protected abstract void openerCountChanged(Level level, BlockPos pos, BlockState state, int count, int openCount);
+ // CraftBukkit start
-+ public void onAPIOpen(Level level, BlockPos blockPos, BlockState blockState) {
-+ this.onOpen(level, blockPos, blockState);
-+ }
-+
-+ public void onAPIClose(Level level, BlockPos blockPos, BlockState blockState) {
-+ this.onClose(level, blockPos, blockState);
-+ }
-+
-+ public void openerAPICountChanged(Level level, BlockPos blockPos, BlockState blockState, int count, int openCount) {
-+ this.openerCountChanged(level, blockPos, blockState, count, openCount);
-+ }
++ // Paper start - Replace with new Lidded API
++ // public void onAPIOpen(Level level, BlockPos blockPos, BlockState blockState) {
++ // this.onOpen(level, blockPos, blockState);
++ // }
++ //
++ // public void onAPIClose(Level level, BlockPos blockPos, BlockState blockState) {
++ // this.onClose(level, blockPos, blockState);
++ // }
++ //
++ // public void openerAPICountChanged(Level level, BlockPos blockPos, BlockState blockState, int count, int openCount) {
++ // this.openerCountChanged(level, blockPos, blockState, count, openCount);
++ // }
++ // Paper end - Replace with new Lidded API
+ // CraftBukkit end
+
protected abstract boolean isOwnContainer(Player player);
- public void incrementOpeners(Player player, Level level, BlockPos pos, BlockState state) {
+- public void incrementOpeners(Player player, Level level, BlockPos pos, BlockState state) {
++ // Paper start - add Improved Lidded API
++ private io.papermc.paper.block.LidMode apiLidMode = io.papermc.paper.block.LidMode.DEFAULT;
++ public void startForceLiddedLidOpen(Level level, BlockPos pos, BlockState state) {
++ incrementOpeners(null, level, pos, state);
++ }
++ public void stopForceLiddedLidOpen(Level level, BlockPos pos, BlockState state) {
++ decrementOpeners(null, level, pos, state);
++ apiLidMode = io.papermc.paper.block.LidMode.DEFAULT;
++ }
++ public void startForceLiddedLidClose(Level level, BlockPos pos, BlockState state) {
++ if (this.getTrueLidState() == io.papermc.paper.block.LidState.OPEN) {
++ this.onClose(level, pos, state);
++ level.gameEvent(null, GameEvent.CONTAINER_CLOSE, pos);
++ }
++ this.openerCountChanged(level, pos, state, this.openCount, 0);
++ }
++ public void stopForceLiddedLidClose(Level level, BlockPos pos, BlockState state) {
++ if (this.getTrueLidState() == io.papermc.paper.block.LidState.OPEN) {
++ this.onOpen(level, pos, state);
++ level.gameEvent(null, GameEvent.CONTAINER_OPEN, pos);
++ scheduleRecheck(level, pos, state);
++ }
++ this.openerCountChanged(level, pos, state, 0, this.openCount);
++ apiLidMode = io.papermc.paper.block.LidMode.DEFAULT;
++ }
++ public io.papermc.paper.block.LidMode getLidMode() {
++ return apiLidMode;
++ }
++ public void setLidMode(final io.papermc.paper.block.LidMode targetLidMode) {
++ apiLidMode = targetLidMode;
++ }
++ public io.papermc.paper.block.LidState getEffectiveLidState() {
++ return switch (apiLidMode) {
++ case OPEN_UNTIL_VIEWED, FORCED_OPEN -> io.papermc.paper.block.LidState.OPEN;
++ case CLOSED_UNTIL_NOT_VIEWED, FORCED_CLOSED -> io.papermc.paper.block.LidState.CLOSED;
++ default -> getTrueLidState();
++ };
++ }
++ public io.papermc.paper.block.LidState getTrueLidState() {
++ boolean virtualViewerPresent = (apiLidMode == io.papermc.paper.block.LidMode.FORCED_OPEN || apiLidMode == io.papermc.paper.block.LidMode.OPEN_UNTIL_VIEWED);
++ int trueOpenCount = this.openCount - (virtualViewerPresent ? 1 : 0);
++ if (trueOpenCount < 0) {
++ throw new IllegalStateException("trueOpenCount is negative: " + trueOpenCount + " openCount: " + openCount + " virtualViewerPresent: " + virtualViewerPresent);
++ }
++ return trueOpenCount > 0 ? io.papermc.paper.block.LidState.OPEN : io.papermc.paper.block.LidState.CLOSED;
++ }
++ // Paper end - add Improved Lidded API
++
++ public void incrementOpeners(@javax.annotation.Nullable Player player, Level level, BlockPos pos, BlockState state) { // Paper - make player nullable for New Lidded API
++ // Paper start - add Improved Lidded API
++ if (this.openCount == 0 && apiLidMode == io.papermc.paper.block.LidMode.CLOSED_UNTIL_NOT_VIEWED) {
++ apiLidMode = io.papermc.paper.block.LidMode.DEFAULT;
++ stopForceLiddedLidClose(level, pos, state);
++ }
++ // Paper end - add Improved Lidded API
++
+ int oldPower = Math.max(0, Math.min(15, this.openCount)); // CraftBukkit - Get power before new viewer is added
int i = this.openCount++;
++ if (apiLidMode == io.papermc.paper.block.LidMode.FORCED_CLOSED || apiLidMode == io.papermc.paper.block.LidMode.CLOSED_UNTIL_NOT_VIEWED) return; // Paper - add improved Lidded API
+
+ // CraftBukkit start - Call redstone event
+ if (level.getBlockState(pos).is(net.minecraft.world.level.block.Blocks.TRAPPED_CHEST)) {
@@ -45,14 +104,38 @@
if (i == 0) {
this.onOpen(level, pos, state);
level.gameEvent(player, GameEvent.CONTAINER_OPEN, pos);
-@@ -35,7 +_,20 @@
+@@ -31,11 +_,43 @@
+ }
+
+ this.openerCountChanged(level, pos, state, i, this.openCount);
++ if (player != null) // Paper - make player nullable for improved Lidded API
+ this.maxInteractionRange = Math.max(player.blockInteractionRange(), this.maxInteractionRange);
++
++ // Paper start - add Improved Lidded API
++ if (player != null && apiLidMode == io.papermc.paper.block.LidMode.OPEN_UNTIL_VIEWED) {
++ // reset to default
++ apiLidMode = io.papermc.paper.block.LidMode.DEFAULT;
++ stopForceLiddedLidOpen(level, pos, state);
++ }
++ // Paper end - add Improved Lidded API
}
- public void decrementOpeners(Player player, Level level, BlockPos pos, BlockState state) {
+- public void decrementOpeners(Player player, Level level, BlockPos pos, BlockState state) {
++ public void decrementOpeners(@javax.annotation.Nullable Player player, Level level, BlockPos pos, BlockState state) { // Paper - make player nullable for New Lidded API
+ int oldPower = Math.max(0, Math.min(15, this.openCount)); // CraftBukkit - Get power before new viewer is added
+ if (this.openCount == 0) return; // Paper - Prevent ContainerOpenersCounter openCount from going negative
int i = this.openCount--;
+
++ // Paper start - add Improved Lidded API
++ if (apiLidMode == io.papermc.paper.block.LidMode.FORCED_CLOSED || apiLidMode == io.papermc.paper.block.LidMode.CLOSED_UNTIL_NOT_VIEWED) {
++ if (this.openCount == 0 && apiLidMode == io.papermc.paper.block.LidMode.CLOSED_UNTIL_NOT_VIEWED) {
++ apiLidMode = io.papermc.paper.block.LidMode.DEFAULT;
++ stopForceLiddedLidClose(level, pos, state);
++ }
++ return;
++ }
++ // Paper end - add Improved Lidded API
++
+ // CraftBukkit start - Call redstone event
+ if (level.getBlockState(pos).is(net.minecraft.world.level.block.Blocks.TRAPPED_CHEST)) {
+ int newPower = Math.max(0, Math.min(15, this.openCount));
@@ -66,11 +149,20 @@
if (this.openCount == 0) {
this.onClose(level, pos, state);
level.gameEvent(player, GameEvent.CONTAINER_CLOSE, pos);
-@@ -60,6 +_,7 @@
+@@ -59,8 +_,14 @@
+ this.maxInteractionRange = Math.max(player.blockInteractionRange(), this.maxInteractionRange);
}
- int size = playersWithContainerOpen.size();
-+ if (this.opened) size++; // CraftBukkit - add dummy count from API
- int i = this.openCount;
+- int size = playersWithContainerOpen.size();
+- int i = this.openCount;
++ // Paper Start - Replace with add Improved Lidded API
++ boolean forceClosed = apiLidMode == io.papermc.paper.block.LidMode.CLOSED_UNTIL_NOT_VIEWED || apiLidMode == io.papermc.paper.block.LidMode.FORCED_CLOSED;
++ boolean forceOpened = apiLidMode == io.papermc.paper.block.LidMode.OPEN_UNTIL_VIEWED || apiLidMode == io.papermc.paper.block.LidMode.FORCED_OPEN;
++ int size = forceClosed ? 0 : playersWithContainerOpen.size() + (forceOpened ? 1 : 0);
++ // if (this.opened) size++; // CraftBukkit - add dummy count from API
++ int i = forceClosed ? 0 : this.openCount;
++ // Paper End - Replace with add Improved Lidded API
++
if (i != size) {
boolean flag = size != 0;
+ boolean flag1 = i != 0;
diff --git a/paper-server/patches/sources/net/minecraft/world/level/block/entity/ShulkerBoxBlockEntity.java.patch b/paper-server/patches/sources/net/minecraft/world/level/block/entity/ShulkerBoxBlockEntity.java.patch
index dba115b265..8350411188 100644
--- a/paper-server/patches/sources/net/minecraft/world/level/block/entity/ShulkerBoxBlockEntity.java.patch
+++ b/paper-server/patches/sources/net/minecraft/world/level/block/entity/ShulkerBoxBlockEntity.java.patch
@@ -7,7 +7,7 @@
+ // CraftBukkit start - add fields and methods
+ public List transaction = new java.util.ArrayList<>();
+ private int maxStack = MAX_STACK;
-+ public boolean opened;
++ // public boolean opened; // Paper - replace with new Lidded API
+
+ public List getContents() {
+ return this.itemStacks;
@@ -38,19 +38,140 @@
public ShulkerBoxBlockEntity(@Nullable DyeColor color, BlockPos pos, BlockState blockState) {
super(BlockEntityType.SHULKER_BOX, pos, blockState);
this.color = color;
-@@ -167,6 +_,7 @@
+@@ -139,6 +_,7 @@
+ @Override
+ public boolean triggerEvent(int id, int type) {
+ if (id == 1) {
++ if (apiLidMode != io.papermc.paper.block.LidMode.FORCED_CLOSED && apiLidMode != io.papermc.paper.block.LidMode.CLOSED_UNTIL_NOT_VIEWED) this.openCount = type; // Paper - Skip mutate when forced closed by lidded api
+ this.openCount = type;
+ if (type == 0) {
+ this.animationStatus = ShulkerBoxBlockEntity.AnimationStatus.CLOSING;
+@@ -159,6 +_,71 @@
+ level.updateNeighborsAt(pos, state.getBlock());
+ }
+
++ // Paper start - add Improved Lidded API
++ private io.papermc.paper.block.LidMode apiLidMode = io.papermc.paper.block.LidMode.DEFAULT;
++
++ public void startForceLiddedLidOpen() {
++ this.openCount++;
++ this.level.blockEvent(this.worldPosition, this.getBlockState().getBlock(), 1, this.openCount);
++ if (this.openCount == 1) {
++ this.level.gameEvent(null, GameEvent.CONTAINER_OPEN, this.worldPosition);
++ this.level.playSound(null, this.worldPosition, SoundEvents.SHULKER_BOX_OPEN, SoundSource.BLOCKS, 0.5F, this.level.random.nextFloat() * 0.1F + 0.9F);
++ }
++ }
++
++ public void stopForceLiddedLidOpen() {
++ this.openCount--;
++ this.level.blockEvent(this.worldPosition, this.getBlockState().getBlock(), 1, this.openCount);
++ if (this.openCount <= 0) {
++ this.level.gameEvent(null, GameEvent.CONTAINER_CLOSE, this.worldPosition);
++ this.level.playSound(null, this.worldPosition, SoundEvents.SHULKER_BOX_CLOSE, SoundSource.BLOCKS, 0.5F, this.level.random.nextFloat() * 0.1F + 0.9F);
++ }
++ apiLidMode = io.papermc.paper.block.LidMode.DEFAULT;
++ }
++
++ public void startForceLiddedLidClose() {
++ if (this.getTrueLidState() == io.papermc.paper.block.LidState.OPEN) {
++ this.level.gameEvent(null, GameEvent.CONTAINER_CLOSE, this.worldPosition);
++ this.level.playSound(null, this.worldPosition, SoundEvents.SHULKER_BOX_CLOSE, SoundSource.BLOCKS, 0.5F, this.level.random.nextFloat() * 0.1F + 0.9F);
++ }
++ this.level.blockEvent(this.worldPosition, this.getBlockState().getBlock(), 1, 0);
++ }
++
++ public void stopForceLiddedLidClose() {
++ if (this.getTrueLidState() == io.papermc.paper.block.LidState.OPEN) {
++ this.level.gameEvent(null, GameEvent.CONTAINER_OPEN, this.worldPosition);
++ this.level.playSound(null, this.worldPosition, SoundEvents.SHULKER_BOX_OPEN, SoundSource.BLOCKS, 0.5F, this.level.random.nextFloat() * 0.1F + 0.9F);
++ }
++ this.level.blockEvent(this.worldPosition, this.getBlockState().getBlock(), 1, this.openCount);
++ }
++
++ public io.papermc.paper.block.LidMode getLidMode() {
++ return apiLidMode;
++ }
++
++ public void setLidMode(final io.papermc.paper.block.LidMode lidMode) {
++ this.apiLidMode = lidMode;
++ }
++
++ public io.papermc.paper.block.LidState getEffectiveLidState() {
++ return switch (apiLidMode) {
++ case OPEN_UNTIL_VIEWED, FORCED_OPEN -> io.papermc.paper.block.LidState.OPEN;
++ case CLOSED_UNTIL_NOT_VIEWED, FORCED_CLOSED -> io.papermc.paper.block.LidState.CLOSED;
++ default -> getTrueLidState();
++ };
++ }
++
++ public io.papermc.paper.block.LidState getTrueLidState() {
++ boolean virtualViewerPresent = (apiLidMode == io.papermc.paper.block.LidMode.FORCED_OPEN || apiLidMode == io.papermc.paper.block.LidMode.OPEN_UNTIL_VIEWED);
++ int trueOpenCount = this.openCount - (virtualViewerPresent ? 1 : 0);
++ // ensure trueOpenCount is never negative, throw
++ if (trueOpenCount < 0) {
++ throw new IllegalStateException("trueOpenCount is negative: " + trueOpenCount + " openCount: " + openCount + " virtualViewerPresent: " + virtualViewerPresent);
++ }
++ return trueOpenCount > 0 ? io.papermc.paper.block.LidState.OPEN : io.papermc.paper.block.LidState.CLOSED;
++ }
++ // Paper end - add Improved Lidded API
++
+ @Override
+ public void startOpen(Player player) {
+ if (!this.remove && !player.isSpectator()) {
+@@ -166,13 +_,36 @@
+ this.openCount = 0;
}
++ // Paper start - add Improved Lidded API
++ if (this.openCount == 0) {
++ if (apiLidMode == io.papermc.paper.block.LidMode.CLOSED_UNTIL_NOT_VIEWED) {
++ apiLidMode = io.papermc.paper.block.LidMode.DEFAULT;
++ stopForceLiddedLidClose();
++ }
++ }
++ // Paper end - add Improved Lidded API
++
this.openCount++;
-+ if (this.opened) return; // CraftBukkit - only animate if the ShulkerBox hasn't been forced open already by an API call
++
++ // Paper start - replace with Improved Lidded API
++ // if (this.opened) return; // CraftBukkit - only animate if the ShulkerBox hasn't been forced open already by an API call
++ if (this.apiLidMode == io.papermc.paper.block.LidMode.FORCED_CLOSED || this.apiLidMode == io.papermc.paper.block.LidMode.CLOSED_UNTIL_NOT_VIEWED) return;
++ // Paper end - replace with Improved Lidded API
++
this.level.blockEvent(this.worldPosition, this.getBlockState().getBlock(), 1, this.openCount);
if (this.openCount == 1) {
this.level.gameEvent(player, GameEvent.CONTAINER_OPEN, this.worldPosition);
-@@ -180,6 +_,7 @@
+ this.level
+ .playSound(null, this.worldPosition, SoundEvents.SHULKER_BOX_OPEN, SoundSource.BLOCKS, 0.5F, this.level.random.nextFloat() * 0.1F + 0.9F);
+ }
++
++ // Paper start - add Improved Lidded API
++ if (apiLidMode == io.papermc.paper.block.LidMode.OPEN_UNTIL_VIEWED) {
++ // reset to default
++ apiLidMode = io.papermc.paper.block.LidMode.DEFAULT;
++ stopForceLiddedLidOpen();
++ }
++ // Paper end - add Improved Lidded API
+ }
+ }
+
+@@ -180,6 +_,19 @@
public void stopOpen(Player player) {
if (!this.remove && !player.isSpectator()) {
this.openCount--;
-+ if (this.opened) return; // CraftBukkit - only animate if the ShulkerBox hasn't been forced open already by an API call.
++
++ // Paper start - add Improved Lidded API
++ // if (this.opened) return; // CraftBukkit - only animate if the ShulkerBox hasn't been forced open already by an API call.
++ if (this.apiLidMode == io.papermc.paper.block.LidMode.FORCED_CLOSED || this.apiLidMode == io.papermc.paper.block.LidMode.CLOSED_UNTIL_NOT_VIEWED) {
++ if (this.openCount <= 0 && this.apiLidMode == io.papermc.paper.block.LidMode.CLOSED_UNTIL_NOT_VIEWED) {
++ this.openCount = 0;
++ this.apiLidMode = io.papermc.paper.block.LidMode.DEFAULT;
++ this.stopForceLiddedLidClose();
++ }
++ return;
++ }
++ // Paper end - add Improved Lidded API
++
this.level.blockEvent(this.worldPosition, this.getBlockState().getBlock(), 1, this.openCount);
if (this.openCount <= 0) {
this.level.gameEvent(player, GameEvent.CONTAINER_CLOSE, this.worldPosition);
diff --git a/paper-server/src/main/java/io/papermc/paper/block/PaperLidded.java b/paper-server/src/main/java/io/papermc/paper/block/PaperLidded.java
new file mode 100644
index 0000000000..56596a8dbd
--- /dev/null
+++ b/paper-server/src/main/java/io/papermc/paper/block/PaperLidded.java
@@ -0,0 +1,87 @@
+package io.papermc.paper.block;
+
+import org.jspecify.annotations.NullMarked;
+
+@NullMarked
+public interface PaperLidded extends Lidded, org.bukkit.block.Lidded {
+
+ @Override
+ default LidMode setLidMode(final LidMode targetLidMode) {
+ final LidMode oldLidMode = getLidMode();
+ final LidMode newLidMode = getResultantLidMode(targetLidMode);
+
+ if (oldLidMode == newLidMode) {
+ // already in correct state
+ return newLidMode;
+ }
+
+ boolean wasForcedOpen =
+ oldLidMode == LidMode.FORCED_OPEN || oldLidMode == LidMode.OPEN_UNTIL_VIEWED;
+ boolean wasForcedClosed =
+ oldLidMode == LidMode.FORCED_CLOSED || oldLidMode == LidMode.CLOSED_UNTIL_NOT_VIEWED;
+ boolean isForcedOpen =
+ newLidMode == LidMode.FORCED_OPEN || newLidMode == LidMode.OPEN_UNTIL_VIEWED;
+ boolean isForcedClosed =
+ newLidMode == LidMode.FORCED_CLOSED || newLidMode == LidMode.CLOSED_UNTIL_NOT_VIEWED;
+
+ // stop any existing force open/close, if next state doesn't need it.
+ if (wasForcedOpen && !isForcedOpen) {
+ stopForceLiddedLidOpen();
+ } else if (wasForcedClosed && !isForcedClosed) {
+ stopForceLiddedLidClose();
+ }
+
+ // start new force open/close, if it wasn't previously.
+ if (isForcedOpen && !wasForcedOpen) {
+ startForceLiddedLidOpen();
+ } else if (isForcedClosed && !wasForcedClosed) {
+ startForceLiddedLidClose();
+ }
+
+ // return the new lid mode, so it can be stored by the implementation.
+ return newLidMode;
+ }
+
+ private LidMode getResultantLidMode(LidMode targetLidMode) {
+ final LidState trueLidState = getTrueLidState();
+
+ // check that target lid mode is valid for true lid state.
+ LidMode newLidMode;
+
+ if (targetLidMode == LidMode.CLOSED_UNTIL_NOT_VIEWED
+ && trueLidState == LidState.CLOSED) {
+ // insta-revert to default, as the lid is already closed.
+ newLidMode = LidMode.DEFAULT;
+ } else if (targetLidMode == LidMode.OPEN_UNTIL_VIEWED
+ && trueLidState == LidState.OPEN) {
+ // insta-revert to default, as the lid is already open.
+ newLidMode = LidMode.DEFAULT;
+ } else {
+ newLidMode = targetLidMode;
+ }
+ return newLidMode;
+ }
+
+ // these should be similar to the vanilla open/close behavior.
+ void startForceLiddedLidOpen();
+ void stopForceLiddedLidOpen();
+ void startForceLiddedLidClose();
+ void stopForceLiddedLidClose();
+
+ // bukkit lidded impl using the paper lidded api.
+
+ @Override
+ default boolean isOpen() {
+ return getLidMode() == LidMode.FORCED_OPEN || getLidMode() == LidMode.OPEN_UNTIL_VIEWED;
+ }
+
+ @Override
+ default void close() {
+ setLidMode(LidMode.DEFAULT);
+ }
+
+ @Override
+ default void open() {
+ setLidMode(LidMode.FORCED_OPEN);
+ }
+}
diff --git a/paper-server/src/main/java/org/bukkit/craftbukkit/block/CraftBarrel.java b/paper-server/src/main/java/org/bukkit/craftbukkit/block/CraftBarrel.java
index 6063f0e1fd..297605ac38 100644
--- a/paper-server/src/main/java/org/bukkit/craftbukkit/block/CraftBarrel.java
+++ b/paper-server/src/main/java/org/bukkit/craftbukkit/block/CraftBarrel.java
@@ -9,8 +9,9 @@ import org.bukkit.World;
import org.bukkit.block.Barrel;
import org.bukkit.craftbukkit.inventory.CraftInventory;
import org.bukkit.inventory.Inventory;
+import org.jetbrains.annotations.NotNull;
-public class CraftBarrel extends CraftLootable implements Barrel {
+public class CraftBarrel extends CraftLootable implements Barrel, io.papermc.paper.block.PaperLidded {
public CraftBarrel(World world, BarrelBlockEntity tileEntity) {
super(world, tileEntity);
@@ -34,36 +35,6 @@ public class CraftBarrel extends CraftLootable implements Bar
return new CraftInventory(this.getTileEntity());
}
- @Override
- public void open() {
- this.requirePlaced();
- if (!this.getTileEntity().openersCounter.opened) {
- BlockState blockData = this.getTileEntity().getBlockState();
- boolean open = blockData.getValue(BarrelBlock.OPEN);
-
- if (!open) {
- this.getTileEntity().updateBlockState(blockData, true);
- if (this.getWorldHandle() instanceof net.minecraft.world.level.Level) {
- this.getTileEntity().playSound(blockData, SoundEvents.BARREL_OPEN);
- }
- }
- }
- this.getTileEntity().openersCounter.opened = true;
- }
-
- @Override
- public void close() {
- this.requirePlaced();
- if (this.getTileEntity().openersCounter.opened) {
- BlockState blockData = this.getTileEntity().getBlockState();
- this.getTileEntity().updateBlockState(blockData, false);
- if (this.getWorldHandle() instanceof net.minecraft.world.level.Level) {
- this.getTileEntity().playSound(blockData, SoundEvents.BARREL_CLOSE);
- }
- }
- this.getTileEntity().openersCounter.opened = false;
- }
-
@Override
public CraftBarrel copy() {
return new CraftBarrel(this, null);
@@ -74,10 +45,54 @@ public class CraftBarrel extends CraftLootable implements Bar
return new CraftBarrel(this, location);
}
- // Paper start - More Lidded Block API
@Override
- public boolean isOpen() {
- return getTileEntity().openersCounter.opened;
+ public void startForceLiddedLidOpen() {
+ this.requirePlaced();
+ this.getTileEntity().openersCounter.startForceLiddedLidOpen(this.getTileEntity().getLevel(), this.getTileEntity().getBlockPos(), this.getTileEntity().getBlockState());
}
- // Paper end - More Lidded Block API
+
+ @Override
+ public void stopForceLiddedLidOpen() {
+ this.requirePlaced();
+ this.getTileEntity().openersCounter.stopForceLiddedLidOpen(this.getTileEntity().getLevel(), this.getTileEntity().getBlockPos(), this.getTileEntity().getBlockState());
+ }
+
+ @Override
+ public void startForceLiddedLidClose() {
+ this.requirePlaced();
+ this.getTileEntity().openersCounter.startForceLiddedLidClose(this.getTileEntity().getLevel(), this.getTileEntity().getBlockPos(), this.getTileEntity().getBlockState());
+ }
+
+ @Override
+ public void stopForceLiddedLidClose() {
+ this.requirePlaced();
+ this.getTileEntity().openersCounter.stopForceLiddedLidClose(this.getTileEntity().getLevel(), this.getTileEntity().getBlockPos(), this.getTileEntity().getBlockState());
+ }
+
+ @Override
+ public io.papermc.paper.block.@NotNull LidState getEffectiveLidState() {
+ this.requirePlaced();
+ return this.getTileEntity().openersCounter.getEffectiveLidState();
+ }
+
+ @Override
+ public io.papermc.paper.block.@NotNull LidState getTrueLidState() {
+ this.requirePlaced();
+ return this.getTileEntity().openersCounter.getTrueLidState();
+ }
+
+ @Override
+ public io.papermc.paper.block.@NotNull LidMode getLidMode() {
+ this.requirePlaced();
+ return this.getTileEntity().openersCounter.getLidMode();
+ }
+
+ @Override
+ public io.papermc.paper.block.@NotNull LidMode setLidMode(final io.papermc.paper.block.@NotNull LidMode targetLidMode) {
+ this.requirePlaced();
+ io.papermc.paper.block.LidMode newEffectiveMode = io.papermc.paper.block.PaperLidded.super.setLidMode(targetLidMode);
+ this.getTileEntity().openersCounter.setLidMode(newEffectiveMode);
+ return newEffectiveMode;
+ }
+
}
diff --git a/paper-server/src/main/java/org/bukkit/craftbukkit/block/CraftChest.java b/paper-server/src/main/java/org/bukkit/craftbukkit/block/CraftChest.java
index cc7bf4d39b..9ec7d528cb 100644
--- a/paper-server/src/main/java/org/bukkit/craftbukkit/block/CraftChest.java
+++ b/paper-server/src/main/java/org/bukkit/craftbukkit/block/CraftChest.java
@@ -13,8 +13,9 @@ import org.bukkit.craftbukkit.CraftWorld;
import org.bukkit.craftbukkit.inventory.CraftInventory;
import org.bukkit.craftbukkit.inventory.CraftInventoryDoubleChest;
import org.bukkit.inventory.Inventory;
+import org.jetbrains.annotations.NotNull;
-public class CraftChest extends CraftLootable implements Chest {
+public class CraftChest extends CraftLootable implements Chest, io.papermc.paper.block.PaperLidded {
public CraftChest(World world, ChestBlockEntity tileEntity) {
super(world, tileEntity);
@@ -57,32 +58,6 @@ public class CraftChest extends CraftLootable implements Chest
return inventory;
}
- @Override
- public void open() {
- this.requirePlaced();
- if (!this.getTileEntity().openersCounter.opened && this.getWorldHandle() instanceof net.minecraft.world.level.Level) {
- BlockState block = this.getTileEntity().getBlockState();
- int openCount = this.getTileEntity().openersCounter.getOpenerCount();
-
- this.getTileEntity().openersCounter.onAPIOpen((net.minecraft.world.level.Level) this.getWorldHandle(), this.getPosition(), block);
- this.getTileEntity().openersCounter.openerAPICountChanged((net.minecraft.world.level.Level) this.getWorldHandle(), this.getPosition(), block, openCount, openCount + 1);
- }
- this.getTileEntity().openersCounter.opened = true;
- }
-
- @Override
- public void close() {
- this.requirePlaced();
- if (this.getTileEntity().openersCounter.opened && this.getWorldHandle() instanceof net.minecraft.world.level.Level) {
- BlockState block = this.getTileEntity().getBlockState();
- int openCount = this.getTileEntity().openersCounter.getOpenerCount();
-
- this.getTileEntity().openersCounter.onAPIClose((net.minecraft.world.level.Level) this.getWorldHandle(), this.getPosition(), block);
- this.getTileEntity().openersCounter.openerAPICountChanged((net.minecraft.world.level.Level) this.getWorldHandle(), this.getPosition(), block, openCount, 0);
- }
- this.getTileEntity().openersCounter.opened = false;
- }
-
@Override
public CraftChest copy() {
return new CraftChest(this, null);
@@ -93,13 +68,6 @@ public class CraftChest extends CraftLootable implements Chest
return new CraftChest(this, location);
}
- // Paper start - More Lidded Block API
- @Override
- public boolean isOpen() {
- return getTileEntity().openersCounter.opened;
- }
- // Paper end - More Lidded Block API
-
// Paper start - More Chest Block API
@Override
public boolean isBlocked() {
@@ -124,4 +92,54 @@ public class CraftChest extends CraftLootable implements Chest
&& ChestBlock.isChestBlockedAt(world, neighbourBlockPos);
}
// Paper end - More Chest Block API
+
+ @Override
+ public void startForceLiddedLidOpen() {
+ this.requirePlaced();
+ this.getTileEntity().openersCounter.startForceLiddedLidOpen(this.getTileEntity().getLevel(), this.getTileEntity().getBlockPos(), this.getTileEntity().getBlockState());
+ }
+
+ @Override
+ public void stopForceLiddedLidOpen() {
+ this.requirePlaced();
+ this.getTileEntity().openersCounter.stopForceLiddedLidOpen(this.getTileEntity().getLevel(), this.getTileEntity().getBlockPos(), this.getTileEntity().getBlockState());
+ }
+
+ @Override
+ public void startForceLiddedLidClose() {
+ this.requirePlaced();
+ this.getTileEntity().openersCounter.startForceLiddedLidClose(this.getTileEntity().getLevel(), this.getTileEntity().getBlockPos(), this.getTileEntity().getBlockState());
+ }
+
+ @Override
+ public void stopForceLiddedLidClose() {
+ this.requirePlaced();
+ this.getTileEntity().openersCounter.stopForceLiddedLidClose(this.getTileEntity().getLevel(), this.getTileEntity().getBlockPos(), this.getTileEntity().getBlockState());
+ }
+
+ @Override
+ public io.papermc.paper.block.@NotNull LidState getEffectiveLidState() {
+ this.requirePlaced();
+ return this.getTileEntity().openersCounter.getEffectiveLidState();
+ }
+
+ @Override
+ public io.papermc.paper.block.@NotNull LidState getTrueLidState() {
+ this.requirePlaced();
+ return this.getTileEntity().openersCounter.getTrueLidState();
+ }
+
+ @Override
+ public io.papermc.paper.block.@NotNull LidMode getLidMode() {
+ this.requirePlaced();
+ return this.getTileEntity().openersCounter.getLidMode();
+ }
+
+ @Override
+ public io.papermc.paper.block.@NotNull LidMode setLidMode(final io.papermc.paper.block.@NotNull LidMode targetLidMode) {
+ this.requirePlaced();
+ io.papermc.paper.block.LidMode newEffectiveMode = io.papermc.paper.block.PaperLidded.super.setLidMode(targetLidMode);
+ this.getTileEntity().openersCounter.setLidMode(newEffectiveMode);
+ return newEffectiveMode;
+ }
}
diff --git a/paper-server/src/main/java/org/bukkit/craftbukkit/block/CraftEnderChest.java b/paper-server/src/main/java/org/bukkit/craftbukkit/block/CraftEnderChest.java
index f45ee675a1..0c0d3038f0 100644
--- a/paper-server/src/main/java/org/bukkit/craftbukkit/block/CraftEnderChest.java
+++ b/paper-server/src/main/java/org/bukkit/craftbukkit/block/CraftEnderChest.java
@@ -5,8 +5,9 @@ import net.minecraft.world.level.block.state.BlockState;
import org.bukkit.Location;
import org.bukkit.World;
import org.bukkit.block.EnderChest;
+import org.jetbrains.annotations.NotNull;
-public class CraftEnderChest extends CraftBlockEntityState implements EnderChest {
+public class CraftEnderChest extends CraftBlockEntityState implements EnderChest, io.papermc.paper.block.PaperLidded {
public CraftEnderChest(World world, EnderChestBlockEntity tileEntity) {
super(world, tileEntity);
@@ -16,32 +17,6 @@ public class CraftEnderChest extends CraftBlockEntityState implements ShulkerBox {
+public class CraftShulkerBox extends CraftLootable implements ShulkerBox, io.papermc.paper.block.PaperLidded {
public CraftShulkerBox(World world, ShulkerBoxBlockEntity tileEntity) {
super(world, tileEntity);
@@ -42,28 +43,6 @@ public class CraftShulkerBox extends CraftLootable implem
return (color == null) ? null : DyeColor.getByWoolData((byte) color.getId());
}
- @Override
- public void open() {
- this.requirePlaced();
- if (!this.getTileEntity().opened && this.getWorldHandle() instanceof net.minecraft.world.level.Level) {
- net.minecraft.world.level.Level world = this.getTileEntity().getLevel();
- world.blockEvent(this.getPosition(), this.getTileEntity().getBlockState().getBlock(), 1, 1);
- world.playSound(null, this.getPosition(), SoundEvents.SHULKER_BOX_OPEN, SoundSource.BLOCKS, 0.5F, world.random.nextFloat() * 0.1F + 0.9F);
- }
- this.getTileEntity().opened = true;
- }
-
- @Override
- public void close() {
- this.requirePlaced();
- if (this.getTileEntity().opened && this.getWorldHandle() instanceof net.minecraft.world.level.Level) {
- net.minecraft.world.level.Level world = this.getTileEntity().getLevel();
- world.blockEvent(this.getPosition(), this.getTileEntity().getBlockState().getBlock(), 1, 0);
- world.playSound(null, this.getPosition(), SoundEvents.SHULKER_BOX_CLOSE, SoundSource.BLOCKS, 0.5F, world.random.nextFloat() * 0.1F + 0.9F); // Paper - More Lidded Block API (Wrong sound)
- }
- this.getTileEntity().opened = false;
- }
-
@Override
public CraftShulkerBox copy() {
return new CraftShulkerBox(this, null);
@@ -74,10 +53,53 @@ public class CraftShulkerBox extends CraftLootable implem
return new CraftShulkerBox(this, location);
}
- // Paper start - More Lidded Block API
@Override
- public boolean isOpen() {
- return getTileEntity().opened;
+ public void startForceLiddedLidOpen() {
+ this.requirePlaced();
+ this.getTileEntity().startForceLiddedLidOpen();
+ }
+
+ @Override
+ public void stopForceLiddedLidOpen() {
+ this.requirePlaced();
+ this.getTileEntity().stopForceLiddedLidOpen();
+ }
+
+ @Override
+ public void startForceLiddedLidClose() {
+ this.requirePlaced();
+ this.getTileEntity().startForceLiddedLidClose();
+ }
+
+ @Override
+ public void stopForceLiddedLidClose() {
+ this.requirePlaced();
+ this.getTileEntity().stopForceLiddedLidClose();
+ }
+
+ @Override
+ public io.papermc.paper.block.@NotNull LidState getEffectiveLidState() {
+ this.requirePlaced();
+ return this.getTileEntity().getEffectiveLidState();
+ }
+
+ @Override
+ public io.papermc.paper.block.@NotNull LidState getTrueLidState() {
+ this.requirePlaced();
+ return this.getTileEntity().getTrueLidState();
+ }
+
+ @Override
+ public io.papermc.paper.block.@NotNull LidMode getLidMode() {
+ this.requirePlaced();
+ return this.getTileEntity().getLidMode();
+ }
+
+ @Override
+ public io.papermc.paper.block.@NotNull LidMode setLidMode(final io.papermc.paper.block.@NotNull LidMode targetLidMode) {
+ this.requirePlaced();
+ io.papermc.paper.block.LidMode newEffectiveMode = io.papermc.paper.block.PaperLidded.super.setLidMode(targetLidMode);
+ this.getTileEntity().setLidMode(newEffectiveMode);
+ return newEffectiveMode;
}
- // Paper end - More Lidded Block API
}