diff --git a/src/main/java/eu/decentsoftware/holograms/api/DecentHolograms.java b/src/main/java/eu/decentsoftware/holograms/api/DecentHolograms.java index 96b64521..809142b1 100644 --- a/src/main/java/eu/decentsoftware/holograms/api/DecentHolograms.java +++ b/src/main/java/eu/decentsoftware/holograms/api/DecentHolograms.java @@ -4,6 +4,7 @@ import eu.decentsoftware.holograms.api.commands.CommandManager; import eu.decentsoftware.holograms.api.features.FeatureManager; import eu.decentsoftware.holograms.api.holograms.Hologram; +import eu.decentsoftware.holograms.api.holograms.HologramLineDisplayHandler; import eu.decentsoftware.holograms.api.holograms.HologramManager; import eu.decentsoftware.holograms.api.nms.NMS; import eu.decentsoftware.holograms.api.nms.PacketListener; @@ -44,10 +45,12 @@ public final class DecentHolograms { private final JavaPlugin plugin; private HologramManager hologramManager; + private HologramLineDisplayHandler lineDisplayHandler; private CommandManager commandManager; private FeatureManager featureManager; private AnimationManager animationManager; private PacketListener packetListener; + private PlayerListener playerListener; private Ticker ticker; private boolean updateAvailable; @@ -80,13 +83,14 @@ void enable() { this.ticker = new Ticker(); this.hologramManager = new HologramManager(this); + this.lineDisplayHandler = new HologramLineDisplayHandler(); this.commandManager = new CommandManager(); this.featureManager = new FeatureManager(); this.animationManager = new AnimationManager(this); this.packetListener = new PacketListener(this); PluginManager pm = Bukkit.getPluginManager(); - pm.registerEvents(new PlayerListener(this), this.plugin); + pm.registerEvents((playerListener = new PlayerListener(this)), this.plugin); pm.registerEvents(new WorldListener(this), this.plugin); // Setup metrics diff --git a/src/main/java/eu/decentsoftware/holograms/api/Settings.java b/src/main/java/eu/decentsoftware/holograms/api/Settings.java index ae53bf68..67a24310 100644 --- a/src/main/java/eu/decentsoftware/holograms/api/Settings.java +++ b/src/main/java/eu/decentsoftware/holograms/api/Settings.java @@ -34,6 +34,8 @@ public class Settings { public static double DEFAULT_HEIGHT_SMALLHEAD = 0.6; @Key(value = "defaults.display-range", min = 1, max = 48) public static int DEFAULT_DISPLAY_RANGE = 48; + @Key(value = "defaults.item-display-range", min = 1, max = 48) + public static int DEFAULT_ITEM_DISPLAY_RANGE = 16; @Key(value = "defaults.update-range", min = 1, max = 48) public static int DEFAULT_UPDATE_RANGE = 48; @Key(value = "defaults.update-interval", min = 1, max = 1200) @@ -42,6 +44,8 @@ public class Settings { public static int DEFAULT_LRU_CACHE_SIZE = 500; @Key("allow-placeholders-inside-animations") public static boolean ALLOW_PLACEHOLDERS_INSIDE_ANIMATIONS = false; + @Key(value = "defaults.minimum-session-ticks-item-line", min = 0) + public static int DEFAULT_MINIMUM_SESSION_TICKS_ITEM_LINE = 40; public static Map CUSTOM_REPLACEMENTS = ImmutableMap.builder() .put("[x]", "\u2588") diff --git a/src/main/java/eu/decentsoftware/holograms/api/holograms/Hologram.java b/src/main/java/eu/decentsoftware/holograms/api/holograms/Hologram.java index 0a4dae7f..0bb547a1 100644 --- a/src/main/java/eu/decentsoftware/holograms/api/holograms/Hologram.java +++ b/src/main/java/eu/decentsoftware/holograms/api/holograms/Hologram.java @@ -633,14 +633,10 @@ public boolean show(@NonNull Player player, int pageIndex) { if (page != null && page.size() > 0 && canShow(player) && isInDisplayRange(player)) { if (isVisible(player)) { hide(player); + return true; // Skip a tick before updating, should fix the previous issue with clients desyncing. } - if (Version.after(8)) { - showPageTo(player, page, pageIndex); - } else { - // We need to run the task later on older versions as, if we don't, it causes issues with some holograms *randomly* becoming invisible. - // I *think* this is from despawning and spawning the entities (with the same ID) in the same tick. - S.sync(() -> showPageTo(player, page, pageIndex), 0L); - } + + showPageTo(player, page, pageIndex); return true; } return false; diff --git a/src/main/java/eu/decentsoftware/holograms/api/holograms/HologramLine.java b/src/main/java/eu/decentsoftware/holograms/api/holograms/HologramLine.java index b5fc122b..7cce2e9f 100644 --- a/src/main/java/eu/decentsoftware/holograms/api/holograms/HologramLine.java +++ b/src/main/java/eu/decentsoftware/holograms/api/holograms/HologramLine.java @@ -409,6 +409,9 @@ public void show(Player... players) { if (parent != null && parent.getParent().isHideState(player)) { continue; } + + DECENT_HOLOGRAMS.getLineDisplayHandler().removeFromQueue(this, player); + if (!isVisible(player) && canShow(player) && isInDisplayRange(player)) { switch (type) { case TEXT: @@ -418,14 +421,11 @@ public void show(Player... players) { case HEAD: case SMALLHEAD: nms.showFakeEntityArmorStand(player, getLocation(), entityIds[0], true, HologramLineType.HEAD != type, false); - ItemStack itemStack = containsPlaceholders ? HologramItem.parseItemStack(item.getContent(), player) : item.parse(); - nms.helmetFakeEntity(player, itemStack, entityIds[0]); + DECENT_HOLOGRAMS.getLineDisplayHandler().queue(this, player); break; case ICON: nms.showFakeEntityArmorStand(player, getLocation(), entityIds[0], true, true, false); - ItemStack itemStack1 = containsPlaceholders ? HologramItem.parseItemStack(item.getContent(), player) : item.parse(); - nms.showFakeEntityItem(player, getLocation(), itemStack1, entityIds[1]); - nms.attachFakeEntity(player, entityIds[0], entityIds[1]); + DECENT_HOLOGRAMS.getLineDisplayHandler().queue(this, player); break; case ENTITY: EntityType entityType = new HologramEntity(PAPI.setPlaceholders(player, getEntity().getContent())).getType(); @@ -447,6 +447,26 @@ public void show(Player... players) { } } + protected void showItem(Player player) { + if (!viewers.contains(player.getUniqueId())) { + return; + } + + NMS nms = NMS.getInstance(); + + switch (type) { + case HEAD: + case SMALLHEAD: + ItemStack itemStack = containsPlaceholders ? HologramItem.parseItemStack(item.getContent(), player) : item.parse(); + nms.helmetFakeEntity(player, itemStack, entityIds[0]); + break; + case ICON: + ItemStack itemStack1 = containsPlaceholders ? HologramItem.parseItemStack(item.getContent(), player) : item.parse(); + nms.showFakeEntityItem(player, getLocation(), itemStack1, entityIds[1]); + nms.attachFakeEntity(player, entityIds[0], entityIds[1]); + } + } + /** * Update this line for given players. * @@ -519,6 +539,7 @@ public void updateAnimations(Player... players) { public void hide(Player... players) { List playerList = getPlayers(true, players); for (Player player : playerList) { + DECENT_HOLOGRAMS.getLineDisplayHandler().removeFromQueue(this, player); NMS.getInstance().hideFakeEntities(player, entityIds[0], entityIds[1]); viewers.remove(player.getUniqueId()); } @@ -565,10 +586,4 @@ public void setOffsetZ(double offsetZ) { public boolean hasFlag(@NonNull EnumFlag flag) { return super.hasFlag(flag) || (parent != null && parent.getParent().hasFlag(flag)); } - - @Override - public boolean canShow(@NonNull Player player) { - return super.canShow(player) && (parent == null || parent.getParent().canShow(player)); - } - } diff --git a/src/main/java/eu/decentsoftware/holograms/api/holograms/HologramLineDisplayHandler.java b/src/main/java/eu/decentsoftware/holograms/api/holograms/HologramLineDisplayHandler.java new file mode 100644 index 00000000..5b516653 --- /dev/null +++ b/src/main/java/eu/decentsoftware/holograms/api/holograms/HologramLineDisplayHandler.java @@ -0,0 +1,107 @@ +package eu.decentsoftware.holograms.api.holograms; + +import eu.decentsoftware.holograms.api.DecentHologramsAPI; +import eu.decentsoftware.holograms.api.Settings; +import eu.decentsoftware.holograms.api.utils.tick.Ticked; +import lombok.Getter; +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; + +import java.util.LinkedHashSet; +import java.util.Queue; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.TimeUnit; + +public class HologramLineDisplayHandler extends Ticked { + + private static final long TTL = TimeUnit.SECONDS.toMillis(30); + private static final int MAX_QUEUE_SIZE = 10_000; + + private final Queue queue = new ConcurrentLinkedQueue<>(); + + public HologramLineDisplayHandler() { + super(1L); + this.register(); + } + + public void queue(HologramLine line, Player player) { + if (queue.stream().anyMatch(entry -> entry.getLine() == line && player.getUniqueId().equals(entry.getPlayerId()))) { + return; + } + + queue.add(new LineDisplayEntry(line, player.getUniqueId())); + } + + public void removeFromQueue(HologramLine line, Player player) { + queue.removeIf(entry -> entry.getLine() == line && player.getUniqueId().equals(entry.getPlayerId())); + } + + @Override + public void tick() { + final Set playersUpdated = new LinkedHashSet<>(); // We only want to send the player one item per tick + final Set reQueue = new LinkedHashSet<>(); + + while (!queue.isEmpty()) { + LineDisplayEntry poll = queue.poll(); + + if (poll == null) { + continue; + } + + if (System.currentTimeMillis() - poll.getTimestamp() > TTL) { + continue; + } + + Player player = Bukkit.getPlayer(poll.getPlayerId()); + + if (player == null) { + continue; + } + + if (playersUpdated.contains(player.getUniqueId())) { + reQueue.add(poll); + continue; + } + + HologramLine line = poll.getLine(); + + if (!line.isVisible(player)) { + continue; + } + + if (canDisplay(player)) { + line.showItem(player); + playersUpdated.add(player.getUniqueId()); + } else { + reQueue.add(poll); + } + } + + queue.addAll(reQueue); + playersUpdated.clear(); + reQueue.clear(); + + if (queue.size() >= MAX_QUEUE_SIZE) { + queue.clear(); + } + } + + private boolean canDisplay(Player player) { + return DecentHologramsAPI.get().getPlayerListener().getTicksSinceLogin(player) >= Settings.DEFAULT_MINIMUM_SESSION_TICKS_ITEM_LINE; + } + + @Getter + private static class LineDisplayEntry { + + private final HologramLine line; + private final UUID playerId; + private final long timestamp = System.currentTimeMillis(); + + public LineDisplayEntry(HologramLine line, UUID playerId) { + this.line = line; + this.playerId = playerId; + } + } +} diff --git a/src/main/java/eu/decentsoftware/holograms/api/holograms/HologramManager.java b/src/main/java/eu/decentsoftware/holograms/api/holograms/HologramManager.java index 34301506..490e02a8 100644 --- a/src/main/java/eu/decentsoftware/holograms/api/holograms/HologramManager.java +++ b/src/main/java/eu/decentsoftware/holograms/api/holograms/HologramManager.java @@ -8,6 +8,7 @@ import eu.decentsoftware.holograms.api.utils.file.FileUtils; import eu.decentsoftware.holograms.api.utils.scheduler.S; import eu.decentsoftware.holograms.api.utils.tick.Ticked; +import lombok.Getter; import lombok.NonNull; import org.bukkit.Bukkit; import org.bukkit.Location; @@ -68,6 +69,13 @@ public void updateVisibility(@NonNull Player player) { } } + /** + * Update the visibility of the hologram for the given player. + * + * @param player - The player to update the visibility for. + * @param hologram - The hologram to update the visibility for. + * @return True if the hologram was shown, false otherwise. + */ public void updateVisibility(@NonNull Player player, @NonNull Hologram hologram) { if (hologram.isDisabled()) { return; diff --git a/src/main/java/eu/decentsoftware/holograms/api/listeners/PlayerListener.java b/src/main/java/eu/decentsoftware/holograms/api/listeners/PlayerListener.java index 6cb40bb2..f514f1c6 100644 --- a/src/main/java/eu/decentsoftware/holograms/api/listeners/PlayerListener.java +++ b/src/main/java/eu/decentsoftware/holograms/api/listeners/PlayerListener.java @@ -12,10 +12,15 @@ import org.bukkit.event.player.PlayerRespawnEvent; import org.bukkit.event.player.PlayerTeleportEvent; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; + @SuppressWarnings("unused") public class PlayerListener implements Listener { private final DecentHolograms decentHolograms; + private final Map playerLoginTime = new ConcurrentHashMap<>(); // Hologram lines can be updated asynchronously public PlayerListener(DecentHolograms decentHolograms) { this.decentHolograms = decentHolograms; @@ -24,11 +29,12 @@ public PlayerListener(DecentHolograms decentHolograms) { @EventHandler(priority = EventPriority.MONITOR) public void onJoin(PlayerJoinEvent e) { Player player = e.getPlayer(); - S.async(() -> decentHolograms.getHologramManager().updateVisibility(player)); decentHolograms.getPacketListener().hook(player); if (decentHolograms.isUpdateAvailable() && player.hasPermission("dh.admin")) { Lang.sendUpdateMessage(player); } + + playerLoginTime.put(player.getUniqueId(), System.currentTimeMillis()); } @EventHandler @@ -36,6 +42,7 @@ public void onQuit(PlayerQuitEvent e) { Player player = e.getPlayer(); S.async(() -> decentHolograms.getHologramManager().onQuit(player)); decentHolograms.getPacketListener().unhook(player); + playerLoginTime.remove(player.getUniqueId()); } @EventHandler @@ -59,4 +66,13 @@ public void onTeleport(PlayerTeleportEvent e) { S.async(() -> decentHolograms.getHologramManager().hideAll(player)); } + public int getTicksSinceLogin(Player player) { + Long epoch = playerLoginTime.get(player.getUniqueId()); + + if (epoch == null) { + return 0; + } + + return (int) ((System.currentTimeMillis() - epoch)) / 50; + } }