From ea101de1168805e00155c30f14aeac82e53f0e6b Mon Sep 17 00:00:00 2001 From: Angel Ramirez Date: Tue, 30 Jun 2026 20:27:23 -0600 Subject: [PATCH 1/2] fix: prevent Warden and new mobs from targeting/attacking KO'd players Fixed a critical bug where Warden and other mobs would continue attacking players even after they were knocked down (KO state). Root cause: - KOProtectionListener listened to EntityTargetEvent, but Bukkit dispatches EntityTargetLivingEntityEvent (separate HandlerList) for LivingEntity targets - This caused the listener to never fire for player targets - Warden accumulates anger through vibrations/smell independently of targeting Changes: 1. KOProtectionListener.java - Add EntityTargetLivingEntityEvent handler to catch all targeting attempts - Call warden.clearAnger(player) when canceling target - Clear Warden anger in onMobDamageKOPlayer if hit lands before cancellation 2. KOManager.java - New neutralizeNearbyWardens(player) runs every 20 ticks in enforcer task - Proactive scan for Wardens within 32 block radius - Reset anger/target toward KO'd player All protections respect knockout.mobs_attack_ko config flag. --- .../listeners/KOProtectionListener.java | 33 +++++++++++++++++-- .../reanimatemc/managers/KOManager.java | 24 ++++++++++++++ 2 files changed, 54 insertions(+), 3 deletions(-) diff --git a/src/main/java/fr/jachou/reanimatemc/listeners/KOProtectionListener.java b/src/main/java/fr/jachou/reanimatemc/listeners/KOProtectionListener.java index 665ae64..dbe21b8 100644 --- a/src/main/java/fr/jachou/reanimatemc/listeners/KOProtectionListener.java +++ b/src/main/java/fr/jachou/reanimatemc/listeners/KOProtectionListener.java @@ -6,11 +6,13 @@ import org.bukkit.entity.Mob; import org.bukkit.entity.Player; import org.bukkit.entity.Projectile; +import org.bukkit.entity.Warden; import org.bukkit.event.EventHandler; import org.bukkit.event.EventPriority; import org.bukkit.event.Listener; import org.bukkit.event.entity.EntityDamageByEntityEvent; import org.bukkit.event.entity.EntityTargetEvent; +import org.bukkit.event.entity.EntityTargetLivingEntityEvent; import org.bukkit.util.Vector; public class KOProtectionListener implements Listener { @@ -29,14 +31,32 @@ public void onEntityKnockback(EntityKnockbackEvent event) { event.setKnockback(new Vector(0, 0, 0)); } - /** Prevents new mobs from choosing a KO'd player as a target. */ + /** + * Prevents new mobs from choosing a KO'd player as a target. + * Players are always a LivingEntity, so vanilla mob AI fires + * EntityTargetLivingEntityEvent (a subclass with its own HandlerList) + * rather than the plain EntityTargetEvent. Both are handled here so + * no targeting attempt slips through, including the Warden's. + */ @EventHandler(priority = EventPriority.HIGH, ignoreCancelled = true) public void onEntityTarget(EntityTargetEvent event) { + blockTargetingIfKO(event); + } + + @EventHandler(priority = EventPriority.HIGH, ignoreCancelled = true) + public void onEntityTargetLiving(EntityTargetLivingEntityEvent event) { + blockTargetingIfKO(event); + } + + private void blockTargetingIfKO(EntityTargetEvent event) { if (!(event.getTarget() instanceof Player player)) return; if (!koManager.isKO(player)) return; if (ReanimateMC.getInstance().getConfig().getBoolean("knockout.mobs_attack_ko", false)) return; - if (!(event.getEntity() instanceof Mob)) return; + if (!(event.getEntity() instanceof Mob mob)) return; event.setCancelled(true); + if (mob instanceof Warden warden) { + warden.clearAnger(player); + } } /** @@ -52,10 +72,14 @@ public void onMobDamageKOPlayer(EntityDamageByEntityEvent event) { org.bukkit.entity.Entity damager = event.getDamager(); - // Direct melee attack from a mob + // Direct melee attack from a mob (includes the Warden's sonic boom, + // which is dealt as a direct hit from the Warden entity itself) if (damager instanceof Mob mob) { event.setCancelled(true); mob.setTarget(null); + if (mob instanceof Warden warden) { + warden.clearAnger(player); + } return; } @@ -64,6 +88,9 @@ public void onMobDamageKOPlayer(EntityDamageByEntityEvent event) { && projectile.getShooter() instanceof Mob mob) { event.setCancelled(true); mob.setTarget(null); + if (mob instanceof Warden warden) { + warden.clearAnger(player); + } projectile.remove(); } } diff --git a/src/main/java/fr/jachou/reanimatemc/managers/KOManager.java b/src/main/java/fr/jachou/reanimatemc/managers/KOManager.java index 93d1447..35146fe 100644 --- a/src/main/java/fr/jachou/reanimatemc/managers/KOManager.java +++ b/src/main/java/fr/jachou/reanimatemc/managers/KOManager.java @@ -19,8 +19,10 @@ import org.bukkit.ChatColor; import org.bukkit.Material; import org.bukkit.entity.ArmorStand; +import org.bukkit.entity.Entity; import org.bukkit.entity.EntityType; import org.bukkit.entity.Player; +import org.bukkit.entity.Warden; import org.bukkit.inventory.ItemStack; import org.bukkit.plugin.java.JavaPlugin; import org.bukkit.potion.PotionEffect; @@ -146,6 +148,7 @@ public void setKO(final Player player, int durationSeconds) { if (!isKO(player)) return; KOData d = koPlayers.get(player.getUniqueId()); if (d == null) return; + neutralizeNearbyWardens(player); boolean allowCrawl = plugin.getConfig().getBoolean("prone.allow_crawl", false); boolean crawling = d.isCrawling() && allowCrawl; if (crawling) { @@ -217,6 +220,27 @@ public void setKO(final Player player, int durationSeconds) { ReanimateMC.getInstance().getStatsManager().addKnockout(); } + /** + * Resets any nearby Warden's anger and target toward a KO'd player. + * The Warden builds anger from vibrations and smell independently of + * the normal target-selection goal, so it can still lock onto a player + * without ever firing an EntityTarget event. Running this alongside the + * reactive listeners keeps the player effectively invisible to it while KO'd. + */ + private void neutralizeNearbyWardens(Player player) { + if (plugin.getConfig().getBoolean("knockout.mobs_attack_ko", false)) return; + double radius = 32.0; + for (Entity entity : player.getNearbyEntities(radius, radius, radius)) { + if (!(entity instanceof Warden warden)) continue; + if (warden.getAnger(player) > 0) { + warden.clearAnger(player); + } + if (warden.getTarget() == player) { + warden.setTarget(null); + } + } + } + private void restoreListName(Player player, KOData data) { if (plugin.getConfig().getBoolean("tablist.enabled")) { String originalName = data.getOriginalListName(); From e5bdc052f1eb1083a94541314f5460145e2557c0 Mon Sep 17 00:00:00 2001 From: Angel Ramirez Date: Tue, 30 Jun 2026 20:45:33 -0600 Subject: [PATCH 2/2] fix: preserve NPC identity and data on server restart --- .../jachou/reanimatemc/data/ReanimatorNPC.java | 13 ++++++++++++- .../managers/NPCPersistenceManager.java | 13 +++++++++++-- .../reanimatemc/managers/NPCSummonManager.java | 18 ++++++++++++------ 3 files changed, 35 insertions(+), 9 deletions(-) diff --git a/src/main/java/fr/jachou/reanimatemc/data/ReanimatorNPC.java b/src/main/java/fr/jachou/reanimatemc/data/ReanimatorNPC.java index 7a735ea..e4c55d3 100644 --- a/src/main/java/fr/jachou/reanimatemc/data/ReanimatorNPC.java +++ b/src/main/java/fr/jachou/reanimatemc/data/ReanimatorNPC.java @@ -86,7 +86,18 @@ public String getDisplayName() { } public ReanimatorNPC(UUID ownerId, String ownerName, Entity entity, ReanimatorType type, long lifetimeSeconds) { - this.id = UUID.randomUUID(); + this(ownerId, ownerName, entity, type, lifetimeSeconds, null); + } + + /** + * Constructor for persistence restore: accepts a pre-existing NPC ID instead of generating a new one. + * Used when loading NPCs from disk to maintain the same NPC identity across restarts. + * + * @param npcId if null, generates UUID.randomUUID(); if provided, uses the restored ID + */ + public ReanimatorNPC(UUID ownerId, String ownerName, Entity entity, ReanimatorType type, + long lifetimeSeconds, UUID npcId) { + this.id = npcId != null ? npcId : UUID.randomUUID(); this.ownerId = ownerId; this.ownerName = ownerName; this.entity = entity; diff --git a/src/main/java/fr/jachou/reanimatemc/managers/NPCPersistenceManager.java b/src/main/java/fr/jachou/reanimatemc/managers/NPCPersistenceManager.java index 8c03b43..0211ab4 100644 --- a/src/main/java/fr/jachou/reanimatemc/managers/NPCPersistenceManager.java +++ b/src/main/java/fr/jachou/reanimatemc/managers/NPCPersistenceManager.java @@ -53,6 +53,8 @@ public void save(Iterable npcs) { if (expiresAt > 0 && (expiresAt - now) < 5000) continue; YamlConfiguration entry = new YamlConfiguration(); + entry.set("npcId", npc.getId().toString()); + entry.set("entityUuid", npc.getEntity().getUniqueId().toString()); entry.set("owner", npc.getOwnerId().toString()); entry.set("ownerName", npc.getOwnerName()); entry.set("type", npc.getType().name()); @@ -94,6 +96,8 @@ public List load() { if (!(obj instanceof YamlConfiguration)) continue; YamlConfiguration entry = (YamlConfiguration) obj; try { + UUID npcId = UUID.fromString(entry.getString("npcId", "")); + UUID entityUuid = UUID.fromString(entry.getString("entityUuid", "")); UUID ownerId = UUID.fromString(entry.getString("owner", "")); String ownerName= entry.getString("ownerName", "unknown"); ReanimatorType type = ReanimatorType.valueOf(entry.getString("type", "GOLEM")); @@ -111,7 +115,7 @@ public List load() { Player owner = Bukkit.getPlayer(ownerId); if (owner == null || !owner.isOnline()) continue; - result.add(new PendingRestore(owner, type, remainingSeconds, targetId)); + result.add(new PendingRestore(owner, type, remainingSeconds, targetId, npcId, entityUuid)); } catch (IllegalArgumentException ignored) { } } @@ -128,12 +132,17 @@ public static final class PendingRestore { public final ReanimatorType type; public final long lifetimeSeconds; // 0 = unlimited public final UUID targetId; + public final UUID npcId; + public final UUID entityUuid; - public PendingRestore(Player owner, ReanimatorType type, long lifetimeSeconds, UUID targetId) { + public PendingRestore(Player owner, ReanimatorType type, long lifetimeSeconds, UUID targetId, + UUID npcId, UUID entityUuid) { this.owner = owner; this.type = type; this.lifetimeSeconds = lifetimeSeconds; this.targetId = targetId; + this.npcId = npcId; + this.entityUuid = entityUuid; } } } diff --git a/src/main/java/fr/jachou/reanimatemc/managers/NPCSummonManager.java b/src/main/java/fr/jachou/reanimatemc/managers/NPCSummonManager.java index 0755ac2..5ae2290 100644 --- a/src/main/java/fr/jachou/reanimatemc/managers/NPCSummonManager.java +++ b/src/main/java/fr/jachou/reanimatemc/managers/NPCSummonManager.java @@ -404,8 +404,8 @@ private void restoreFromDisk() { List pending = persistence.load(); for (NPCPersistenceManager.PendingRestore pr : pending) { Player target = pr.targetId != null ? Bukkit.getPlayer(pr.targetId) : null; - // Use the saved remaining lifetime, not the config default - summonWithLifetime(pr.owner, pr.type, target, pr.lifetimeSeconds); + // Use the saved remaining lifetime and npcId, not config defaults + summonWithLifetime(pr.owner, pr.type, target, pr.lifetimeSeconds, pr.npcId); } if (!pending.isEmpty()) { plugin.getLogger().info("[ReanimateMC] Restored " + pending.size() + " NPC(s) from disk."); @@ -413,14 +413,20 @@ private void restoreFromDisk() { } /** - * Internal summon that overrides the lifetime from config with an explicit - * value. Used by persistence restore so golems don't get a fresh lifetime - * when the owner reconnects — they keep their remaining time. + * Internal summon that overrides the lifetime from config with an explicit value. + * Used by persistence restore so golems don't get a fresh lifetime when the owner + * reconnects — they keep their remaining time and NPC identity. * * @param lifetimeSeconds remaining seconds (0 = unlimited) + * @param npcId if null, generates a new UUID; if provided, uses the restored ID */ private boolean summonWithLifetime(Player summoner, ReanimatorType type, Player targetPlayer, long lifetimeSeconds) { + return summonWithLifetime(summoner, type, targetPlayer, lifetimeSeconds, null); + } + + private boolean summonWithLifetime(Player summoner, ReanimatorType type, + Player targetPlayer, long lifetimeSeconds, UUID npcId) { Player owner = (targetPlayer != null) ? targetPlayer : summoner; long lifetime = lifetimeSeconds > 0 ? lifetimeSeconds : plugin.getConfig().getLong("npc_summon." + type.name().toLowerCase() + ".lifetime_seconds", 600L); @@ -430,7 +436,7 @@ private boolean summonWithLifetime(Player summoner, ReanimatorType type, if (entity == null) return false; ReanimatorNPC npc = new ReanimatorNPC(owner.getUniqueId(), owner.getName(), - entity, type, lifetime); + entity, type, lifetime, npcId); NPCSummonedEvent event = new NPCSummonedEvent(summoner, npc); Bukkit.getPluginManager().callEvent(event); if (event.isCancelled()) { entity.remove(); return false; }