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); + } + } + +}