Skip to content

Fix double chests double-counting their viewers - #604

Open
KostiaFed wants to merge 1 commit into
CardboardPowered:ver/26.1from
KostiaFed:fix/double-chest-opener-count
Open

Fix double chests double-counting their viewers#604
KostiaFed wants to merge 1 commit into
CardboardPowered:ver/26.1from
KostiaFed:fix/double-chest-opener-count

Conversation

@KostiaFed

@KostiaFed KostiaFed commented Aug 23, 2026

Copy link
Copy Markdown
Contributor

Follow-up to #570, which fixed the same double count on the other path — that is why shulker boxes got better and chests did not.

Problem

The Bukkit-level viewer hooks on CompoundContainer called the vanilla startOpen/stopOpen, which drive ContainerOpenersCounter:

public void onOpen(CraftHumanEntity who) {
    this.container1.startOpen(who.getHandle());   // vanilla opener counter
    this.container2.startOpen(who.getHandle());

CraftBukkit calls container1.onOpen(who) there — transaction tracking only. ChestMenu's constructor already calls startOpen once, and CompoundContainer.startOpen fans that out to both halves, so every viewer of a large chest was counted twice. Single chests are unaffected: ChestBlockEntity's onOpen only adds to transaction, so the count stays balanced. Large chests are affected because CraftInventoryDoubleChest wraps the CompoundContainer.

Counted against 26.1.2 bytecode (ChestMenu ctor → one startOpen; removed → one stopOpen):

Step Counter, per half
ChestMenu ctor → startOpen 0→1 — open sound, scheduleRecheck(+5 ticks)
transferToonOpenstartOpen 1→2
recheckOpeners five ticks later one real viewer → corrects to 1, reschedules
close: transferToonClosestopOpen 1→0 — close sound
close: ChestMenu.removedstopOpen 0→−1, silently (decrementOpeners tests openCount == 0)
pending recheck 0 openers vs stored −1 → the actual == 0 branch → second close sound

Hence the erratic behaviour. Closed within five ticks, the sequence happens to balance (2→1→0) and everything is correct. Held open longer — nearly always — the counter ends at −1 and a second close sound arrives. While it sits negative, the next open never makes the 0→1 transition, so onOpen does not fire and no recheck is scheduled: the lid animation and the trapped-chest redstone output are left in the wrong state.

Changes

CompoundContainerMixinonOpen/onClose delegate to the halves' ContainerBridge.onOpen/onClose, as CraftBukkit does. Each half still records the viewer in its own transaction (needed for per-half getViewers()), and only vanilla touches the opener count, so it moves exactly +1/−1.

ServerPlayerMixin — the InventoryOpenEvent cancel path leaked an opener per half on large chests. A large chest arrives as the anonymous MenuProvider that DoubleBlockCombiner builds, so neither factory instanceof Container nor factory instanceof CompoundContainer matched and the constructor's startOpen was never undone (+1 per half, lid stuck open). The CompoundContainer branch was unreachable regardless — CompoundContainer is a Container, so the first branch would have claimed it — and it only stopped container1. Now, when factory is not a Container, the container is taken from the ChestMenu that was created, and CompoundContainer.stopOpen fans out to both halves.

Scope and testing

gradlew compileJava passes. This is code and bytecode analysis, not an in-game test — worth verifying on a live server.

I found no counting asymmetry on the pure single-chest path: block click, player.openInventory, client close, server-side closeContainer(), and player disconnect all balance +1/−1. Reports of single chests misbehaving are most likely halves of large chests, or a half whose counter was already driven negative and stays broken after the pair is split.

The Bukkit-level viewer hooks on CompoundContainer called the vanilla
startOpen/stopOpen, which drive ContainerOpenersCounter. ChestMenu already
calls startOpen once per open and CompoundContainer fans it out to both
halves, so every viewer of a large chest was counted twice.

The five-tick recheck corrects the count to 1 while the chest is still open,
so the two decrements on close take the counter to -1: the first plays the
close sound, and the pending recheck then sees 0 openers against a stored
-1 and fires onClose a second time. Held open for less than five ticks the
sequence happens to balance, which is why the symptom is erratic - a second
close sound, or a lid and redstone output stuck in the wrong state, since a
counter left negative never makes the 0 -> 1 transition that fires onOpen
and schedules the next recheck.

Delegate to the halves' onOpen/onClose instead, as CraftBukkit does, so each
half still tracks the viewer for getViewers() while only vanilla touches the
opener count.

Also fix the InventoryOpenEvent cancel path, which leaked an opener per half
on large chests: a large chest arrives as the MenuProvider that
DoubleBlockCombiner builds, so neither the Container nor the CompoundContainer
branch matched and the constructor's startOpen was never undone. The
CompoundContainer branch was unreachable anyway - CompoundContainer is a
Container, so the first branch would have claimed it - and it only stopped
container1. Take the container from the menu that was created instead, and
let CompoundContainer.stopOpen fan out to both halves.

Follow-up to CardboardPowered#570, which fixed the same double count on the other path.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant