Log armor stand breaks from the retired callback on Folia - #1010
Merged
Intelli merged 4 commits intoOct 2, 2026
Merged
Conversation
When an armor stand dies from non-player damage, the break and its equipment are logged from a task on the stand's scheduler. On Folia the stand is removed in the same tick, so the task is retired instead of run and nothing is logged. The same runnable is now passed as the retired callback, so exactly one of them logs the break.
❌ Deploy Preview for coreprotect failed. Why did it fail? →
|
Contributor
|
Thanks -- automated review is requesting the following changes:
|
The damage listeners now only capture the attacker and the stand's contents, and EntityDeathEvent logs the break when the stand actually dies. The scheduled task and Folia's retired callback just discard a capture that was never used, so a stand that survived the hit and was unloaded or otherwise removed before the callback ran no longer produces break and container rows, and the retired callback never touches the world.
…a-armor-stand-rows
…a-armor-stand-rows
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
On Folia, an armor stand killed by anything other than a player's direct hit (a cactus, a mob, an arrow, an explosion) is never logged: no block row, no rows for its armor and hand items. A rollback cannot bring it back. This change logs the break from the entity scheduler's retired callback as well as the normal one.
The problem
The armor stand's contents can only be read before it dies, so both listeners capture them and schedule the actual logging on the stand's own scheduler, to run once the stand is dead:
listener/entity/EntityDamageByBlockListener.java:71(damage from a block, for example a cactus or magma)listener/entity/EntityDamageByEntityListener.java:130(damage from an entity, for example a zombie, a skeleton's arrow or TNT)Scheduler.runTask(plugin, task, entity)isscheduleSyncDelayedTask(plugin, task, null, entity, 0). On Folia that becomesentity.getScheduler().run(plugin, task, retired)with a null retired callback. A lethal hit removes the stand in the same tick, so when the entity scheduler gets to the task the entity is gone. Folia then runs the retired callback instead of the task, and since that is null, nothing is logged. Paper and Spigot run the task on the main thread one tick later and log normally.In my testing on Folia 1.21.11 this dropped four rows per stand (one block row and three container rows for armor and hand items), and
/co rollbackleft the stand missing.The fix
Both listeners build the runnable once and pass it as both the task and the retired callback, through the
scheduleSyncDelayedTask(plugin, task, retiredTask, entity, 0)overload that already exists inthread/Scheduler.java.Folia runs exactly one of the two callbacks, so the break is logged once. The runnable still checks
isDead(), which is true for a removed entity, so a stand that survived (for example a cancelled hit) is still not logged.Behaviour change
Risk
The retired callback runs when the entity is removed, on the thread that owns it at that point, which also owns the stand's block location. That is the same ownership the normal task has, so
block.getState()and the queue calls are safe there.Testing
Build:
mvn packagepasses.Live, the 47-step scenario on Folia 1.21.11 and Paper 26.2 with SQLite, compared against upstream run the same way. The scenario gives an armor stand an iron helmet, a leather chestplate and a diamond sword, and has a zombie kill it.
#zombiebreaking thearmor_standplus one container row each for the helmet, the chestplate and the sword. Every other row matches.