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 000000000000..a4f85cf32aad
--- /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 000000000000..b6e86005ce94
--- /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 000000000000..23c97fd9b89a
--- /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/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 000000000000..3dbb9c1c17d7
--- /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-api/src/main/java/org/bukkit/block/Barrel.java b/paper-api/src/main/java/org/bukkit/block/Barrel.java
index d3789b2b7dd7..c714361ba516 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 @@
/**
* 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 5d02f9c938d0..85fb81c77f28 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 @@
/**
* 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 6b66f38e5509..9ec76ad38d67 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 @@
/**
* 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 30c7df0021df..0318ee1a8a6d 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,36 @@
package org.bukkit.block;
+import io.papermc.paper.block.LidMode;
+
+/**
+ * @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(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(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 5dc5318b0a45..23995c48f26b 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 @@
/**
* 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 6d0edb5d1d89..759bb0beaeb6 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,107 @@
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 +_,109 @@
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);
++ // 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) {
++ // 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 onAPIClose(Level level, BlockPos blockPos, BlockState blockState) {
-+ this.onClose(level, blockPos, blockState);
++ public void stopForceLiddedLidOpen(Level level, BlockPos pos, BlockState state) {
++ decrementOpeners(null, level, pos, state);
++ apiLidMode = io.papermc.paper.block.LidMode.DEFAULT;
+ }
+
-+ public void openerAPICountChanged(Level level, BlockPos blockPos, BlockState blockState, int count, int openCount) {
-+ this.openerCountChanged(level, blockPos, blockState, count, openCount);
++ 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);
+ }
-+ // CraftBukkit end
+
- protected abstract boolean isOwnContainer(Player player);
-
- public void incrementOpeners(Player player, Level level, BlockPos pos, BlockState state) {
++ 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 - 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;
++ 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 +119,39 @@
if (i == 0) {
this.onOpen(level, pos, state);
level.gameEvent(player, GameEvent.CONTAINER_OPEN, pos);
-@@ -35,7 +_,20 @@
+@@ -31,11 +_,44 @@
+ }
+
+ 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
++ 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--;
+
++ // 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 +165,30 @@
if (this.openCount == 0) {
this.onClose(level, pos, state);
level.gameEvent(player, GameEvent.CONTAINER_CLOSE, pos);
-@@ -60,6 +_,7 @@
+@@ -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);
}
- 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 dba115b26558..1481778f3b63 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,148 @@
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 +_,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++;
++ 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,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) {
++ 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
+ }
+ }
+
+ @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--;
-+ 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 000000000000..56596a8dbd75
--- /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 6063f0e1fdc2..6e65463f08d7 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
@@ -1,5 +1,8 @@
package org.bukkit.craftbukkit.block;
+import io.papermc.paper.block.LidMode;
+import io.papermc.paper.block.LidState;
+import io.papermc.paper.block.PaperLidded;
import net.minecraft.sounds.SoundEvents;
import net.minecraft.world.level.block.BarrelBlock;
import net.minecraft.world.level.block.entity.BarrelBlockEntity;
@@ -9,8 +12,9 @@
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, PaperLidded {
public CraftBarrel(World world, BarrelBlockEntity tileEntity) {
super(world, tileEntity);
@@ -35,49 +39,63 @@ public Inventory getInventory() {
}
@Override
- public void open() {
+ public CraftBarrel copy() {
+ return new CraftBarrel(this, null);
+ }
+
+ @Override
+ public CraftBarrel copy(Location location) {
+ return new CraftBarrel(this, location);
+ }
+
+ @Override
+ public void startForceLiddedLidOpen() {
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;
+ this.getTileEntity().openersCounter.startForceLiddedLidOpen(this.getTileEntity().getLevel(), this.getTileEntity().getBlockPos(), this.getTileEntity().getBlockState());
}
@Override
- public void close() {
+ public void stopForceLiddedLidOpen() {
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;
+ this.getTileEntity().openersCounter.stopForceLiddedLidOpen(this.getTileEntity().getLevel(), this.getTileEntity().getBlockPos(), this.getTileEntity().getBlockState());
}
@Override
- public CraftBarrel copy() {
- return new CraftBarrel(this, null);
+ public void startForceLiddedLidClose() {
+ this.requirePlaced();
+ this.getTileEntity().openersCounter.startForceLiddedLidClose(this.getTileEntity().getLevel(), this.getTileEntity().getBlockPos(), this.getTileEntity().getBlockState());
}
@Override
- public CraftBarrel copy(Location location) {
- return new CraftBarrel(this, location);
+ public void stopForceLiddedLidClose() {
+ this.requirePlaced();
+ this.getTileEntity().openersCounter.stopForceLiddedLidClose(this.getTileEntity().getLevel(), this.getTileEntity().getBlockPos(), this.getTileEntity().getBlockState());
+ }
+
+ @Override
+ public @NotNull LidState getEffectiveLidState() {
+ this.requirePlaced();
+ return this.getTileEntity().openersCounter.getEffectiveLidState();
+ }
+
+ @Override
+ public @NotNull LidState getTrueLidState() {
+ this.requirePlaced();
+ return this.getTileEntity().openersCounter.getTrueLidState();
+ }
+
+ @Override
+ public @NotNull LidMode getLidMode() {
+ this.requirePlaced();
+ return this.getTileEntity().openersCounter.getLidMode();
}
- // Paper start - More Lidded Block API
@Override
- public boolean isOpen() {
- return getTileEntity().openersCounter.opened;
+ public @NotNull LidMode setLidMode(final @NotNull LidMode targetLidMode) {
+ this.requirePlaced();
+ LidMode newEffectiveMode = PaperLidded.super.setLidMode(targetLidMode);
+ this.getTileEntity().openersCounter.setLidMode(newEffectiveMode);
+ return newEffectiveMode;
}
- // Paper end - More Lidded Block API
+
}
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 cc7bf4d39b83..315b17dad606 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
@@ -1,5 +1,8 @@
package org.bukkit.craftbukkit.block;
+import io.papermc.paper.block.LidMode;
+import io.papermc.paper.block.LidState;
+import io.papermc.paper.block.PaperLidded;
import net.minecraft.world.MenuProvider;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.ChestBlock;
@@ -13,8 +16,9 @@
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, PaperLidded {
public CraftChest(World world, ChestBlockEntity tileEntity) {
super(world, tileEntity);
@@ -57,32 +61,6 @@ public Inventory getInventory() {
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 +71,6 @@ public CraftChest copy(Location location) {
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 +95,54 @@ public boolean isBlocked() {
&& 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 @NotNull LidState getEffectiveLidState() {
+ this.requirePlaced();
+ return this.getTileEntity().openersCounter.getEffectiveLidState();
+ }
+
+ @Override
+ public @NotNull LidState getTrueLidState() {
+ this.requirePlaced();
+ return this.getTileEntity().openersCounter.getTrueLidState();
+ }
+
+ @Override
+ public @NotNull LidMode getLidMode() {
+ this.requirePlaced();
+ return this.getTileEntity().openersCounter.getLidMode();
+ }
+
+ @Override
+ public @NotNull LidMode setLidMode(final @NotNull LidMode targetLidMode) {
+ this.requirePlaced();
+ LidMode newEffectiveMode = 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 f45ee675a107..d61a0b4dcd81 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
@@ -1,12 +1,16 @@
package org.bukkit.craftbukkit.block;
+import io.papermc.paper.block.LidMode;
+import io.papermc.paper.block.LidState;
+import io.papermc.paper.block.PaperLidded;
import net.minecraft.world.level.block.entity.EnderChestBlockEntity;
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, PaperLidded {
public CraftEnderChest(World world, EnderChestBlockEntity tileEntity) {
super(world, tileEntity);
@@ -17,54 +21,71 @@ protected CraftEnderChest(CraftEnderChest state, Location location) {
}
@Override
- public void open() {
+ public CraftEnderChest copy() {
+ return new CraftEnderChest(this, null);
+ }
+
+ @Override
+ public CraftEnderChest copy(Location location) {
+ return new CraftEnderChest(this, location);
+ }
+
+ // Paper start - More Chest Block API
+ @Override
+ public boolean isBlocked() {
+ // Uses the same logic as EnderChestBlock's check for opening container
+ final net.minecraft.core.BlockPos abovePos = this.getPosition().above();
+ return this.isPlaced() && this.getWorldHandle().getBlockState(abovePos).isRedstoneConductor(this.getWorldHandle(), abovePos);
+ }
+ // Paper end - More Chest Block API
+
+ @Override
+ public void startForceLiddedLidOpen() {
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.startForceLiddedLidOpen(this.getTileEntity().getLevel(), this.getTileEntity().getBlockPos(), this.getTileEntity().getBlockState());
+ }
- 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 stopForceLiddedLidOpen() {
+ this.requirePlaced();
+ this.getTileEntity().openersCounter.stopForceLiddedLidOpen(this.getTileEntity().getLevel(), this.getTileEntity().getBlockPos(), this.getTileEntity().getBlockState());
}
@Override
- public void close() {
+ public void startForceLiddedLidClose() {
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.startForceLiddedLidClose(this.getTileEntity().getLevel(), this.getTileEntity().getBlockPos(), this.getTileEntity().getBlockState());
+ }
- 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 void stopForceLiddedLidClose() {
+ this.requirePlaced();
+ this.getTileEntity().openersCounter.stopForceLiddedLidClose(this.getTileEntity().getLevel(), this.getTileEntity().getBlockPos(), this.getTileEntity().getBlockState());
}
@Override
- public CraftEnderChest copy() {
- return new CraftEnderChest(this, null);
+ public @NotNull LidState getEffectiveLidState() {
+ this.requirePlaced();
+ return this.getTileEntity().openersCounter.getEffectiveLidState();
}
@Override
- public CraftEnderChest copy(Location location) {
- return new CraftEnderChest(this, location);
+ public @NotNull LidState getTrueLidState() {
+ this.requirePlaced();
+ return this.getTileEntity().openersCounter.getTrueLidState();
}
- // Paper start - More Lidded Block API
@Override
- public boolean isOpen() {
- return getTileEntity().openersCounter.opened;
+ public @NotNull LidMode getLidMode() {
+ this.requirePlaced();
+ return this.getTileEntity().openersCounter.getLidMode();
}
- // Paper end - More Lidded Block API
- // Paper start - More Chest Block API
@Override
- public boolean isBlocked() {
- // Uses the same logic as EnderChestBlock's check for opening container
- final net.minecraft.core.BlockPos abovePos = this.getPosition().above();
- return this.isPlaced() && this.getWorldHandle().getBlockState(abovePos).isRedstoneConductor(this.getWorldHandle(), abovePos);
+ public @NotNull LidMode setLidMode(final @NotNull LidMode targetLidMode) {
+ this.requirePlaced();
+ LidMode newEffectiveMode = PaperLidded.super.setLidMode(targetLidMode);
+ this.getTileEntity().openersCounter.setLidMode(newEffectiveMode);
+ return newEffectiveMode;
}
- // Paper end - More Chest Block API
}
diff --git a/paper-server/src/main/java/org/bukkit/craftbukkit/block/CraftShulkerBox.java b/paper-server/src/main/java/org/bukkit/craftbukkit/block/CraftShulkerBox.java
index f7b199fbc7a7..670924ae4d8b 100644
--- a/paper-server/src/main/java/org/bukkit/craftbukkit/block/CraftShulkerBox.java
+++ b/paper-server/src/main/java/org/bukkit/craftbukkit/block/CraftShulkerBox.java
@@ -1,5 +1,8 @@
package org.bukkit.craftbukkit.block;
+import io.papermc.paper.block.LidMode;
+import io.papermc.paper.block.LidState;
+import io.papermc.paper.block.PaperLidded;
import net.minecraft.sounds.SoundEvents;
import net.minecraft.sounds.SoundSource;
import net.minecraft.world.level.block.ShulkerBoxBlock;
@@ -10,8 +13,10 @@
import org.bukkit.block.ShulkerBox;
import org.bukkit.craftbukkit.inventory.CraftInventory;
import org.bukkit.inventory.Inventory;
+import org.jetbrains.annotations.NotNull;
-public class CraftShulkerBox extends CraftLootable implements ShulkerBox {
+public class CraftShulkerBox extends CraftLootable implements ShulkerBox,
+ PaperLidded {
public CraftShulkerBox(World world, ShulkerBoxBlockEntity tileEntity) {
super(world, tileEntity);
@@ -43,41 +48,62 @@ public DyeColor getColor() {
}
@Override
- public void open() {
+ public CraftShulkerBox copy() {
+ return new CraftShulkerBox(this, null);
+ }
+
+ @Override
+ public CraftShulkerBox copy(Location location) {
+ return new CraftShulkerBox(this, location);
+ }
+
+ @Override
+ public void startForceLiddedLidOpen() {
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;
+ this.getTileEntity().startForceLiddedLidOpen();
}
@Override
- public void close() {
+ public void stopForceLiddedLidOpen() {
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;
+ this.getTileEntity().stopForceLiddedLidOpen();
}
@Override
- public CraftShulkerBox copy() {
- return new CraftShulkerBox(this, null);
+ public void startForceLiddedLidClose() {
+ this.requirePlaced();
+ this.getTileEntity().startForceLiddedLidClose();
}
@Override
- public CraftShulkerBox copy(Location location) {
- return new CraftShulkerBox(this, location);
+ public void stopForceLiddedLidClose() {
+ this.requirePlaced();
+ this.getTileEntity().stopForceLiddedLidClose();
+ }
+
+ @Override
+ public @NotNull LidState getEffectiveLidState() {
+ this.requirePlaced();
+ return this.getTileEntity().getEffectiveLidState();
}
- // Paper start - More Lidded Block API
@Override
- public boolean isOpen() {
- return getTileEntity().opened;
+ public @NotNull LidState getTrueLidState() {
+ this.requirePlaced();
+ return this.getTileEntity().getTrueLidState();
+ }
+
+ @Override
+ public @NotNull LidMode getLidMode() {
+ this.requirePlaced();
+ return this.getTileEntity().getLidMode();
+ }
+
+ @Override
+ public @NotNull LidMode setLidMode(final @NotNull LidMode targetLidMode) {
+ this.requirePlaced();
+ LidMode newEffectiveMode = PaperLidded.super.setLidMode(targetLidMode);
+ this.getTileEntity().setLidMode(newEffectiveMode);
+ return newEffectiveMode;
}
- // Paper end - More Lidded Block 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 e37aaf77f94b..eeb9f9d9e5c4 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
@@ -5,6 +5,8 @@
import com.google.common.base.Preconditions;
import com.google.common.collect.Lists;
import com.mojang.datafixers.util.Either;
+import io.papermc.paper.block.PaperLidded;
+import io.papermc.paper.event.player.PlayerLiddedOpenEvent;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.util.ArrayList;
@@ -2271,4 +2273,14 @@ public static io.papermc.paper.event.entity.EntityFertilizeEggEvent callEntityFe
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);
+ PaperLidded blockState = (PaperLidded) CraftBlockStates.getBlockState(block);
+
+ PlayerLiddedOpenEvent event = new PlayerLiddedOpenEvent(player, blockState, block);
+
+ return event.callEvent();
+ }
}
diff --git a/test-plugin/src/main/java/io/papermc/testplugin/TestPlugin.java b/test-plugin/src/main/java/io/papermc/testplugin/TestPlugin.java
index 671c37fa4096..ac180dca580f 100644
--- a/test-plugin/src/main/java/io/papermc/testplugin/TestPlugin.java
+++ b/test-plugin/src/main/java/io/papermc/testplugin/TestPlugin.java
@@ -1,5 +1,7 @@
package io.papermc.testplugin;
+import io.papermc.testplugin.lidded.LiddedCommands;
+import io.papermc.testplugin.lidded.LiddedTestListener;
import org.bukkit.event.Listener;
import org.bukkit.plugin.java.JavaPlugin;
@@ -9,6 +11,10 @@ public final class TestPlugin extends JavaPlugin implements Listener {
public void onEnable() {
this.getServer().getPluginManager().registerEvents(this, this);
+ LiddedCommands.registerAll(this);
+ getServer().getPluginManager()
+ .registerEvents(new LiddedTestListener(this.getLogger()), this);
+
// io.papermc.testplugin.brigtests.Registration.registerViaOnEnable(this);
}
diff --git a/test-plugin/src/main/java/io/papermc/testplugin/lidded/LidModeArgument.java b/test-plugin/src/main/java/io/papermc/testplugin/lidded/LidModeArgument.java
new file mode 100644
index 000000000000..a409498e54b7
--- /dev/null
+++ b/test-plugin/src/main/java/io/papermc/testplugin/lidded/LidModeArgument.java
@@ -0,0 +1,90 @@
+package io.papermc.testplugin.lidded;
+
+import com.mojang.brigadier.LiteralMessage;
+import com.mojang.brigadier.arguments.ArgumentType;
+import com.mojang.brigadier.arguments.StringArgumentType;
+import com.mojang.brigadier.context.CommandContext;
+import com.mojang.brigadier.exceptions.CommandSyntaxException;
+import com.mojang.brigadier.exceptions.SimpleCommandExceptionType;
+import com.mojang.brigadier.suggestion.Suggestions;
+import com.mojang.brigadier.suggestion.SuggestionsBuilder;
+import io.papermc.paper.block.LidMode;
+import io.papermc.paper.command.brigadier.argument.CustomArgumentType;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.LinkedHashMap;
+import java.util.Locale;
+import java.util.concurrent.CompletableFuture;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+import org.jetbrains.annotations.NotNull;
+
+public class LidModeArgument implements CustomArgumentType.Converted {
+
+ private static final SimpleCommandExceptionType INVALID_MODE = new SimpleCommandExceptionType(
+ new LiteralMessage("Invalid lid mode"));
+
+ private static final LinkedHashMap BY_STRING = Arrays.stream(
+ LidMode.values())
+ .collect(
+ Collectors.toMap(LidMode::name, Function.identity(), (lidMode, lidMode2) -> lidMode,
+ LinkedHashMap::new));
+
+ static boolean matchesSubStr(String remaining, String candidate) {
+ for (int i = 0; !candidate.startsWith(remaining, i); i++) {
+ int j = candidate.indexOf(46, i);
+ int k = candidate.indexOf(95, i);
+ if (Math.max(j, k) < 0) {
+ return false;
+ }
+
+ if (j >= 0 && k >= 0) {
+ i = Math.min(k, j);
+ } else {
+ i = j >= 0 ? j : k;
+ }
+ }
+
+ return true;
+ }
+
+ public static LidModeArgument lidMode() {
+ return new LidModeArgument();
+ }
+
+ public static LidMode getLidMode(CommandContext> context, String name) {
+ return context.getArgument(name, LidMode.class);
+ }
+
+ @Override
+ public @NotNull LidMode convert(@NotNull final String nativeType)
+ throws CommandSyntaxException {
+ LidMode mode = BY_STRING.get(nativeType.toUpperCase(Locale.ROOT));
+ if (mode == null) {
+ throw INVALID_MODE.create();
+ }
+ return mode;
+ }
+
+ @Override
+ public @NotNull ArgumentType getNativeType() {
+ return StringArgumentType.word();
+ }
+
+ @Override
+ public @NotNull Collection getExamples() {
+ return BY_STRING.keySet();
+ }
+
+ @Override
+ public @NotNull CompletableFuture listSuggestions(
+ final @NotNull CommandContext context, final @NotNull SuggestionsBuilder builder
+ ) {
+
+ String string = builder.getRemaining().toLowerCase(Locale.ROOT);
+ BY_STRING.keySet().stream()
+ .filter(candidate -> matchesSubStr(string, candidate.toLowerCase(Locale.ROOT)))
+ .forEach(builder::suggest);
+ return builder.buildFuture();
+ }
+}
diff --git a/test-plugin/src/main/java/io/papermc/testplugin/lidded/LiddedCommands.java b/test-plugin/src/main/java/io/papermc/testplugin/lidded/LiddedCommands.java
new file mode 100644
index 000000000000..bf3677e4efdb
--- /dev/null
+++ b/test-plugin/src/main/java/io/papermc/testplugin/lidded/LiddedCommands.java
@@ -0,0 +1,251 @@
+package io.papermc.testplugin.lidded;
+
+import com.mojang.brigadier.LiteralMessage;
+import com.mojang.brigadier.context.CommandContext;
+import com.mojang.brigadier.exceptions.CommandSyntaxException;
+import com.mojang.brigadier.exceptions.Dynamic3CommandExceptionType;
+import io.papermc.paper.block.LidMode;
+import io.papermc.paper.block.LidState;
+import io.papermc.paper.command.brigadier.CommandSourceStack;
+import io.papermc.paper.command.brigadier.argument.ArgumentTypes;
+import io.papermc.paper.command.brigadier.argument.resolvers.BlockPositionResolver;
+import io.papermc.paper.math.BlockPosition;
+import io.papermc.paper.plugin.lifecycle.event.LifecycleEventManager;
+import io.papermc.paper.plugin.lifecycle.event.types.LifecycleEvents;
+import io.papermc.testplugin.TestPlugin;
+import net.kyori.adventure.text.Component;
+import net.kyori.adventure.text.format.NamedTextColor;
+import org.bukkit.Location;
+import org.bukkit.block.BlockState;
+import org.bukkit.block.Lidded;
+import org.bukkit.plugin.Plugin;
+import org.bukkit.plugin.java.JavaPlugin;
+
+import static net.kyori.adventure.text.Component.text;
+
+public class LiddedCommands {
+
+ private static final Dynamic3CommandExceptionType NOT_LIDDED = new Dynamic3CommandExceptionType(
+ (objPos, objExpected, objActual) -> {
+ if (!(objPos instanceof BlockPosition pos) || !(objExpected instanceof String expected)
+ || !(objActual instanceof String actual)) {
+ TestPlugin.getPlugin(TestPlugin.class).getLogger().warning(
+ "Internal Exception while making error message" + objPos.getClass()
+ + objExpected.getClass() + objActual.getClass());
+ return new LiteralMessage("Internal Exception while making error message");
+ }
+
+ return new LiteralMessage(
+ "Block at %d, %d, %d is not a lidded block. Expected: \"%s\", Actual: \"%s\"".formatted(
+ pos.blockX(), pos.blockY(), pos.blockZ(), expected, actual));
+ });
+
+
+ public static void registerAll(JavaPlugin plugin) {
+ LifecycleEventManager lifecycleManager = plugin.getLifecycleManager();
+
+ lifecycleManager.registerEventHandler(LifecycleEvents.COMMANDS, event -> {
+ final io.papermc.paper.command.brigadier.Commands registrar = event.registrar();
+ LiddedCommands.registerNew(registrar);
+ LiddedCommands.registerOld(registrar);
+ });
+ }
+
+ public static void registerNew(io.papermc.paper.command.brigadier.Commands commands) {
+ commands.register(io.papermc.paper.command.brigadier.Commands.literal("test_lidded_new")
+ .then(io.papermc.paper.command.brigadier.Commands.argument("pos",
+ ArgumentTypes.blockPosition())
+ .then(io.papermc.paper.command.brigadier.Commands.literal("query")
+ .executes(context -> {
+ BlockPosition pos = context.getArgument("pos",
+ BlockPositionResolver.class).resolve(context.getSource());
+ io.papermc.paper.block.Lidded lidded = getPaperLidded(context, pos);
+ if (!(lidded instanceof BlockState state)) {
+ throw new IllegalStateException("Impossible");
+ }
+ Component msg = text()
+ .append(text("---", NamedTextColor.DARK_GRAY)).appendNewline()
+ .append(text("Lidded Block: "))
+ .append(text(state.getType().key().asString())).appendNewline()
+ .append(text("Lid Mode: "))
+ .append(text(lidded.getLidMode().name())).appendNewline()
+ .append(text("Effective LidState: "))
+ .append(text(lidded.getEffectiveLidState().name(),
+ lidded.getEffectiveLidState() == LidState.OPEN
+ ? NamedTextColor.GREEN : NamedTextColor.RED)).appendSpace()
+ .append(text("True LidState: "))
+ .append(text(lidded.getTrueLidState().name(),
+ lidded.getTrueLidState() == LidState.OPEN ? NamedTextColor.GREEN
+ : NamedTextColor.RED))
+ .build();
+ context.getSource().getSender().sendMessage(msg);
+ return 1;
+ })
+ )
+ .then(io.papermc.paper.command.brigadier.Commands.literal("set")
+ .then(io.papermc.paper.command.brigadier.Commands.argument("lidMode",
+ LidModeArgument.lidMode())
+ .executes(context -> {
+ BlockPosition pos = context.getArgument("pos",
+ BlockPositionResolver.class).resolve(context.getSource());
+ io.papermc.paper.block.Lidded lidded = getPaperLidded(context, pos);
+ if (!(lidded instanceof BlockState state)) {
+ throw new IllegalStateException("Impossible");
+ }
+
+ LidMode previousMode = lidded.getLidMode();
+ LidState oldEffectiveState = lidded.getEffectiveLidState();
+ LidState oldTrueState = lidded.getTrueLidState();
+ LidMode targetMode = LidModeArgument.getLidMode(context, "lidMode");
+ LidMode resultantMode = lidded.setLidMode(targetMode);
+
+ Component msg = text()
+ .append(text("---", NamedTextColor.DARK_GRAY)).appendNewline()
+ .append(text("Lidded Block: "))
+ .append(text(state.getType().key().asString())).appendNewline()
+ .append(text("Lid Mode: "))
+ .append(text("Old: "))
+ .append(text(previousMode.name())).appendSpace()
+ .append(text("Target: "))
+ .append(text(targetMode.name())).appendSpace()
+ .append(text("New: "))
+ .append(text(resultantMode.name())).appendNewline()
+ .append(text("Effective LidState: "))
+ .append(text("Old: "))
+ .append(text(oldEffectiveState.name(),
+ oldEffectiveState == LidState.OPEN ? NamedTextColor.GREEN
+ : NamedTextColor.RED)).appendSpace()
+ .append(text("New: "))
+ .append(text(lidded.getEffectiveLidState().name(),
+ lidded.getEffectiveLidState() == LidState.OPEN
+ ? NamedTextColor.GREEN : NamedTextColor.RED))
+ .appendNewline()
+ .append(text("True LidState: "))
+ .append(text("Old: "))
+ .append(text(oldTrueState.name(),
+ oldTrueState == LidState.OPEN ? NamedTextColor.GREEN
+ : NamedTextColor.RED)).appendSpace()
+ .append(text("New: "))
+ .append(text(lidded.getTrueLidState().name(),
+ lidded.getTrueLidState() == LidState.OPEN ? NamedTextColor.GREEN
+ : NamedTextColor.RED))
+ .build();
+ context.getSource().getSender().sendMessage(msg);
+
+ return 1;
+ }
+ )
+ )
+ )
+ )
+ .build()
+ );
+ }
+
+ private static io.papermc.paper.block.Lidded getPaperLidded(
+ CommandContext context,
+ BlockPosition pos
+ )
+ throws CommandSyntaxException {
+
+ Location targetLoc = new Location(context.getSource().getLocation().getWorld(),
+ pos.blockX(), pos.blockY(), pos.blockZ());
+ BlockState state = targetLoc.getBlock().getState();
+ if (state instanceof io.papermc.paper.block.Lidded lidded) {
+ return lidded;
+ }
+ throw NOT_LIDDED.create(pos, "A block Implementing Paper Lidded",
+ state.getType().key().asString());
+ }
+
+ public static void registerOld(io.papermc.paper.command.brigadier.Commands commands) {
+ commands.register(
+ io.papermc.paper.command.brigadier.Commands.literal("test_lidded_old")
+ .then(io.papermc.paper.command.brigadier.Commands.argument("pos",
+ ArgumentTypes.blockPosition())
+ .then(io.papermc.paper.command.brigadier.Commands.literal("is_open")
+ .executes(context -> {
+ BlockPosition pos = context.getArgument("pos",
+ BlockPositionResolver.class).resolve(context.getSource());
+ Lidded lidded = getBukkitLidded(context, pos);
+ if (!(lidded instanceof BlockState state)) {
+ throw new IllegalStateException("Impossible");
+ }
+
+ Component msg = text()
+ .append(text("Lidded Block: "))
+ .append(text(state.getType().key().asString()))
+ .append(text(" is open: "))
+ .append(text(lidded.isOpen() ? "Yes" : "No",
+ lidded.isOpen() ? NamedTextColor.GREEN : NamedTextColor.RED))
+ .build();
+
+ context.getSource().getSender().sendMessage(msg);
+
+ return 1;
+ })
+ )
+ .then(io.papermc.paper.command.brigadier.Commands.literal("open")
+ .executes(context -> {
+ BlockPosition pos = context.getArgument("pos",
+ BlockPositionResolver.class).resolve(context.getSource());
+ Lidded lidded = getBukkitLidded(context, pos);
+ if (!(lidded instanceof BlockState state)) {
+ throw new IllegalStateException("Impossible");
+ }
+
+ lidded.open();
+
+ Component msg = text()
+ .append(text("Lidded Block: "))
+ .append(text(state.getType().key().asString()))
+ .append(text(" set "))
+ .append(text("open", NamedTextColor.GREEN))
+ .build();
+
+ context.getSource().getSender().sendMessage(msg);
+
+ return 1;
+ })
+ )
+ .then(io.papermc.paper.command.brigadier.Commands.literal("close")
+ .executes(context -> {
+ BlockPosition pos = context.getArgument("pos",
+ BlockPositionResolver.class).resolve(context.getSource());
+ Lidded lidded = getBukkitLidded(context, pos);
+ if (!(lidded instanceof BlockState state)) {
+ throw new IllegalStateException("Impossible");
+ }
+
+ lidded.close();
+ Component msg = text()
+ .append(text("Lidded Block: "))
+ .append(text(state.getType().key().asString()))
+ .append(text(" set "))
+ .append(text("closed", NamedTextColor.RED))
+ .build();
+
+ context.getSource().getSender().sendMessage(msg);
+
+ return 1;
+ })
+ )
+ )
+ .build()
+ );
+ }
+
+ private static Lidded getBukkitLidded(
+ CommandContext context,
+ BlockPosition pos
+ ) throws CommandSyntaxException {
+ Location targetLoc = new Location(context.getSource().getLocation().getWorld(),
+ pos.blockX(), pos.blockY(), pos.blockZ());
+ BlockState state = targetLoc.getBlock().getState();
+ if (state instanceof Lidded lidded) {
+ return lidded;
+ }
+ throw NOT_LIDDED.create(pos, "A block Implementing Bukkit Lidded",
+ state.getType().key().asString());
+ }
+}
diff --git a/test-plugin/src/main/java/io/papermc/testplugin/lidded/LiddedTestListener.java b/test-plugin/src/main/java/io/papermc/testplugin/lidded/LiddedTestListener.java
new file mode 100644
index 000000000000..c5d918a3b0e6
--- /dev/null
+++ b/test-plugin/src/main/java/io/papermc/testplugin/lidded/LiddedTestListener.java
@@ -0,0 +1,52 @@
+package io.papermc.testplugin.lidded;
+
+import io.papermc.paper.event.player.PlayerLiddedOpenEvent;
+import io.papermc.paper.registry.RegistryAccess;
+import io.papermc.paper.registry.RegistryKey;
+import io.papermc.paper.registry.TypedKey;
+import io.papermc.paper.registry.tag.Tag;
+import io.papermc.paper.registry.tag.TagKey;
+import java.util.Objects;
+import java.util.logging.Logger;
+import net.kyori.adventure.key.Key;
+import org.bukkit.Registry;
+import org.bukkit.event.EventHandler;
+import org.bukkit.event.Listener;
+import org.bukkit.inventory.ItemStack;
+import org.bukkit.inventory.ItemType;
+
+public class LiddedTestListener implements Listener {
+
+ private static final TagKey WOOL_TAG_KEY = TagKey.create(RegistryKey.ITEM,
+ Key.key("minecraft:wool"));
+ private final Logger logger;
+
+ public LiddedTestListener(Logger logger) {
+ this.logger = logger;
+ }
+
+ @EventHandler
+ public void onLiddedEvent(PlayerLiddedOpenEvent event) {
+ Registry itemTypeRegistry = RegistryAccess.registryAccess()
+ .getRegistry(RegistryKey.ITEM);
+ if (!itemTypeRegistry.hasTag(WOOL_TAG_KEY)) {
+ logger.warning("Wool tag not found");
+ return;
+ }
+ Tag woolTag = itemTypeRegistry.getTag(WOOL_TAG_KEY);
+
+ ItemStack mainHandItem = event.getPlayer().getInventory().getItemInMainHand();
+
+ boolean isWool = woolTag.contains(
+ TypedKey.create(RegistryKey.ITEM, itemTypeRegistry.getKeyOrThrow(
+ Objects.requireNonNull(mainHandItem.getType().asItemType()))));
+
+ if (isWool) {
+ event.getPlayer().sendRichMessage(
+ "Opening " + event.getLidded().getType().getKey().asString()
+ + " Quietly");
+ event.setCancelled(true);
+ }
+ }
+
+}