Expected behavior
When a ZombifiedPiglin's persistentAngerTarget ends up pointing to a non-Player entity (e.g. another piglin) that subsequently becomes dead/removed, vanilla NeutralMob.updatePersistentAnger should clean up the anger state, and the piglin should return to neutral.
The intent is already partially expressed by the existing cleanup branch:
if (target != null && target.isDeadOrDying() && persistentAngerTarget != null
&& persistentAngerTarget.matches(target) && target instanceof Mob) {
this.stopBeingAngry();
}
…but it only fires while the dead entity is still referenced by target. Once TargetGoal.stop() clears target to null, no remaining branch handles the leftover stale persistentAngerTarget.
Observed/Actual behavior
ZombifiedPiglins become permanently stuck in:
getPersistentAngerEndTime() > level.getGameTime() → isAngry() == true
persistentAngerTarget is non-null, but resolves to a removed Mob (or to nothing)
target is null (cleared by TargetGoal.stop() after the attacker piglin died)
Visible symptoms:
- Anger sound plays continuously
- Mob does not move toward any player
- LookControl still ticks (random head turns)
- Hitting one stuck mob restores normal aggro on only that mob
- Unloading + reloading the chunk fixes every stuck mob in that chunk
- MSPT-independent (observed at 2–50 mspt)
The stuck count grows monotonically over time on a busy farm (sample data below).
Steps/models to reproduce
- Paper 1.21.11+ server, default config
- Build / use any standard
ZombifiedPiglin gold/XP farm in the Nether
- Start AFK-attacking a stack at the kill chamber. A fake-player / bot at the kill chamber works best for sustained reproducibility (
/fp attack interval 13 with Leaves' built-in fakeplayer was used in the original report, but any Carpet-style fakeplayer mod also works)
- Within ~20–30 minutes, an increasing fraction of nearby angry piglins stop pathfinding while continuing to play angry sounds
The tick-by-tick trigger sequence (instrumented build confirmed all of this)
-
Piglin A uses the new 1.21.11 SpearUseGoal to throw a spear and friendly-fires another piglin B
-
B.setLastHurtByMob(A) is set (vanilla LivingEntity behaviour)
-
Next target-selector tick, HurtByTargetGoal.start() calls B.setTarget(A, TARGET_ATTACKED_ENTITY)
-
B.customServerAiStep then calls updatePersistentAnger(level, true). The MC-305388 backport branch fires:
boolean newTarget = persistentAngerTarget == null || !persistentAngerTarget.matches(target);
if (newTarget) {
this.setPersistentAngerTarget(EntityReference.of(target)); // ← B.persistentAngerTarget overwritten to point to piglin A
}
if (newTarget || updateAnger) {
this.startPersistentAngerTimer();
}
-
Player kills A. A becomes dead, then removed.
-
TargetGoal.canContinueToUse() returns false because mob.canAttack(A) → A.canBeSeenAsEnemy() → A.isAlive() is false. TargetGoal.stop() calls B.setTarget(null, FORGOT_TARGET). B.target becomes null.
-
updatePersistentAnger(level, true) runs:
- BRANCH 1 (target dead + matches): fails —
target == null
- BRANCH 2 maintenance (
if (target != null) { … }): skipped
- BRANCH 2 cleanup (
persistentAngerTarget != null && !this.isAngry() && (target == null || …)): fails — isAngry() is still true because the timer was just refreshed in step 4 immediately before A died
-
No cleanup branch fires. B is stuck.
The timer alone would expire in 20–39 seconds and BRANCH 2 cleanup would then fire — but on a busy piglin farm SpearUseGoal keeps friendly-firing B onto fresh attackers, restarting the timer through step 4 again. Stuck count grows roughly exponentially until the kill chamber becomes a graveyard of standing piglins.
Diagnostic data
A reflection-based sampler (a debug build of RoseStacker) classified ZombifiedPiglins by anger state every 3 s:
[#843 SAMPLE] near <bot> sound=53 healthy=1 stuck=52 (no_uuid=0 no_target=0 dead_target=52 other=0)
[#843 STUCK-SAMPLE] entity=6baaf74b state=DEAD_TARGET timer=563 UUID=8ffbdc24-…
target=ZOMBIFIED_PIGLIN(dead) pos=(371.5,-41.0,-259.5)
target=ZOMBIFIED_PIGLIN(dead) confirms a target field briefly observed still holding a removed-piglin reference; persistentAngerTarget is non-null but resolves to nothing valid; timer is still active.
Stuck count over time on a single farm:
T+0 min stuck=0
T+18 min stuck=13
T+19 min stuck=40
T+20 min stuck=50
… never recovers without external intervention (chunk reload or hitting each individually)
Plugin and Datapack List
Reproduces with no plugins (standard farm + sustained AFK-attacker would expose it, just slower). Easiest reproducer:
- A fakeplayer / bot for sustained AFK attacking
- Optionally a mob-stacker plugin for higher mob density (accelerates the bug; not required)
Originally reported with: Leaves built-in fakeplayer + RoseStacker. Independently confirmed without RoseStacker by Leaves maintainer (see "Other" below).
Paper version
This server is running Paper version 1.21.11-130-ver/1.21.11@c5a2736 (2026-04-11T11:14:19Z) (Implementing API version 1.21.11-R0.1-SNAPSHOT)
You are running the latest version
Originally observed on Leaves 1.21.11-DEV-HEAD@3c199cc (2026-03-15) (Implementing API version 1.21.11-R0.1-SNAPSHOT). Independently reproduced on stock Paper 1.21.11 by Leaves maintainer @Fortern (see Leaves #843 thread).
Other
Background
This was originally reported downstream as LeavesMC/Leaves#843 ("【插件不兼容】僵尸猪灵在启用堆叠时经常失去愤怒行为"). Investigation went through several wrong turns before the upstream cause was pinned down:
-
Initially suspected as a Leaves-only regression (the original reporter's 6-hour test on stock Paper 1.21.11 found no issue). The "Paper is fine" claim turned out to be a sampling artefact — the bug is probabilistic, dependent on farm density, kill rate and friendly-fire frequency. Leaves core member @Fortern independently reproduced the same symptoms on stock Paper 1.21.11 and noted: "1.21.11 zombified piglins holding spears can friendly-fire each other, possibly causing aggro transfer."
-
Then suspected as RoseStacker-specific. Reported as Rosewood-Development/RoseStacker#174 and the RoseStacker maintainer committed 2cc706e to correctly transfer persistentAngerTarget (EntityReference) and persistentAngerEndTime on decreaseStackSize. That fix is correct and resolves the unstack-boundary path of the issue, but does not cover the SpearUseGoal-induced vanilla path described in this report — that path lives entirely inside NeutralMob.updatePersistentAnger and never goes through any plugin.
Related Paper / Mojang work
- PaperMC/Paper#13318 — "Zombified Piglin pathfinding breaks after a short relog". Symptoms appear identical to ours, triggered by a different vanilla path (
EntityReference cache invalidation on chunk unload/reload). Currently open, no investigation. Plausibly the same root-cause family.
- PaperMC/Paper PR #13546 — "Fix Bee anger never gets timeout", merged 2026-01-30. Backported MC-305388 from 26.1-snapshot-5. The backport's scope is
updateAnger == false mobs (Bees); it intentionally leaves updateAnger == true mobs (ZombifiedPiglin, EnderMan, Wolf, PolarBear) to refresh timer every tick. The scenario in this report is a sibling failure mode that the existing fix doesn't address.
- Mojang MC-305388 — the Bee variant of the broader 1.21.11 anger-system rewrite issue. (No Mojang ticket found for the piglin/
SpearUseGoal variant; happy to file one if it would help the conversation.)
Suggested fix direction (subject to maintainer judgement)
Extend NeutralMob.updatePersistentAnger's BRANCH 2 cleanup to also clean up when persistentAngerTarget resolves to a non-Player (or to nothing) and the runtime target has been cleared:
// near the existing BRANCH 2 cleanup:
if (persistentAngerTarget != null && target == null) {
LivingEntity resolved = EntityReference.getLivingEntity(persistentAngerTarget, level);
if (resolved == null || !isValidPlayerTarget(resolved)) {
this.stopBeingAngry();
return;
}
}
This is conceptually consistent with BRANCH 1 (which cleans up when target is dead and matches anger UUID) and avoids the timer-refresh interaction.
If Paper would prefer to not patch this above vanilla, treating it as a known upstream issue and tracking the (to-be-filed) Mojang ticket would also be helpful for downstream forks/plugins to coordinate on.
Workaround currently in use
A periodic janitor that scans angry piglins and calls NeutralMob.stopBeingAngry() on those with stale anger targets has been added to a fork of RoseStacker and verified to keep stuck=0 over multi-hour runs on the affected farm. Reference implementation: https://github.com/Rosewood-Development/RoseStacker/ (commit 15103d03). This is a stopgap; the proper fix belongs in vanilla / Paper.
Expected behavior
When a
ZombifiedPiglin'spersistentAngerTargetends up pointing to a non-Player entity (e.g. another piglin) that subsequently becomes dead/removed, vanillaNeutralMob.updatePersistentAngershould clean up the anger state, and the piglin should return to neutral.The intent is already partially expressed by the existing cleanup branch:
…but it only fires while the dead entity is still referenced by
target. OnceTargetGoal.stop()clearstargettonull, no remaining branch handles the leftover stalepersistentAngerTarget.Observed/Actual behavior
ZombifiedPiglins become permanently stuck in:
getPersistentAngerEndTime() > level.getGameTime()→isAngry() == truepersistentAngerTargetis non-null, but resolves to a removed Mob (or to nothing)targetisnull(cleared byTargetGoal.stop()after the attacker piglin died)Visible symptoms:
The stuck count grows monotonically over time on a busy farm (sample data below).
Steps/models to reproduce
ZombifiedPiglingold/XP farm in the Nether/fp attack interval 13with Leaves' built-in fakeplayer was used in the original report, but any Carpet-style fakeplayer mod also works)The tick-by-tick trigger sequence (instrumented build confirmed all of this)
Piglin A uses the new 1.21.11
SpearUseGoalto throw a spear and friendly-fires another piglin BB.setLastHurtByMob(A)is set (vanillaLivingEntitybehaviour)Next target-selector tick,
HurtByTargetGoal.start()callsB.setTarget(A, TARGET_ATTACKED_ENTITY)B.customServerAiStepthen callsupdatePersistentAnger(level, true). The MC-305388 backport branch fires:Player kills A. A becomes dead, then removed.
TargetGoal.canContinueToUse()returnsfalsebecausemob.canAttack(A)→A.canBeSeenAsEnemy()→A.isAlive()isfalse.TargetGoal.stop()callsB.setTarget(null, FORGOT_TARGET).B.targetbecomesnull.updatePersistentAnger(level, true)runs:target == nullif (target != null) { … }): skippedpersistentAngerTarget != null && !this.isAngry() && (target == null || …)): fails —isAngry()is stilltruebecause the timer was just refreshed in step 4 immediately before A diedNo cleanup branch fires.
Bis stuck.The timer alone would expire in 20–39 seconds and
BRANCH 2 cleanupwould then fire — but on a busy piglin farmSpearUseGoalkeeps friendly-firingBonto fresh attackers, restarting the timer through step 4 again. Stuck count grows roughly exponentially until the kill chamber becomes a graveyard of standing piglins.Diagnostic data
A reflection-based sampler (a debug build of RoseStacker) classified ZombifiedPiglins by anger state every 3 s:
target=ZOMBIFIED_PIGLIN(dead)confirms atargetfield briefly observed still holding a removed-piglin reference;persistentAngerTargetis non-null but resolves to nothing valid; timer is still active.Stuck count over time on a single farm:
Plugin and Datapack List
Reproduces with no plugins (standard farm + sustained AFK-attacker would expose it, just slower). Easiest reproducer:
Originally reported with: Leaves built-in fakeplayer + RoseStacker. Independently confirmed without RoseStacker by Leaves maintainer (see "Other" below).
Paper version
This server is running Paper version 1.21.11-130-ver/1.21.11@c5a2736 (2026-04-11T11:14:19Z) (Implementing API version 1.21.11-R0.1-SNAPSHOT)
You are running the latest version
Originally observed on
Leaves 1.21.11-DEV-HEAD@3c199cc (2026-03-15) (Implementing API version 1.21.11-R0.1-SNAPSHOT). Independently reproduced on stock Paper 1.21.11 by Leaves maintainer @Fortern (see Leaves #843 thread).Other
Background
This was originally reported downstream as LeavesMC/Leaves#843 ("【插件不兼容】僵尸猪灵在启用堆叠时经常失去愤怒行为"). Investigation went through several wrong turns before the upstream cause was pinned down:
Initially suspected as a Leaves-only regression (the original reporter's 6-hour test on stock Paper 1.21.11 found no issue). The "Paper is fine" claim turned out to be a sampling artefact — the bug is probabilistic, dependent on farm density, kill rate and friendly-fire frequency. Leaves core member @Fortern independently reproduced the same symptoms on stock Paper 1.21.11 and noted: "1.21.11 zombified piglins holding spears can friendly-fire each other, possibly causing aggro transfer."
Then suspected as RoseStacker-specific. Reported as Rosewood-Development/RoseStacker#174 and the RoseStacker maintainer committed
2cc706eto correctly transferpersistentAngerTarget(EntityReference) andpersistentAngerEndTimeondecreaseStackSize. That fix is correct and resolves the unstack-boundary path of the issue, but does not cover theSpearUseGoal-induced vanilla path described in this report — that path lives entirely insideNeutralMob.updatePersistentAngerand never goes through any plugin.Related Paper / Mojang work
EntityReferencecache invalidation on chunk unload/reload). Currently open, no investigation. Plausibly the same root-cause family.updateAnger == falsemobs (Bees); it intentionally leavesupdateAnger == truemobs (ZombifiedPiglin,EnderMan,Wolf,PolarBear) to refresh timer every tick. The scenario in this report is a sibling failure mode that the existing fix doesn't address.SpearUseGoalvariant; happy to file one if it would help the conversation.)Suggested fix direction (subject to maintainer judgement)
Extend
NeutralMob.updatePersistentAnger'sBRANCH 2cleanup to also clean up whenpersistentAngerTargetresolves to a non-Player (or to nothing) and the runtimetargethas been cleared:This is conceptually consistent with
BRANCH 1(which cleans up when target is dead and matches anger UUID) and avoids the timer-refresh interaction.If Paper would prefer to not patch this above vanilla, treating it as a known upstream issue and tracking the (to-be-filed) Mojang ticket would also be helpful for downstream forks/plugins to coordinate on.
Workaround currently in use
A periodic janitor that scans angry piglins and calls
NeutralMob.stopBeingAngry()on those with stale anger targets has been added to a fork of RoseStacker and verified to keepstuck=0over multi-hour runs on the affected farm. Reference implementation: https://github.com/Rosewood-Development/RoseStacker/ (commit15103d03). This is a stopgap; the proper fix belongs in vanilla / Paper.