From 81238f58db71f2f936578bdd2c12a34eb1809657 Mon Sep 17 00:00:00 2001 From: MLG-SERBUR <19244210+MLG-SERBUR@users.noreply.github.com> Date: Fri, 21 Aug 2026 01:59:13 +0000 Subject: [PATCH 1/8] fix: stop operating on dead entity in EntitySpinner spin loop Return immediately after selfDestruct() and never re-bind a dead instance via Bukkit.getEntity(uuid). --- .../net/awesomepowered/rotator/types/EntitySpinner.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/main/java/net/awesomepowered/rotator/types/EntitySpinner.java b/src/main/java/net/awesomepowered/rotator/types/EntitySpinner.java index f0fbaf0..39ed763 100644 --- a/src/main/java/net/awesomepowered/rotator/types/EntitySpinner.java +++ b/src/main/java/net/awesomepowered/rotator/types/EntitySpinner.java @@ -136,10 +136,11 @@ public void spoolUp() { if (entity.isDead()) { RotatoR.getMain().getLogger().log(Level.WARNING, "Oh noes! An entity is ded!"); selfDestruct(); + return; } if (!entity.isValid()) { Entity newEntity = Bukkit.getEntity(entity.getUniqueId()); - if (newEntity == null) + if (newEntity == null || newEntity.isDead()) return; entity = newEntity; } @@ -162,10 +163,11 @@ public void spoolUp() { if (entity.isDead()) { RotatoR.getMain().getLogger().log(Level.WARNING, "Oh noes! An entity is ded!"); selfDestruct(); + return; } if (!entity.isValid()) { Entity newEntity = Bukkit.getEntity(entity.getUniqueId()); - if (newEntity == null) + if (newEntity == null || newEntity.isDead()) return; entity = newEntity; } From 6207aa8ca70743a4bc41367635a2dfc18e5607c7 Mon Sep 17 00:00:00 2001 From: MLG-SERBUR <19244210+MLG-SERBUR@users.noreply.github.com> Date: Fri, 21 Aug 2026 03:15:06 +0000 Subject: [PATCH 2/8] fix: load espinner chunk synchronously and retry on ChunkLoadEvent Resolves startup race where Bukkit.getEntity(uuid) returned null because the chunk was not yet materialized after the async chunk load request. Force a synchronous load before lookup, and re-attempt spooling any un-spun entity spinner when its chunk loads. --- .../net/awesomepowered/rotator/RotatoR.java | 86 +++++++++++++------ 1 file changed, 62 insertions(+), 24 deletions(-) diff --git a/src/main/java/net/awesomepowered/rotator/RotatoR.java b/src/main/java/net/awesomepowered/rotator/RotatoR.java index 04a4095..6b34489 100644 --- a/src/main/java/net/awesomepowered/rotator/RotatoR.java +++ b/src/main/java/net/awesomepowered/rotator/RotatoR.java @@ -20,10 +20,12 @@ import org.bukkit.Location; import org.bukkit.World; import org.bukkit.block.BlockState; +import org.bukkit.Chunk; import org.bukkit.entity.Entity; import org.bukkit.entity.Player; import org.bukkit.event.EventHandler; import org.bukkit.event.Listener; +import org.bukkit.event.world.ChunkLoadEvent; import org.bukkit.event.world.WorldLoadEvent; import org.bukkit.event.world.WorldUnloadEvent; import org.bukkit.plugin.Plugin; @@ -63,6 +65,31 @@ public void onWorldLoad(WorldLoadEvent event) { spoolSpinners(); } @EventHandler + public void onChunkLoad(ChunkLoadEvent event) { + if (getConfig().getConfigurationSection("espinner") == null) { + return; + } + Chunk loaded = event.getChunk(); + for (String s : getConfig().getConfigurationSection("espinner").getKeys(false)) { + if (entitySpinners.containsKey(UUID.fromString(s))) { + continue; + } + String location = getConfig().getString("espinner." + s + ".loc"); + if (location == null) { + continue; + } + Location loc = stringToLoc(location); + if (loc == null || loc.getWorld() == null) { + continue; + } + if (loaded.getWorld().equals(loc.getWorld()) + && loaded.getX() == (loc.getBlockX() >> 4) + && loaded.getZ() == (loc.getBlockZ() >> 4)) { + spoolEntitySpinner(s); + } + } + } + @EventHandler public void onWorldUnload(WorldUnloadEvent event) { World world = event.getWorld(); blockSpinners.entrySet().removeIf(entry -> { @@ -148,35 +175,46 @@ public void spoolSpinners() { } else { for (String s : getConfig().getConfigurationSection("espinner").getKeys(false)) { debug("Loading espinner", s); - String location = getConfig().getString("espinner."+s+".loc"); - if (location != null) { - Location loc = stringToLoc(location); - if (loc != null && loc.getWorld() != null) { - loc.getChunk().load(false); - } - } - Entity entity = Bukkit.getEntity(UUID.fromString(s)); - if (entity != null && !entitySpinners.containsKey(UUID.fromString(s)) && Spinner.isSpinnable(entity)) { - debug("It's espinnable"); - String sound = getConfig().getString("espinner."+s+".sound"); - String effect = getConfig().getString("espinner."+s+".effect"); - String particle = getConfig().getString("espinner."+s+".particle"); - int rpm = getConfig().getInt("espinner."+s+".rpm", this.rpm); - double yaw = getConfig().getDouble("espinner."+s+".yaw", 12.5); - EntitySpinner entitySpinner = new EntitySpinner(entity, 0, rpm); - entitySpinner.setEffect(effect); - entitySpinner.setSound(sound); - entitySpinner.setParticle(particle); - entitySpinner.setYawChange(yaw); - debug( "Main", "Spooling up espinner id " + s, "RPM: " + rpm, "Sound: " + sound, "Effect: " + effect, "Particle: " + particle, "Yaw: " + yaw); - entitySpinner.spoolUp(); - entitySpinners.put(UUID.fromString(s), entitySpinner); - } + spoolEntitySpinner(s); } } } + public void spoolEntitySpinner(String s) { + UUID uuid = UUID.fromString(s); + if (entitySpinners.containsKey(uuid)) { + return; + } + Entity entity = Bukkit.getEntity(uuid); + if (entity == null) { + String location = getConfig().getString("espinner." + s + ".loc"); + if (location != null) { + Location loc = stringToLoc(location); + if (loc != null && loc.getWorld() != null) { + loc.getWorld().loadChunk(loc.getBlockX() >> 4, loc.getBlockZ() >> 4, true); + entity = Bukkit.getEntity(uuid); + } + } + } + if (entity == null || entity.isDead() || !Spinner.isSpinnable(entity)) { + return; + } + String sound = getConfig().getString("espinner." + s + ".sound"); + String effect = getConfig().getString("espinner." + s + ".effect"); + String particle = getConfig().getString("espinner." + s + ".particle"); + int rpm = getConfig().getInt("espinner." + s + ".rpm", this.rpm); + double yaw = getConfig().getDouble("espinner." + s + ".yaw", 12.5); + EntitySpinner entitySpinner = new EntitySpinner(entity, 0, rpm); + entitySpinner.setEffect(effect); + entitySpinner.setSound(sound); + entitySpinner.setParticle(particle); + entitySpinner.setYawChange(yaw); + debug("Main", "Spooling up espinner id " + s, "RPM: " + rpm, "Sound: " + sound, "Effect: " + effect, "Particle: " + particle, "Yaw: " + yaw); + entitySpinner.spoolUp(); + entitySpinners.put(uuid, entitySpinner); + } + public void saveSpinners() { /* Sets the entries to null to remove dead/stopped spinners. From 8815d2473e01450b024a904fe1d223403290e290 Mon Sep 17 00:00:00 2001 From: MLG-SERBUR <19244210+MLG-SERBUR@users.noreply.github.com> Date: Fri, 21 Aug 2026 04:28:00 +0000 Subject: [PATCH 3/8] fix: re-acquire live entity by UUID before declaring dead; add diagnostics The held entity reference goes stale after its chunk unloads/reloads, so entity.isDead() on the stale ref falsely killed the spinner on rejoin. resolveEntity() now re-fetches the live entity by UUID first: only self-destructs if no live entity exists, otherwise pauses while unloaded and rebinds when the chunk reloads. Adds WARNING/debug logging in the spin loop and in spoolEntitySpinner/ChunkLoadEvent to trace spool and dead-detection. --- .../net/awesomepowered/rotator/RotatoR.java | 11 +++- .../rotator/types/EntitySpinner.java | 55 ++++++++++++++----- 2 files changed, 51 insertions(+), 15 deletions(-) diff --git a/src/main/java/net/awesomepowered/rotator/RotatoR.java b/src/main/java/net/awesomepowered/rotator/RotatoR.java index 6b34489..3b96778 100644 --- a/src/main/java/net/awesomepowered/rotator/RotatoR.java +++ b/src/main/java/net/awesomepowered/rotator/RotatoR.java @@ -85,6 +85,7 @@ public void onChunkLoad(ChunkLoadEvent event) { if (loaded.getWorld().equals(loc.getWorld()) && loaded.getX() == (loc.getBlockX() >> 4) && loaded.getZ() == (loc.getBlockZ() >> 4)) { + debug("eSpin", "ChunkLoadEvent: attempting to spool espinner " + s); spoolEntitySpinner(s); } } @@ -197,7 +198,15 @@ public void spoolEntitySpinner(String s) { } } } - if (entity == null || entity.isDead() || !Spinner.isSpinnable(entity)) { + if (entity == null) { + getLogger().log(Level.WARNING, "[espinner] Could not find entity " + s + + " (loc=" + getConfig().getString("espinner." + s + ".loc") + + "). It may not be loaded yet or was removed."); + return; + } + if (entity.isDead() || !Spinner.isSpinnable(entity)) { + getLogger().log(Level.WARNING, "[espinner] Entity " + s + " is " + + (entity.isDead() ? "dead" : "not spinnable") + ", skipping spool."); return; } String sound = getConfig().getString("espinner." + s + ".sound"); diff --git a/src/main/java/net/awesomepowered/rotator/types/EntitySpinner.java b/src/main/java/net/awesomepowered/rotator/types/EntitySpinner.java index 39ed763..f685e69 100644 --- a/src/main/java/net/awesomepowered/rotator/types/EntitySpinner.java +++ b/src/main/java/net/awesomepowered/rotator/types/EntitySpinner.java @@ -122,6 +122,27 @@ public void selfDestruct() { RotatoR.getMain().entitySpinners.remove(entity.getUniqueId()); } + /** + * Re-acquire the live entity if our held reference went stale (e.g. chunk unload/reload). + * @return 0 = live and ready, 1 = not in a loaded chunk right now (pause, retry later), 2 = truly dead + */ + private int resolveEntity() { + if (entity != null && !entity.isDead() && entity.isValid()) { + return 0; + } + Entity live = Bukkit.getEntity(entity.getUniqueId()); + if (live != null && !live.isDead()) { + entity = live; + RotatoR.getMain().debug("eSpin", "Re-acquired live entity " + entity.getUniqueId() + + " type=" + entity.getType() + " valid=" + entity.isValid()); + return 0; + } + if (live != null) { + return 2; // a live entity with this UUID exists but is dead + } + return 1; // not currently in any loaded chunk + } + public void spoolUp() { Location constant = entity.getLocation(); RotatoR.getMain().debug("eSpool","Using mode 0"); @@ -133,16 +154,19 @@ public void spoolUp() { if (rotatorSpinEvent.isCancelled()) { return; } - if (entity.isDead()) { - RotatoR.getMain().getLogger().log(Level.WARNING, "Oh noes! An entity is ded!"); + int status = resolveEntity(); + if (status == 2) { + RotatoR.getMain().getLogger().log(Level.WARNING, + "Oh noes! An entity is ded! uuid=" + entity.getUniqueId() + + " type=" + entity.getType() + + " world=" + (entity.getWorld() != null ? entity.getWorld().getName() : "null") + + " liveLookup=" + (Bukkit.getEntity(entity.getUniqueId()) != null)); selfDestruct(); return; } - if (!entity.isValid()) { - Entity newEntity = Bukkit.getEntity(entity.getUniqueId()); - if (newEntity == null || newEntity.isDead()) - return; - entity = newEntity; + if (status == 1) { + RotatoR.getMain().debug("eSpin", "Entity " + entity.getUniqueId() + " not in a loaded chunk, pausing spin task"); + return; } ItemFrame itemFrame = (ItemFrame) entity; if (mode == 0) { @@ -160,16 +184,19 @@ public void spoolUp() { if (rotatorSpinEvent.isCancelled()) { return; } - if (entity.isDead()) { - RotatoR.getMain().getLogger().log(Level.WARNING, "Oh noes! An entity is ded!"); + int status = resolveEntity(); + if (status == 2) { + RotatoR.getMain().getLogger().log(Level.WARNING, + "Oh noes! An entity is ded! uuid=" + entity.getUniqueId() + + " type=" + entity.getType() + + " world=" + (entity.getWorld() != null ? entity.getWorld().getName() : "null") + + " liveLookup=" + (Bukkit.getEntity(entity.getUniqueId()) != null)); selfDestruct(); return; } - if (!entity.isValid()) { - Entity newEntity = Bukkit.getEntity(entity.getUniqueId()); - if (newEntity == null || newEntity.isDead()) - return; - entity = newEntity; + if (status == 1) { + RotatoR.getMain().debug("eSpin", "Entity " + entity.getUniqueId() + " not in a loaded chunk, pausing spin task"); + return; } constant.setYaw((float) trouble.getAndAdd(yawChange) % 360); //shhh entity.teleport(constant); From c30583eb130433706ccc5d736c414963a6d1e3d0 Mon Sep 17 00:00:00 2001 From: MLG-SERBUR <19244210+MLG-SERBUR@users.noreply.github.com> Date: Sat, 22 Aug 2026 04:55:29 +0000 Subject: [PATCH 4/8] fix: prevent re-entrant double-spool of espinner spoolEntitySpinner() calls World.loadChunk(), which fires ChunkLoadEvent synchronously; the new ChunkLoadEvent handler re-entered spoolEntitySpinner for the same UUID before the outer call registered it, producing two spin tasks (only one tracked by taskID, leaking the other). Guard with a spoolingNow set so a UUID mid-spool is skipped. --- src/main/java/net/awesomepowered/rotator/RotatoR.java | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/main/java/net/awesomepowered/rotator/RotatoR.java b/src/main/java/net/awesomepowered/rotator/RotatoR.java index 3b96778..fbd6a13 100644 --- a/src/main/java/net/awesomepowered/rotator/RotatoR.java +++ b/src/main/java/net/awesomepowered/rotator/RotatoR.java @@ -32,9 +32,12 @@ import org.bukkit.plugin.java.JavaPlugin; import java.util.Arrays; +import java.util.Collections; import java.util.HashMap; import java.util.Map; +import java.util.Set; import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; import java.util.logging.Level; public final class RotatoR extends JavaPlugin { @@ -43,6 +46,7 @@ public final class RotatoR extends JavaPlugin { public Map blockSpinners = new HashMap<>(); public Map entitySpinners = new HashMap<>(); public Map leSigners = new HashMap<>(); //todo make lesigner object instead + private final Set spoolingNow = Collections.newSetFromMap(new ConcurrentHashMap<>()); public int rpm = 10; public static boolean debug = false; public static boolean isPremium = false; @@ -184,9 +188,10 @@ public void spoolSpinners() { public void spoolEntitySpinner(String s) { UUID uuid = UUID.fromString(s); - if (entitySpinners.containsKey(uuid)) { + if (entitySpinners.containsKey(uuid) || !spoolingNow.add(uuid)) { return; } + try { Entity entity = Bukkit.getEntity(uuid); if (entity == null) { String location = getConfig().getString("espinner." + s + ".loc"); @@ -222,6 +227,9 @@ public void spoolEntitySpinner(String s) { debug("Main", "Spooling up espinner id " + s, "RPM: " + rpm, "Sound: " + sound, "Effect: " + effect, "Particle: " + particle, "Yaw: " + yaw); entitySpinner.spoolUp(); entitySpinners.put(uuid, entitySpinner); + } finally { + spoolingNow.remove(uuid); + } } public void saveSpinners() { From b607630cf1d918718117801327073de25d52367f Mon Sep 17 00:00:00 2001 From: MLG-SERBUR <19244210+MLG-SERBUR@users.noreply.github.com> Date: Sat, 22 Aug 2026 05:10:50 +0000 Subject: [PATCH 5/8] diag: log entity state transitions; drop forced chunk load resolveEntity() now logs a full snapshot whenever the held reference is unhealthy (heldDead/heldValid, liveLookup present/absent, liveDead/liveValid, sameInstance) plus an originalLogicWouldSelfDestruct flag, throttled to state transitions. Removed the forced World.loadChunk() from spoolEntitySpinner: unspun espinners now wait for their natural ChunkLoadEvent instead of force-loading/generating chunks at startup. --- .../net/awesomepowered/rotator/RotatoR.java | 16 +---- .../rotator/types/EntitySpinner.java | 72 +++++++++++-------- 2 files changed, 45 insertions(+), 43 deletions(-) diff --git a/src/main/java/net/awesomepowered/rotator/RotatoR.java b/src/main/java/net/awesomepowered/rotator/RotatoR.java index fbd6a13..a3d1fb9 100644 --- a/src/main/java/net/awesomepowered/rotator/RotatoR.java +++ b/src/main/java/net/awesomepowered/rotator/RotatoR.java @@ -194,19 +194,9 @@ public void spoolEntitySpinner(String s) { try { Entity entity = Bukkit.getEntity(uuid); if (entity == null) { - String location = getConfig().getString("espinner." + s + ".loc"); - if (location != null) { - Location loc = stringToLoc(location); - if (loc != null && loc.getWorld() != null) { - loc.getWorld().loadChunk(loc.getBlockX() >> 4, loc.getBlockZ() >> 4, true); - entity = Bukkit.getEntity(uuid); - } - } - } - if (entity == null) { - getLogger().log(Level.WARNING, "[espinner] Could not find entity " + s - + " (loc=" + getConfig().getString("espinner." + s + ".loc") - + "). It may not be loaded yet or was removed."); + getLogger().log(Level.WARNING, "[espinner] Entity " + s + " not loaded yet (chunk not loaded)," + + " waiting for its ChunkLoadEvent. loc=" + + getConfig().getString("espinner." + s + ".loc")); return; } if (entity.isDead() || !Spinner.isSpinnable(entity)) { diff --git a/src/main/java/net/awesomepowered/rotator/types/EntitySpinner.java b/src/main/java/net/awesomepowered/rotator/types/EntitySpinner.java index f685e69..c66433e 100644 --- a/src/main/java/net/awesomepowered/rotator/types/EntitySpinner.java +++ b/src/main/java/net/awesomepowered/rotator/types/EntitySpinner.java @@ -15,6 +15,7 @@ import org.bukkit.entity.ItemFrame; import org.bukkit.entity.LivingEntity; +import java.util.UUID; import java.util.logging.Level; public class EntitySpinner implements Spinnable { @@ -124,23 +125,48 @@ public void selfDestruct() { /** * Re-acquire the live entity if our held reference went stale (e.g. chunk unload/reload). + * Logs a full state snapshot whenever the held reference is unhealthy; throttled to state + * transitions so a paused spinner doesn't spam. * @return 0 = live and ready, 1 = not in a loaded chunk right now (pause, retry later), 2 = truly dead */ + private int lastResolveStatus = 0; + private int resolveEntity() { if (entity != null && !entity.isDead() && entity.isValid()) { + lastResolveStatus = 0; return 0; } - Entity live = Bukkit.getEntity(entity.getUniqueId()); + // Held reference is unhealthy; snapshot everything BEFORE touching anything + UUID uuid = entity.getUniqueId(); + boolean heldDead = entity.isDead(); + boolean heldValid = entity.isValid(); + Entity live = Bukkit.getEntity(uuid); + String diag = "uuid=" + uuid + + " type=" + entity.getType() + + " heldDead=" + heldDead + + " heldValid=" + heldValid + + " | liveLookup=" + (live == null ? "absent" : "present") + + (live == null ? "" : " liveDead=" + live.isDead() + + " liveValid=" + live.isValid() + + " sameInstance=" + (live == entity)) + + " | originalLogicWouldSelfDestruct=" + heldDead; + + int status; if (live != null && !live.isDead()) { entity = live; - RotatoR.getMain().debug("eSpin", "Re-acquired live entity " + entity.getUniqueId() - + " type=" + entity.getType() + " valid=" + entity.isValid()); - return 0; - } - if (live != null) { - return 2; // a live entity with this UUID exists but is dead + status = 0; + RotatoR.getMain().getLogger().log(Level.INFO, "[diag] stale held ref, re-acquired live entity: " + diag); + } else if (live != null) { + status = 2; + RotatoR.getMain().getLogger().log(Level.WARNING, "Oh noes! An entity is ded! " + diag); + } else { + status = 1; + if (lastResolveStatus != 1) { + RotatoR.getMain().debug("eSpin", "pausing spin task, entity not in any loaded chunk: " + diag); + } } - return 1; // not currently in any loaded chunk + lastResolveStatus = status; + return status; } public void spoolUp() { @@ -155,17 +181,10 @@ public void spoolUp() { return; } int status = resolveEntity(); - if (status == 2) { - RotatoR.getMain().getLogger().log(Level.WARNING, - "Oh noes! An entity is ded! uuid=" + entity.getUniqueId() - + " type=" + entity.getType() - + " world=" + (entity.getWorld() != null ? entity.getWorld().getName() : "null") - + " liveLookup=" + (Bukkit.getEntity(entity.getUniqueId()) != null)); - selfDestruct(); - return; - } - if (status == 1) { - RotatoR.getMain().debug("eSpin", "Entity " + entity.getUniqueId() + " not in a loaded chunk, pausing spin task"); + if (status != 0) { + if (status == 2) { + selfDestruct(); + } return; } ItemFrame itemFrame = (ItemFrame) entity; @@ -185,17 +204,10 @@ public void spoolUp() { return; } int status = resolveEntity(); - if (status == 2) { - RotatoR.getMain().getLogger().log(Level.WARNING, - "Oh noes! An entity is ded! uuid=" + entity.getUniqueId() - + " type=" + entity.getType() - + " world=" + (entity.getWorld() != null ? entity.getWorld().getName() : "null") - + " liveLookup=" + (Bukkit.getEntity(entity.getUniqueId()) != null)); - selfDestruct(); - return; - } - if (status == 1) { - RotatoR.getMain().debug("eSpin", "Entity " + entity.getUniqueId() + " not in a loaded chunk, pausing spin task"); + if (status != 0) { + if (status == 2) { + selfDestruct(); + } return; } constant.setYaw((float) trouble.getAndAdd(yawChange) % 360); //shhh From f86b6b706736fd5772685e7edcb1179e045a361f Mon Sep 17 00:00:00 2001 From: MLG-SERBUR <19244210+MLG-SERBUR@users.noreply.github.com> Date: Sat, 22 Aug 2026 05:34:38 +0000 Subject: [PATCH 6/8] fix: simplify entity resolution logic in EntitySpinner; remove stale reference handling --- .../net/awesomepowered/rotator/RotatoR.java | 11 +---- .../rotator/types/EntitySpinner.java | 48 +++---------------- 2 files changed, 8 insertions(+), 51 deletions(-) diff --git a/src/main/java/net/awesomepowered/rotator/RotatoR.java b/src/main/java/net/awesomepowered/rotator/RotatoR.java index a3d1fb9..6ec70da 100644 --- a/src/main/java/net/awesomepowered/rotator/RotatoR.java +++ b/src/main/java/net/awesomepowered/rotator/RotatoR.java @@ -32,12 +32,9 @@ import org.bukkit.plugin.java.JavaPlugin; import java.util.Arrays; -import java.util.Collections; import java.util.HashMap; import java.util.Map; -import java.util.Set; import java.util.UUID; -import java.util.concurrent.ConcurrentHashMap; import java.util.logging.Level; public final class RotatoR extends JavaPlugin { @@ -46,7 +43,6 @@ public final class RotatoR extends JavaPlugin { public Map blockSpinners = new HashMap<>(); public Map entitySpinners = new HashMap<>(); public Map leSigners = new HashMap<>(); //todo make lesigner object instead - private final Set spoolingNow = Collections.newSetFromMap(new ConcurrentHashMap<>()); public int rpm = 10; public static boolean debug = false; public static boolean isPremium = false; @@ -188,10 +184,10 @@ public void spoolSpinners() { public void spoolEntitySpinner(String s) { UUID uuid = UUID.fromString(s); - if (entitySpinners.containsKey(uuid) || !spoolingNow.add(uuid)) { + if (entitySpinners.containsKey(uuid)) { return; } - try { + Entity entity = Bukkit.getEntity(uuid); if (entity == null) { getLogger().log(Level.WARNING, "[espinner] Entity " + s + " not loaded yet (chunk not loaded)," @@ -217,9 +213,6 @@ public void spoolEntitySpinner(String s) { debug("Main", "Spooling up espinner id " + s, "RPM: " + rpm, "Sound: " + sound, "Effect: " + effect, "Particle: " + particle, "Yaw: " + yaw); entitySpinner.spoolUp(); entitySpinners.put(uuid, entitySpinner); - } finally { - spoolingNow.remove(uuid); - } } public void saveSpinners() { diff --git a/src/main/java/net/awesomepowered/rotator/types/EntitySpinner.java b/src/main/java/net/awesomepowered/rotator/types/EntitySpinner.java index c66433e..29404cf 100644 --- a/src/main/java/net/awesomepowered/rotator/types/EntitySpinner.java +++ b/src/main/java/net/awesomepowered/rotator/types/EntitySpinner.java @@ -8,16 +8,12 @@ import org.bukkit.Effect; import org.bukkit.Location; import org.bukkit.Particle; -import org.bukkit.Rotation; import org.bukkit.Sound; import org.bukkit.block.BlockFace; import org.bukkit.entity.Entity; import org.bukkit.entity.ItemFrame; import org.bukkit.entity.LivingEntity; -import java.util.UUID; -import java.util.logging.Level; - public class EntitySpinner implements Spinnable { private Entity entity; @@ -123,50 +119,18 @@ public void selfDestruct() { RotatoR.getMain().entitySpinners.remove(entity.getUniqueId()); } - /** - * Re-acquire the live entity if our held reference went stale (e.g. chunk unload/reload). - * Logs a full state snapshot whenever the held reference is unhealthy; throttled to state - * transitions so a paused spinner doesn't spam. - * @return 0 = live and ready, 1 = not in a loaded chunk right now (pause, retry later), 2 = truly dead - */ - private int lastResolveStatus = 0; - private int resolveEntity() { if (entity != null && !entity.isDead() && entity.isValid()) { - lastResolveStatus = 0; return 0; } - // Held reference is unhealthy; snapshot everything BEFORE touching anything - UUID uuid = entity.getUniqueId(); - boolean heldDead = entity.isDead(); - boolean heldValid = entity.isValid(); - Entity live = Bukkit.getEntity(uuid); - String diag = "uuid=" + uuid - + " type=" + entity.getType() - + " heldDead=" + heldDead - + " heldValid=" + heldValid - + " | liveLookup=" + (live == null ? "absent" : "present") - + (live == null ? "" : " liveDead=" + live.isDead() - + " liveValid=" + live.isValid() - + " sameInstance=" + (live == entity)) - + " | originalLogicWouldSelfDestruct=" + heldDead; - - int status; - if (live != null && !live.isDead()) { + + Entity live = Bukkit.getEntity(entity.getUniqueId()); + if (live != null && !live.isDead() && live.isValid()) { entity = live; - status = 0; - RotatoR.getMain().getLogger().log(Level.INFO, "[diag] stale held ref, re-acquired live entity: " + diag); - } else if (live != null) { - status = 2; - RotatoR.getMain().getLogger().log(Level.WARNING, "Oh noes! An entity is ded! " + diag); - } else { - status = 1; - if (lastResolveStatus != 1) { - RotatoR.getMain().debug("eSpin", "pausing spin task, entity not in any loaded chunk: " + diag); - } + return 0; } - lastResolveStatus = status; - return status; + + return live == null ? 1 : 2; } public void spoolUp() { From 3b022186bb41ee2842c70ce8b77e79d58c5fa6b7 Mon Sep 17 00:00:00 2001 From: MLG-SERBUR <19244210+MLG-SERBUR@users.noreply.github.com> Date: Sat, 22 Aug 2026 05:56:02 +0000 Subject: [PATCH 7/8] refactor: streamline entity resolution in EntitySpinner; replace resolveEntity with refreshEntity --- .../net/awesomepowered/rotator/RotatoR.java | 89 ++++++------------- .../rotator/types/EntitySpinner.java | 31 +++---- 2 files changed, 40 insertions(+), 80 deletions(-) diff --git a/src/main/java/net/awesomepowered/rotator/RotatoR.java b/src/main/java/net/awesomepowered/rotator/RotatoR.java index 6ec70da..1ef71e5 100644 --- a/src/main/java/net/awesomepowered/rotator/RotatoR.java +++ b/src/main/java/net/awesomepowered/rotator/RotatoR.java @@ -20,12 +20,10 @@ import org.bukkit.Location; import org.bukkit.World; import org.bukkit.block.BlockState; -import org.bukkit.Chunk; import org.bukkit.entity.Entity; import org.bukkit.entity.Player; import org.bukkit.event.EventHandler; import org.bukkit.event.Listener; -import org.bukkit.event.world.ChunkLoadEvent; import org.bukkit.event.world.WorldLoadEvent; import org.bukkit.event.world.WorldUnloadEvent; import org.bukkit.plugin.Plugin; @@ -65,32 +63,6 @@ public void onWorldLoad(WorldLoadEvent event) { spoolSpinners(); } @EventHandler - public void onChunkLoad(ChunkLoadEvent event) { - if (getConfig().getConfigurationSection("espinner") == null) { - return; - } - Chunk loaded = event.getChunk(); - for (String s : getConfig().getConfigurationSection("espinner").getKeys(false)) { - if (entitySpinners.containsKey(UUID.fromString(s))) { - continue; - } - String location = getConfig().getString("espinner." + s + ".loc"); - if (location == null) { - continue; - } - Location loc = stringToLoc(location); - if (loc == null || loc.getWorld() == null) { - continue; - } - if (loaded.getWorld().equals(loc.getWorld()) - && loaded.getX() == (loc.getBlockX() >> 4) - && loaded.getZ() == (loc.getBlockZ() >> 4)) { - debug("eSpin", "ChunkLoadEvent: attempting to spool espinner " + s); - spoolEntitySpinner(s); - } - } - } - @EventHandler public void onWorldUnload(WorldUnloadEvent event) { World world = event.getWorld(); blockSpinners.entrySet().removeIf(entry -> { @@ -176,45 +148,38 @@ public void spoolSpinners() { } else { for (String s : getConfig().getConfigurationSection("espinner").getKeys(false)) { debug("Loading espinner", s); - spoolEntitySpinner(s); + UUID uuid = UUID.fromString(s); + if (entitySpinners.containsKey(uuid)) { + continue; + } + Entity entity = Bukkit.getEntity(uuid); + if (entity == null) { + getLogger().log(Level.WARNING, "[espinner] Entity " + s + " not loaded, skipping spool."); + continue; + } + if (entity.isDead() || !Spinner.isSpinnable(entity)) { + getLogger().log(Level.WARNING, "[espinner] Entity " + s + " is " + + (entity.isDead() ? "dead" : "not spinnable") + ", skipping spool."); + continue; + } + String sound = getConfig().getString("espinner." + s + ".sound"); + String effect = getConfig().getString("espinner." + s + ".effect"); + String particle = getConfig().getString("espinner." + s + ".particle"); + int rpm = getConfig().getInt("espinner." + s + ".rpm", this.rpm); + double yaw = getConfig().getDouble("espinner." + s + ".yaw", 12.5); + EntitySpinner entitySpinner = new EntitySpinner(entity, 0, rpm); + entitySpinner.setEffect(effect); + entitySpinner.setSound(sound); + entitySpinner.setParticle(particle); + entitySpinner.setYawChange(yaw); + debug("Main", "Spooling up espinner id " + s, "RPM: " + rpm, "Sound: " + sound, "Effect: " + effect, "Particle: " + particle, "Yaw: " + yaw); + entitySpinner.spoolUp(); + entitySpinners.put(uuid, entitySpinner); } } } - public void spoolEntitySpinner(String s) { - UUID uuid = UUID.fromString(s); - if (entitySpinners.containsKey(uuid)) { - return; - } - - Entity entity = Bukkit.getEntity(uuid); - if (entity == null) { - getLogger().log(Level.WARNING, "[espinner] Entity " + s + " not loaded yet (chunk not loaded)," - + " waiting for its ChunkLoadEvent. loc=" - + getConfig().getString("espinner." + s + ".loc")); - return; - } - if (entity.isDead() || !Spinner.isSpinnable(entity)) { - getLogger().log(Level.WARNING, "[espinner] Entity " + s + " is " - + (entity.isDead() ? "dead" : "not spinnable") + ", skipping spool."); - return; - } - String sound = getConfig().getString("espinner." + s + ".sound"); - String effect = getConfig().getString("espinner." + s + ".effect"); - String particle = getConfig().getString("espinner." + s + ".particle"); - int rpm = getConfig().getInt("espinner." + s + ".rpm", this.rpm); - double yaw = getConfig().getDouble("espinner." + s + ".yaw", 12.5); - EntitySpinner entitySpinner = new EntitySpinner(entity, 0, rpm); - entitySpinner.setEffect(effect); - entitySpinner.setSound(sound); - entitySpinner.setParticle(particle); - entitySpinner.setYawChange(yaw); - debug("Main", "Spooling up espinner id " + s, "RPM: " + rpm, "Sound: " + sound, "Effect: " + effect, "Particle: " + particle, "Yaw: " + yaw); - entitySpinner.spoolUp(); - entitySpinners.put(uuid, entitySpinner); - } - public void saveSpinners() { /* Sets the entries to null to remove dead/stopped spinners. diff --git a/src/main/java/net/awesomepowered/rotator/types/EntitySpinner.java b/src/main/java/net/awesomepowered/rotator/types/EntitySpinner.java index 29404cf..49cda1d 100644 --- a/src/main/java/net/awesomepowered/rotator/types/EntitySpinner.java +++ b/src/main/java/net/awesomepowered/rotator/types/EntitySpinner.java @@ -119,18 +119,21 @@ public void selfDestruct() { RotatoR.getMain().entitySpinners.remove(entity.getUniqueId()); } - private int resolveEntity() { - if (entity != null && !entity.isDead() && entity.isValid()) { - return 0; + private boolean refreshEntity() { + if (!entity.isDead() && entity.isValid()) { + return true; } Entity live = Bukkit.getEntity(entity.getUniqueId()); - if (live != null && !live.isDead() && live.isValid()) { - entity = live; - return 0; + if (live == null) { + return false; } - - return live == null ? 1 : 2; + if (live.isDead()) { + selfDestruct(); + return false; + } + entity = live; + return true; } public void spoolUp() { @@ -144,11 +147,7 @@ public void spoolUp() { if (rotatorSpinEvent.isCancelled()) { return; } - int status = resolveEntity(); - if (status != 0) { - if (status == 2) { - selfDestruct(); - } + if (!refreshEntity()) { return; } ItemFrame itemFrame = (ItemFrame) entity; @@ -167,11 +166,7 @@ public void spoolUp() { if (rotatorSpinEvent.isCancelled()) { return; } - int status = resolveEntity(); - if (status != 0) { - if (status == 2) { - selfDestruct(); - } + if (!refreshEntity()) { return; } constant.setYaw((float) trouble.getAndAdd(yawChange) % 360); //shhh From 31a5569eaa99921eb858d722414414b22808c298 Mon Sep 17 00:00:00 2001 From: MLG-SERBUR <19244210+MLG-SERBUR@users.noreply.github.com> Date: Sat, 22 Aug 2026 06:02:35 +0000 Subject: [PATCH 8/8] fix: improve entity validation in EntitySpinner; replace refreshEntity with inline checks --- .../rotator/types/EntitySpinner.java | 41 +++++++++---------- 1 file changed, 20 insertions(+), 21 deletions(-) diff --git a/src/main/java/net/awesomepowered/rotator/types/EntitySpinner.java b/src/main/java/net/awesomepowered/rotator/types/EntitySpinner.java index 49cda1d..6b005a6 100644 --- a/src/main/java/net/awesomepowered/rotator/types/EntitySpinner.java +++ b/src/main/java/net/awesomepowered/rotator/types/EntitySpinner.java @@ -119,23 +119,6 @@ public void selfDestruct() { RotatoR.getMain().entitySpinners.remove(entity.getUniqueId()); } - private boolean refreshEntity() { - if (!entity.isDead() && entity.isValid()) { - return true; - } - - Entity live = Bukkit.getEntity(entity.getUniqueId()); - if (live == null) { - return false; - } - if (live.isDead()) { - selfDestruct(); - return false; - } - entity = live; - return true; - } - public void spoolUp() { Location constant = entity.getLocation(); RotatoR.getMain().debug("eSpool","Using mode 0"); @@ -147,8 +130,16 @@ public void spoolUp() { if (rotatorSpinEvent.isCancelled()) { return; } - if (!refreshEntity()) { - return; + if (!entity.isValid() || entity.isDead()) { + Entity newEntity = Bukkit.getEntity(entity.getUniqueId()); + if (newEntity == null) { + return; + } + if (newEntity.isDead()) { + selfDestruct(); + return; + } + entity = newEntity; } ItemFrame itemFrame = (ItemFrame) entity; if (mode == 0) { @@ -166,8 +157,16 @@ public void spoolUp() { if (rotatorSpinEvent.isCancelled()) { return; } - if (!refreshEntity()) { - return; + if (!entity.isValid() || entity.isDead()) { + Entity newEntity = Bukkit.getEntity(entity.getUniqueId()); + if (newEntity == null) { + return; + } + if (newEntity.isDead()) { + selfDestruct(); + return; + } + entity = newEntity; } constant.setYaw((float) trouble.getAndAdd(yawChange) % 360); //shhh entity.teleport(constant);