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.IgnoreService;
|
||||||
import com.loki.lochat.api.service.MuteService;
|
import com.loki.lochat.api.service.MuteService;
|
||||||
|
import com.loki.lochat.api.service.NickService;
|
||||||
import com.loki.lochat.commands.*;
|
import com.loki.lochat.commands.*;
|
||||||
import com.loki.lochat.config.ConfigManager;
|
import com.loki.lochat.config.ConfigManager;
|
||||||
import com.loki.lochat.config.MessageConfig;
|
import com.loki.lochat.config.MessageConfig;
|
||||||
|
|
@ -33,7 +34,7 @@ public final class LoChat extends JavaPlugin {
|
||||||
@Override
|
@Override
|
||||||
public void onEnable() {
|
public void onEnable() {
|
||||||
instance = this;
|
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);
|
configManager = new ConfigManager(this);
|
||||||
|
|
@ -71,6 +72,9 @@ public final class LoChat extends JavaPlugin {
|
||||||
MuteService muteService = serviceRegistry.get(MuteService.class);
|
MuteService muteService = serviceRegistry.get(MuteService.class);
|
||||||
if (muteService != null) muteService.save();
|
if (muteService != null) muteService.save();
|
||||||
|
|
||||||
|
NickService nickService = serviceRegistry.get(NickService.class);
|
||||||
|
if (nickService != null) nickService.save();
|
||||||
|
|
||||||
if (gradientModule != null) gradientModule.shutdown();
|
if (gradientModule != null) gradientModule.shutdown();
|
||||||
getLogger().info("LoChat отключен!");
|
getLogger().info("LoChat отключен!");
|
||||||
}
|
}
|
||||||
|
|
@ -103,6 +107,10 @@ public final class LoChat extends JavaPlugin {
|
||||||
getCommand("chatspy").setExecutor(new ChatSpyCommand(this));
|
getCommand("chatspy").setExecutor(new ChatSpyCommand(this));
|
||||||
getCommand("clearchat").setExecutor(new ClearChatCommand(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);
|
LoChatCommand loChatCmd = new LoChatCommand(this);
|
||||||
getCommand("lochat").setExecutor(loChatCmd);
|
getCommand("lochat").setExecutor(loChatCmd);
|
||||||
getCommand("lochat").setTabCompleter(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) {
|
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("󅒐Команды:"));
|
||||||
sender.sendMessage(MM.deserialize("&#FFFF00/lochat reload - Перезагрузить конфиги"));
|
sender.sendMessage(MM.deserialize("&#FFFF00/lochat reload - Перезагрузить конфиги"));
|
||||||
sender.sendMessage(MM.deserialize("&#FFFF00/lochat commands - Список кастомных команд"));
|
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("§6=== Информация об игроке ==="));
|
||||||
sender.sendMessage(ChatFormatter.parse("§eИмя: §f" + target.getName()));
|
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" + target.getLevel()));
|
||||||
sender.sendMessage(ChatFormatter.parse("§eЗдоровье: §f" + Math.round(target.getHealth()) + "/20"));
|
sender.sendMessage(ChatFormatter.parse("§eЗдоровье: §f" + Math.round(target.getHealth()) + "/20"));
|
||||||
sender.sendMessage(ChatFormatter.parse("§eМир: §f" + target.getWorld().getName()));
|
sender.sendMessage(ChatFormatter.parse("§eМир: §f" + target.getWorld().getName()));
|
||||||
|
|
|
||||||
|
|
@ -48,6 +48,7 @@ public class ServiceRegistry {
|
||||||
register(IgnoreService.class, new IgnoreServiceImpl(plugin));
|
register(IgnoreService.class, new IgnoreServiceImpl(plugin));
|
||||||
register(SpyService.class, new SpyServiceImpl(plugin, messageConfig));
|
register(SpyService.class, new SpyServiceImpl(plugin, messageConfig));
|
||||||
register(MentionService.class, new MentionServiceImpl(configManager));
|
register(MentionService.class, new MentionServiceImpl(configManager));
|
||||||
|
register(NickService.class, new NickServiceImpl(plugin));
|
||||||
}
|
}
|
||||||
|
|
||||||
public <T> void register(Class<T> serviceClass, T implementation) {
|
public <T> void register(Class<T> serviceClass, T implementation) {
|
||||||
|
|
|
||||||
|
|
@ -34,11 +34,13 @@ public class ChatServiceImpl implements ChatService {
|
||||||
? (Component) message
|
? (Component) message
|
||||||
: ChatFormatter.parse(message.toString());
|
: ChatFormatter.parse(message.toString());
|
||||||
|
|
||||||
String formatted = format
|
// Используем displayName игрока (с градиентом если есть)
|
||||||
.replace("{prefix}", prefix)
|
Component playerName = sender.displayName();
|
||||||
.replace("{player}", sender.getName());
|
|
||||||
|
String formatted = format.replace("{prefix}", prefix);
|
||||||
|
|
||||||
Component finalComponent = ChatFormatter.parse(formatted)
|
Component finalComponent = ChatFormatter.parse(formatted)
|
||||||
|
.replaceText(builder -> builder.matchLiteral("{player}").replacement(playerName))
|
||||||
.replaceText(builder -> builder.matchLiteral("{message}").replacement(messageComponent));
|
.replaceText(builder -> builder.matchLiteral("{message}").replacement(messageComponent));
|
||||||
|
|
||||||
for (Player player : Bukkit.getOnlinePlayers()) {
|
for (Player player : Bukkit.getOnlinePlayers()) {
|
||||||
|
|
@ -58,9 +60,11 @@ public class ChatServiceImpl implements ChatService {
|
||||||
? (Component) message
|
? (Component) message
|
||||||
: ChatFormatter.parse(message.toString());
|
: 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));
|
.replaceText(builder -> builder.matchLiteral("{message}").replacement(messageComponent));
|
||||||
|
|
||||||
for (Player player : Bukkit.getOnlinePlayers()) {
|
for (Player player : Bukkit.getOnlinePlayers()) {
|
||||||
|
|
|
||||||
|
|
@ -37,22 +37,15 @@ public class MessageServiceImpl implements MessageService {
|
||||||
// Создаем объект сообщения
|
// Создаем объект сообщения
|
||||||
ChatMessage message = ChatMessage.create(player, rawMessage);
|
ChatMessage message = ChatMessage.create(player, rawMessage);
|
||||||
|
|
||||||
// Применяем фильтры
|
// Применяем фильтры (мут, кулдаун)
|
||||||
for (MessageFilter filter : filters) {
|
for (MessageFilter filter : filters) {
|
||||||
if (!filter.apply(player, message)) {
|
if (!filter.apply(player, message)) {
|
||||||
return false; // Сообщение заблокировано
|
return false; // Сообщение заблокировано
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Отправляем сообщение
|
// Возвращаем true - сообщение разрешено
|
||||||
Component formattedMessage = ChatFormatter.parse(message.getContent());
|
// Рендерер сам отправит его
|
||||||
|
|
||||||
if (message.isGlobal()) {
|
|
||||||
chatService.sendGlobalMessage(player, formattedMessage);
|
|
||||||
} else {
|
|
||||||
chatService.sendLocalMessage(player, formattedMessage);
|
|
||||||
}
|
|
||||||
|
|
||||||
return 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.setPrefixEnabled(true);
|
||||||
data.setPrefixPurchased(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();
|
Player onlineTarget = target.getPlayer();
|
||||||
if (onlineTarget != null) {
|
if (onlineTarget != null) {
|
||||||
|
|
@ -244,10 +246,12 @@ public class GradientAdminCommand implements CommandExecutor, TabCompleter {
|
||||||
GradientPlayerData data = module.getDataManager().getPlayerData(target.getUniqueId());
|
GradientPlayerData data = module.getDataManager().getPlayerData(target.getUniqueId());
|
||||||
data.setPrefixEnabled(enable);
|
data.setPrefixEnabled(enable);
|
||||||
|
|
||||||
if (enable && data.hasPrefix()) {
|
if (module.getLuckPermsHook() != null) {
|
||||||
module.getLuckPermsHook().setPrefix(target.getUniqueId(), DisplayNameUtil.buildColoredPrefix(module, data));
|
if (enable && data.hasPrefix()) {
|
||||||
} else {
|
module.getLuckPermsHook().setPrefix(target.getUniqueId(), DisplayNameUtil.buildColoredPrefix(module, data));
|
||||||
module.getLuckPermsHook().removePrefix(target.getUniqueId());
|
} else {
|
||||||
|
module.getLuckPermsHook().removePrefix(target.getUniqueId());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Player onlineTarget = target.getPlayer();
|
Player onlineTarget = target.getPlayer();
|
||||||
|
|
@ -306,7 +310,9 @@ public class GradientAdminCommand implements CommandExecutor, TabCompleter {
|
||||||
data.setPrefix(null);
|
data.setPrefix(null);
|
||||||
data.setPrefixEnabled(false);
|
data.setPrefixEnabled(false);
|
||||||
|
|
||||||
module.getLuckPermsHook().removePrefix(target.getUniqueId());
|
if (module.getLuckPermsHook() != null) {
|
||||||
|
module.getLuckPermsHook().removePrefix(target.getUniqueId());
|
||||||
|
}
|
||||||
|
|
||||||
Player onlineTarget = target.getPlayer();
|
Player onlineTarget = target.getPlayer();
|
||||||
if (onlineTarget != null) {
|
if (onlineTarget != null) {
|
||||||
|
|
|
||||||
|
|
@ -84,7 +84,9 @@ public class GradientPrefixCommand implements CommandExecutor, TabCompleter {
|
||||||
}
|
}
|
||||||
|
|
||||||
data.setPrefixEnabled(true);
|
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);
|
saveAndUpdate(player, data);
|
||||||
msg.send(player, "prefix-enabled");
|
msg.send(player, "prefix-enabled");
|
||||||
return true;
|
return true;
|
||||||
|
|
@ -93,7 +95,9 @@ public class GradientPrefixCommand implements CommandExecutor, TabCompleter {
|
||||||
private boolean handleOff(Player player, String[] args) {
|
private boolean handleOff(Player player, String[] args) {
|
||||||
GradientPlayerData data = module.getDataManager().getPlayerData(player.getUniqueId());
|
GradientPlayerData data = module.getDataManager().getPlayerData(player.getUniqueId());
|
||||||
data.setPrefixEnabled(false);
|
data.setPrefixEnabled(false);
|
||||||
module.getLuckPermsHook().removePrefix(player);
|
if (module.getLuckPermsHook() != null) {
|
||||||
|
module.getLuckPermsHook().removePrefix(player);
|
||||||
|
}
|
||||||
saveAndUpdate(player, data);
|
saveAndUpdate(player, data);
|
||||||
module.getMessages().send(player, "prefix-disabled");
|
module.getMessages().send(player, "prefix-disabled");
|
||||||
return true;
|
return true;
|
||||||
|
|
@ -110,7 +114,9 @@ public class GradientPrefixCommand implements CommandExecutor, TabCompleter {
|
||||||
|
|
||||||
data.setPrefix(null);
|
data.setPrefix(null);
|
||||||
data.setPrefixEnabled(false);
|
data.setPrefixEnabled(false);
|
||||||
module.getLuckPermsHook().removePrefix(player);
|
if (module.getLuckPermsHook() != null) {
|
||||||
|
module.getLuckPermsHook().removePrefix(player);
|
||||||
|
}
|
||||||
saveAndUpdate(player, data);
|
saveAndUpdate(player, data);
|
||||||
msg.send(player, "prefix-reset-success");
|
msg.send(player, "prefix-reset-success");
|
||||||
return true;
|
return true;
|
||||||
|
|
|
||||||
|
|
@ -129,7 +129,7 @@ public class GradientConfirmGUI implements InventoryHolder {
|
||||||
boolean useLegacyForPreview = true;
|
boolean useLegacyForPreview = true;
|
||||||
|
|
||||||
// Если нет кастомного префикса - используем LuckPerms префикс
|
// Если нет кастомного префикса - используем 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);
|
String lpPrefix = module.getLuckPermsHook().getActivePrefix(player);
|
||||||
if (lpPrefix != null && !lpPrefix.isEmpty()) {
|
if (lpPrefix != null && !lpPrefix.isEmpty()) {
|
||||||
// Конвертируем LP префикс в legacy формат
|
// Конвертируем LP префикс в legacy формат
|
||||||
|
|
|
||||||
|
|
@ -82,7 +82,9 @@ public class GradientGUIListener implements Listener {
|
||||||
data.setColors(gui.getColors());
|
data.setColors(gui.getColors());
|
||||||
data.setColorEnabled(true);
|
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",
|
msg.send(player, price > 0 ? "prefix-success" : "prefix-success-free",
|
||||||
"price", String.valueOf(price));
|
"price", String.valueOf(price));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,7 @@ public final class DisplayNameUtil {
|
||||||
if (data.isPrefixEnabled() && data.hasPrefix()) {
|
if (data.isPrefixEnabled() && data.hasPrefix()) {
|
||||||
// Есть кастомный префикс
|
// Есть кастомный префикс
|
||||||
prefix = data.getPrefix();
|
prefix = data.getPrefix();
|
||||||
} else if (module.getLuckPermsHook().isEnabled()) {
|
} else if (module.getLuckPermsHook() != null && module.getLuckPermsHook().isEnabled()) {
|
||||||
// Нет кастомного префикса, проверяем LuckPerms
|
// Нет кастомного префикса, проверяем LuckPerms
|
||||||
String lpPrefix = module.getLuckPermsHook().getActivePrefix(player);
|
String lpPrefix = module.getLuckPermsHook().getActivePrefix(player);
|
||||||
if (lpPrefix != null && !lpPrefix.isEmpty()) {
|
if (lpPrefix != null && !lpPrefix.isEmpty()) {
|
||||||
|
|
|
||||||
|
|
@ -41,7 +41,7 @@ public class PlaceholderAPIHook extends PlaceholderExpansion {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public @NotNull String getVersion() {
|
public @NotNull String getVersion() {
|
||||||
return plugin.getDescription().getVersion();
|
return plugin.getPluginMeta().getVersion();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
||||||
|
|
@ -3,15 +3,25 @@ package com.loki.lochat.listener;
|
||||||
import com.loki.lochat.api.service.MessageService;
|
import com.loki.lochat.api.service.MessageService;
|
||||||
import com.loki.lochat.core.registry.ServiceRegistry;
|
import com.loki.lochat.core.registry.ServiceRegistry;
|
||||||
import com.loki.lochat.util.FoliaUtil;
|
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.entity.Player;
|
||||||
import org.bukkit.event.EventHandler;
|
import org.bukkit.event.EventHandler;
|
||||||
import org.bukkit.event.EventPriority;
|
import org.bukkit.event.EventPriority;
|
||||||
import org.bukkit.event.Listener;
|
import org.bukkit.event.Listener;
|
||||||
import org.bukkit.event.player.AsyncPlayerChatEvent;
|
|
||||||
import org.bukkit.plugin.java.JavaPlugin;
|
import org.bukkit.plugin.java.JavaPlugin;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Слушатель событий чата (минимальная ответственность)
|
* Слушатель событий чата с Paper Chat API
|
||||||
|
* Поддерживает hover tooltips, click actions, mentions
|
||||||
*/
|
*/
|
||||||
public class ChatEventListener implements Listener {
|
public class ChatEventListener implements Listener {
|
||||||
private final JavaPlugin plugin;
|
private final JavaPlugin plugin;
|
||||||
|
|
@ -23,15 +33,196 @@ public class ChatEventListener implements Listener {
|
||||||
}
|
}
|
||||||
|
|
||||||
@EventHandler(priority = EventPriority.HIGHEST)
|
@EventHandler(priority = EventPriority.HIGHEST)
|
||||||
public void onChat(AsyncPlayerChatEvent event) {
|
public void onChat(AsyncChatEvent event) {
|
||||||
event.setCancelled(true);
|
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 обработка
|
if (!allowed) {
|
||||||
FoliaUtil.runEntityTask(plugin, player, () -> {
|
event.setCancelled(true);
|
||||||
messageService.processMessage(player, message);
|
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;
|
package com.loki.lochat.listener;
|
||||||
|
|
||||||
|
import com.loki.lochat.api.service.NickService;
|
||||||
import com.loki.lochat.api.service.PlayerDataService;
|
import com.loki.lochat.api.service.PlayerDataService;
|
||||||
import com.loki.lochat.core.registry.ServiceRegistry;
|
import com.loki.lochat.core.registry.ServiceRegistry;
|
||||||
import org.bukkit.entity.Player;
|
import org.bukkit.entity.Player;
|
||||||
import org.bukkit.event.EventHandler;
|
import org.bukkit.event.EventHandler;
|
||||||
import org.bukkit.event.Listener;
|
import org.bukkit.event.Listener;
|
||||||
|
import org.bukkit.event.player.PlayerJoinEvent;
|
||||||
import org.bukkit.event.player.PlayerQuitEvent;
|
import org.bukkit.event.player.PlayerQuitEvent;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -12,9 +14,20 @@ import org.bukkit.event.player.PlayerQuitEvent;
|
||||||
*/
|
*/
|
||||||
public class PlayerEventListener implements Listener {
|
public class PlayerEventListener implements Listener {
|
||||||
private final PlayerDataService playerDataService;
|
private final PlayerDataService playerDataService;
|
||||||
|
private final NickService nickService;
|
||||||
|
|
||||||
public PlayerEventListener(ServiceRegistry registry) {
|
public PlayerEventListener(ServiceRegistry registry) {
|
||||||
this.playerDataService = registry.get(PlayerDataService.class);
|
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
|
@EventHandler
|
||||||
|
|
|
||||||
|
|
@ -63,6 +63,22 @@ chat:
|
||||||
sound: true
|
sound: true
|
||||||
sound-type: "BLOCK_NOTE_BLOCK_PLING"
|
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
|
permission: chat.announce.use
|
||||||
aliases: [объявление, анонс, фттщгтсу]
|
aliases: [объявление, анонс, фттщгтсу]
|
||||||
|
|
||||||
|
nick:
|
||||||
|
description: Set custom nickname
|
||||||
|
usage: /nick [nickname|reset]
|
||||||
|
permission: lochat.nick
|
||||||
|
aliases: [ник, тшсл]
|
||||||
|
|
||||||
chatspy:
|
chatspy:
|
||||||
description: Toggle chat spy mode
|
description: Toggle chat spy mode
|
||||||
usage: /chatspy
|
usage: /chatspy
|
||||||
|
|
@ -232,3 +238,6 @@ permissions:
|
||||||
lochat.heademoji.use:
|
lochat.heademoji.use:
|
||||||
description: Can use head emojis in chat
|
description: Can use head emojis in chat
|
||||||
default: true
|
default: true
|
||||||
|
lochat.nick:
|
||||||
|
description: Can use custom nicknames
|
||||||
|
default: true
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue