diff --git a/paper-api/src/main/java/io/papermc/paper/event/player/PlayerLiddedOpenEvent.java b/paper-api/src/main/java/io/papermc/paper/event/player/PlayerLiddedOpenEvent.java
new file mode 100644
index 0000000000..3dbb9c1c17
--- /dev/null
+++ b/paper-api/src/main/java/io/papermc/paper/event/player/PlayerLiddedOpenEvent.java
@@ -0,0 +1,89 @@
+package io.papermc.paper.event.player;
+
+import io.papermc.paper.block.LidMode;
+import io.papermc.paper.block.LidState;
+import io.papermc.paper.block.Lidded;
+import org.bukkit.block.Block;
+import org.bukkit.entity.Player;
+import org.bukkit.event.Cancellable;
+import org.bukkit.event.HandlerList;
+import org.bukkit.event.player.PlayerEvent;
+import org.jetbrains.annotations.ApiStatus;
+import org.jetbrains.annotations.NotNull;
+import org.jspecify.annotations.NullMarked;
+
+/**
+ * Called when a player opens a {@link Lidded} block.
+ *
+ *
+ * This is called every time a player opens a {@link Lidded} block
+ * regardless of if the lid is already open (e.g. multiple players).
+ *
+ * Cancelling this event prevents the player from being considered in other {@link Lidded} methods:
+ * they will not contribute to the {@link Lidded#getTrueLidState()} and {@link Lidded#getEffectiveLidState()}.
+ *
+ * This event is called twice for double chests, once for each half.
+ */
+@NullMarked
+public class PlayerLiddedOpenEvent extends PlayerEvent implements Cancellable {
+
+ private static final HandlerList HANDLER_LIST = new HandlerList();
+ private final Lidded blockState;
+ private final Block block;
+ private boolean cancelled;
+
+ @ApiStatus.Internal
+ public PlayerLiddedOpenEvent(final @NotNull Player who, final @NotNull Lidded blockState, final @NotNull Block block) {
+ super(who);
+ this.cancelled = false;
+ this.blockState = blockState;
+ this.block = block;
+ }
+
+
+ @Override
+ public boolean isCancelled() {
+ return cancelled;
+ }
+
+ @Override
+ public void setCancelled(final boolean cancel) {
+ this.cancelled = cancel;
+ }
+
+ /**
+ * Gets the {@link Lidded} block involved in this event.
+ * @return the lidded block
+ */
+ @NotNull
+ public Lidded getLidded() {
+ return blockState;
+ }
+
+ /**
+ * Gets the block involved in this event.
+ * @return the block
+ */
+ @NotNull
+ public Block getBlock() {
+ return block;
+ }
+
+ /**
+ * Gets if the block would appear to open, if this event is not cancelled.
+ * return if the block would appear to open
+ */
+ public boolean isOpening() {
+ return blockState.getLidMode() == LidMode.DEFAULT && blockState.getTrueLidState() == LidState.CLOSED;
+ }
+
+ @Override
+ @NotNull
+ public HandlerList getHandlers() {
+ return HANDLER_LIST;
+ }
+
+ public static @NotNull HandlerList getHandlerList() {
+ return HANDLER_LIST;
+ }
+}
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 55c2d9d02a..759bb0beae 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
@@ -8,7 +8,7 @@
protected abstract void onOpen(Level level, BlockPos pos, BlockState state);
-@@ -20,10 +_,94 @@
+@@ -20,10 +_,109 @@
protected abstract void openerCountChanged(Level level, BlockPos pos, BlockState state, int count, int openCount);
@@ -33,13 +33,17 @@
- 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;
++ private final java.util.Set cancelledPlayers = new java.util.HashSet<>(); // Paper - store players whose opening was cancelled by PlayerLiddedOpenEvent
++
+ 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);
@@ -47,6 +51,7 @@
+ }
+ 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);
@@ -56,12 +61,15 @@
+ 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;
@@ -69,6 +77,7 @@
+ 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);
@@ -80,6 +89,12 @@
+ // 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 - Call PlayerLiddedOpenEvent
++ if (player != null && !org.bukkit.craftbukkit.event.CraftEventFactory.callPlayerLiddedOpenEvent(player, level, pos)) {
++ cancelledPlayers.add(player);
++ return;
++ }
++ // Paper end - Call PlayerLiddedOpenEvent
+ // 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;
@@ -104,7 +119,7 @@
if (i == 0) {
this.onOpen(level, pos, state);
level.gameEvent(player, GameEvent.CONTAINER_OPEN, pos);
-@@ -31,11 +_,43 @@
+@@ -31,11 +_,44 @@
}
this.openerCountChanged(level, pos, state, i, this.openCount);
@@ -122,6 +137,7 @@
- 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
++ if (player != null && cancelledPlayers.remove(player)) return; // Paper - do not decrement if player's opening was cancelled by PlayerLiddedOpenEvent
+ 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--;
@@ -149,7 +165,17 @@
if (this.openCount == 0) {
this.onClose(level, pos, state);
level.gameEvent(player, GameEvent.CONTAINER_CLOSE, pos);
-@@ -59,8 +_,14 @@
+@@ -53,14 +_,24 @@
+
+ public void recheckOpeners(Level level, BlockPos pos, BlockState state) {
+ List playersWithContainerOpen = this.getPlayersWithContainerOpen(level, pos);
++ // Paper start - maintain cancelledPlayers, list of players with the chest open, but without the lid.
++ cancelledPlayers.removeIf(java.util.function.Predicate.not(playersWithContainerOpen::contains));
++ playersWithContainerOpen.removeIf(cancelledPlayers::contains);
++ // Paper end - maintain cancelledPlayers, list of players with the chest open, but without the lid.
+ this.maxInteractionRange = 0.0;
+
+ for (Player player : playersWithContainerOpen) {
this.maxInteractionRange = Math.max(player.blockInteractionRange(), this.maxInteractionRange);
}
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 8350411188..1481778f3b 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
@@ -46,12 +46,13 @@
this.openCount = type;
if (type == 0) {
this.animationStatus = ShulkerBoxBlockEntity.AnimationStatus.CLOSING;
-@@ -159,6 +_,71 @@
+@@ -159,6 +_,72 @@
level.updateNeighborsAt(pos, state.getBlock());
}
+ // Paper start - add Improved Lidded API
+ private io.papermc.paper.block.LidMode apiLidMode = io.papermc.paper.block.LidMode.DEFAULT;
++ private final java.util.Set cancelledPlayers = new java.util.HashSet<>(); // Paper - store players whose opening was cancelled by PlayerLiddedOpenEvent
+
+ public void startForceLiddedLidOpen() {
+ this.openCount++;
@@ -118,10 +119,16 @@
@Override
public void startOpen(Player player) {
if (!this.remove && !player.isSpectator()) {
-@@ -166,13 +_,36 @@
+@@ -166,20 +_,63 @@
this.openCount = 0;
}
++ // Paper start - Call PlayerLiddedOpenEvent
++ if (!org.bukkit.craftbukkit.event.CraftEventFactory.callPlayerLiddedOpenEvent(player, this.level, this.worldPosition)) {
++ cancelledPlayers.add(player);
++ return;
++ }
++ // Paper end - Call PlayerLiddedOpenEvent
+ // Paper start - add Improved Lidded API
+ if (this.openCount == 0) {
+ if (apiLidMode == io.papermc.paper.block.LidMode.CLOSED_UNTIL_NOT_VIEWED) {
@@ -155,9 +162,10 @@
}
}
-@@ -180,6 +_,19 @@
+ @Override
public void stopOpen(Player player) {
if (!this.remove && !player.isSpectator()) {
++ if (cancelledPlayers.remove(player)) return; // Paper - do not decrement if player's opening was cancelled by PlayerLiddedOpenEvent
this.openCount--;
+
+ // Paper start - add Improved Lidded API
diff --git a/paper-server/src/main/java/org/bukkit/craftbukkit/event/CraftEventFactory.java b/paper-server/src/main/java/org/bukkit/craftbukkit/event/CraftEventFactory.java
index e37aaf77f9..e9225e2f52 100644
--- a/paper-server/src/main/java/org/bukkit/craftbukkit/event/CraftEventFactory.java
+++ b/paper-server/src/main/java/org/bukkit/craftbukkit/event/CraftEventFactory.java
@@ -2271,4 +2271,14 @@ public class CraftEventFactory {
return event;
}
// Paper end - add EntityFertilizeEggEvent
+
+ public static boolean callPlayerLiddedOpenEvent(net.minecraft.world.entity.player.Player who, final Level world, final BlockPos pos) {
+ Player player = (Player) who.getBukkitEntity();
+ Block block = CraftBlock.at(world, pos);
+ io.papermc.paper.block.PaperLidded blockState = (io.papermc.paper.block.PaperLidded) CraftBlockStates.getBlockState(block);
+
+ io.papermc.paper.event.player.PlayerLiddedOpenEvent event = new io.papermc.paper.event.player.PlayerLiddedOpenEvent(player, blockState, block);
+
+ return event.callEvent();
+ }
}