feat: Add Paper Chat API with hover/click/mentions/emojis/URLs + custom nicknames
This commit is contained in:
parent
7658f6f3e6
commit
69cae99476
21 changed files with 670 additions and 229 deletions
0
496
0
496
|
|
@ -1,188 +0,0 @@
|
|||
# Отчёт по очистке кода LoChat
|
||||
|
||||
## Дата: 23.02.2026
|
||||
|
||||
---
|
||||
|
||||
## ✅ Что удалено
|
||||
|
||||
### 1. Эмодзи (полностью)
|
||||
- ❌ `EmojiManager.java` (157 строк)
|
||||
- ❌ `HeadEmojiManager.java` (157 строк)
|
||||
- ❌ `src/main/resources/emojis.yml`
|
||||
- ❌ Все вызовы из `ChatListener`
|
||||
- ❌ Настройки `head-emoji.*` из `ConfigManager`
|
||||
- ❌ Вызовы `replaceEmojis()` из `AutoMessageManager`
|
||||
- ✅ Оставлены только заглушки в `ChatFormatter` для совместимости
|
||||
|
||||
**Экономия:** ~320 строк кода + конфиг файл
|
||||
|
||||
### 2. Фильтрация чата (удалена)
|
||||
- ❌ `SpamFilter.java` (44 строки)
|
||||
- ❌ `WordFilter.java` (60 строк)
|
||||
- ❌ `filters/WordFilter.java` (старая версия)
|
||||
- ❌ Методы фильтрации из `ConfigManager`:
|
||||
- `isFilterEnabled()`
|
||||
- `getFilterAction()`
|
||||
- `getFilterReplacement()`
|
||||
- `getFilterWords()`
|
||||
- ❌ Логика фильтрации из `ChatListener`
|
||||
|
||||
**Экономия:** ~150 строк кода
|
||||
|
||||
### 3. Старый ChatListener
|
||||
- ❌ `listeners/ChatListener.java` (185 строк)
|
||||
- ✅ Заменён на `listener/ChatEventListener.java` (35 строк)
|
||||
|
||||
**Экономия:** 150 строк кода
|
||||
|
||||
---
|
||||
|
||||
## 📊 Статистика
|
||||
|
||||
### Размер JAR
|
||||
- **До очистки:** 508 829 байт
|
||||
- **После удаления legacy:** 473 495 байт
|
||||
- **Экономия:** 35 334 байт (6.9%)
|
||||
|
||||
### Строки кода
|
||||
- **Удалено эмодзи и фильтров:** ~620 строк
|
||||
- **Удалено legacy менеджеров:** 998 строк
|
||||
- **Удалено тестов legacy:** 97 строк
|
||||
- **Добавлено новых сервисов:** ~800 строк
|
||||
- **Чистая экономия:** ~915 строк
|
||||
|
||||
### Файлы
|
||||
- **Удалено:** 16 файлов (7 эмодзи/фильтры + 7 менеджеры + 2 теста)
|
||||
- **Создано:** 19 файлов (9 интерфейсов + 9 реализаций + 1 слушатель)
|
||||
- **Чистая разница:** +3 файла (но лучшая структура)
|
||||
|
||||
---
|
||||
|
||||
## ✅ Что работает
|
||||
|
||||
### Новая архитектура (SOLID + DI)
|
||||
```
|
||||
✅ ServiceRegistry - центральный реестр сервисов
|
||||
✅ ChatService - отправка сообщений
|
||||
✅ MuteService - система мутов
|
||||
✅ CooldownService - кулдауны
|
||||
✅ MessageService - обработка сообщений
|
||||
✅ PlayerDataService - данные игроков
|
||||
✅ AntiSpamService - антиспам
|
||||
```
|
||||
|
||||
### Фильтры (Chain of Responsibility)
|
||||
```
|
||||
✅ MuteFilter - проверка мута
|
||||
✅ CooldownFilter - проверка кулдауна
|
||||
❌ SpamFilter - удалён
|
||||
❌ WordFilter - удалён
|
||||
```
|
||||
|
||||
### Слушатели
|
||||
```
|
||||
✅ ChatEventListener - обработка чата (35 строк)
|
||||
✅ PlayerEventListener - события игрока (23 строки)
|
||||
❌ ChatListener - удалён (185 строк)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ✅ Legacy код удалён
|
||||
|
||||
### Удалённые менеджеры (заменены на сервисы)
|
||||
```
|
||||
✅ MuteManager.java (447 строк) → MuteService
|
||||
✅ ChatManager.java (162 строки) → ChatService
|
||||
✅ CooldownManager.java (45 строк) → CooldownService
|
||||
✅ PMManager.java (27 строк) → PMService
|
||||
✅ IgnoreManager.java (105 строк) → IgnoreService
|
||||
✅ SpyManager.java (71 строка) → SpyService
|
||||
✅ MentionManager.java (141 строка) → MentionService
|
||||
```
|
||||
|
||||
**Экономия:** 998 строк кода
|
||||
|
||||
### Команды (мигрированы на новые сервисы)
|
||||
```
|
||||
✅ MuteCommand.java → MuteService
|
||||
✅ UnmuteCommand.java → MuteService
|
||||
✅ MuteListCommand.java → MuteService
|
||||
✅ MuteHistoryCommand.java → MuteService
|
||||
✅ MuteBlameCommand.java → MuteService
|
||||
✅ GlobalChatCommand.java → ChatService
|
||||
✅ LocalChatCommand.java → ChatService
|
||||
✅ MsgCommand.java → PMService, IgnoreService
|
||||
✅ ReplyCommand.java → PMService, IgnoreService
|
||||
✅ IgnoreCommand.java → IgnoreService
|
||||
✅ UnignoreCommand.java → IgnoreService
|
||||
✅ ChatSpyCommand.java → SpyService
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Следующие шаги
|
||||
|
||||
### ✅ Завершено
|
||||
1. ✅ Удалены эмодзи (320+ строк)
|
||||
2. ✅ Удалена фильтрация чата (150+ строк)
|
||||
3. ✅ Создана SOLID архитектура (ServiceRegistry + DI)
|
||||
4. ✅ Созданы все сервисы (9 интерфейсов + 9 реализаций)
|
||||
5. ✅ Мигрированы все команды на новые сервисы
|
||||
6. ✅ Удалены все legacy менеджеры (998 строк)
|
||||
7. ✅ Плагин собирается и работает
|
||||
|
||||
### 🔄 Опционально (для дальнейшей оптимизации)
|
||||
1. [ ] Разбить ConfigManager на категории (если станет >200 строк)
|
||||
2. [ ] Добавить больше unit-тестов для новых сервисов
|
||||
3. [ ] Интеграционные тесты для полного флоу
|
||||
|
||||
---
|
||||
|
||||
## 🧪 Тесты
|
||||
|
||||
### Текущее покрытие
|
||||
```
|
||||
✅ 52 теста пройдено
|
||||
✅ 0 тестов упало
|
||||
✅ ChatFormatterTest (9 тестов)
|
||||
✅ GradientUtilTest
|
||||
✅ GradientPlayerDataTest
|
||||
✅ PMManagerTest
|
||||
✅ CooldownManagerTest
|
||||
```
|
||||
|
||||
### Нужно добавить
|
||||
```
|
||||
[ ] PMServiceTest
|
||||
[ ] IgnoreServiceTest
|
||||
[ ] SpyServiceTest
|
||||
[ ] MentionServiceTest
|
||||
[ ] Интеграционные тесты
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📝 Выводы
|
||||
|
||||
### ✅ Успешно выполнено
|
||||
1. Полностью удалены эмодзи (320+ строк)
|
||||
2. Удалена фильтрация чата (150+ строк)
|
||||
3. Удалён старый ChatListener (185 строк)
|
||||
4. Создана новая архитектура (ServiceRegistry + DI)
|
||||
5. Созданы все сервисы (9 интерфейсов + 9 реализаций)
|
||||
6. Мигрированы все команды на новые сервисы
|
||||
7. Удалены все legacy менеджеры (998 строк)
|
||||
8. Плагин собирается и работает
|
||||
9. Все тесты проходят (37 тестов)
|
||||
|
||||
### 🎯 Результат
|
||||
- **Код стал чище:** -915 строк чистой экономии
|
||||
- **JAR стал меньше:** -35 KB (6.9%)
|
||||
- **Архитектура улучшена:** SOLID принципы + DI
|
||||
- **Все legacy менеджеры удалены**
|
||||
- **Все команды используют новые сервисы**
|
||||
|
||||
### 📋 Рефакторинг завершён
|
||||
Все основные задачи из `REFACTORING_PLAN.md` выполнены. Плагин полностью переведён на новую архитектуру.
|
||||
|
|
@ -2,6 +2,7 @@ package com.loki.lochat;
|
|||
|
||||
import com.loki.lochat.api.service.IgnoreService;
|
||||
import com.loki.lochat.api.service.MuteService;
|
||||
import com.loki.lochat.api.service.NickService;
|
||||
import com.loki.lochat.commands.*;
|
||||
import com.loki.lochat.config.ConfigManager;
|
||||
import com.loki.lochat.config.MessageConfig;
|
||||
|
|
@ -33,7 +34,7 @@ public final class LoChat extends JavaPlugin {
|
|||
@Override
|
||||
public void onEnable() {
|
||||
instance = this;
|
||||
getLogger().info("Загрузка LoChat v" + getDescription().getVersion() + " для Paper/Folia " + Bukkit.getMinecraftVersion() + "...");
|
||||
getLogger().info("Загрузка LoChat v" + getPluginMeta().getVersion() + " для Paper/Folia " + Bukkit.getMinecraftVersion() + "...");
|
||||
|
||||
// Конфиги
|
||||
configManager = new ConfigManager(this);
|
||||
|
|
@ -71,6 +72,9 @@ public final class LoChat extends JavaPlugin {
|
|||
MuteService muteService = serviceRegistry.get(MuteService.class);
|
||||
if (muteService != null) muteService.save();
|
||||
|
||||
NickService nickService = serviceRegistry.get(NickService.class);
|
||||
if (nickService != null) nickService.save();
|
||||
|
||||
if (gradientModule != null) gradientModule.shutdown();
|
||||
getLogger().info("LoChat отключен!");
|
||||
}
|
||||
|
|
@ -103,6 +107,10 @@ public final class LoChat extends JavaPlugin {
|
|||
getCommand("chatspy").setExecutor(new ChatSpyCommand(this));
|
||||
getCommand("clearchat").setExecutor(new ClearChatCommand(this));
|
||||
|
||||
NickCommand nickCmd = new NickCommand(this);
|
||||
getCommand("nick").setExecutor(nickCmd);
|
||||
getCommand("nick").setTabCompleter(nickCmd);
|
||||
|
||||
LoChatCommand loChatCmd = new LoChatCommand(this);
|
||||
getCommand("lochat").setExecutor(loChatCmd);
|
||||
getCommand("lochat").setTabCompleter(loChatCmd);
|
||||
|
|
|
|||
58
src/main/java/com/loki/lochat/api/service/NickService.java
Normal file
58
src/main/java/com/loki/lochat/api/service/NickService.java
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
package com.loki.lochat.api.service;
|
||||
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Сервис для управления кастомными никами
|
||||
*/
|
||||
public interface NickService {
|
||||
|
||||
/**
|
||||
* Устанавливает кастомный ник игроку
|
||||
* @param player UUID игрока
|
||||
* @param nickname Новый ник (с поддержкой цветов и русских символов)
|
||||
* @return true если успешно, false если ник занят или невалиден
|
||||
*/
|
||||
boolean setNickname(UUID player, String nickname);
|
||||
|
||||
/**
|
||||
* Сбрасывает кастомный ник игрока
|
||||
* @param player UUID игрока
|
||||
*/
|
||||
void resetNickname(UUID player);
|
||||
|
||||
/**
|
||||
* Получает кастомный ник игрока
|
||||
* @param player UUID игрока
|
||||
* @return Optional с ником или empty если нет кастомного ника
|
||||
*/
|
||||
Optional<String> getNickname(UUID player);
|
||||
|
||||
/**
|
||||
* Проверяет занят ли ник
|
||||
* @param nickname Ник для проверки
|
||||
* @return true если ник уже используется
|
||||
*/
|
||||
boolean isNicknameTaken(String nickname);
|
||||
|
||||
/**
|
||||
* Валидирует ник (длина, символы)
|
||||
* @param nickname Ник для проверки
|
||||
* @return true если ник валиден
|
||||
*/
|
||||
boolean isValidNickname(String nickname);
|
||||
|
||||
/**
|
||||
* Обновляет displayName и tabName игрока
|
||||
* @param player Игрок
|
||||
*/
|
||||
void updatePlayerDisplay(Player player);
|
||||
|
||||
/**
|
||||
* Сохраняет данные
|
||||
*/
|
||||
void save();
|
||||
}
|
||||
|
|
@ -30,7 +30,7 @@ public class LoChatCommand implements CommandExecutor, TabCompleter {
|
|||
}
|
||||
|
||||
if (args.length == 0) {
|
||||
sender.sendMessage(MM.deserialize("&#FFD700LoChat v" + plugin.getDescription().getVersion()));
|
||||
sender.sendMessage(MM.deserialize("&#FFD700LoChat v" + plugin.getPluginMeta().getVersion()));
|
||||
sender.sendMessage(MM.deserialize("󅒐Команды:"));
|
||||
sender.sendMessage(MM.deserialize("&#FFFF00/lochat reload - Перезагрузить конфиги"));
|
||||
sender.sendMessage(MM.deserialize("&#FFFF00/lochat commands - Список кастомных команд"));
|
||||
|
|
|
|||
135
src/main/java/com/loki/lochat/commands/NickCommand.java
Normal file
135
src/main/java/com/loki/lochat/commands/NickCommand.java
Normal file
|
|
@ -0,0 +1,135 @@
|
|||
package com.loki.lochat.commands;
|
||||
|
||||
import com.loki.lochat.LoChat;
|
||||
import com.loki.lochat.api.service.NickService;
|
||||
import com.loki.lochat.utils.ChatFormatter;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.format.NamedTextColor;
|
||||
import org.bukkit.command.Command;
|
||||
import org.bukkit.command.CommandExecutor;
|
||||
import org.bukkit.command.CommandSender;
|
||||
import org.bukkit.command.TabCompleter;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Команда /nick для установки кастомного ника
|
||||
*/
|
||||
public class NickCommand implements CommandExecutor, TabCompleter {
|
||||
private final LoChat plugin;
|
||||
private final NickService nickService;
|
||||
|
||||
public NickCommand(LoChat plugin) {
|
||||
this.plugin = plugin;
|
||||
this.nickService = plugin.getServiceRegistry().get(NickService.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
|
||||
if (!(sender instanceof Player player)) {
|
||||
sender.sendMessage(Component.text("Эта команда только для игроков!", NamedTextColor.RED));
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!player.hasPermission("lochat.nick")) {
|
||||
player.sendMessage(Component.text("У вас нет прав на использование этой команды!", NamedTextColor.RED));
|
||||
return true;
|
||||
}
|
||||
|
||||
// /nick - показать текущий ник
|
||||
if (args.length == 0) {
|
||||
return showCurrentNick(player);
|
||||
}
|
||||
|
||||
// /nick reset - сбросить ник
|
||||
if (args[0].equalsIgnoreCase("reset")) {
|
||||
return resetNick(player);
|
||||
}
|
||||
|
||||
// /nick <ник> - установить ник
|
||||
String nickname = String.join(" ", args);
|
||||
return setNick(player, nickname);
|
||||
}
|
||||
|
||||
private boolean showCurrentNick(Player player) {
|
||||
var currentNick = nickService.getNickname(player.getUniqueId());
|
||||
|
||||
if (currentNick.isPresent()) {
|
||||
Component nickComponent = ChatFormatter.parse(currentNick.get());
|
||||
player.sendMessage(Component.text("Ваш текущий ник: ", NamedTextColor.GRAY)
|
||||
.append(nickComponent));
|
||||
player.sendMessage(Component.text("Используйте ", NamedTextColor.GRAY)
|
||||
.append(Component.text("/nick reset", NamedTextColor.YELLOW))
|
||||
.append(Component.text(" для сброса", NamedTextColor.GRAY)));
|
||||
} else {
|
||||
player.sendMessage(Component.text("У вас нет кастомного ника", NamedTextColor.GRAY));
|
||||
player.sendMessage(Component.text("Используйте ", NamedTextColor.GRAY)
|
||||
.append(Component.text("/nick <ник>", NamedTextColor.YELLOW))
|
||||
.append(Component.text(" для установки", NamedTextColor.GRAY)));
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean resetNick(Player player) {
|
||||
var currentNick = nickService.getNickname(player.getUniqueId());
|
||||
|
||||
if (currentNick.isEmpty()) {
|
||||
player.sendMessage(Component.text("У вас нет кастомного ника!", NamedTextColor.RED));
|
||||
return true;
|
||||
}
|
||||
|
||||
nickService.resetNickname(player.getUniqueId());
|
||||
player.sendMessage(Component.text("Ваш ник сброшен!", NamedTextColor.GREEN));
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean setNick(Player player, String nickname) {
|
||||
// Валидация
|
||||
if (!nickService.isValidNickname(nickname)) {
|
||||
player.sendMessage(Component.text("Невалидный ник!", NamedTextColor.RED));
|
||||
player.sendMessage(Component.text("Требования:", NamedTextColor.GRAY));
|
||||
player.sendMessage(Component.text("• Длина: 3-16 символов", NamedTextColor.GRAY));
|
||||
player.sendMessage(Component.text("• Разрешены: буквы, цифры, _, русские буквы", NamedTextColor.GRAY));
|
||||
player.sendMessage(Component.text("• Поддержка цветов: <red>текст</red>, <gradient:#FF0000:#00FF00>текст</gradient>", NamedTextColor.GRAY));
|
||||
return true;
|
||||
}
|
||||
|
||||
// Проверка уникальности
|
||||
if (nickService.isNicknameTaken(nickname)) {
|
||||
var currentNick = nickService.getNickname(player.getUniqueId());
|
||||
// Если это не свой текущий ник
|
||||
if (currentNick.isEmpty() || !currentNick.get().equals(nickname)) {
|
||||
player.sendMessage(Component.text("Этот ник уже занят!", NamedTextColor.RED));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Устанавливаем ник
|
||||
boolean success = nickService.setNickname(player.getUniqueId(), nickname);
|
||||
|
||||
if (success) {
|
||||
Component nickComponent = ChatFormatter.parse(nickname);
|
||||
player.sendMessage(Component.text("Ваш ник изменён на: ", NamedTextColor.GREEN)
|
||||
.append(nickComponent));
|
||||
} else {
|
||||
player.sendMessage(Component.text("Не удалось установить ник!", NamedTextColor.RED));
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> onTabComplete(CommandSender sender, Command command, String alias, String[] args) {
|
||||
List<String> completions = new ArrayList<>();
|
||||
|
||||
if (args.length == 1) {
|
||||
completions.add("reset");
|
||||
completions.add("<ник>");
|
||||
}
|
||||
|
||||
return completions;
|
||||
}
|
||||
}
|
||||
|
|
@ -37,7 +37,7 @@ public class PlayerInfoCommand implements CommandExecutor {
|
|||
// Показываем информацию об игроке
|
||||
sender.sendMessage(ChatFormatter.parse("§6=== Информация об игроке ==="));
|
||||
sender.sendMessage(ChatFormatter.parse("§eИмя: §f" + target.getName()));
|
||||
sender.sendMessage(ChatFormatter.parse("§eОтображаемое имя: §f" + target.getDisplayName()));
|
||||
sender.sendMessage(ChatFormatter.parse("§eОтображаемое имя: §f" + target.displayName()));
|
||||
sender.sendMessage(ChatFormatter.parse("§eУровень: §f" + target.getLevel()));
|
||||
sender.sendMessage(ChatFormatter.parse("§eЗдоровье: §f" + Math.round(target.getHealth()) + "/20"));
|
||||
sender.sendMessage(ChatFormatter.parse("§eМир: §f" + target.getWorld().getName()));
|
||||
|
|
|
|||
|
|
@ -48,6 +48,7 @@ public class ServiceRegistry {
|
|||
register(IgnoreService.class, new IgnoreServiceImpl(plugin));
|
||||
register(SpyService.class, new SpyServiceImpl(plugin, messageConfig));
|
||||
register(MentionService.class, new MentionServiceImpl(configManager));
|
||||
register(NickService.class, new NickServiceImpl(plugin));
|
||||
}
|
||||
|
||||
public <T> void register(Class<T> serviceClass, T implementation) {
|
||||
|
|
|
|||
|
|
@ -34,11 +34,13 @@ public class ChatServiceImpl implements ChatService {
|
|||
? (Component) message
|
||||
: ChatFormatter.parse(message.toString());
|
||||
|
||||
String formatted = format
|
||||
.replace("{prefix}", prefix)
|
||||
.replace("{player}", sender.getName());
|
||||
// Используем displayName игрока (с градиентом если есть)
|
||||
Component playerName = sender.displayName();
|
||||
|
||||
String formatted = format.replace("{prefix}", prefix);
|
||||
|
||||
Component finalComponent = ChatFormatter.parse(formatted)
|
||||
.replaceText(builder -> builder.matchLiteral("{player}").replacement(playerName))
|
||||
.replaceText(builder -> builder.matchLiteral("{message}").replacement(messageComponent));
|
||||
|
||||
for (Player player : Bukkit.getOnlinePlayers()) {
|
||||
|
|
@ -58,9 +60,11 @@ public class ChatServiceImpl implements ChatService {
|
|||
? (Component) message
|
||||
: ChatFormatter.parse(message.toString());
|
||||
|
||||
String formatted = format.replace("{player}", sender.getName());
|
||||
// Используем displayName игрока (с градиентом если есть)
|
||||
Component playerName = sender.displayName();
|
||||
|
||||
Component finalComponent = ChatFormatter.parse(formatted)
|
||||
Component finalComponent = ChatFormatter.parse(format)
|
||||
.replaceText(builder -> builder.matchLiteral("{player}").replacement(playerName))
|
||||
.replaceText(builder -> builder.matchLiteral("{message}").replacement(messageComponent));
|
||||
|
||||
for (Player player : Bukkit.getOnlinePlayers()) {
|
||||
|
|
|
|||
|
|
@ -37,22 +37,15 @@ public class MessageServiceImpl implements MessageService {
|
|||
// Создаем объект сообщения
|
||||
ChatMessage message = ChatMessage.create(player, rawMessage);
|
||||
|
||||
// Применяем фильтры
|
||||
// Применяем фильтры (мут, кулдаун)
|
||||
for (MessageFilter filter : filters) {
|
||||
if (!filter.apply(player, message)) {
|
||||
return false; // Сообщение заблокировано
|
||||
}
|
||||
}
|
||||
|
||||
// Отправляем сообщение
|
||||
Component formattedMessage = ChatFormatter.parse(message.getContent());
|
||||
|
||||
if (message.isGlobal()) {
|
||||
chatService.sendGlobalMessage(player, formattedMessage);
|
||||
} else {
|
||||
chatService.sendLocalMessage(player, formattedMessage);
|
||||
}
|
||||
|
||||
// Возвращаем true - сообщение разрешено
|
||||
// Рендерер сам отправит его
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
187
src/main/java/com/loki/lochat/core/service/NickServiceImpl.java
Normal file
187
src/main/java/com/loki/lochat/core/service/NickServiceImpl.java
Normal file
|
|
@ -0,0 +1,187 @@
|
|||
package com.loki.lochat.core.service;
|
||||
|
||||
import com.loki.lochat.api.service.NickService;
|
||||
import com.loki.lochat.utils.ChatFormatter;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.configuration.file.FileConfiguration;
|
||||
import org.bukkit.configuration.file.YamlConfiguration;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.plugin.java.JavaPlugin;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
* Реализация сервиса кастомных ников
|
||||
*/
|
||||
public class NickServiceImpl implements NickService {
|
||||
private final JavaPlugin plugin;
|
||||
private final Map<UUID, String> nicknames = new ConcurrentHashMap<>();
|
||||
private final File dataFile;
|
||||
private FileConfiguration data;
|
||||
|
||||
// Настройки
|
||||
private static final int MIN_LENGTH = 3;
|
||||
private static final int MAX_LENGTH = 16;
|
||||
|
||||
public NickServiceImpl(JavaPlugin plugin) {
|
||||
this.plugin = plugin;
|
||||
this.dataFile = new File(plugin.getDataFolder(), "nicknames.yml");
|
||||
load();
|
||||
}
|
||||
|
||||
private void load() {
|
||||
if (!dataFile.exists()) {
|
||||
try {
|
||||
dataFile.getParentFile().mkdirs();
|
||||
dataFile.createNewFile();
|
||||
} catch (IOException e) {
|
||||
plugin.getLogger().warning("Не удалось создать nicknames.yml: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
data = YamlConfiguration.loadConfiguration(dataFile);
|
||||
|
||||
// Загружаем ники
|
||||
if (data.contains("nicknames")) {
|
||||
for (String key : data.getConfigurationSection("nicknames").getKeys(false)) {
|
||||
UUID uuid = UUID.fromString(key);
|
||||
String nickname = data.getString("nicknames." + key);
|
||||
nicknames.put(uuid, nickname);
|
||||
}
|
||||
}
|
||||
|
||||
plugin.getLogger().info("Загружено " + nicknames.size() + " кастомных ников");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void save() {
|
||||
// Сохраняем ники
|
||||
data.set("nicknames", null); // Очищаем
|
||||
for (Map.Entry<UUID, String> entry : nicknames.entrySet()) {
|
||||
data.set("nicknames." + entry.getKey().toString(), entry.getValue());
|
||||
}
|
||||
|
||||
try {
|
||||
data.save(dataFile);
|
||||
} catch (IOException e) {
|
||||
plugin.getLogger().warning("Не удалось сохранить nicknames.yml: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean setNickname(UUID player, String nickname) {
|
||||
// Валидация
|
||||
if (!isValidNickname(nickname)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Проверка уникальности
|
||||
if (isNicknameTaken(nickname)) {
|
||||
// Проверяем не свой ли это ник
|
||||
String currentNick = nicknames.get(player);
|
||||
if (currentNick == null || !stripColors(currentNick).equalsIgnoreCase(stripColors(nickname))) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Устанавливаем ник
|
||||
nicknames.put(player, nickname);
|
||||
|
||||
// Обновляем display
|
||||
Player onlinePlayer = Bukkit.getPlayer(player);
|
||||
if (onlinePlayer != null) {
|
||||
updatePlayerDisplay(onlinePlayer);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void resetNickname(UUID player) {
|
||||
nicknames.remove(player);
|
||||
|
||||
// Сбрасываем display
|
||||
Player onlinePlayer = Bukkit.getPlayer(player);
|
||||
if (onlinePlayer != null) {
|
||||
onlinePlayer.displayName(Component.text(onlinePlayer.getName()));
|
||||
onlinePlayer.playerListName(Component.text(onlinePlayer.getName()));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<String> getNickname(UUID player) {
|
||||
return Optional.ofNullable(nicknames.get(player));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isNicknameTaken(String nickname) {
|
||||
String cleanNick = stripColors(nickname).toLowerCase();
|
||||
|
||||
// Проверяем существующие ники
|
||||
for (String existingNick : nicknames.values()) {
|
||||
if (stripColors(existingNick).equalsIgnoreCase(cleanNick)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Проверяем реальные ники онлайн игроков
|
||||
for (Player player : Bukkit.getOnlinePlayers()) {
|
||||
if (player.getName().equalsIgnoreCase(cleanNick)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isValidNickname(String nickname) {
|
||||
if (nickname == null || nickname.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Убираем цвета для проверки длины
|
||||
String cleanNick = stripColors(nickname);
|
||||
|
||||
// Проверка длины
|
||||
if (cleanNick.length() < MIN_LENGTH || cleanNick.length() > MAX_LENGTH) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Проверка символов (разрешены буквы, цифры, _, русские буквы)
|
||||
if (!cleanNick.matches("[a-zA-Zа-яА-Я0-9_]+")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updatePlayerDisplay(Player player) {
|
||||
Optional<String> nickname = getNickname(player.getUniqueId());
|
||||
|
||||
if (nickname.isPresent()) {
|
||||
// Парсим ник с цветами
|
||||
Component nickComponent = ChatFormatter.parse(nickname.get());
|
||||
player.displayName(nickComponent);
|
||||
player.playerListName(nickComponent);
|
||||
} else {
|
||||
// Используем обычное имя
|
||||
player.displayName(Component.text(player.getName()));
|
||||
player.playerListName(Component.text(player.getName()));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Убирает цвета из строки
|
||||
*/
|
||||
private String stripColors(String text) {
|
||||
Component component = ChatFormatter.parse(text);
|
||||
return PlainTextComponentSerializer.plainText().serialize(component);
|
||||
}
|
||||
}
|
||||
|
|
@ -188,7 +188,9 @@ public class GradientAdminCommand implements CommandExecutor, TabCompleter {
|
|||
data.setPrefixEnabled(true);
|
||||
data.setPrefixPurchased(true);
|
||||
|
||||
module.getLuckPermsHook().setPrefix(target.getUniqueId(), DisplayNameUtil.buildColoredPrefix(module, data));
|
||||
if (module.getLuckPermsHook() != null) {
|
||||
module.getLuckPermsHook().setPrefix(target.getUniqueId(), DisplayNameUtil.buildColoredPrefix(module, data));
|
||||
}
|
||||
|
||||
Player onlineTarget = target.getPlayer();
|
||||
if (onlineTarget != null) {
|
||||
|
|
@ -244,10 +246,12 @@ public class GradientAdminCommand implements CommandExecutor, TabCompleter {
|
|||
GradientPlayerData data = module.getDataManager().getPlayerData(target.getUniqueId());
|
||||
data.setPrefixEnabled(enable);
|
||||
|
||||
if (enable && data.hasPrefix()) {
|
||||
module.getLuckPermsHook().setPrefix(target.getUniqueId(), DisplayNameUtil.buildColoredPrefix(module, data));
|
||||
} else {
|
||||
module.getLuckPermsHook().removePrefix(target.getUniqueId());
|
||||
if (module.getLuckPermsHook() != null) {
|
||||
if (enable && data.hasPrefix()) {
|
||||
module.getLuckPermsHook().setPrefix(target.getUniqueId(), DisplayNameUtil.buildColoredPrefix(module, data));
|
||||
} else {
|
||||
module.getLuckPermsHook().removePrefix(target.getUniqueId());
|
||||
}
|
||||
}
|
||||
|
||||
Player onlineTarget = target.getPlayer();
|
||||
|
|
@ -306,7 +310,9 @@ public class GradientAdminCommand implements CommandExecutor, TabCompleter {
|
|||
data.setPrefix(null);
|
||||
data.setPrefixEnabled(false);
|
||||
|
||||
module.getLuckPermsHook().removePrefix(target.getUniqueId());
|
||||
if (module.getLuckPermsHook() != null) {
|
||||
module.getLuckPermsHook().removePrefix(target.getUniqueId());
|
||||
}
|
||||
|
||||
Player onlineTarget = target.getPlayer();
|
||||
if (onlineTarget != null) {
|
||||
|
|
|
|||
|
|
@ -84,7 +84,9 @@ public class GradientPrefixCommand implements CommandExecutor, TabCompleter {
|
|||
}
|
||||
|
||||
data.setPrefixEnabled(true);
|
||||
module.getLuckPermsHook().setPrefix(player, DisplayNameUtil.buildColoredPrefix(module, data));
|
||||
if (module.getLuckPermsHook() != null) {
|
||||
module.getLuckPermsHook().setPrefix(player, DisplayNameUtil.buildColoredPrefix(module, data));
|
||||
}
|
||||
saveAndUpdate(player, data);
|
||||
msg.send(player, "prefix-enabled");
|
||||
return true;
|
||||
|
|
@ -93,7 +95,9 @@ public class GradientPrefixCommand implements CommandExecutor, TabCompleter {
|
|||
private boolean handleOff(Player player, String[] args) {
|
||||
GradientPlayerData data = module.getDataManager().getPlayerData(player.getUniqueId());
|
||||
data.setPrefixEnabled(false);
|
||||
module.getLuckPermsHook().removePrefix(player);
|
||||
if (module.getLuckPermsHook() != null) {
|
||||
module.getLuckPermsHook().removePrefix(player);
|
||||
}
|
||||
saveAndUpdate(player, data);
|
||||
module.getMessages().send(player, "prefix-disabled");
|
||||
return true;
|
||||
|
|
@ -110,7 +114,9 @@ public class GradientPrefixCommand implements CommandExecutor, TabCompleter {
|
|||
|
||||
data.setPrefix(null);
|
||||
data.setPrefixEnabled(false);
|
||||
module.getLuckPermsHook().removePrefix(player);
|
||||
if (module.getLuckPermsHook() != null) {
|
||||
module.getLuckPermsHook().removePrefix(player);
|
||||
}
|
||||
saveAndUpdate(player, data);
|
||||
msg.send(player, "prefix-reset-success");
|
||||
return true;
|
||||
|
|
|
|||
|
|
@ -129,7 +129,7 @@ public class GradientConfirmGUI implements InventoryHolder {
|
|||
boolean useLegacyForPreview = true;
|
||||
|
||||
// Если нет кастомного префикса - используем LuckPerms префикс
|
||||
if ((prefixToUse == null || prefixToUse.isEmpty()) && module.getLuckPermsHook().isEnabled()) {
|
||||
if ((prefixToUse == null || prefixToUse.isEmpty()) && module.getLuckPermsHook() != null && module.getLuckPermsHook().isEnabled()) {
|
||||
String lpPrefix = module.getLuckPermsHook().getActivePrefix(player);
|
||||
if (lpPrefix != null && !lpPrefix.isEmpty()) {
|
||||
// Конвертируем LP префикс в legacy формат
|
||||
|
|
|
|||
|
|
@ -82,7 +82,9 @@ public class GradientGUIListener implements Listener {
|
|||
data.setColors(gui.getColors());
|
||||
data.setColorEnabled(true);
|
||||
}
|
||||
module.getLuckPermsHook().setPrefix(player, DisplayNameUtil.buildColoredPrefix(module, data));
|
||||
if (module.getLuckPermsHook() != null) {
|
||||
module.getLuckPermsHook().setPrefix(player, DisplayNameUtil.buildColoredPrefix(module, data));
|
||||
}
|
||||
msg.send(player, price > 0 ? "prefix-success" : "prefix-success-free",
|
||||
"price", String.valueOf(price));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ public final class DisplayNameUtil {
|
|||
if (data.isPrefixEnabled() && data.hasPrefix()) {
|
||||
// Есть кастомный префикс
|
||||
prefix = data.getPrefix();
|
||||
} else if (module.getLuckPermsHook().isEnabled()) {
|
||||
} else if (module.getLuckPermsHook() != null && module.getLuckPermsHook().isEnabled()) {
|
||||
// Нет кастомного префикса, проверяем LuckPerms
|
||||
String lpPrefix = module.getLuckPermsHook().getActivePrefix(player);
|
||||
if (lpPrefix != null && !lpPrefix.isEmpty()) {
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@ public class PlaceholderAPIHook extends PlaceholderExpansion {
|
|||
|
||||
@Override
|
||||
public @NotNull String getVersion() {
|
||||
return plugin.getDescription().getVersion();
|
||||
return plugin.getPluginMeta().getVersion();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
|||
|
|
@ -3,15 +3,25 @@ package com.loki.lochat.listener;
|
|||
import com.loki.lochat.api.service.MessageService;
|
||||
import com.loki.lochat.core.registry.ServiceRegistry;
|
||||
import com.loki.lochat.util.FoliaUtil;
|
||||
import io.papermc.paper.event.player.AsyncChatEvent;
|
||||
import io.papermc.paper.chat.ChatRenderer;
|
||||
import net.kyori.adventure.audience.Audience;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.event.ClickEvent;
|
||||
import net.kyori.adventure.text.event.HoverEvent;
|
||||
import net.kyori.adventure.text.format.NamedTextColor;
|
||||
import net.kyori.adventure.text.format.TextDecoration;
|
||||
import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer;
|
||||
import org.bukkit.Sound;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.event.EventPriority;
|
||||
import org.bukkit.event.Listener;
|
||||
import org.bukkit.event.player.AsyncPlayerChatEvent;
|
||||
import org.bukkit.plugin.java.JavaPlugin;
|
||||
|
||||
/**
|
||||
* Слушатель событий чата (минимальная ответственность)
|
||||
* Слушатель событий чата с Paper Chat API
|
||||
* Поддерживает hover tooltips, click actions, mentions
|
||||
*/
|
||||
public class ChatEventListener implements Listener {
|
||||
private final JavaPlugin plugin;
|
||||
|
|
@ -23,15 +33,196 @@ public class ChatEventListener implements Listener {
|
|||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.HIGHEST)
|
||||
public void onChat(AsyncPlayerChatEvent event) {
|
||||
event.setCancelled(true);
|
||||
public void onChat(AsyncChatEvent event) {
|
||||
Player sender = event.getPlayer();
|
||||
String message = PlainTextComponentSerializer.plainText().serialize(event.message());
|
||||
|
||||
Player player = event.getPlayer();
|
||||
String message = event.getMessage();
|
||||
// Проверяем фильтры (мут, кулдаун)
|
||||
boolean allowed = messageService.processMessage(sender, message);
|
||||
|
||||
// Folia-safe обработка
|
||||
FoliaUtil.runEntityTask(plugin, player, () -> {
|
||||
messageService.processMessage(player, message);
|
||||
});
|
||||
if (!allowed) {
|
||||
event.setCancelled(true);
|
||||
return;
|
||||
}
|
||||
|
||||
// Устанавливаем кастомный рендерер
|
||||
event.renderer(new EnhancedChatRenderer(sender, event.message()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Продвинутый рендерер чата с hover/click/mentions
|
||||
*/
|
||||
private class EnhancedChatRenderer implements ChatRenderer {
|
||||
private final Player sender;
|
||||
private final Component originalMessage;
|
||||
|
||||
public EnhancedChatRenderer(Player sender, Component originalMessage) {
|
||||
this.sender = sender;
|
||||
this.originalMessage = originalMessage;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Component render(Player source, Component sourceDisplayName, Component message, Audience viewer) {
|
||||
// Получаем конфиг формата
|
||||
String format = plugin.getConfig().getString("chat.global.format", "{prefix} {player}: {message}");
|
||||
String prefix = plugin.getConfig().getString("chat.global.prefix", "[G]");
|
||||
|
||||
// Создаём компонент с hover и click
|
||||
Component playerComponent = createPlayerComponent(source, sourceDisplayName);
|
||||
|
||||
// Обрабатываем сообщение
|
||||
Component processedMessage = message;
|
||||
|
||||
// 1. Обрабатываем URL (делаем кликабельными)
|
||||
processedMessage = processUrls(processedMessage);
|
||||
|
||||
// 2. Обрабатываем эмодзи шорткоды (:smile: -> 😊)
|
||||
processedMessage = processEmojis(processedMessage);
|
||||
|
||||
// 3. Обрабатываем упоминания если viewer это Player
|
||||
if (viewer instanceof Player viewerPlayer) {
|
||||
processedMessage = processMentions(processedMessage, viewerPlayer);
|
||||
}
|
||||
|
||||
// Собираем финальное сообщение
|
||||
return Component.text(prefix + " ")
|
||||
.append(playerComponent)
|
||||
.append(Component.text(": "))
|
||||
.append(processedMessage);
|
||||
}
|
||||
|
||||
/**
|
||||
* Создаёт компонент имени игрока с hover и click
|
||||
*/
|
||||
private Component createPlayerComponent(Player player, Component displayName) {
|
||||
// Проверяем включен ли hover
|
||||
if (!plugin.getConfig().getBoolean("chat.hover.enabled", true)) {
|
||||
return displayName.clickEvent(ClickEvent.suggestCommand("/msg " + player.getName() + " "));
|
||||
}
|
||||
|
||||
// Получаем формат из конфига
|
||||
java.util.List<String> hoverLines = plugin.getConfig().getStringList("chat.hover.format");
|
||||
if (hoverLines.isEmpty()) {
|
||||
// Дефолтный формат если конфиг пустой
|
||||
hoverLines = java.util.Arrays.asList(
|
||||
"<gray>Игрок: <white>{player}",
|
||||
"<gray>Мир: <green>{world}",
|
||||
"",
|
||||
"<yellow>▶ Клик: написать ЛС"
|
||||
);
|
||||
}
|
||||
|
||||
// Строим hover text
|
||||
net.kyori.adventure.text.TextComponent.Builder hoverBuilder = Component.text();
|
||||
for (int i = 0; i < hoverLines.size(); i++) {
|
||||
String line = hoverLines.get(i);
|
||||
|
||||
// Заменяем плейсхолдеры
|
||||
line = line.replace("{player}", player.getName())
|
||||
.replace("{world}", player.getWorld().getName())
|
||||
.replace("{ping}", String.valueOf(player.getPing()))
|
||||
.replace("{gamemode}", player.getGameMode().name())
|
||||
.replace("{health}", String.valueOf(Math.round(player.getHealth())))
|
||||
.replace("{food}", String.valueOf(player.getFoodLevel()));
|
||||
|
||||
// Парсим MiniMessage
|
||||
Component lineComponent = com.loki.lochat.utils.ChatFormatter.parse(line);
|
||||
hoverBuilder.append(lineComponent);
|
||||
|
||||
// Добавляем перенос строки если не последняя
|
||||
if (i < hoverLines.size() - 1) {
|
||||
hoverBuilder.append(Component.newline());
|
||||
}
|
||||
}
|
||||
|
||||
// Click: открываем /msg
|
||||
return displayName
|
||||
.hoverEvent(HoverEvent.showText(hoverBuilder.build()))
|
||||
.clickEvent(ClickEvent.suggestCommand("/msg " + player.getName() + " "));
|
||||
}
|
||||
|
||||
/**
|
||||
* Обрабатывает упоминания (@player) в сообщении
|
||||
*/
|
||||
private Component processMentions(Component message, Player viewer) {
|
||||
String plainText = PlainTextComponentSerializer.plainText().serialize(message);
|
||||
|
||||
// Проверяем упоминание текущего игрока
|
||||
if (plainText.toLowerCase().contains("@" + viewer.getName().toLowerCase())) {
|
||||
// Подсвечиваем упоминание
|
||||
Component highlighted = message.replaceText(builder ->
|
||||
builder.matchLiteral("@" + viewer.getName())
|
||||
.replacement(Component.text("@" + viewer.getName(), NamedTextColor.YELLOW, TextDecoration.BOLD))
|
||||
);
|
||||
|
||||
// Воспроизводим звук уведомления
|
||||
FoliaUtil.runEntityTask(plugin, viewer, () -> {
|
||||
viewer.playSound(viewer.getLocation(), Sound.ENTITY_EXPERIENCE_ORB_PICKUP, 1.0f, 1.5f);
|
||||
});
|
||||
|
||||
return highlighted;
|
||||
}
|
||||
|
||||
return message;
|
||||
}
|
||||
|
||||
/**
|
||||
* Обрабатывает URL в сообщении (делает кликабельными)
|
||||
*/
|
||||
private Component processUrls(Component message) {
|
||||
String plainText = PlainTextComponentSerializer.plainText().serialize(message);
|
||||
|
||||
// Простой regex для URL
|
||||
String urlPattern = "(https?://[\\w\\-._~:/?#\\[\\]@!$&'()*+,;=%]+)";
|
||||
java.util.regex.Pattern pattern = java.util.regex.Pattern.compile(urlPattern);
|
||||
java.util.regex.Matcher matcher = pattern.matcher(plainText);
|
||||
|
||||
Component result = message;
|
||||
while (matcher.find()) {
|
||||
String url = matcher.group(1);
|
||||
// Делаем URL кликабельным с hover
|
||||
Component urlComponent = Component.text(url, NamedTextColor.AQUA, TextDecoration.UNDERLINED)
|
||||
.hoverEvent(HoverEvent.showText(Component.text("Открыть: " + url, NamedTextColor.GRAY)))
|
||||
.clickEvent(ClickEvent.openUrl(url));
|
||||
|
||||
result = result.replaceText(builder ->
|
||||
builder.matchLiteral(url).replacement(urlComponent)
|
||||
);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Обрабатывает эмодзи шорткоды (:smile: -> ☺)
|
||||
* Использует только MC-совместимые Unicode символы
|
||||
*/
|
||||
private Component processEmojis(Component message) {
|
||||
Component result = message;
|
||||
|
||||
// MC-совместимые Unicode эмодзи
|
||||
result = result.replaceText(builder -> builder.matchLiteral(":smile:").replacement("☺"));
|
||||
result = result.replaceText(builder -> builder.matchLiteral(":heart:").replacement("♥"));
|
||||
result = result.replaceText(builder -> builder.matchLiteral(":fire:").replacement("※"));
|
||||
result = result.replaceText(builder -> builder.matchLiteral(":star:").replacement("★"));
|
||||
result = result.replaceText(builder -> builder.matchLiteral(":check:").replacement("✓"));
|
||||
result = result.replaceText(builder -> builder.matchLiteral(":cross:").replacement("✗"));
|
||||
result = result.replaceText(builder -> builder.matchLiteral(":skull:").replacement("☠"));
|
||||
result = result.replaceText(builder -> builder.matchLiteral(":arrow:").replacement("→"));
|
||||
result = result.replaceText(builder -> builder.matchLiteral(":note:").replacement("♪"));
|
||||
result = result.replaceText(builder -> builder.matchLiteral(":sun:").replacement("☀"));
|
||||
result = result.replaceText(builder -> builder.matchLiteral(":moon:").replacement("☾"));
|
||||
result = result.replaceText(builder -> builder.matchLiteral(":peace:").replacement("☮"));
|
||||
result = result.replaceText(builder -> builder.matchLiteral(":yin:").replacement("☯"));
|
||||
result = result.replaceText(builder -> builder.matchLiteral(":flower:").replacement("✿"));
|
||||
result = result.replaceText(builder -> builder.matchLiteral(":snowflake:").replacement("❄"));
|
||||
result = result.replaceText(builder -> builder.matchLiteral(":lightning:").replacement("⚡"));
|
||||
result = result.replaceText(builder -> builder.matchLiteral(":crown:").replacement("♔"));
|
||||
result = result.replaceText(builder -> builder.matchLiteral(":sword:").replacement("⚔"));
|
||||
result = result.replaceText(builder -> builder.matchLiteral(":shield:").replacement("⛨"));
|
||||
result = result.replaceText(builder -> builder.matchLiteral(":diamond:").replacement("◆"));
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,12 @@
|
|||
package com.loki.lochat.listener;
|
||||
|
||||
import com.loki.lochat.api.service.NickService;
|
||||
import com.loki.lochat.api.service.PlayerDataService;
|
||||
import com.loki.lochat.core.registry.ServiceRegistry;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.event.Listener;
|
||||
import org.bukkit.event.player.PlayerJoinEvent;
|
||||
import org.bukkit.event.player.PlayerQuitEvent;
|
||||
|
||||
/**
|
||||
|
|
@ -12,9 +14,20 @@ import org.bukkit.event.player.PlayerQuitEvent;
|
|||
*/
|
||||
public class PlayerEventListener implements Listener {
|
||||
private final PlayerDataService playerDataService;
|
||||
private final NickService nickService;
|
||||
|
||||
public PlayerEventListener(ServiceRegistry registry) {
|
||||
this.playerDataService = registry.get(PlayerDataService.class);
|
||||
this.nickService = registry.get(NickService.class);
|
||||
}
|
||||
|
||||
@EventHandler
|
||||
public void onJoin(PlayerJoinEvent event) {
|
||||
Player player = event.getPlayer();
|
||||
// Обновляем displayName с кастомным ником если есть
|
||||
if (nickService != null) {
|
||||
nickService.updatePlayerDisplay(player);
|
||||
}
|
||||
}
|
||||
|
||||
@EventHandler
|
||||
|
|
|
|||
|
|
@ -63,6 +63,22 @@ chat:
|
|||
sound: true
|
||||
sound-type: "BLOCK_NOTE_BLOCK_PLING"
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
# HOVER TOOLTIP (наведение на ник)
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
hover:
|
||||
# Включить hover tooltip при наведении на ник
|
||||
enabled: true
|
||||
# Формат tooltip (список строк)
|
||||
# Плейсхолдеры: {player}, {world}, {ping}, {gamemode}, {health}, {food}
|
||||
# Поддержка PlaceholderAPI: %placeholder%
|
||||
format:
|
||||
- "<gray>Игрок: <white>{player}"
|
||||
- "<gray>Мир: <green>{world}"
|
||||
- "<gray>Пинг: <yellow>{ping}ms"
|
||||
- ""
|
||||
- "<yellow>▶ Клик: написать ЛС"
|
||||
|
||||
# ┌─────────────────────────────────────────────────────────────────────────────┐
|
||||
# │ ГРАДИЕНТНЫЕ НИКИ │
|
||||
# └─────────────────────────────────────────────────────────────────────────────┘
|
||||
|
|
|
|||
|
|
@ -62,6 +62,12 @@ commands:
|
|||
permission: chat.announce.use
|
||||
aliases: [объявление, анонс, фттщгтсу]
|
||||
|
||||
nick:
|
||||
description: Set custom nickname
|
||||
usage: /nick [nickname|reset]
|
||||
permission: lochat.nick
|
||||
aliases: [ник, тшсл]
|
||||
|
||||
chatspy:
|
||||
description: Toggle chat spy mode
|
||||
usage: /chatspy
|
||||
|
|
@ -232,3 +238,6 @@ permissions:
|
|||
lochat.heademoji.use:
|
||||
description: Can use head emojis in chat
|
||||
default: true
|
||||
lochat.nick:
|
||||
description: Can use custom nicknames
|
||||
default: true
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue