diff --git a/core/src/main/java/github/nighter/smartspawner/commands/clear/ClearHologramsSubCommand.java b/core/src/main/java/github/nighter/smartspawner/commands/clear/ClearHologramsSubCommand.java index f67a13ae..e39a20b7 100644 --- a/core/src/main/java/github/nighter/smartspawner/commands/clear/ClearHologramsSubCommand.java +++ b/core/src/main/java/github/nighter/smartspawner/commands/clear/ClearHologramsSubCommand.java @@ -37,7 +37,9 @@ public int execute(CommandContext context) { try { // Execute the Minecraft command to kill all text_display entities Bukkit.dispatchCommand(Bukkit.getConsoleSender(), "minecraft:kill @e[type=text_display]"); - + + plugin.getSpawnerManager().reloadAllHolograms(); + // Send success message to player plugin.getMessageService().sendMessage(sender, "hologram.cleared"); diff --git a/core/src/main/java/github/nighter/smartspawner/commands/hologram/HologramClearSubCommand.java b/core/src/main/java/github/nighter/smartspawner/commands/hologram/HologramClearSubCommand.java index 7d08950d..684f6607 100644 --- a/core/src/main/java/github/nighter/smartspawner/commands/hologram/HologramClearSubCommand.java +++ b/core/src/main/java/github/nighter/smartspawner/commands/hologram/HologramClearSubCommand.java @@ -37,7 +37,9 @@ public int execute(CommandContext context) { try { // Execute the Minecraft command to kill all text_display entities Bukkit.dispatchCommand(Bukkit.getConsoleSender(), "minecraft:kill @e[type=text_display]"); - + + plugin.getSpawnerManager().reloadAllHolograms(); + // Send success message to player plugin.getMessageService().sendMessage(sender, "hologram.cleared"); diff --git a/core/src/main/java/github/nighter/smartspawner/commands/hologram/SpawnerHologram.java b/core/src/main/java/github/nighter/smartspawner/commands/hologram/SpawnerHologram.java index 5be0eb94..958882cd 100644 --- a/core/src/main/java/github/nighter/smartspawner/commands/hologram/SpawnerHologram.java +++ b/core/src/main/java/github/nighter/smartspawner/commands/hologram/SpawnerHologram.java @@ -164,9 +164,20 @@ private static String formatOneDecimal(double value) { // Public update API // ------------------------------------------------------------------------- + public boolean isAlive() { + TextDisplay display = textDisplay.get(); + return display != null && display.isValid(); + } + public void updateText() { TextDisplay display = textDisplay.get(); - if (display == null || entityType == null) return; + if (entityType == null) return; + if (display == null) return; + if (!display.isValid()) { + textDisplay.set(null); + createHologram(); + return; + } // Compute the text on the calling (region) thread – avoids doing string work // inside the entity-thread lambda and keeps the lambda allocation tiny. @@ -182,8 +193,14 @@ public void updateText() { public void updateData(int stackSize, EntityType entityType, long currentExp, long maxExp, int currentItems, int maxSlots) { TextDisplay display = textDisplay.get(); - // Skip entirely when nothing has changed and the hologram already exists. + if (display != null && !display.isValid()) { + textDisplay.set(null); + display = null; + } + + // Skip entirely when nothing has changed and the hologram display is still alive. if (display != null + && display.isValid() && this.stackSize == stackSize && this.entityType == entityType && this.currentExp == currentExp @@ -205,13 +222,14 @@ public void updateData(int stackSize, EntityType entityType, long currentExp, lo } else { // Pre-compute text here (region thread) so the entity-thread lambda // only needs to call display.setText() – no extra task dispatch. + final TextDisplay activeDisplay = display; final String finalText = computeText(); - Scheduler.runEntityTask(display, () -> { - if (!display.isValid()) { + Scheduler.runEntityTask(activeDisplay, () -> { + if (!activeDisplay.isValid()) { textDisplay.set(null); createHologram(); } else { - display.setText(finalText); + activeDisplay.setText(finalText); } }); } diff --git a/core/src/main/java/github/nighter/smartspawner/spawner/data/WorldEventHandler.java b/core/src/main/java/github/nighter/smartspawner/spawner/data/WorldEventHandler.java index d0966d64..8ad7d242 100644 --- a/core/src/main/java/github/nighter/smartspawner/spawner/data/WorldEventHandler.java +++ b/core/src/main/java/github/nighter/smartspawner/spawner/data/WorldEventHandler.java @@ -4,10 +4,14 @@ import github.nighter.smartspawner.spawner.properties.SpawnerData; import github.nighter.smartspawner.Scheduler; import org.bukkit.Bukkit; +import org.bukkit.Chunk; +import org.bukkit.Material; import org.bukkit.World; +import org.bukkit.block.BlockState; import org.bukkit.event.EventHandler; import org.bukkit.event.EventPriority; import org.bukkit.event.Listener; +import org.bukkit.event.world.ChunkLoadEvent; import org.bukkit.event.world.WorldInitEvent; import org.bukkit.event.world.WorldLoadEvent; import org.bukkit.event.world.WorldSaveEvent; @@ -65,6 +69,8 @@ public void onWorldLoad(WorldLoadEvent event) { // Try to load any pending spawners for this world loadPendingSpawnersForWorld(worldName); + ensureHologramsInLoadedChunks(world); + // If this is during server startup, also attempt initial load if (!initialLoadAttempted) { // Delay slightly to ensure world is fully ready @@ -105,6 +111,47 @@ public void onWorldUnload(WorldUnloadEvent event) { unloadSpawnersFromWorld(worldName); } + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + public void onChunkLoad(ChunkLoadEvent event) { + ensureHologramsInChunk(event.getChunk()); + } + + private void ensureHologramsInChunk(Chunk chunk) { + if (!plugin.getConfig().getBoolean("hologram.enabled", false)) { + return; + } + + World world = chunk.getWorld(); + int chunkX = chunk.getX(); + int chunkZ = chunk.getZ(); + + Scheduler.runChunkTask(world, chunkX, chunkZ, () -> { + if (!world.isChunkLoaded(chunkX, chunkZ)) { + return; + } + + Chunk loadedChunk = world.getChunkAt(chunkX, chunkZ); + for (BlockState state : loadedChunk.getTileEntities( + block -> block.getType() == Material.SPAWNER, false)) { + SpawnerData spawner = plugin.getSpawnerManager() + .getSpawnerByLocation(state.getBlock().getLocation()); + if (spawner != null) { + spawner.ensureHologram(); + } + } + }); + } + + private void ensureHologramsInLoadedChunks(World world) { + if (!plugin.getConfig().getBoolean("hologram.enabled", false)) { + return; + } + + for (Chunk loadedChunk : world.getLoadedChunks()) { + ensureHologramsInChunk(loadedChunk); + } + } + /** * Attempt to perform initial spawner loading, checking for available worlds */ diff --git a/core/src/main/java/github/nighter/smartspawner/spawner/properties/SpawnerData.java b/core/src/main/java/github/nighter/smartspawner/spawner/properties/SpawnerData.java index efd6a6c6..6c07cd04 100644 --- a/core/src/main/java/github/nighter/smartspawner/spawner/properties/SpawnerData.java +++ b/core/src/main/java/github/nighter/smartspawner/spawner/properties/SpawnerData.java @@ -9,6 +9,7 @@ import lombok.Setter; import org.bukkit.Location; import org.bukkit.Material; +import org.bukkit.World; import org.bukkit.entity.EntityType; import org.bukkit.inventory.ItemStack; import org.bukkit.inventory.meta.ItemMeta; @@ -392,22 +393,58 @@ public void updateHologramData() { } public void reloadHologramData() { + if (!plugin.getConfig().getBoolean("hologram.enabled", false)) { + removeHologram(); + return; + } if (hologram != null) { hologram.remove(); - createHologram(); } + if (!isSpawnerChunkLoaded()) { + return; + } + createHologram(); } public void refreshHologram() { if (plugin.getConfig().getBoolean("hologram.enabled", false)) { - if (hologram == null) { - createHologram(); - } - } else if (hologram != null) { + ensureHologram(); + } else { removeHologram(); } } + /** + * Recreate hologram display if missing or dead. No-op when display is alive. + * Must be invoked on or scheduled to the spawner's region thread. + */ + public void ensureHologram() { + if (!plugin.getConfig().getBoolean("hologram.enabled", false)) { + removeHologram(); + return; + } + if (!isSpawnerChunkLoaded()) { + return; + } + if (hologram != null && hologram.isAlive()) { + return; + } + if (hologram != null) { + hologram.remove(); + } + createHologram(); + } + + private boolean isSpawnerChunkLoaded() { + if (spawnerLocation == null || spawnerLocation.getWorld() == null) { + return false; + } + World world = spawnerLocation.getWorld(); + int chunkX = spawnerLocation.getBlockX() >> 4; + int chunkZ = spawnerLocation.getBlockZ() >> 4; + return world.isChunkLoaded(chunkX, chunkZ); + } + public void removeHologram() { if (hologram != null) { hologram.remove();