From dbb6a61351ac472cb1add31d9eddff05c05e6841 Mon Sep 17 00:00:00 2001 From: loki5512344 Date: Tue, 30 Dec 2025 23:28:10 +0100 Subject: [PATCH] Add TextDisplay system for floating gradient names above players (1.19.4+) --- .../loki/lochat/gradient/GradientModule.java | 9 + .../commands/GradientAdminCommand.java | 37 ++- .../gradient/config/GradientConfig.java | 8 + .../listeners/GradientPlayerListener.java | 6 + .../lochat/gradient/util/DisplayNameUtil.java | 6 + .../gradient/util/TextDisplayManager.java | 242 ++++++++++++++++++ src/main/resources/gradient-config.yml | 6 + 7 files changed, 313 insertions(+), 1 deletion(-) create mode 100644 src/main/java/com/loki/lochat/gradient/util/TextDisplayManager.java diff --git a/src/main/java/com/loki/lochat/gradient/GradientModule.java b/src/main/java/com/loki/lochat/gradient/GradientModule.java index b0a41ec..bf9499d 100644 --- a/src/main/java/com/loki/lochat/gradient/GradientModule.java +++ b/src/main/java/com/loki/lochat/gradient/GradientModule.java @@ -12,6 +12,7 @@ import com.loki.lochat.gradient.listeners.GradientGUIListener; import com.loki.lochat.gradient.listeners.GradientPlayerListener; import com.loki.lochat.gradient.util.DisplayNameUtil; import com.loki.lochat.gradient.util.GradientUtil; +import com.loki.lochat.gradient.util.TextDisplayManager; import org.black_ixx.playerpoints.PlayerPoints; import org.black_ixx.playerpoints.PlayerPointsAPI; import org.bukkit.Bukkit; @@ -29,6 +30,7 @@ public class GradientModule { private GradientDataManager dataManager; private GradientLuckPermsHook luckPermsHook; private PlayerPointsAPI playerPointsAPI; + private TextDisplayManager textDisplayManager; private boolean enabled; public GradientModule(JavaPlugin plugin) { @@ -64,6 +66,9 @@ public class GradientModule { // Подключение LuckPerms luckPermsHook = new GradientLuckPermsHook(plugin); + // Инициализация TextDisplayManager + textDisplayManager = new TextDisplayManager(this); + // Регистрация команд registerCommands(); @@ -100,6 +105,9 @@ public class GradientModule { * Выключение модуля */ public void shutdown() { + if (textDisplayManager != null) { + textDisplayManager.removeAllDisplays(); + } if (dataManager != null) { dataManager.saveAll(); } @@ -260,6 +268,7 @@ public class GradientModule { public GradientDataManager getDataManager() { return dataManager; } public GradientLuckPermsHook getLuckPermsHook() { return luckPermsHook; } public PlayerPointsAPI getPlayerPointsAPI() { return playerPointsAPI; } + public TextDisplayManager getTextDisplayManager() { return textDisplayManager; } public boolean hasPlayerPoints() { return playerPointsAPI != null; } public boolean isEnabled() { return enabled; } } diff --git a/src/main/java/com/loki/lochat/gradient/commands/GradientAdminCommand.java b/src/main/java/com/loki/lochat/gradient/commands/GradientAdminCommand.java index 100e0b4..0b37d58 100644 --- a/src/main/java/com/loki/lochat/gradient/commands/GradientAdminCommand.java +++ b/src/main/java/com/loki/lochat/gradient/commands/GradientAdminCommand.java @@ -87,6 +87,9 @@ public class GradientAdminCommand implements CommandExecutor, TabCompleter { case "updateall" -> { handleUpdateAll(sender, msg); } + case "toggledisplay" -> { + handleToggleDisplay(sender, msg); + } default -> sendHelp(sender); } @@ -104,6 +107,7 @@ public class GradientAdminCommand implements CommandExecutor, TabCompleter { sender.sendMessage("§e/aprefix resetcolor <игрок> §7— сбросить цвет"); sender.sendMessage("§e/aprefix resetprefix <игрок> §7— сбросить префикс"); sender.sendMessage("§e/aprefix updateall §7— обновить display names всех игроков"); + sender.sendMessage("§e/aprefix toggledisplay §7— переключить TextDisplay/обычный режим"); } private void handleInfo(CommandSender sender, String playerName, GradientMessages msg) { @@ -330,6 +334,37 @@ public class GradientAdminCommand implements CommandExecutor, TabCompleter { sender.sendMessage("§aОбновлены display names для §f" + count + " §aигроков"); } + private void handleToggleDisplay(CommandSender sender, GradientMessages msg) { + // Переключаем режим TextDisplay в конфиге + boolean currentMode = module.getConfig().isUseTextDisplay(); + boolean newMode = !currentMode; + + // Обновляем конфиг (временно в памяти) + module.getConfig().setUseTextDisplay(newMode); + + if (newMode) { + // Переключаемся на TextDisplay + sender.sendMessage("§aВключен режим TextDisplay"); + // Удаляем все обычные display names и создаем TextDisplay + for (Player player : Bukkit.getOnlinePlayers()) { + player.displayName(null); // Сбрасываем обычный display name + GradientPlayerData data = module.getDataManager().getPlayerData(player.getUniqueId()); + FoliaUtil.runEntityTask(module.getPlugin(), player, + () -> module.getTextDisplayManager().updatePlayerDisplay(player)); + } + } else { + // Переключаемся на обычный display name + sender.sendMessage("§cВыключен режим TextDisplay"); + // Удаляем все TextDisplay и обновляем обычные display names + module.getTextDisplayManager().removeAllDisplays(); + for (Player player : Bukkit.getOnlinePlayers()) { + GradientPlayerData data = module.getDataManager().getPlayerData(player.getUniqueId()); + FoliaUtil.runEntityTask(module.getPlugin(), player, + () -> DisplayNameUtil.updateDisplayName(module, player, data)); + } + } + } + @Override public @Nullable List onTabComplete(@NotNull CommandSender sender, @NotNull Command command, @NotNull String alias, @NotNull String[] args) { @@ -337,7 +372,7 @@ public class GradientAdminCommand implements CommandExecutor, TabCompleter { if (args.length == 1) { return Arrays.asList("reload", "info", "setcolor", "setprefix", "coloron", "coloroff", - "prefixon", "prefixoff", "resetcolor", "resetprefix", "updateall").stream() + "prefixon", "prefixoff", "resetcolor", "resetprefix", "updateall", "toggledisplay").stream() .filter(s -> s.startsWith(args[0].toLowerCase())) .toList(); } diff --git a/src/main/java/com/loki/lochat/gradient/config/GradientConfig.java b/src/main/java/com/loki/lochat/gradient/config/GradientConfig.java index 9e1c7d5..2ea0d13 100644 --- a/src/main/java/com/loki/lochat/gradient/config/GradientConfig.java +++ b/src/main/java/com/loki/lochat/gradient/config/GradientConfig.java @@ -83,4 +83,12 @@ public class GradientConfig { // Display settings public boolean isUpdateDisplayName() { return config.getBoolean("display.update-display-name", true); } public boolean isUpdateTabList() { return config.getBoolean("display.update-tab-list", true); } + public boolean isUseTextDisplay() { return config.getBoolean("display.use-text-display", true); } + public double getTextDisplayHeight() { return config.getDouble("display.text-display-height", 2.3); } + public float getTextDisplayScale() { return (float) config.getDouble("display.text-display-scale", 0.8); } + + // Метод для временного изменения настройки TextDisplay + public void setUseTextDisplay(boolean useTextDisplay) { + config.set("display.use-text-display", useTextDisplay); + } } diff --git a/src/main/java/com/loki/lochat/gradient/listeners/GradientPlayerListener.java b/src/main/java/com/loki/lochat/gradient/listeners/GradientPlayerListener.java index f6f23f0..158c247 100644 --- a/src/main/java/com/loki/lochat/gradient/listeners/GradientPlayerListener.java +++ b/src/main/java/com/loki/lochat/gradient/listeners/GradientPlayerListener.java @@ -48,6 +48,12 @@ public class GradientPlayerListener implements Listener { @EventHandler public void onPlayerQuit(PlayerQuitEvent event) { Player player = event.getPlayer(); + + // Удаляем TextDisplay если есть + if (module.getTextDisplayManager() != null) { + module.getTextDisplayManager().removePlayerDisplay(player.getUniqueId()); + } + FoliaUtil.runAsync(module.getPlugin(), () -> module.getDataManager().savePlayerData(player.getUniqueId())); } diff --git a/src/main/java/com/loki/lochat/gradient/util/DisplayNameUtil.java b/src/main/java/com/loki/lochat/gradient/util/DisplayNameUtil.java index a007829..e4a45f7 100644 --- a/src/main/java/com/loki/lochat/gradient/util/DisplayNameUtil.java +++ b/src/main/java/com/loki/lochat/gradient/util/DisplayNameUtil.java @@ -28,6 +28,12 @@ public final class DisplayNameUtil { public static void updateDisplayName(GradientModule module, Player player, GradientPlayerData data) { GradientConfig cfg = module.getConfig(); + // Если включен TextDisplay, используем его вместо обычного display name + if (cfg.isUseTextDisplay()) { + module.getTextDisplayManager().updatePlayerDisplay(player); + return; + } + String prefix = null; String prefixFormat = cfg.getPrefixFormat(); diff --git a/src/main/java/com/loki/lochat/gradient/util/TextDisplayManager.java b/src/main/java/com/loki/lochat/gradient/util/TextDisplayManager.java new file mode 100644 index 0000000..0023dc0 --- /dev/null +++ b/src/main/java/com/loki/lochat/gradient/util/TextDisplayManager.java @@ -0,0 +1,242 @@ +package com.loki.lochat.gradient.util; + +import com.loki.lochat.gradient.GradientModule; +import com.loki.lochat.gradient.data.GradientPlayerData; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.minimessage.MiniMessage; +import org.bukkit.entity.Player; +import org.bukkit.entity.TextDisplay; +import org.bukkit.util.Transformation; +import org.joml.AxisAngle4f; +import org.joml.Vector3f; + +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Менеджер для управления TextDisplay над игроками + */ +public class TextDisplayManager { + + private final GradientModule module; + private final Map playerDisplays = new ConcurrentHashMap<>(); + private static final MiniMessage MINI_MESSAGE = MiniMessage.miniMessage(); + + public TextDisplayManager(GradientModule module) { + this.module = module; + } + + /** + * Создает или обновляет TextDisplay над игроком + */ + public void updatePlayerDisplay(Player player) { + if (!module.getConfig().isUpdateDisplayName() || !module.getConfig().isUseTextDisplay()) { + return; + } + + GradientPlayerData data = module.getDataManager().getPlayerData(player.getUniqueId()); + + // Получаем текст для отображения + String displayText = getDisplayText(player, data); + Component textComponent = MINI_MESSAGE.deserialize(displayText); + + TextDisplay display = playerDisplays.get(player.getUniqueId()); + + if (display == null || !display.isValid()) { + // Создаем новый TextDisplay + createTextDisplay(player, textComponent); + } else { + // Обновляем существующий + updateTextDisplay(display, player, textComponent); + } + } + + /** + * Создает новый TextDisplay над игроком + */ + private void createTextDisplay(Player player, Component textComponent) { + // Удаляем старый display если есть + removePlayerDisplay(player.getUniqueId()); + + double height = module.getConfig().getTextDisplayHeight(); + float scale = module.getConfig().getTextDisplayScale(); + + // Создаем новый TextDisplay + TextDisplay display = player.getWorld().spawn( + player.getLocation().add(0, height, 0), // Над головой игрока + TextDisplay.class, + entity -> { + entity.text(textComponent); + entity.setBillboard(org.bukkit.entity.Display.Billboard.CENTER); + entity.setViewRange(64.0f); // Видимость на 64 блока + entity.setSeeThrough(true); // Видно через стены + entity.setShadowRadius(0.0f); // Без тени + entity.setShadowStrength(0.0f); // Без тени + + // Настройка трансформации (размер текста) + entity.setTransformation(new Transformation( + new Vector3f(0, 0, 0), // Без смещения + new AxisAngle4f(0, 0, 0, 1), // Без поворота + new Vector3f(scale, scale, scale), // Размер текста + new AxisAngle4f(0, 0, 0, 1) // Без поворота после масштабирования + )); + } + ); + + // Привязываем TextDisplay к игроку как пассажира + player.addPassenger(display); + + // Сохраняем в карте + playerDisplays.put(player.getUniqueId(), display); + } + + /** + * Обновляет существующий TextDisplay + */ + private void updateTextDisplay(TextDisplay display, Player player, Component textComponent) { + display.text(textComponent); + + // Проверяем, что display все еще привязан к игроку + if (!player.getPassengers().contains(display)) { + player.addPassenger(display); + } + } + + /** + * Получает текст для отображения над игроком + */ + private String getDisplayText(Player player, GradientPlayerData data) { + String prefix = null; + String prefixFormat = module.getConfig().getPrefixFormat(); + + // Определяем префикс: сначала кастомный, потом LuckPerms + if (data.isPrefixEnabled() && data.hasPrefix()) { + prefix = data.getPrefix(); + } else if (module.getLuckPermsHook().isEnabled()) { + String lpPrefix = module.getLuckPermsHook().getActivePrefix(player); + if (lpPrefix != null && !lpPrefix.isEmpty()) { + // Для LuckPerms префикса используем специальную логику + return buildWithLuckPermsPrefix( + lpPrefix, + player.getName(), + data.isColorEnabled() ? data.getColors() : null, + module.getConfig().isUseLegacyRgbFormat(), + module.getConfig().isContinuousGradient() + ); + } + } + + // Строим display text с кастомным префиксом или без префикса + String displayText = GradientUtil.buildDisplayName( + prefix, + player.getName(), + data.isColorEnabled() ? data.getColors() : null, + module.getConfig().isGradientOnPrefix(), + module.getConfig().isContinuousGradient(), + prefixFormat, + false // Используем MiniMessage формат для TextDisplay + ); + + // Конвертируем &#RRGGBB в <#RRGGBB> для MiniMessage + return convertToMiniMessageFormat(displayText); + } + + /** + * Строит текст с LuckPerms префиксом + */ + private String buildWithLuckPermsPrefix(String lpPrefix, String nick, + java.util.List colors, + boolean useLegacyFormat, + boolean continuousGradient) { + if (colors == null || colors.isEmpty()) { + return convertToMiniMessageFormat(lpPrefix + nick); + } + + if (continuousGradient) { + String cleanPrefix = stripColors(lpPrefix); + String fullText = cleanPrefix + nick; + String result = GradientUtil.applyGradient(fullText, colors, false); // MiniMessage формат + return convertToMiniMessageFormat(result); + } else { + String result = convertToMiniMessageFormat(lpPrefix) + + convertToMiniMessageFormat(GradientUtil.applyGradient(nick, colors, false)); + return result; + } + } + + /** + * Убирает цветовые коды из строки + */ + private String stripColors(String text) { + if (text == null) return ""; + return text.replaceAll("(?i)(§x(§[0-9a-f]){6}|§[0-9a-fk-or]|&[0-9a-fk-or]|&#[0-9a-f]{6}|<[^>]+>)", ""); + } + + /** + * Конвертирует &#RRGGBB в <#RRGGBB> для MiniMessage + */ + private String convertToMiniMessageFormat(String text) { + if (text == null) return ""; + // Конвертируем &#RRGGBB в <#RRGGBB> + text = text.replaceAll("&#([0-9a-fA-F]{6})", "<#$1>"); + // Конвертируем §x§R§R§G§G§B§B в <#RRGGBB> (используем Pattern и Matcher) + java.util.regex.Pattern pattern = java.util.regex.Pattern.compile("§x(§[0-9a-fA-F]){6}"); + java.util.regex.Matcher matcher = pattern.matcher(text); + StringBuffer sb = new StringBuffer(); + while (matcher.find()) { + String hex = matcher.group().replaceAll("§", "").substring(1); // Убираем все § и x + matcher.appendReplacement(sb, "<#" + hex + ">"); + } + matcher.appendTail(sb); + text = sb.toString(); + + // Конвертируем обычные цветовые коды §a в и т.д. + text = text.replaceAll("§([0-9a-fk-or])", "<$1>"); + return text; + } + + /** + * Удаляет TextDisplay игрока + */ + public void removePlayerDisplay(UUID playerId) { + TextDisplay display = playerDisplays.remove(playerId); + if (display != null && display.isValid()) { + display.remove(); + } + } + + /** + * Удаляет все TextDisplay + */ + public void removeAllDisplays() { + for (TextDisplay display : playerDisplays.values()) { + if (display != null && display.isValid()) { + display.remove(); + } + } + playerDisplays.clear(); + } + + /** + * Обновляет позицию TextDisplay (вызывается при движении игрока) + */ + public void updateDisplayPosition(Player player) { + TextDisplay display = playerDisplays.get(player.getUniqueId()); + if (display != null && display.isValid()) { + // TextDisplay автоматически следует за игроком как пассажир + // Но можем обновить высоту если нужно + if (!player.getPassengers().contains(display)) { + player.addPassenger(display); + } + } + } + + /** + * Проверяет, есть ли у игрока TextDisplay + */ + public boolean hasDisplay(UUID playerId) { + TextDisplay display = playerDisplays.get(playerId); + return display != null && display.isValid(); + } +} \ No newline at end of file diff --git a/src/main/resources/gradient-config.yml b/src/main/resources/gradient-config.yml index 19bf76d..8214240 100644 --- a/src/main/resources/gradient-config.yml +++ b/src/main/resources/gradient-config.yml @@ -49,6 +49,12 @@ display: update-display-name: true # Обновлять tab list update-tab-list: true + # Использовать TextDisplay вместо обычного display name (1.19.4+) + use-text-display: true + # Высота TextDisplay над игроком (в блоках) + text-display-height: 2.3 + # Размер TextDisplay (от 0.1 до 2.0) + text-display-scale: 0.8 # Тип хранилища: YAML или SQLITE storage-type: YAML