Skip to content

Fix #182: reconcile blocks consumed by wither and golem builds - #260

Open
LoralonMC wants to merge 1 commit into
InsightsPlugin:mainfrom
LoralonMC:fix/182-build-pattern-blocks
Open

LoralonMC wants to merge 1 commit into
InsightsPlugin:mainfrom
LoralonMC:fix/182-build-pattern-blocks

Conversation

@LoralonMC

Copy link
Copy Markdown

Fixes #182.

That issue is marked wontfix on the grounds that the pattern block locations can't be recovered from CreatureSpawnEvent:

Almost, it's still not trivial to get the locations of the wither skulls / soul sand just from this event. We could assume all of the blocks are in the chunk of the spawned wither but that does not really hold when the statue is built over two chunks.

That's true, and this PR doesn't try to recover them. It reconciles the area instead, which sidesteps the problem entirely.

Why nothing sees these blocks today

CarvedPumpkinBlock#clearPatternBlocks (shared by WitherSkullBlock#checkSpawn) removes each pattern block with a bare Level#setBlock(pos, AIR, 2) — not destroyBlock, and no CraftEventFactory call anywhere on the path. So no Bukkit block event fires at all, and none of BlockListener, PaperBlockListener (BlockDestroyEvent), EntityListener, PistonListener or WorldListener can observe it. The counts drift upward until the chunk unloads and ChunkListener#onChunkUnload drops the cache entry.

Approach

CreatureSpawnEvent fires before the pattern is cleared — addFreshEntity(wither, BUILD_WITHER) returns first, and clearPatternBlocks runs after it — so at MONITOR every pattern block is still in place. This snapshots the area around the spawn and reconciles whatever actually changed one tick later, via handleModification(location, from, to, 1).

Three things fall out of doing it this way rather than decrementing known pattern positions:

  • No pattern geometry in the plugin. Nothing to keep in sync with vanilla, and the two-chunk case from the issue isn't a special case at all — handleModification resolves each block's own chunk (and addon region) independently.
  • Copper golems are covered. 26.1.2 added BUILD_COPPERGOLEM, and CarvedPumpkinBlock#replaceCopperBlockWithChest turns the copper block into a copper chest in the same tick — a block entity appearing with no event, i.e. the same bug in the opposite direction. A diff picks it up; decrementing pattern blocks would not.
  • Future spawn reasons are covered. BUILD_SPAWN_REASONS is derived from SpawnReason#values() filtered on the BUILD_ prefix at class-init, so a new built-mob reason needs no code change here. The hot path stays a single EnumSet#contains.

A radius of 2 is sufficient for every current pattern. The widest (wither, iron golem) spawns the mob at match.getBlock(1, 2, 0), so relative to that position the other blocks sit within 1 along the pattern's right axis and 2 along its up axis, whatever orientation it matched in. Snow and copper golems are well inside that.

The delayed re-check follows the existing idiom in BlockListener#handlePistonEvent (snapshot materials, re-read after a getRegionScheduler().runDelayed). Chunks are never loaded for the snapshot — an unloaded chunk has no cached storage to correct anyway.

Listener registration

The handler is annotated @AllowDisabling so CreatureSpawnEvent can be turned off in settings.disabled-listeners, since it's a hot event and some servers won't want the listener at all. That required adding EntityListener to the AllowDisabling scan and moving the entity listeners into disableListeners in ListenerManager. EntityListener has no other @AllowDisabling methods, so nothing else becomes disableable as a result.

Testing

Built and running on a live 26.1.2 Paper server (~10 players). Verified in game: the tile count rises by 3 as the skulls go down and returns to its pre-build value the moment the wither spawns, where previously it stayed high until the chunk unloaded. The four configured disabled-listeners still unregister correctly after the ListenerManager change, and the plugin loads with no errors.

One note unrelated to this change: main doesn't currently compile against minecraft = "26.2.build.+", because CommandSourceStack#getEntityOrThrow is abstract in every published 26.2 build and CommandSenderMapper's anonymous implementation doesn't override it. I built against 26.1.2.build.74-stable to get around it. Happy to open a separate issue if that's news.

🤖 Generated with Claude Code

… builds

The blocks making up a wither or golem are consumed by CarvedPumpkinBlock
#clearPatternBlocks, which removes them with a direct Level#setBlock call.
That fires no block event at all, so nothing tells the cache those blocks
are gone and the counts drift upwards until the chunk unloads. The chest a
copper golem leaves behind is placed the same way and drifts the other way.

Getting the exact pattern positions out of CreatureSpawnEvent is not
possible, so instead snapshot the area around the spawn (the widest pattern
reaches two blocks out in any orientation) and reconcile whatever actually
changed a tick later, once the pattern has been cleared. This needs no
knowledge of the pattern shapes and picks up the copper golem's chest for
free.

The spawn reasons are derived from SpawnReason#values() at runtime so that
reasons added by later Minecraft versions are covered without a code change.
The listener is opt-out via settings.disabled-listeners, which meant moving
the entity listeners into the disable-capable list.

Co-Authored-By: Claude Opus 5 (1M context) <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.

If a wither is summoned using the wither skull, the tile limit does not change.

1 participant