From 3571fc3fbdaa7ca46149b9631b6725d7c3486449 Mon Sep 17 00:00:00 2001 From: Pixel Date: Wed, 7 Jan 2026 13:28:50 +0100 Subject: [PATCH] fancyholograms: Add support for pre-1.19.4 clients --- plugins/fancyholograms-v2/build.gradle.kts | 6 + .../hologram/version/HologramImpl.java | 254 +++++++++++++++++- .../listeners/PlayerListener.java | 3 + .../fancyholograms/util/PluginUtils.java | 51 ++++ 4 files changed, 306 insertions(+), 8 deletions(-) diff --git a/plugins/fancyholograms-v2/build.gradle.kts b/plugins/fancyholograms-v2/build.gradle.kts index f8b125ea..c8764196 100644 --- a/plugins/fancyholograms-v2/build.gradle.kts +++ b/plugins/fancyholograms-v2/build.gradle.kts @@ -52,6 +52,7 @@ dependencies { compileOnly(project(":plugins:fancynpcs:fn-api")) compileOnly("org.lushplugins:ChatColorHandler:6.0.4") compileOnly("org.geysermc.floodgate:api:2.2.4-SNAPSHOT") + compileOnly("com.viaversion:viaversion-api:5.7.0") } paper { @@ -82,6 +83,11 @@ paper { load = PaperPluginDescription.RelativeLoadOrder.BEFORE joinClasspath = true } + register("ViaVersion") { + required = false + load = PaperPluginDescription.RelativeLoadOrder.BEFORE + joinClasspath = true + } } } diff --git a/plugins/fancyholograms-v2/src/main/java/de/oliver/fancyholograms/hologram/version/HologramImpl.java b/plugins/fancyholograms-v2/src/main/java/de/oliver/fancyholograms/hologram/version/HologramImpl.java index 20325f56..2662b77e 100644 --- a/plugins/fancyholograms-v2/src/main/java/de/oliver/fancyholograms/hologram/version/HologramImpl.java +++ b/plugins/fancyholograms-v2/src/main/java/de/oliver/fancyholograms/hologram/version/HologramImpl.java @@ -4,23 +4,48 @@ import de.oliver.fancyholograms.api.events.HologramHideEvent; import de.oliver.fancyholograms.api.events.HologramShowEvent; import de.oliver.fancyholograms.api.hologram.Hologram; +import de.oliver.fancyholograms.util.PluginUtils; import de.oliver.fancysitula.api.entities.*; import de.oliver.fancysitula.factories.FancySitula; +import net.kyori.adventure.text.Component; +import org.bukkit.Color; +import org.bukkit.Location; import org.bukkit.entity.Player; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.joml.Quaternionf; +import org.lushplugins.chatcolorhandler.ModernChatColorHandler; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; public final class HologramImpl extends Hologram { + /** + * Vertical spacing between lines for legacy clients (in blocks). + */ + private static final float LINE_SPACING = 0.3f; + private FS_Display fsDisplay; + /** + * Per-player legacy line displays for pre-1.19.4 clients. + * Maps player UUID to a list of single-line text displays. + */ + private final Map> legacyLineDisplays = new ConcurrentHashMap<>(); + public HologramImpl(@NotNull final HologramData data) { super(data); } @Override public int getEntityId() { + if (fsDisplay == null) { + return -1; + } return fsDisplay.getId(); } @@ -53,6 +78,7 @@ public void create() { @Override public void delete() { this.fsDisplay = null; + legacyLineDisplays.clear(); } @Override @@ -99,8 +125,6 @@ public void update() { itemDisplay.setItem(itemData.getItemStack()); } else if (fsDisplay instanceof FS_BlockDisplay blockDisplay && data instanceof BlockHologramData blockData) { // block - -// BlockType blockType = RegistryAccess.registryAccess().getRegistry(RegistryKey.BLOCK).get(blockData.getBlock().getKey()); blockDisplay.setBlock(blockData.getBlock().createBlockData().createBlockState()); } @@ -146,13 +170,14 @@ public boolean show(@NotNull final Player player) { return false; } - // TODO: cache player protocol version - // TODO: fix this -// final var protocolVersion = FancyHologramsPlugin.get().isUsingViaVersion() ? Via.getAPI().getPlayerVersion(player) : MINIMUM_PROTOCOL_VERSION; -// if (protocolVersion < MINIMUM_PROTOCOL_VERSION) { -// return false; -// } + // Handle legacy clients (pre-1.19.4) that need single-line text displays + if (PluginUtils.isViaVersionEnabled() && PluginUtils.isLegacyClient(player)) { + if (data instanceof TextHologramData) { + return showLegacyLines(player); + } + } + // Normal flow for modern clients FS_RealPlayer fsPlayer = new FS_RealPlayer(player); FancySitula.ENTITY_FACTORY.spawnEntityFor(fsPlayer, fsDisplay); @@ -168,6 +193,11 @@ public boolean hide(@NotNull final Player player) { return false; } + // Check for legacy displays first + if (legacyLineDisplays.containsKey(player.getUniqueId())) { + return hideLegacyLines(player); + } + if (fsDisplay == null) { return false; // doesn't exist, nothing to hide } @@ -190,6 +220,13 @@ public void refresh(@NotNull final Player player) { return; } + // Check for legacy displays + if (legacyLineDisplays.containsKey(player.getUniqueId())) { + refreshLegacyLines(player); + return; + } + + // Normal flow for modern clients FS_RealPlayer fsPlayer = new FS_RealPlayer(player); FancySitula.PACKET_FACTORY.createTeleportEntityPacket( @@ -210,4 +247,205 @@ public void refresh(@NotNull final Player player) { FancySitula.ENTITY_FACTORY.setEntityDataFor(fsPlayer, fsDisplay); } + // ==================== Legacy Client Support Methods ==================== + + /** + * Shows the hologram to a legacy client by spawning multiple single-line text displays. + * + * @param player The player to show the hologram to + * @return true if the hologram was shown successfully + */ + private boolean showLegacyLines(@NotNull Player player) { + if (!(data instanceof TextHologramData textData)) { + return false; + } + + List lines = textData.getText(); + List lineDisplays = new ArrayList<>(lines.size()); + FS_RealPlayer fsPlayer = new FS_RealPlayer(player); + + for (int i = 0; i < lines.size(); i++) { + FS_TextDisplay lineDisplay = createLineDisplay(); + lineDisplays.add(lineDisplay); + FancySitula.ENTITY_FACTORY.spawnEntityFor(fsPlayer, lineDisplay); + } + + legacyLineDisplays.put(player.getUniqueId(), lineDisplays); + this.viewers.add(player.getUniqueId()); + refreshLegacyLines(player); + + return true; + } + + /** + * Hides the hologram from a legacy client by despawning all single-line text displays. + * + * @param player The player to hide the hologram from + * @return true if the hologram was hidden successfully + */ + private boolean hideLegacyLines(@NotNull Player player) { + List lineDisplays = legacyLineDisplays.remove(player.getUniqueId()); + if (lineDisplays == null) { + return false; + } + + FS_RealPlayer fsPlayer = new FS_RealPlayer(player); + for (FS_TextDisplay lineDisplay : lineDisplays) { + FancySitula.ENTITY_FACTORY.despawnEntityFor(fsPlayer, lineDisplay); + } + + this.viewers.remove(player.getUniqueId()); + return true; + } + + /** + * Refreshes the hologram for a legacy client by updating all single-line text displays. + * + * @param player The player to refresh the hologram for + */ + private void refreshLegacyLines(@NotNull Player player) { + if (!(data instanceof TextHologramData textData)) { + return; + } + + List lineDisplays = legacyLineDisplays.get(player.getUniqueId()); + if (lineDisplays == null) { + return; + } + + List lines = textData.getText(); + FS_RealPlayer fsPlayer = new FS_RealPlayer(player); + Location baseLoc = data.getLocation(); + + handleLineCountChange(player, lines.size()); + lineDisplays = legacyLineDisplays.get(player.getUniqueId()); // Refresh reference after potential changes + + if (lineDisplays == null || lineDisplays.isEmpty()) { + return; + } + + double offsetX = 0, offsetY = 0, offsetZ = 0; + if (data instanceof DisplayHologramData displayData && displayData.getTranslation() != null) { + offsetX = displayData.getTranslation().x(); + offsetY = displayData.getTranslation().y(); + offsetZ = displayData.getTranslation().z(); + } + + float totalHeight = (lines.size() - 1) * LINE_SPACING; + double startY = baseLoc.getY() + offsetY + (totalHeight / 2); + + for (int i = 0; i < Math.min(lines.size(), lineDisplays.size()); i++) { + FS_TextDisplay lineDisplay = lineDisplays.get(i); + double lineX = baseLoc.getX() + offsetX; + double lineY = startY - (i * LINE_SPACING); + double lineZ = baseLoc.getZ() + offsetZ; + + lineDisplay.setLocation(lineX, lineY, lineZ); + lineDisplay.setRotation(baseLoc.getYaw(), baseLoc.getPitch()); + + Component lineText = ModernChatColorHandler.translate(lines.get(i), player); + lineDisplay.setText(lineText); + + applyTextDisplayProperties(lineDisplay, textData); + + FancySitula.PACKET_FACTORY.createTeleportEntityPacket( + lineDisplay.getId(), + lineX, + lineY, + lineZ, + baseLoc.getYaw(), + baseLoc.getPitch(), + true) + .send(fsPlayer); + + FancySitula.ENTITY_FACTORY.setEntityDataFor(fsPlayer, lineDisplay); + } + } + + /** + * Creates a new single-line text display entity for legacy clients. + * + * @return A new FS_TextDisplay + */ + private FS_TextDisplay createLineDisplay() { + return new FS_TextDisplay(); + } + + /** + * Applies basic text display properties from the hologram data to a line display. + * Only applies essential properties - complex transformations (scale, translation, etc.) + * are intentionally skipped for legacy clients. + * + * @param lineDisplay The line display to apply properties to + * @param textData The text hologram data to read properties from + */ + private void applyTextDisplayProperties(FS_TextDisplay lineDisplay, TextHologramData textData) { + lineDisplay.setLineWidth(Hologram.LINE_WIDTH); + + Color background = textData.getBackground(); + if (background == null) { + lineDisplay.setBackground(1073741824); // default background + } else if (background == Hologram.TRANSPARENT) { + lineDisplay.setBackground(0); + } else { + lineDisplay.setBackground(background.asARGB()); + } + + lineDisplay.setStyleFlags((byte) 0); + lineDisplay.setShadow(textData.hasTextShadow()); + lineDisplay.setSeeThrough(textData.isSeeThrough()); + + switch (textData.getTextAlignment()) { + case LEFT -> lineDisplay.setAlignLeft(true); + case RIGHT -> lineDisplay.setAlignRight(true); + case CENTER -> { + lineDisplay.setAlignLeft(false); + lineDisplay.setAlignRight(false); + } + } + + if (data instanceof DisplayHologramData displayData) { + lineDisplay.setBillboard(FS_Display.Billboard.valueOf(displayData.getBillboard().name())); + + if (displayData.getBrightness() != null) { + lineDisplay.setBrightnessOverride( + displayData.getBrightness().getBlockLight() << 4 | + displayData.getBrightness().getSkyLight() << 20 + ); + } + + lineDisplay.setViewRange(displayData.getVisibilityDistance()); + } + } + + /** + * Handles changes in the number of lines by adding or removing line displays. + * + * @param player The player to update displays for + * @param newLineCount The new number of lines in the hologram + */ + private void handleLineCountChange(Player player, int newLineCount) { + List lineDisplays = legacyLineDisplays.get(player.getUniqueId()); + if (lineDisplays == null) { + return; + } + + int currentCount = lineDisplays.size(); + FS_RealPlayer fsPlayer = new FS_RealPlayer(player); + + if (newLineCount > currentCount) { + // Add new line displays + for (int i = currentCount; i < newLineCount; i++) { + FS_TextDisplay newLine = createLineDisplay(); + lineDisplays.add(newLine); + FancySitula.ENTITY_FACTORY.spawnEntityFor(fsPlayer, newLine); + } + } else if (newLineCount < currentCount) { + // Remove excess line displays + for (int i = currentCount - 1; i >= newLineCount; i--) { + FS_TextDisplay removedLine = lineDisplays.remove(i); + FancySitula.ENTITY_FACTORY.despawnEntityFor(fsPlayer, removedLine); + } + } + } } diff --git a/plugins/fancyholograms-v2/src/main/java/de/oliver/fancyholograms/listeners/PlayerListener.java b/plugins/fancyholograms-v2/src/main/java/de/oliver/fancyholograms/listeners/PlayerListener.java index 34c89827..28f2f355 100644 --- a/plugins/fancyholograms-v2/src/main/java/de/oliver/fancyholograms/listeners/PlayerListener.java +++ b/plugins/fancyholograms-v2/src/main/java/de/oliver/fancyholograms/listeners/PlayerListener.java @@ -2,6 +2,7 @@ import de.oliver.fancyholograms.FancyHolograms; import de.oliver.fancyholograms.api.hologram.Hologram; +import de.oliver.fancyholograms.util.PluginUtils; import org.bukkit.event.EventHandler; import org.bukkit.event.EventPriority; import org.bukkit.event.Listener; @@ -35,6 +36,8 @@ public PlayerListener(@NotNull final FancyHolograms plugin) { @EventHandler(priority = EventPriority.MONITOR) public void onQuit(@NotNull final PlayerQuitEvent event) { + PluginUtils.clearProtocolCache(event.getPlayer().getUniqueId()); + FancyHolograms.get().getHologramThread().submit(() -> { for (final var hologram : this.plugin.getHologramsManager().getHolograms()) { hologram.forceHideHologram(event.getPlayer()); diff --git a/plugins/fancyholograms-v2/src/main/java/de/oliver/fancyholograms/util/PluginUtils.java b/plugins/fancyholograms-v2/src/main/java/de/oliver/fancyholograms/util/PluginUtils.java index 3ec815ed..9f8b86c9 100644 --- a/plugins/fancyholograms-v2/src/main/java/de/oliver/fancyholograms/util/PluginUtils.java +++ b/plugins/fancyholograms-v2/src/main/java/de/oliver/fancyholograms/util/PluginUtils.java @@ -1,9 +1,22 @@ package de.oliver.fancyholograms.util; +import com.viaversion.viaversion.api.Via; import org.bukkit.Bukkit; +import org.bukkit.entity.Player; + +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; public class PluginUtils { + /** + * Protocol version for 1.19.4 - the minimum version that supports multi-line text displays. + */ + private static final int MINIMUM_PROTOCOL_VERSION = 762; + + private static final Map protocolVersionCache = new ConcurrentHashMap<>(); + public static boolean isFancyNpcsEnabled() { return Bukkit.getPluginManager().getPlugin("FancyNpcs") != null; } @@ -15,4 +28,42 @@ public static boolean isFloodgateEnabled() { public static boolean isViaVersionEnabled() { return Bukkit.getPluginManager().getPlugin("ViaVersion") != null; } + + /** + * Gets the protocol version of a player, using a cache to avoid repeated API calls. + * If ViaVersion is not installed, returns the minimum protocol version (assumes modern client). + * + * @param player The player to get the protocol version for + * @return The player's protocol version + */ + public static int getPlayerProtocolVersion(Player player) { + if (!isViaVersionEnabled()) { + return MINIMUM_PROTOCOL_VERSION; + } + return protocolVersionCache.computeIfAbsent( + player.getUniqueId(), + uuid -> Via.getAPI().getPlayerProtocolVersion(uuid).getVersion() + ); + } + + /** + * Checks if a player is using a legacy client (pre-1.19.4, protocol < 762). + * Legacy clients cannot properly display multi-line text displays. + * + * @param player The player to check + * @return true if the player is using a legacy client + */ + public static boolean isLegacyClient(Player player) { + return getPlayerProtocolVersion(player) < MINIMUM_PROTOCOL_VERSION; + } + + /** + * Clears the cached protocol version for a player. + * Should be called when a player disconnects. + * + * @param playerId The UUID of the player to clear the cache for + */ + public static void clearProtocolCache(UUID playerId) { + protocolVersionCache.remove(playerId); + } }