refactor: Remove all legacy managers, complete SOLID migration

This commit is contained in:
loki5512344 2026-02-23 12:17:37 +01:00
parent 74482851aa
commit 4ee5393a5e
14 changed files with 147 additions and 1151 deletions

0
496 Normal file
View file

View file

@ -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` выполнены. Плагин полностью переведён на новую архитектуру.

Binary file not shown.

View file

@ -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<UUID> 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<Player> 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>", prefix)
.replace("<player>", playerDisplay)
.replace("<message>", 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<Player> 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<Player> 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("<player>", playerDisplay)
.replace("<message>", 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);
}
}

View file

@ -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<UUID, Long> globalCooldowns = new ConcurrentHashMap<>();
private final Map<UUID, Long> localCooldowns = new ConcurrentHashMap<>();
public boolean isOnCooldown(UUID player, String chatType, int cooldownSeconds) {
Map<UUID, Long> 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<UUID, Long> 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<UUID, Long> cooldowns = chatType.equals("global") ? globalCooldowns : localCooldowns;
cooldowns.put(player, System.currentTimeMillis());
}
public void removeCooldown(UUID player) {
globalCooldowns.remove(player);
localCooldowns.remove(player);
}
}

View file

@ -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<UUID, Set<UUID>> 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<String> ignoredList = config.getStringList(key);
Set<UUID> 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<UUID, Set<UUID>> entry : ignoreMap.entrySet()) {
List<String> 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<UUID> ignored = ignoreMap.get(player);
return ignored != null && ignored.contains(target);
}
public boolean addIgnore(UUID player, UUID target) {
Set<UUID> 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<UUID> ignored = ignoreMap.get(player);
if (ignored == null) return false;
boolean removed = ignored.remove(target);
if (removed) saveAsync();
return removed;
}
public Set<UUID> getIgnoredPlayers(UUID player) {
return ignoreMap.getOrDefault(player, Collections.emptySet());
}
public int getIgnoredCount(UUID player) {
Set<UUID> 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);
}
}

View file

@ -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<Player, Set<String>> highlightedNicks = new HashMap<>();
public MentionManager(LoChat plugin) {
this.plugin = plugin;
}
/**
* Обрабатывает упоминания в сообщении
* Возвращает сообщение с подсвеченными никами
*/
public String processMentions(String message, Set<Player> 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 = "<bold>" + highlightedName + "</bold>";
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<Player> 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) {}
}
}
}

View file

@ -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<UUID, MuteData> mutes = new ConcurrentHashMap<>();
private final Map<UUID, List<MuteHistoryEntry>> 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<Map<String, MuteData>>(){}.getType();
Map<String, MuteData> 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<String, MuteData> 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<Map<String, List<MuteHistoryEntry>>>(){}.getType();
Map<String, List<MuteHistoryEntry>> 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<String, List<MuteHistoryEntry>> 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<MuteHistoryEntry> 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<MuteHistoryEntry> 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<UUID, MuteData> getActiveMutes() {
// Очищаем истёкшие муты
mutes.entrySet().removeIf(entry -> {
MuteData data = entry.getValue();
return data.endTime > 0 && System.currentTimeMillis() > data.endTime;
});
return new HashMap<>(mutes);
}
/**
* Получить историю мутов игрока
*/
public List<MuteHistoryEntry> getPlayerHistory(UUID uuid) {
return history.getOrDefault(uuid, new ArrayList<>());
}
/**
* Получить UUID по имени из истории
*/
public UUID getUUIDByName(String name) {
for (Map.Entry<UUID, List<MuteHistoryEntry>> entry : history.entrySet()) {
for (MuteHistoryEntry historyEntry : entry.getValue()) {
if (historyEntry.playerName != null && historyEntry.playerName.equalsIgnoreCase(name)) {
return entry.getKey();
}
}
}
return null;
}
/**
* Получить историю мутов выданных модератором
*/
public List<MuteHistoryEntry> getMutesByOperator(String operatorName) {
List<MuteHistoryEntry> result = new ArrayList<>();
for (List<MuteHistoryEntry> 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;
}
}
}

View file

@ -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<UUID, UUID> 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);
}
}

View file

@ -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<UUID> 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);
}
}

View file

@ -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;
}
}

View file

@ -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));
}
}

View file

@ -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));
}
}