Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion src/main/java/fr/jachou/reanimatemc/data/ReanimatorNPC.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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);
}
}

/**
Expand All @@ -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;
}

Expand All @@ -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();
}
}
Expand Down
24 changes: 24 additions & 0 deletions src/main/java/fr/jachou/reanimatemc/managers/KOManager.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ public void save(Iterable<ReanimatorNPC> 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());
Expand Down Expand Up @@ -94,6 +96,8 @@ public List<PendingRestore> 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"));
Expand All @@ -111,7 +115,7 @@ public List<PendingRestore> 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) { }
}

Expand All @@ -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;
}
}
}
18 changes: 12 additions & 6 deletions src/main/java/fr/jachou/reanimatemc/managers/NPCSummonManager.java
Original file line number Diff line number Diff line change
Expand Up @@ -404,23 +404,29 @@ private void restoreFromDisk() {
List<NPCPersistenceManager.PendingRestore> 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.");
}
}

/**
* 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);
Expand All @@ -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; }
Expand Down
Loading