Skip to content

Commit 6bac3c9

Browse files
committed
Delay open/close callbacks for chests
The logic for chests may invoke block updates, which may not be safe to perform. For example, inventory closing due to player logout (which restricts chunk loading due to being inside an entity status callback) or during shutdown (which has chunk loading restrictions due to the chunk system being halted). This also fixes the recheckOpeners function not firing the redstone event if the open count changes.
1 parent 9b1dd57 commit 6bac3c9

File tree

2 files changed

+133
-1
lines changed

2 files changed

+133
-1
lines changed
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
2+
From: Spottedleaf <Spottedleaf@users.noreply.github.com>
3+
Date: Mon, 20 Apr 2026 02:56:22 -0700
4+
Subject: [PATCH] Delay open/close callbacks for chests
5+
6+
The logic for chests may invoke block updates, which may not
7+
be safe to perform. For example, inventory closing due to
8+
player logout (which restricts chunk loading due to being
9+
inside an entity status callback) or during shutdown (which has
10+
chunk loading restrictions due to the chunk system being halted).
11+
12+
diff --git a/net/minecraft/world/level/block/entity/BarrelBlockEntity.java b/net/minecraft/world/level/block/entity/BarrelBlockEntity.java
13+
index 9eb6cb86ff70f04863cae3def1006f82bcb4fa6b..da5fdf77dd26d97c8b92bac9a6af8d922c31eb9c 100644
14+
--- a/net/minecraft/world/level/block/entity/BarrelBlockEntity.java
15+
+++ b/net/minecraft/world/level/block/entity/BarrelBlockEntity.java
16+
@@ -66,6 +66,13 @@ public class BarrelBlockEntity extends RandomizableContainerBlockEntity {
17+
Objects.requireNonNull(BarrelBlockEntity.this);
18+
}
19+
20+
+ // Paper start - delay open/close callbacks
21+
+ @Override
22+
+ public boolean delayCallbacks() {
23+
+ return true;
24+
+ }
25+
+ // Paper end - delay open/close callbacks
26+
+
27+
@Override
28+
protected void onOpen(final Level level, final BlockPos pos, final BlockState state) {
29+
BarrelBlockEntity.this.playSound(state, SoundEvents.BARREL_OPEN);
30+
diff --git a/net/minecraft/world/level/block/entity/ChestBlockEntity.java b/net/minecraft/world/level/block/entity/ChestBlockEntity.java
31+
index 81a293cc13653a9a0336cb25d75273969fee4d16..8ebcce4c4971a11d367fb39e9c7fd280f63594e3 100644
32+
--- a/net/minecraft/world/level/block/entity/ChestBlockEntity.java
33+
+++ b/net/minecraft/world/level/block/entity/ChestBlockEntity.java
34+
@@ -35,6 +35,13 @@ public class ChestBlockEntity extends RandomizableContainerBlockEntity implement
35+
Objects.requireNonNull(ChestBlockEntity.this);
36+
}
37+
38+
+ // Paper start - delay open/close callbacks
39+
+ @Override
40+
+ public boolean delayCallbacks() {
41+
+ return true;
42+
+ }
43+
+ // Paper end - delay open/close callbacks
44+
+
45+
@Override
46+
protected void onOpen(final Level level, final BlockPos pos, final BlockState blockState) {
47+
if (blockState.getBlock() instanceof ChestBlock chestBlock) {
48+
diff --git a/net/minecraft/world/level/block/entity/ContainerOpenersCounter.java b/net/minecraft/world/level/block/entity/ContainerOpenersCounter.java
49+
index 9af046cbe2c5024040057118a0e9ecd539815781..aa8b384b05738139fa3492b3c09084cda09448ce 100644
50+
--- a/net/minecraft/world/level/block/entity/ContainerOpenersCounter.java
51+
+++ b/net/minecraft/world/level/block/entity/ContainerOpenersCounter.java
52+
@@ -37,11 +37,23 @@ public abstract class ContainerOpenersCounter {
53+
54+
protected abstract void openerCountChanged(final Level level, final BlockPos pos, final BlockState blockState, int previous, int current);
55+
56+
+ // Paper start - delay open/close callbacks
57+
+ public boolean delayCallbacks() {
58+
+ return false;
59+
+ }
60+
+ // Paper end - delay open/close callbacks
61+
+
62+
public abstract boolean isOwnContainer(final Player player);
63+
64+
public void incrementOpeners(
65+
final LivingEntity entity, final Level level, final BlockPos pos, final BlockState blockState, final double maxInteractionRange
66+
) {
67+
+ // Paper start - delay open/close callbacks
68+
+ if (this.delayCallbacks()) {
69+
+ scheduleRecheck(level, pos, blockState, 1);
70+
+ return;
71+
+ }
72+
+ // Paper end - delay open/close callbacks
73+
int oldPower = Math.max(0, Math.min(15, this.openCount)); // CraftBukkit - Get power before new viewer is added
74+
int previous = this.openCount++;
75+
76+
@@ -66,6 +78,12 @@ public abstract class ContainerOpenersCounter {
77+
}
78+
79+
public void decrementOpeners(final LivingEntity entity, final Level level, final BlockPos pos, final BlockState blockState) {
80+
+ // Paper start - delay open/close callbacks
81+
+ if (this.delayCallbacks()) {
82+
+ scheduleRecheck(level, pos, blockState, 1);
83+
+ return;
84+
+ }
85+
+ // Paper end - delay open/close callbacks
86+
int oldPower = Math.max(0, Math.min(15, this.openCount)); // CraftBukkit - Get power before new viewer is added
87+
if (this.openCount == 0) return; // Paper - Prevent ContainerOpenersCounter openCount from going negative
88+
int previous = this.openCount--;
89+
@@ -151,6 +169,11 @@ public abstract class ContainerOpenersCounter {
90+
}
91+
92+
private static void scheduleRecheck(final Level level, final BlockPos blockPos, final BlockState blockState) {
93+
- level.scheduleTick(blockPos, blockState.getBlock(), 5);
94+
+ // Paper start - delay open/close callbacks
95+
+ scheduleRecheck(level, blockPos, blockState, 5);
96+
+ }
97+
+ private static void scheduleRecheck(final Level level, final BlockPos blockPos, final BlockState blockState, final int delay) {
98+
+ level.scheduleTick(blockPos, blockState.getBlock(), delay);
99+
+ // Paper end - delay open/close callbacks
100+
}
101+
}
102+
diff --git a/net/minecraft/world/level/block/entity/EnderChestBlockEntity.java b/net/minecraft/world/level/block/entity/EnderChestBlockEntity.java
103+
index eeac644f60760570244380d3c454fd33e37b5a7f..751625124a87de63d933d9c7809e46361e3136a5 100644
104+
--- a/net/minecraft/world/level/block/entity/EnderChestBlockEntity.java
105+
+++ b/net/minecraft/world/level/block/entity/EnderChestBlockEntity.java
106+
@@ -18,6 +18,13 @@ public class EnderChestBlockEntity extends BlockEntity implements LidBlockEntity
107+
Objects.requireNonNull(EnderChestBlockEntity.this);
108+
}
109+
110+
+ // Paper start - delay open/close callbacks
111+
+ @Override
112+
+ public boolean delayCallbacks() {
113+
+ return true;
114+
+ }
115+
+ // Paper end - delay open/close callbacks
116+
+
117+
@Override
118+
protected void onOpen(final Level level, final BlockPos pos, final BlockState blockState) {
119+
level.playSound(

paper-server/patches/sources/net/minecraft/world/level/block/entity/ContainerOpenersCounter.java.patch

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,11 +62,24 @@
6262
if (this.openCount == 0) {
6363
this.onClose(level, pos, blockState);
6464
level.gameEvent(entity, GameEvent.CONTAINER_CLOSE, pos);
65-
@@ -74,6 +_,7 @@
65+
@@ -74,8 +_,20 @@
6666
}
6767

6868
int openCount = containerUsers.size();
6969
+ if (this.opened) openCount++; // CraftBukkit - add dummy count from API
7070
int prevCount = this.openCount;
7171
if (prevCount != openCount) {
72+
+ // CraftBukkit start - Call redstone event
73+
+ if (level.getBlockState(pos).is(net.minecraft.world.level.block.Blocks.TRAPPED_CHEST)) {
74+
+ int oldPower = net.minecraft.util.Mth.clamp(prevCount, 0, 15);
75+
+ int newPower = net.minecraft.util.Mth.clamp(openCount, 0, 15);
76+
+
77+
+ if (oldPower != newPower) {
78+
+ org.bukkit.craftbukkit.event.CraftEventFactory.callRedstoneChange(level, pos, oldPower, newPower);
79+
+ }
80+
+ }
81+
+ // CraftBukkit end
82+
+
7283
boolean isOpen = openCount != 0;
84+
boolean wasOpen = prevCount != 0;
85+
if (isOpen && !wasOpen) {

0 commit comments

Comments
 (0)