diff --git a/496 b/496 new file mode 100644 index 0000000..e69de29 diff --git a/CLEANUP_REPORT.md b/CLEANUP_REPORT.md index 271971c..dcb3adb 100644 --- a/CLEANUP_REPORT.md +++ b/CLEANUP_REPORT.md @@ -42,18 +42,20 @@ ### Размер JAR - **До очистки:** 508 829 байт -- **После очистки:** 496 726 байт -- **Экономия:** 12 103 байт (2.4%) +- **После удаления legacy:** 473 495 байт +- **Экономия:** 35 334 байт (6.9%) ### Строки кода -- **Удалено:** ~620 строк -- **Добавлено:** ~35 строк (новый ChatEventListener) -- **Чистая экономия:** ~585 строк +- **Удалено эмодзи и фильтров:** ~620 строк +- **Удалено legacy менеджеров:** 998 строк +- **Удалено тестов legacy:** 97 строк +- **Добавлено новых сервисов:** ~800 строк +- **Чистая экономия:** ~915 строк ### Файлы -- **Удалено:** 7 файлов -- **Создано:** 1 файл (ChatEventListener) -- **Чистая экономия:** 6 файлов +- **Удалено:** 16 файлов (7 эмодзи/фильтры + 7 менеджеры + 2 теста) +- **Создано:** 19 файлов (9 интерфейсов + 9 реализаций + 1 слушатель) +- **Чистая разница:** +3 файла (но лучшая структура) --- @@ -87,57 +89,54 @@ --- -## ⚠️ Legacy код (ещё остался) +## ✅ Legacy код удалён -### Менеджеры (нужно мигрировать) +### Удалённые менеджеры (заменены на сервисы) ``` -⚠️ MuteManager.java (409 строк) - используется в командах -⚠️ ChatManager.java (131 строка) - используется в командах -⚠️ AntiSpamManager.java - используется в командах -⚠️ CooldownManager.java - используется в командах -⚠️ PMManager.java - используется в командах -⚠️ IgnoreManager.java - используется в командах -⚠️ SpyManager.java - используется в командах -⚠️ MentionManager.java (123 строки) - используется в командах +✅ 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 ``` -### Команды (используют legacy менеджеры) +**Экономия:** 998 строк кода + +### Команды (мигрированы на новые сервисы) ``` -⚠️ MuteCommand.java -⚠️ UnmuteCommand.java -⚠️ MuteListCommand.java -⚠️ MuteHistoryCommand.java -⚠️ MuteBlameCommand.java -⚠️ GlobalChatCommand.java -⚠️ LocalChatCommand.java -⚠️ MsgCommand.java -⚠️ ReplyCommand.java -⚠️ IgnoreCommand.java -⚠️ UnignoreCommand.java -⚠️ ChatSpyCommand.java +✅ 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 ``` --- -## 🎯 Следующие шаги (по плану REFACTORING_PLAN.md) +## 🎯 Следующие шаги -### Этап 4: Создать новые сервисы -1. [ ] PMService - личные сообщения -2. [ ] IgnoreService - игнорирование -3. [ ] SpyService - шпионаж -4. [ ] MentionService - упоминания +### ✅ Завершено +1. ✅ Удалены эмодзи (320+ строк) +2. ✅ Удалена фильтрация чата (150+ строк) +3. ✅ Создана SOLID архитектура (ServiceRegistry + DI) +4. ✅ Созданы все сервисы (9 интерфейсов + 9 реализаций) +5. ✅ Мигрированы все команды на новые сервисы +6. ✅ Удалены все legacy менеджеры (998 строк) +7. ✅ Плагин собирается и работает -### Этап 5: Мигрировать команды -1. [ ] Mute команды (5 штук) → MuteService -2. [ ] Chat команды (2 штуки) → ChatService -3. [ ] PM команды (2 штуки) → PMService -4. [ ] Ignore команды (2 штуки) → IgnoreService -5. [ ] Spy команды (1 штука) → SpyService - -### Этап 6: Удалить legacy -1. [ ] Удалить 8 legacy менеджеров (~1500 строк) -2. [ ] Очистить LoChat.java от legacy геттеров -3. [ ] Оптимизировать большие классы +### 🔄 Опционально (для дальнейшей оптимизации) +1. [ ] Разбить ConfigManager на категории (если станет >200 строк) +2. [ ] Добавить больше unit-тестов для новых сервисов +3. [ ] Интеграционные тесты для полного флоу --- @@ -172,14 +171,18 @@ 2. Удалена фильтрация чата (150+ строк) 3. Удалён старый ChatListener (185 строк) 4. Создана новая архитектура (ServiceRegistry + DI) -5. Плагин собирается и работает -6. Все тесты проходят +5. Созданы все сервисы (9 интерфейсов + 9 реализаций) +6. Мигрированы все команды на новые сервисы +7. Удалены все legacy менеджеры (998 строк) +8. Плагин собирается и работает +9. Все тесты проходят (37 тестов) ### 🎯 Результат -- **Код стал чище:** -585 строк -- **JAR стал меньше:** -12 KB -- **Архитектура улучшена:** SOLID принципы -- **Готово к дальнейшему рефакторингу** +- **Код стал чище:** -915 строк чистой экономии +- **JAR стал меньше:** -35 KB (6.9%) +- **Архитектура улучшена:** SOLID принципы + DI +- **Все legacy менеджеры удалены** +- **Все команды используют новые сервисы** -### 📋 Следующий шаг -Создать 4 новых сервиса (PM, Ignore, Spy, Mention) и начать миграцию команд согласно плану в `REFACTORING_PLAN.md`. +### 📋 Рефакторинг завершён +Все основные задачи из `REFACTORING_PLAN.md` выполнены. Плагин полностью переведён на новую архитектуру. diff --git a/bin/main/com/loki/lochat/listeners/UniversalChatListener.class b/bin/main/com/loki/lochat/listeners/UniversalChatListener.class new file mode 100644 index 0000000..2cafa0e Binary files /dev/null and b/bin/main/com/loki/lochat/listeners/UniversalChatListener.class differ diff --git a/bin/main/com/loki/lochat/utils/AdventureManager.class b/bin/main/com/loki/lochat/utils/AdventureManager.class new file mode 100644 index 0000000..815bf63 Binary files /dev/null and b/bin/main/com/loki/lochat/utils/AdventureManager.class differ diff --git a/src/main/java/com/loki/lochat/managers/ChatManager.java b/src/main/java/com/loki/lochat/managers/ChatManager.java deleted file mode 100644 index d4d3ffe..0000000 --- a/src/main/java/com/loki/lochat/managers/ChatManager.java +++ /dev/null @@ -1,162 +0,0 @@ -package com.loki.lochat.managers; - -import com.loki.lochat.LoChat; -import com.loki.lochat.api.service.MentionService; -import com.loki.lochat.utils.ChatFormatter; -import com.loki.lochat.utils.DistanceUtil; -import net.kyori.adventure.text.Component; -import net.kyori.adventure.text.minimessage.MiniMessage; -import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer; -import org.bukkit.Bukkit; -import org.bukkit.entity.Player; - -import java.util.HashSet; -import java.util.Set; -import java.util.UUID; -import java.util.concurrent.ConcurrentHashMap; - -/** - * Менеджер чата - использует MentionService для обработки упоминаний - */ -public class ChatManager { - - private final LoChat plugin; - private final MentionService mentionService; - private final Set globalChatDisabled = ConcurrentHashMap.newKeySet(); - private static final MiniMessage MINI_MESSAGE = MiniMessage.miniMessage(); - private static final PlainTextComponentSerializer PLAIN = PlainTextComponentSerializer.plainText(); - - public ChatManager(LoChat plugin) { - this.plugin = plugin; - this.mentionService = plugin.getServiceRegistry().get(MentionService.class); - } - - public void sendGlobalMessage(Player sender, Component message) { - if (!plugin.getConfigManager().isGlobalEnabled()) { - sender.sendMessage(plugin.getMessageConfig().getComponent("global.disabled")); - return; - } - - // Конвертируем Component в String для обработки упоминаний - String messageText = PLAIN.serialize(message); - - // Обработка упоминаний @ник - Set mentionedPlayers = new HashSet<>(); - String processedMessage = mentionService.processMentions(messageText, mentionedPlayers); - - String format = plugin.getConfigManager().getGlobalFormat(); - String prefix = plugin.getConfigManager().getGlobalPrefix(); - - // Получаем отображаемое имя игрока (с градиентом если есть) - String playerDisplay = getPlayerDisplay(sender); - - // Отправка каждому игроку персонализированное сообщение - for (Player player : Bukkit.getOnlinePlayers()) { - if (isGlobalChatDisabled(player.getUniqueId())) continue; - - // Персонализируем сообщение для этого игрока (выделяем его ник) - String personalizedMsg = mentionService.getPersonalizedMessage(processedMessage, player); - - // Формируем итоговую строку - String formatted = format - .replace("", prefix) - .replace("", playerDisplay) - .replace("", personalizedMsg); - - // Парсим в Component и отправляем - Component component = MINI_MESSAGE.deserialize(formatted); - player.sendMessage(component); - - // Проверяем упоминание ника без @ и уведомляем - mentionService.notifyIfMentioned(messageText, player, sender); - } - - // Уведомление упомянутых через @ - mentionService.notifyMentioned(mentionedPlayers); - - plugin.getLogger().info("[G] " + sender.getName() + ": " + ChatFormatter.stripTags(messageText)); - } - - public void sendLocalMessage(Player sender, Component message) { - if (!plugin.getConfigManager().isLocalEnabled()) { - return; - } - - // Конвертируем Component в String для обработки упоминаний - String messageText = PLAIN.serialize(message); - - // Обработка упоминаний @ник - Set mentionedPlayers = new HashSet<>(); - String processedMessage = mentionService.processMentions(messageText, mentionedPlayers); - - int radius = plugin.getConfigManager().getLocalRadius(); - String format = plugin.getConfigManager().getLocalFormat(); - String prefix = plugin.getConfigManager().getLocalPrefix(); - - // Получаем отображаемое имя игрока (с градиентом если есть) - String playerDisplay = getPlayerDisplay(sender); - - boolean foundRecipient = false; - Set recipients = new HashSet<>(); - - for (Player player : Bukkit.getOnlinePlayers()) { - if (DistanceUtil.isInRange(sender, player, radius)) { - // Персонализируем сообщение для этого игрока - String personalizedMsg = mentionService.getPersonalizedMessage(processedMessage, player); - - // Формируем итоговую строку - String formatted = prefix + " " + format - .replace("", playerDisplay) - .replace("", personalizedMsg); - - // Парсим в Component и отправляем - Component component = MINI_MESSAGE.deserialize(formatted); - player.sendMessage(component); - - recipients.add(player); - - // Проверяем упоминание ника без @ - mentionService.notifyIfMentioned(messageText, player, sender); - - if (!player.equals(sender)) { - foundRecipient = true; - } - } - } - - // Уведомление упомянутых через @ (только тех кто в радиусе) - mentionedPlayers.retainAll(recipients); - mentionService.notifyMentioned(mentionedPlayers); - - if (!foundRecipient) { - sender.sendMessage(plugin.getMessageConfig().getComponent("local.no-players")); - } - } - - /** - * Получает отображаемое имя игрока с градиентом (если включено) - * Возвращает строку в MiniMessage формате - */ - private String getPlayerDisplay(Player player) { - if (plugin.getGradientModule() != null && plugin.getGradientModule().isEnabled()) { - String name = plugin.getGradientModule().getFormattedName(player); - // Конвертируем &#RRGGBB формат в MiniMessage - return ChatFormatter.convertAllColors(name); - } - return player.getName(); - } - - public boolean toggleGlobalChat(UUID player) { - if (globalChatDisabled.contains(player)) { - globalChatDisabled.remove(player); - return true; - } else { - globalChatDisabled.add(player); - return false; - } - } - - public boolean isGlobalChatDisabled(UUID player) { - return globalChatDisabled.contains(player); - } -} diff --git a/src/main/java/com/loki/lochat/managers/CooldownManager.java b/src/main/java/com/loki/lochat/managers/CooldownManager.java deleted file mode 100644 index 4f111d5..0000000 --- a/src/main/java/com/loki/lochat/managers/CooldownManager.java +++ /dev/null @@ -1,45 +0,0 @@ -package com.loki.lochat.managers; - -import java.util.Map; -import java.util.UUID; -import java.util.concurrent.ConcurrentHashMap; - -public class CooldownManager { - - private final Map globalCooldowns = new ConcurrentHashMap<>(); - private final Map localCooldowns = new ConcurrentHashMap<>(); - - public boolean isOnCooldown(UUID player, String chatType, int cooldownSeconds) { - Map cooldowns = chatType.equals("global") ? globalCooldowns : localCooldowns; - Long lastTime = cooldowns.get(player); - - if (lastTime == null) { - return false; - } - - long elapsed = (System.currentTimeMillis() - lastTime) / 1000; - return elapsed < cooldownSeconds; - } - - public int getRemainingCooldown(UUID player, String chatType, int cooldownSeconds) { - Map cooldowns = chatType.equals("global") ? globalCooldowns : localCooldowns; - Long lastTime = cooldowns.get(player); - - if (lastTime == null) { - return 0; - } - - long elapsed = (System.currentTimeMillis() - lastTime) / 1000; - return Math.max(0, cooldownSeconds - (int) elapsed); - } - - public void setCooldown(UUID player, String chatType) { - Map cooldowns = chatType.equals("global") ? globalCooldowns : localCooldowns; - cooldowns.put(player, System.currentTimeMillis()); - } - - public void removeCooldown(UUID player) { - globalCooldowns.remove(player); - localCooldowns.remove(player); - } -} diff --git a/src/main/java/com/loki/lochat/managers/IgnoreManager.java b/src/main/java/com/loki/lochat/managers/IgnoreManager.java deleted file mode 100644 index 90d2ab8..0000000 --- a/src/main/java/com/loki/lochat/managers/IgnoreManager.java +++ /dev/null @@ -1,105 +0,0 @@ -package com.loki.lochat.managers; - -import com.loki.lochat.LoChat; -import com.loki.lochat.gradient.util.FoliaUtil; -import org.bukkit.configuration.file.YamlConfiguration; - -import java.io.File; -import java.io.IOException; -import java.util.*; -import java.util.concurrent.ConcurrentHashMap; - -public class IgnoreManager { - - private final LoChat plugin; - private final File ignoreFile; - private final Map> ignoreMap = new ConcurrentHashMap<>(); - - public IgnoreManager(LoChat plugin) { - this.plugin = plugin; - this.ignoreFile = new File(plugin.getDataFolder(), "ignores.yml"); - load(); - } - - public void load() { - if (!ignoreFile.exists()) return; - - YamlConfiguration config = YamlConfiguration.loadConfiguration(ignoreFile); - for (String key : config.getKeys(false)) { - try { - UUID uuid = UUID.fromString(key); - List ignoredList = config.getStringList(key); - Set ignored = ConcurrentHashMap.newKeySet(); - - for (String ignoredStr : ignoredList) { - try { - ignored.add(UUID.fromString(ignoredStr)); - } catch (Exception ignored2) {} - } - - if (!ignored.isEmpty()) { - ignoreMap.put(uuid, ignored); - } - } catch (Exception ignored) {} - } - } - - public void save() { - YamlConfiguration config = new YamlConfiguration(); - - for (Map.Entry> entry : ignoreMap.entrySet()) { - List ignoredList = new ArrayList<>(); - for (UUID ignored : entry.getValue()) { - ignoredList.add(ignored.toString()); - } - config.set(entry.getKey().toString(), ignoredList); - } - - try { - config.save(ignoreFile); - } catch (IOException e) { - plugin.getLogger().severe("Не удалось сохранить игноры: " + e.getMessage()); - } - } - - public boolean isIgnoring(UUID player, UUID target) { - Set ignored = ignoreMap.get(player); - return ignored != null && ignored.contains(target); - } - - public boolean addIgnore(UUID player, UUID target) { - Set ignored = ignoreMap.computeIfAbsent(player, k -> ConcurrentHashMap.newKeySet()); - boolean added = ignored.add(target); - if (added) saveAsync(); - return added; - } - - public boolean removeIgnore(UUID player, UUID target) { - Set ignored = ignoreMap.get(player); - if (ignored == null) return false; - boolean removed = ignored.remove(target); - if (removed) saveAsync(); - return removed; - } - - public Set getIgnoredPlayers(UUID player) { - return ignoreMap.getOrDefault(player, Collections.emptySet()); - } - - public int getIgnoredCount(UUID player) { - Set ignored = ignoreMap.get(player); - return ignored == null ? 0 : ignored.size(); - } - - public void clearIgnores(UUID player) { - ignoreMap.remove(player); - saveAsync(); - } - - /** - * Асинхронное сохранение данных (для Folia совместимости) - */ - private void saveAsync() { - FoliaUtil.runAsync(plugin, this::save); - } -} diff --git a/src/main/java/com/loki/lochat/managers/MentionManager.java b/src/main/java/com/loki/lochat/managers/MentionManager.java deleted file mode 100644 index 8199292..0000000 --- a/src/main/java/com/loki/lochat/managers/MentionManager.java +++ /dev/null @@ -1,141 +0,0 @@ -package com.loki.lochat.managers; - -import com.loki.lochat.LoChat; -import org.bukkit.Bukkit; -import org.bukkit.Sound; -import org.bukkit.entity.Player; - -import java.util.HashMap; -import java.util.HashSet; -import java.util.Map; -import java.util.Set; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -public class MentionManager { - - private final LoChat plugin; - private static final Pattern MENTION_PATTERN = Pattern.compile("@(\\w+)"); - - // Хранит для какого игрока какие части сообщения выделить - private final Map> highlightedNicks = new HashMap<>(); - - public MentionManager(LoChat plugin) { - this.plugin = plugin; - } - - /** - * Обрабатывает упоминания в сообщении - * Возвращает сообщение с подсвеченными никами - */ - public String processMentions(String message, Set mentionedPlayers) { - if (!plugin.getConfigManager().isMentionsEnabled()) { - return message; - } - - highlightedNicks.clear(); - String highlight = plugin.getConfigManager().getMentionHighlight(); - Matcher matcher = MENTION_PATTERN.matcher(message); - StringBuilder result = new StringBuilder(); - - while (matcher.find()) { - String name = matcher.group(1); - Player mentioned = Bukkit.getPlayer(name); - - if (mentioned != null && mentioned.isOnline()) { - mentionedPlayers.add(mentioned); - String replacement = highlight.replace("{player}", mentioned.getName()); - matcher.appendReplacement(result, replacement); - - // Запоминаем что этот игрок упомянут - highlightedNicks.computeIfAbsent(mentioned, k -> new HashSet<>()).add(mentioned.getName()); - } - } - matcher.appendTail(result); - - return result.toString(); - } - - /** - * Создаёт персонализированное сообщение для игрока - * Если его ник упомянут — выделяет его особым образом - */ - public String getPersonalizedMessage(String message, Player viewer) { - if (!plugin.getConfigManager().isMentionsEnabled()) { - return message; - } - - String viewerName = viewer.getName(); - String result = message; - - // Проверяем упоминание через @ник - String highlight = plugin.getConfigManager().getMentionHighlight(); - String highlightedName = highlight.replace("{player}", viewerName); - - // Если в сообщении есть выделенный ник этого игрока — добавляем дополнительное выделение - if (result.contains(highlightedName)) { - // Уже выделено через processMentions, добавляем эффект - String extraHighlight = "" + highlightedName + ""; - result = result.replace(highlightedName, extraHighlight); - } - - // Также проверяем просто ник в сообщении (без @) - // Регистронезависимый поиск - Pattern nickPattern = Pattern.compile("(?i)\\b" + Pattern.quote(viewerName) + "\\b"); - Matcher nickMatcher = nickPattern.matcher(result); - - if (nickMatcher.find()) { - // Ник найден в сообщении — выделяем для этого игрока - String selfHighlight = plugin.getConfigManager().getSelfMentionHighlight(); - result = nickMatcher.replaceAll(selfHighlight.replace("{player}", viewerName)); - } - - return result; - } - - /** - * Проверяет, упомянут ли игрок в сообщении (по нику без @) - */ - public boolean isPlayerMentioned(String message, Player player) { - String name = player.getName(); - Pattern pattern = Pattern.compile("(?i)\\b" + Pattern.quote(name) + "\\b"); - return pattern.matcher(message).find(); - } - - /** - * Уведомляет упомянутых игроков звуком - */ - public void notifyMentioned(Set players) { - if (!plugin.getConfigManager().isMentionSoundEnabled()) { - return; - } - - String soundName = plugin.getConfigManager().getMentionSoundType(); - Sound sound; - try { - sound = Sound.valueOf(soundName); - } catch (IllegalArgumentException e) { - sound = Sound.BLOCK_NOTE_BLOCK_PLING; - } - - for (Player player : players) { - player.playSound(player.getLocation(), sound, 1.0f, 1.0f); - } - } - - /** - * Уведомляет игрока если его ник упомянут (без @) - */ - public void notifyIfMentioned(String message, Player viewer, Player sender) { - if (viewer.equals(sender)) return; - if (!plugin.getConfigManager().isMentionSoundEnabled()) return; - - if (isPlayerMentioned(message, viewer)) { - String soundName = plugin.getConfigManager().getMentionSoundType(); - try { - Sound sound = Sound.valueOf(soundName); - viewer.playSound(viewer.getLocation(), sound, 1.0f, 1.0f); - } catch (IllegalArgumentException ignored) {} - } - } -} diff --git a/src/main/java/com/loki/lochat/managers/MuteManager.java b/src/main/java/com/loki/lochat/managers/MuteManager.java deleted file mode 100644 index e22fdc5..0000000 --- a/src/main/java/com/loki/lochat/managers/MuteManager.java +++ /dev/null @@ -1,447 +0,0 @@ -package com.loki.lochat.managers; - -import com.google.gson.Gson; -import com.google.gson.GsonBuilder; -import com.google.gson.reflect.TypeToken; -import com.loki.lochat.LoChat; -import com.loki.lochat.gradient.util.FoliaUtil; - -import java.io.*; -import java.lang.reflect.Type; -import java.util.*; -import java.util.concurrent.ConcurrentHashMap; - -/** - * Менеджер мутов для LoChat - */ -public class MuteManager { - - private final LoChat plugin; - private final Map mutes = new ConcurrentHashMap<>(); - private final Map> history = new ConcurrentHashMap<>(); - private final File dataFile; - private final File historyFile; - private final Gson gson = new GsonBuilder().setPrettyPrinting().create(); - - public MuteManager(LoChat plugin) { - this.plugin = plugin; - this.dataFile = new File(plugin.getDataFolder(), "mutes.json"); - this.historyFile = new File(plugin.getDataFolder(), "mute-history.json"); - load(); - loadHistory(); - } - - /** - * Замутить игрока - * @param uuid UUID игрока - * @param playerName имя игрока - * @param duration длительность в миллисекундах (0 = перманентный) - * @param reason причина - * @param mutedBy кто замутил - */ - public void mute(UUID uuid, String playerName, long duration, String reason, String mutedBy) { - long endTime = duration > 0 ? System.currentTimeMillis() + duration : 0; - MuteData data = new MuteData(endTime, reason, mutedBy, System.currentTimeMillis(), playerName); - mutes.put(uuid, data); - - // Добавляем в историю - addToHistory(uuid, playerName, duration, reason, mutedBy); - - saveAsync(); - } - - /** - * Замутить игрока (без имени - для совместимости) - */ - public void mute(UUID uuid, long duration, String reason, String mutedBy) { - mute(uuid, null, duration, reason, mutedBy); - } - - /** - * Размутить игрока - * @param uuid UUID игрока - * @param unmutedBy кто размутил (null если автоматически) - * @return true если игрок был замучен - */ - public boolean unmute(UUID uuid, String unmutedBy) { - MuteData removed = mutes.remove(uuid); - if (removed != null) { - // Обновляем историю - помечаем как размученный - updateHistoryUnmute(uuid, unmutedBy); - saveAsync(); - return true; - } - return false; - } - - /** - * Размутить игрока (для совместимости) - */ - public boolean unmute(UUID uuid) { - return unmute(uuid, null); - } - - /** - * Проверить, замучен ли игрок - */ - public boolean isMuted(UUID uuid) { - MuteData data = mutes.get(uuid); - if (data == null) return false; - - // Проверяем истёк ли мут - if (data.endTime > 0 && System.currentTimeMillis() > data.endTime) { - mutes.remove(uuid); - saveAsync(); - return false; - } - return true; - } - - /** - * Получить данные мута - */ - public MuteData getMuteData(UUID uuid) { - if (!isMuted(uuid)) return null; - return mutes.get(uuid); - } - - /** - * Получить оставшееся время мута - */ - public long getRemainingTime(UUID uuid) { - MuteData data = mutes.get(uuid); - if (data == null) return 0; - if (data.endTime == 0) return -1; // Перманентный - return Math.max(0, data.endTime - System.currentTimeMillis()); - } - - /** - * Форматировать время - */ - public String formatTime(long millis) { - if (millis <= 0) return "0с"; - - long seconds = millis / 1000; - long minutes = seconds / 60; - long hours = minutes / 60; - long days = hours / 24; - - if (days > 0) { - return days + "д " + (hours % 24) + "ч"; - } else if (hours > 0) { - return hours + "ч " + (minutes % 60) + "м"; - } else if (minutes > 0) { - return minutes + "м " + (seconds % 60) + "с"; - } else { - return seconds + "с"; - } - } - - /** - * Парсинг времени из строки (1d, 2h, 30m, 60s) - */ - public long parseTime(String timeStr) { - if (timeStr == null || timeStr.isEmpty()) return 0; - - timeStr = timeStr.toLowerCase(); - long multiplier = 1000; // базово в миллисекундах - - try { - if (timeStr.endsWith("d")) { - return Long.parseLong(timeStr.replace("d", "")) * 24 * 60 * 60 * multiplier; - } else if (timeStr.endsWith("h")) { - return Long.parseLong(timeStr.replace("h", "")) * 60 * 60 * multiplier; - } else if (timeStr.endsWith("m")) { - return Long.parseLong(timeStr.replace("m", "")) * 60 * multiplier; - } else if (timeStr.endsWith("s")) { - return Long.parseLong(timeStr.replace("s", "")) * multiplier; - } else { - // Если без суффикса - считаем минутами - return Long.parseLong(timeStr) * 60 * multiplier; - } - } catch (NumberFormatException e) { - return -1; // Ошибка парсинга - } - } - - private void load() { - if (!dataFile.exists()) return; - - try (Reader reader = new FileReader(dataFile)) { - Type type = new TypeToken>(){}.getType(); - Map loaded = gson.fromJson(reader, type); - if (loaded != null) { - loaded.forEach((key, value) -> { - try { - mutes.put(UUID.fromString(key), value); - } catch (IllegalArgumentException ignored) {} - }); - } - } catch (IOException e) { - plugin.getLogger().warning("Ошибка загрузки мутов: " + e.getMessage()); - } - } - - public void save() { - try { - if (!dataFile.getParentFile().exists()) { - dataFile.getParentFile().mkdirs(); - } - - Map toSave = new ConcurrentHashMap<>(); - mutes.forEach((uuid, data) -> toSave.put(uuid.toString(), data)); - - try (Writer writer = new FileWriter(dataFile)) { - gson.toJson(toSave, writer); - } - } catch (IOException e) { - plugin.getLogger().warning("Ошибка сохранения мутов: " + e.getMessage()); - } - } - - private void loadHistory() { - if (!historyFile.exists()) return; - - try (Reader reader = new FileReader(historyFile)) { - Type type = new TypeToken>>(){}.getType(); - Map> loaded = gson.fromJson(reader, type); - if (loaded != null) { - loaded.forEach((key, value) -> { - try { - history.put(UUID.fromString(key), new ArrayList<>(value)); - } catch (IllegalArgumentException ignored) {} - }); - } - } catch (IOException e) { - plugin.getLogger().warning("Ошибка загрузки истории мутов: " + e.getMessage()); - } - } - - public void saveHistory() { - try { - if (!historyFile.getParentFile().exists()) { - historyFile.getParentFile().mkdirs(); - } - - Map> toSave = new ConcurrentHashMap<>(); - history.forEach((uuid, data) -> toSave.put(uuid.toString(), data)); - - try (Writer writer = new FileWriter(historyFile)) { - gson.toJson(toSave, writer); - } - } catch (IOException e) { - plugin.getLogger().warning("Ошибка сохранения истории мутов: " + e.getMessage()); - } - } - - private void saveHistoryAsync() { - FoliaUtil.runAsync(plugin, this::saveHistory); - } - - private void saveAsync() { - FoliaUtil.runAsync(plugin, () -> { - save(); - saveHistory(); - }); - } - - /** - * Добавить запись в историю - */ - private void addToHistory(UUID uuid, String playerName, long duration, String reason, String mutedBy) { - List playerHistory = history.computeIfAbsent(uuid, k -> new ArrayList<>()); - playerHistory.add(new MuteHistoryEntry( - playerName, - duration, - reason, - mutedBy, - System.currentTimeMillis(), - false, - null, - 0 - )); - saveHistoryAsync(); - } - - /** - * Обновить историю при размуте - */ - private void updateHistoryUnmute(UUID uuid, String unmutedBy) { - List playerHistory = history.get(uuid); - if (playerHistory != null && !playerHistory.isEmpty()) { - // Находим последний активный мут - for (int i = playerHistory.size() - 1; i >= 0; i--) { - MuteHistoryEntry entry = playerHistory.get(i); - if (!entry.unmuted) { - entry.unmuted = true; - entry.unmutedBy = unmutedBy; - entry.unmutedAt = System.currentTimeMillis(); - break; - } - } - } - saveHistoryAsync(); - } - - /** - * Получить список всех активных мутов - */ - public Map getActiveMutes() { - // Очищаем истёкшие муты - mutes.entrySet().removeIf(entry -> { - MuteData data = entry.getValue(); - return data.endTime > 0 && System.currentTimeMillis() > data.endTime; - }); - return new HashMap<>(mutes); - } - - /** - * Получить историю мутов игрока - */ - public List getPlayerHistory(UUID uuid) { - return history.getOrDefault(uuid, new ArrayList<>()); - } - - /** - * Получить UUID по имени из истории - */ - public UUID getUUIDByName(String name) { - for (Map.Entry> entry : history.entrySet()) { - for (MuteHistoryEntry historyEntry : entry.getValue()) { - if (historyEntry.playerName != null && historyEntry.playerName.equalsIgnoreCase(name)) { - return entry.getKey(); - } - } - } - return null; - } - - /** - * Получить историю мутов выданных модератором - */ - public List getMutesByOperator(String operatorName) { - List result = new ArrayList<>(); - for (List playerHistory : history.values()) { - for (MuteHistoryEntry entry : playerHistory) { - if (entry.mutedBy != null && entry.mutedBy.equalsIgnoreCase(operatorName)) { - result.add(entry); - } - } - } - // Сортируем по дате (новые первые) - result.sort((a, b) -> Long.compare(b.mutedAt, a.mutedAt)); - return result; - } - - /** - * Получить максимальную доступную длительность для игрока - * @param player игрок (модератор) - * @return длительность в миллисекундах, 0 = перманентный, -1 = нет прав - */ - public long getMaxDuration(org.bukkit.entity.Player player) { - if (player.hasPermission("lochat.mute.dur.perm")) { - return 0; // Перманентный - } - - // Проверяем права от большего к меньшему - String[] durations = {"30d", "14d", "7d", "3d", "1d", "12h", "6h", "1h", "30m", "10m"}; - for (String dur : durations) { - if (player.hasPermission("lochat.mute.dur." + dur)) { - return parseTime(dur); - } - } - - return -1; // Нет прав - } - - /** - * Проверить, может ли игрок выдать мут на указанную длительность - */ - public boolean canMuteForDuration(org.bukkit.entity.Player player, long duration) { - if (player.hasPermission("lochat.mute.dur.perm")) { - return true; // Может всё - } - - if (duration == 0) { - return false; // Перманентный мут требует lochat.mute.dur.perm - } - - long maxDuration = getMaxDuration(player); - if (maxDuration == -1) return false; - if (maxDuration == 0) return true; // Перманентный = может всё - - return duration <= maxDuration; - } - - /** - * Форматировать сообщение с плейсхолдерами - */ - public String formatMessage(String message, String player, String operator, String duration, String reason) { - return message - .replace("%player%", player != null ? player : "") - .replace("%operator%", operator != null ? operator : "") - .replace("%duration%", duration != null ? duration : "") - .replace("%reason%", reason != null ? reason : ""); - } - - /** - * Данные о муте - */ - public static class MuteData { - public long endTime; // 0 = перманентный - public String reason; - public String mutedBy; - public long mutedAt; - public String playerName; - - public MuteData() {} - - public MuteData(long endTime, String reason, String mutedBy, long mutedAt) { - this(endTime, reason, mutedBy, mutedAt, null); - } - - public MuteData(long endTime, String reason, String mutedBy, long mutedAt, String playerName) { - this.endTime = endTime; - this.reason = reason; - this.mutedBy = mutedBy; - this.mutedAt = mutedAt; - this.playerName = playerName; - } - - public boolean isPermanent() { - return endTime == 0; - } - } - - /** - * Запись истории мута - */ - public static class MuteHistoryEntry { - public String playerName; - public long duration; // 0 = перманентный - public String reason; - public String mutedBy; - public long mutedAt; - public boolean unmuted; - public String unmutedBy; - public long unmutedAt; - - public MuteHistoryEntry() {} - - public MuteHistoryEntry(String playerName, long duration, String reason, String mutedBy, - long mutedAt, boolean unmuted, String unmutedBy, long unmutedAt) { - this.playerName = playerName; - this.duration = duration; - this.reason = reason; - this.mutedBy = mutedBy; - this.mutedAt = mutedAt; - this.unmuted = unmuted; - this.unmutedBy = unmutedBy; - this.unmutedAt = unmutedAt; - } - - public boolean isPermanent() { - return duration == 0; - } - } -} diff --git a/src/main/java/com/loki/lochat/managers/PMManager.java b/src/main/java/com/loki/lochat/managers/PMManager.java deleted file mode 100644 index 61105d6..0000000 --- a/src/main/java/com/loki/lochat/managers/PMManager.java +++ /dev/null @@ -1,27 +0,0 @@ -package com.loki.lochat.managers; - -import java.util.Map; -import java.util.UUID; -import java.util.concurrent.ConcurrentHashMap; - -public class PMManager { - - // UUID игрока -> UUID последнего собеседника - private final Map lastConversation = new ConcurrentHashMap<>(); - - public void setLastConversation(UUID player, UUID target) { - lastConversation.put(player, target); - } - - public UUID getLastConversation(UUID player) { - return lastConversation.get(player); - } - - public void removeConversation(UUID player) { - lastConversation.remove(player); - } - - public boolean hasConversation(UUID player) { - return lastConversation.containsKey(player); - } -} diff --git a/src/main/java/com/loki/lochat/managers/SpyManager.java b/src/main/java/com/loki/lochat/managers/SpyManager.java deleted file mode 100644 index 5607a57..0000000 --- a/src/main/java/com/loki/lochat/managers/SpyManager.java +++ /dev/null @@ -1,71 +0,0 @@ -package com.loki.lochat.managers; - -import com.loki.lochat.LoChat; -import com.loki.lochat.utils.ChatFormatter; -import org.bukkit.Bukkit; -import org.bukkit.entity.Player; - -import java.util.Set; -import java.util.UUID; -import java.util.concurrent.ConcurrentHashMap; - -public class SpyManager { - - private final LoChat plugin; - private final Set spyEnabled = ConcurrentHashMap.newKeySet(); - - public SpyManager(LoChat plugin) { - this.plugin = plugin; - } - - public boolean toggleSpy(UUID player) { - if (spyEnabled.contains(player)) { - spyEnabled.remove(player); - return false; - } else { - spyEnabled.add(player); - return true; - } - } - - public boolean isSpying(UUID player) { - return spyEnabled.contains(player); - } - - public void broadcastPM(Player sender, Player receiver, String message) { - String format = plugin.getMessageConfig().get("spy.format"); - - for (UUID spyUuid : spyEnabled) { - Player spy = Bukkit.getPlayer(spyUuid); - if (spy != null && spy.isOnline() && !spy.equals(sender) && !spy.equals(receiver)) { - spy.sendMessage(ChatFormatter.parse(format - .replace("{sender}", sender.getName()) - .replace("{receiver}", receiver.getName()) - .replace("{message}", message))); - } - } - } - - public void sendToSpies(Player sender, net.kyori.adventure.text.Component message, boolean isGlobal) { - if (spyEnabled.isEmpty()) return; - - String chatType = isGlobal ? "Global" : "Local"; - String plainMessage = net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer.plainText().serialize(message); - String format = plugin.getMessageConfig().get("spy.chat-format", - "§7[SPY] §e{type} §7{sender}: §f{message}"); - - for (UUID spyUuid : spyEnabled) { - Player spy = Bukkit.getPlayer(spyUuid); - if (spy != null && spy.isOnline() && !spy.equals(sender)) { - spy.sendMessage(ChatFormatter.parse(format - .replace("{type}", chatType) - .replace("{sender}", sender.getName()) - .replace("{message}", plainMessage))); - } - } - } - - public void removeSpy(UUID player) { - spyEnabled.remove(player); - } -} diff --git a/src/main/java/com/loki/lochat/utils/AdventureManager.java b/src/main/java/com/loki/lochat/utils/AdventureManager.java new file mode 100644 index 0000000..69a05ec --- /dev/null +++ b/src/main/java/com/loki/lochat/utils/AdventureManager.java @@ -0,0 +1,88 @@ +package com.loki.lochat.utils; + +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; +import org.bukkit.plugin.Plugin; + +/** + * Менеджер для работы с Adventure API на Arclight/Forge серверах + */ +public class AdventureManager { + + private static boolean adventureAvailable = false; + private static Object audiences; + + public static void init(Plugin plugin) { + try { + // Проверяем наличие Adventure API + Class.forName("net.kyori.adventure.text.Component"); + Class audiencesClass = Class.forName("net.kyori.adventure.platform.bukkit.BukkitAudiences"); + + // Создаем BukkitAudiences через рефлексию + Object audiencesInstance = audiencesClass.getMethod("create", Plugin.class).invoke(null, plugin); + audiences = audiencesInstance; + adventureAvailable = true; + + plugin.getLogger().info("Adventure API обнаружен - используем нативную поддержку"); + } catch (Exception e) { + adventureAvailable = false; + plugin.getLogger().info("Adventure API не найден - используем legacy режим"); + } + } + + public static void shutdown() { + if (audiences != null && adventureAvailable) { + try { + audiences.getClass().getMethod("close").invoke(audiences); + } catch (Exception e) { + // Игнорируем ошибки при закрытии + } + audiences = null; + } + adventureAvailable = false; + } + + public static void sendMessage(CommandSender sender, Object message) { + if (adventureAvailable && audiences != null) { + try { + // Используем Adventure API + Object senderAudience = audiences.getClass().getMethod("sender", CommandSender.class).invoke(audiences, sender); + senderAudience.getClass().getMethod("sendMessage", Class.forName("net.kyori.adventure.text.Component")).invoke(senderAudience, message); + return; + } catch (Exception e) { + // Fallback на legacy + } + } + + // Legacy fallback + if (message instanceof String) { + sender.sendMessage((String) message); + } else { + sender.sendMessage(message.toString()); + } + } + + public static void sendMessage(Player player, Object message) { + if (adventureAvailable && audiences != null) { + try { + // Используем Adventure API + Object playerAudience = audiences.getClass().getMethod("player", Player.class).invoke(audiences, player); + playerAudience.getClass().getMethod("sendMessage", Class.forName("net.kyori.adventure.text.Component")).invoke(playerAudience, message); + return; + } catch (Exception e) { + // Fallback на legacy + } + } + + // Legacy fallback + if (message instanceof String) { + player.sendMessage((String) message); + } else { + player.sendMessage(message.toString()); + } + } + + public static boolean isAdventureAvailable() { + return adventureAvailable; + } +} \ No newline at end of file diff --git a/src/test/java/com/loki/lochat/managers/CooldownManagerTest.java b/src/test/java/com/loki/lochat/managers/CooldownManagerTest.java deleted file mode 100644 index 01970ba..0000000 --- a/src/test/java/com/loki/lochat/managers/CooldownManagerTest.java +++ /dev/null @@ -1,50 +0,0 @@ -package com.loki.lochat.managers; - -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -import java.util.UUID; - -import static org.junit.jupiter.api.Assertions.*; - -class CooldownManagerTest { - - private CooldownManager cooldownManager; - private UUID testPlayer; - - @BeforeEach - void setUp() { - cooldownManager = new CooldownManager(); - testPlayer = UUID.randomUUID(); - } - - @Test - void isOnCooldown_noCooldown() { - assertFalse(cooldownManager.isOnCooldown(testPlayer, "global", 5)); - } - - @Test - void isOnCooldown_afterSet() { - cooldownManager.setCooldown(testPlayer, "global"); - assertTrue(cooldownManager.isOnCooldown(testPlayer, "global", 5)); - } - - @Test - void getRemainingCooldown_noCooldown() { - assertEquals(0, cooldownManager.getRemainingCooldown(testPlayer, "global", 5)); - } - - @Test - void removeCooldown() { - cooldownManager.setCooldown(testPlayer, "global"); - cooldownManager.removeCooldown(testPlayer); - assertFalse(cooldownManager.isOnCooldown(testPlayer, "global", 5)); - } - - @Test - void separateCooldowns() { - cooldownManager.setCooldown(testPlayer, "global"); - assertTrue(cooldownManager.isOnCooldown(testPlayer, "global", 5)); - assertFalse(cooldownManager.isOnCooldown(testPlayer, "local", 5)); - } -} diff --git a/src/test/java/com/loki/lochat/managers/PMManagerTest.java b/src/test/java/com/loki/lochat/managers/PMManagerTest.java deleted file mode 100644 index 9c9c2ea..0000000 --- a/src/test/java/com/loki/lochat/managers/PMManagerTest.java +++ /dev/null @@ -1,47 +0,0 @@ -package com.loki.lochat.managers; - -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -import java.util.UUID; - -import static org.junit.jupiter.api.Assertions.*; - -class PMManagerTest { - - private PMManager pmManager; - private UUID player1; - private UUID player2; - - @BeforeEach - void setUp() { - pmManager = new PMManager(); - player1 = UUID.randomUUID(); - player2 = UUID.randomUUID(); - } - - @Test - void setAndGetLastConversation() { - pmManager.setLastConversation(player1, player2); - assertEquals(player2, pmManager.getLastConversation(player1)); - } - - @Test - void getLastConversation_noConversation() { - assertNull(pmManager.getLastConversation(player1)); - } - - @Test - void hasConversation() { - assertFalse(pmManager.hasConversation(player1)); - pmManager.setLastConversation(player1, player2); - assertTrue(pmManager.hasConversation(player1)); - } - - @Test - void removeConversation() { - pmManager.setLastConversation(player1, player2); - pmManager.removeConversation(player1); - assertFalse(pmManager.hasConversation(player1)); - } -}