diff --git a/CODE_REVIEW_RU.md b/CODE_REVIEW_RU.md new file mode 100644 index 0000000..326900b --- /dev/null +++ b/CODE_REVIEW_RU.md @@ -0,0 +1,356 @@ +# 🔍 Code Review: Проблемы архитектуры (SOLID, DRY, KISS) + +## 🔴 Критические проблемы + +### 1. DRY: Дублирование кода в конфигах +**Файлы:** `AppearanceConfig.java`, `MessagesConfig.java`, `FiltersConfig.java` + +**Проблема:** +- Каждый конфиг содержит 30-50 геттеров с одинаковой структурой +- ~300 строк повторяющегося кода +- При изменении логики нужно править в 3 местах + +**Решение:** +```java +// Создать базовый класс с generic методами +public abstract class BaseConfig { + protected String getString(String path, String def) { ... } + protected int getInt(String path, int def) { ... } + protected boolean getBoolean(String path, boolean def) { ... } +} + +// Использовать в конфигах +public class MessagesConfig extends BaseConfig { + public String getNoPermission() { + return getString("general.no_permission", "&#CF6679Нет прав!"); + } +} +``` + +--- + +### 2. SRP: ConfigManager делает слишком много +**Файл:** `ConfigManager.java` + +**Проблема:** +- Управляет 5 конфигами +- Содержит логику обновления версий +- Имеет 30+ геттеров для настроек +- Нарушает Single Responsibility Principle + +**Решение:** +```java +// Разделить на отдельные классы +class ConfigManager { + private final AppearanceConfig appearance; + private final MessagesConfig messages; + // Только управление конфигами +} + +class ConfigUpdater { + // Логика обновления версий + void updateConfig(FileConfiguration config) { ... } +} +``` + +--- + +### 3. KISS: ColorConverter неэффективен +**Файл:** `ColorConverter.java` + +**Проблема:** +```java +private static String convertAmpersandColors(String message) { + return message + .replaceAll("&0", "") + .replaceAll("&1", "") + // ... 16 вызовов replaceAll +} +``` +- Каждый `replaceAll` компилирует regex +- O(n * 16) сложность +- Дублирование для `§` и `&` + +**Решение:** +```java +private static final Map COLOR_MAP = Map.ofEntries( + Map.entry("&0", ""), + Map.entry("§0", ""), + Map.entry("&1", ""), + Map.entry("§1", "") + // ... +); + +private static String convertColors(String message) { + for (Map.Entry entry : COLOR_MAP.entrySet()) { + message = message.replace(entry.getKey(), entry.getValue()); + } + return message; +} +``` + +--- + +### 4. DRY: Дублирование проверок permissions +**Файл:** `AdvancedMessageFilter.java` + +**Проблема:** +```java +if (!player.hasPermission("lochat.bypass.swear") && !player.hasPermission("lochat.bypass.filter")) +if (!player.hasPermission("lochat.bypass.flood") && !player.hasPermission("lochat.bypass.filter")) +if (!player.hasPermission("lochat.bypass.spam") && !player.hasPermission("lochat.bypass.filter")) +``` + +**Решение:** +```java +private boolean canBypassFilter(Player player, String filterType) { + return player.hasPermission("lochat.bypass." + filterType) + || player.hasPermission("lochat.bypass.filter"); +} + +// Использование +if (!canBypassFilter(player, "swear")) { + FilterResult swear = swearFilter.filter(player, message); + // ... +} +``` + +--- + +### 5. OCP: EnhancedChatRenderer не расширяем +**Файл:** `EnhancedChatRenderer.java` + +**Проблема:** +```java +switch (placeholder) { + case "emoji" -> result = result.append(emoji); + case "prefix" -> result = result.append(prefix); + // Добавление нового плейсхолдера требует изменения кода +} +``` + +**Решение:** +```java +// Strategy pattern для плейсхолдеров +interface PlaceholderResolver { + Component resolve(Player player, boolean isGlobal); +} + +Map resolvers = Map.of( + "emoji", (p, g) -> buildEmojiComponent(g), + "prefix", (p, g) -> buildPrefix(g), + "player", (p, g) -> buildPlayerComponent(p) +); + +// Использование +Component resolved = resolvers.getOrDefault(placeholder, + (p, g) -> Component.text("{" + placeholder + "}")) + .resolve(player, isGlobal); +``` + +--- + +### 6. ISP: MuteServiceImpl не реализован полностью +**Файл:** `MuteServiceImpl.java` + +**Проблема:** +```java +@Override +public void save() { + // TODO: Implement save to file +} + +private void load() { + // TODO: Implement load from file +} +``` + +**Решение:** +- Либо реализовать методы +- Либо разделить интерфейс на `MuteService` и `PersistentMuteService` + +--- + +### 7. Отсутствует зависимость VoiceChat +**Файл:** `MuteServiceImpl.java` + +**Проблема:** +```java +import de.maxhenkel.voicechat.api.BukkitVoicechatService; // ❌ Не найден +``` + +**Решение:** +```gradle +// build.gradle.kts +dependencies { + compileOnly("de.maxhenkel.voicechat:voicechat-api:2.5.0") +} +``` + +Или сделать опциональной зависимостью: +```java +private void initVoiceChat() { + try { + Class.forName("de.maxhenkel.voicechat.api.BukkitVoicechatService"); + // Инициализация + } catch (ClassNotFoundException e) { + plugin.getLogger().info("VoiceChat not found, voice mute disabled"); + } +} +``` + +--- + +### 8. SRP: ChatEventListener слишком сложный +**Файл:** `ChatEventListener.java` + +**Проблема:** +- Метод `onChat` содержит 80+ строк +- Обрабатывает: режим чата, фильтрацию, радиус, Discord, статистику +- Нарушает Single Responsibility + +**Решение:** +```java +class ChatEventListener { + private final ChatModeHandler modeHandler; + private final MessageFilterHandler filterHandler; + private final LocalChatHandler localHandler; + private final DiscordHandler discordHandler; + private final StatsHandler statsHandler; + + @EventHandler + public void onChat(AsyncChatEvent event) { + Player sender = event.getPlayer(); + String message = extractMessage(event); + + // Делегируем обработку + ChatMode mode = modeHandler.determineMode(message); + if (!filterHandler.filter(sender, message)) { + event.setCancelled(true); + return; + } + + if (mode == ChatMode.LOCAL) { + localHandler.handleLocal(event, sender); + } + + discordHandler.sendToDiscord(sender, message, mode); + statsHandler.recordMessage(sender, mode); + } +} +``` + +--- + +### 9. DRY: Дублирование конвертации цветов +**Файл:** `ColorConverter.java` + +**Проблема:** +```java +private static String convertSectionColors(String message) { ... } +private static String convertAmpersandColors(String message) { ... } +``` +Идентичная логика, только разные символы. + +**Решение:** +```java +private static String convertColors(String message, char symbol) { + return message + .replaceAll(symbol + "0", "") + .replaceAll(symbol + "1", "") + // ... +} + +public static String convertLegacyFormats(String message) { + message = convertColors(message, '§'); + message = convertColors(message, '&'); + return message; +} +``` + +--- + +### 10. DIP: Прямая зависимость от LoChat +**Файлы:** `EnhancedChatRenderer.java`, `ChatEventListener.java`, и др. + +**Проблема:** +```java +com.loki.lochat.LoChat loChat = (com.loki.lochat.LoChat) plugin; +``` +Нарушает Dependency Inversion Principle. + +**Решение:** +```java +// Создать интерфейс +interface ChatPlugin { + ConfigManager getConfigManager(); + GradientModule getGradientModule(); + DiscordIntegration getDiscordIntegration(); +} + +// LoChat реализует интерфейс +class LoChat extends JavaPlugin implements ChatPlugin { ... } + +// Использование +class EnhancedChatRenderer { + private final ChatPlugin plugin; + + public EnhancedChatRenderer(ChatPlugin plugin, boolean isGlobal) { + this.plugin = plugin; + // Нет каста + } +} +``` + +--- + +## 📊 Статистика проблем + +| Принцип | Нарушений | Критичность | +|---------|-----------|-------------| +| DRY | 4 | 🔴 Высокая | +| SRP | 3 | 🔴 Высокая | +| KISS | 2 | 🟡 Средняя | +| OCP | 1 | 🟡 Средняя | +| DIP | 1 | 🟡 Средняя | +| ISP | 1 | 🟢 Низкая | + +--- + +## ✅ Что сделано хорошо + +1. **ServiceRegistry** - правильная реализация DI +2. **Разделение на модули** - gradient, integrations, core +3. **Использование интерфейсов** - api.service.* +4. **PluginInitializer/PluginShutdown** - разделение логики инициализации +5. **BaseConfig** - базовый класс для конфигов (уже есть!) + +--- + +## 🎯 Приоритеты исправления + +### Высокий приоритет +1. Исправить отсутствующую зависимость VoiceChat +2. Упростить ColorConverter (KISS) +3. Вынести проверки permissions в отдельный метод (DRY) + +### Средний приоритет +4. Разделить ChatEventListener на handler'ы (SRP) +5. Убрать касты к LoChat через интерфейс (DIP) +6. Сделать EnhancedChatRenderer расширяемым (OCP) + +### Низкий приоритет +7. Реализовать save/load в MuteServiceImpl (ISP) +8. Упростить ConfigManager (SRP) +9. Объединить методы конвертации цветов (DRY) + +--- + +## 📝 Итого + +Проект имеет хорошую базовую архитектуру (ServiceRegistry, модули, интерфейсы), но есть проблемы с: +- **Дублированием кода** (DRY) - особенно в конфигах и фильтрах +- **Слишком большими классами** (SRP) - ConfigManager, ChatEventListener +- **Неэффективными алгоритмами** (KISS) - ColorConverter + +Рекомендую начать с исправления критических проблем (VoiceChat, ColorConverter, permissions), затем постепенно рефакторить остальное. diff --git a/MIGRATION_PLAN.md b/MIGRATION_PLAN.md deleted file mode 100644 index ffe0533..0000000 --- a/MIGRATION_PLAN.md +++ /dev/null @@ -1,90 +0,0 @@ -# План миграции команд на новую архитектуру - -> **Статус (актуально для репозитория):** централизованная регистрация в `CommandManager` и раскладка по пакетам **уже внедрены**. Ниже — что сделано и что остаётся опциональной доработкой. - -**Что осталось по плану:** миграция команд **завершена**. Дальше — по желанию: перевести оставшиеся `CommandExecutor` на `PlayerCommand`/`AdminCommand`, добавить тесты/ручной регресс, короткую доку «как добавить команду». - -## Что уже сделано - -### Архитектура -- `commands/base/BaseCommand`, `PlayerCommand`, `AdminCommand` -- `CommandManager` — единая точка регистрации (`reg` / `regTab` / `registerBaseCommand`) - -### Пакеты (фактическая структура) -| Пакет | Назначение | -|--------|------------| -| `commands/chat/` | глобальный / локальный чат (`g`, `l`) | -| `commands/messaging/` | ЛС, игнор (`msg`, `reply`, `ignore`, `unignore`, `ignorelist`) | -| `commands/moderation/` | муты, варны, баны (`lmute`, `lunmute`, `lmutelist`, `lmutehistory`, `lmuteblame`, `warn`, `silentwarn`, `lban`, `lunban`) | -| `commands/nick/` | ник, инфо (`nick`, `playerinfo`) | -| `commands/admin/` | админ-утилиты (`announce`, `chatspy`, `clearchat`, `clearchatconfig`, `lochat`, `lochatreload`, `discordadmin`); в репозитории также есть `HubCommand.java` (проверьте `plugin.yml`, если нужна команда хаба) | -| `commands/rp/` | RP (`me`, `try`, `do`, `roll`) | -| Корень `commands/` | `CustomCommandsCommand`, `CustomCommand` | - -Папки `social/` в проекте нет — социальные команды лежат в `messaging/` и `nick/`. - -### Регистрация в `CommandManager` (выполнено) -- [x] Этап 1 — чат: `GlobalChatCommand`, `LocalChatCommand` -- [x] Этап 2 — ЛС: `MsgCommand`, `ReplyCommand` -- [x] Этап 3 — социальное: `IgnoreCommand`, `UnignoreCommand`, `IgnoreListCommand`, `NickCommand` (+ `PlayerInfoCommand`) -- [x] Этап 4 — модерация: все перечисленные в таблице пакета `moderation/` -- [x] Этап 5 — админ: `AnnounceCommand`, `ClearChatCommand`, `ClearChatConfigCommand`, `ChatSpyCommand`, `LoChatCommand`, `ReloadConfigCommand`, `DiscordCommand` -- [x] Этап 6 — `CustomCommandsCommand` и движок кастомных команд - -## Что ещё можно сделать (не блокирует работу) - -### Унификация базового класса -Многие команды по-прежнему реализуют `CommandExecutor` напрямую, а не `PlayerCommand` / `AdminCommand`. Имеет смысл постепенно переводить на базовые классы там, где это убирает дублирование проверок и сообщений. - -### Тесты и ручная проверка -- [ ] Стабильные сценарии для приоритетных команд (чат, ЛС, муты) — ручные или автотесты -- [ ] Проверка TabCompleter на зарегистрированных командах с `regTab` - -### Документация -- [ ] Короткий `CONTRIBUTING` или раздел в README: как добавить команду через `CommandManager` и `plugin.yml` - -## Процесс миграции одной команды (если рефакторите дальше) - -### 1. Новая команда на базовом классе -```java -public class NewCommand extends PlayerCommand { - public NewCommand(LoChat plugin) { - super(plugin); - } - - @Override - protected boolean executePlayerCommand(Player player, Command command, - String label, String[] args) { - return true; - } -} -``` - -### 2. Проверки -- Поведение и права -- Сообщения из `MessagesConfig` / конфигов -- TabCompleter при необходимости - -### 3. Регистрация -В `CommandManager`: `reg("name", new NewCommand(plugin))` или `regTab`, плюс запись в `plugin.yml`. - -### 4. Удаление старого кода -После замены — удалить старый класс и неиспользуемые импорты. - -## Чек-лист для команды - -- [ ] Корректный базовый класс или явный `CommandExecutor` с единым стилем обработки ошибок -- [ ] Права из `plugin.yml` -- [ ] Сообщения из конфигурации -- [ ] Валидация аргументов -- [ ] TabCompleter при необходимости - -## Ожидаемые эффекты от доведения до единого стиля - -- Меньше дублирования проверок отправителя и прав -- Проще добавлять новые команды -- Проще сопровождать код при едином паттерне - ---- - -**Следующие шаги (по желанию):** выбрать 2–3 самых «шумных» по коду команды → перевести на `PlayerCommand`/`AdminCommand` → добавить минимальные тесты или чек-лист ручной регрессии. diff --git a/REFACTORING_SERVICES.md b/REFACTORING_SERVICES.md new file mode 100644 index 0000000..f620025 --- /dev/null +++ b/REFACTORING_SERVICES.md @@ -0,0 +1,287 @@ +# 🔧 Рефакторинг сервисов - уменьшение количества файлов + +## Текущая ситуация +- **11 интерфейсов** в `api/service/` +- **12 реализаций** в `core/service/` +- **23 файла** только для сервисов +- Некоторые сервисы по 30 строк кода + +## Проблема +Слишком много мелких файлов, которые можно объединить по функциональности. + +--- + +## 🎯 План рефакторинга + +### 1. Объединить простые сервисы в один + +**Было:** +``` +PMService.java (28 строк) +SpyService.java (69 строк) +IgnoreService.java (97 строк) +``` + +**Станет:** +```java +// MessagingService.java - всё что связано с общением +public interface MessagingService { + // PM + void setLastConversation(UUID sender, UUID receiver); + UUID getLastConversation(UUID player); + + // Spy + boolean toggleSpy(UUID player); + boolean isSpying(UUID player); + void broadcastPM(Player sender, Player receiver, String message); + + // Ignore + void addIgnore(UUID player, UUID target); + void removeIgnore(UUID player, UUID target); + boolean isIgnoring(UUID player, UUID target); + Set getIgnoreList(UUID player); +} +``` + +**Экономия:** 3 интерфейса + 3 реализации = **6 файлов → 2 файла** + +--- + +### 2. Объединить Mute + Punishment + +**Было:** +``` +MuteService.java + MuteServiceImpl.java (212 строк) +PunishmentService.java + PunishmentServiceImpl.java (173 строки) +MuteHistoryManager.java (77 строк) +``` + +**Станет:** +```java +// ModerationService.java - всё что связано с модерацией +public interface ModerationService { + // Mutes + void mute(UUID uuid, String name, long duration, String reason, String by); + boolean unmute(UUID uuid, String by); + boolean isMuted(UUID uuid); + MuteData getMuteData(UUID uuid); + + // Bans + void ban(UUID uuid, String name, long duration, String reason, String by); + boolean unban(UUID uuid, String by); + boolean isBanned(UUID uuid); + BanData getBanData(UUID uuid); + + // Warns + void warn(UUID uuid, String name, String reason, String by); + List getWarns(UUID uuid); + + // History + List getHistory(UUID uuid); + List getHistoryByModerator(String name); +} +``` + +**Экономия:** 3 интерфейса + 3 реализации = **6 файлов → 2 файла** + +--- + +### 3. Объединить Cooldown + PlayerData + +**Было:** +``` +CooldownService.java + CooldownServiceImpl.java (40 строк) +PlayerDataService.java + PlayerDataServiceImpl.java (102 строки) +``` + +**Станет:** +```java +// PlayerService.java - всё что связано с данными игрока +public interface PlayerService { + // Cooldowns + boolean hasCooldown(UUID player, String type); + void setCooldown(UUID player, String type, long duration); + long getRemainingCooldown(UUID player, String type); + void clearCooldowns(UUID player); + + // Statistics + void recordMessage(UUID player, String chatType); + PlayerStats getStats(UUID player); + void saveAll(); +} +``` + +**Экономия:** 2 интерфейса + 2 реализации = **4 файла → 2 файла** + +--- + +### 4. Оставить как есть (нормальный размер) + +``` +ChatService.java + ChatServiceImpl.java (114 строк) ✅ +MessageService.java + MessageServiceImpl.java (36 строк) ✅ +MentionService.java + MentionServiceImpl.java (104 строки) ✅ +NickService.java + NickServiceImpl.java (155 строк) ✅ +``` + +Эти сервисы имеют чёткую ответственность и нормальный размер. + +--- + +## 📊 Результат рефакторинга + +| До | После | Экономия | +|----|-------|----------| +| 11 интерфейсов | 7 интерфейсов | -4 | +| 12 реализаций | 8 реализаций | -4 | +| **23 файла** | **15 файлов** | **-8 файлов** | + +--- + +## 🔨 Пошаговая реализация + +### Шаг 1: Создать MessagingService + +```java +// api/service/MessagingService.java +public interface MessagingService { + // PM + void setLastConversation(UUID sender, UUID receiver); + UUID getLastConversation(UUID player); + + // Spy + boolean toggleSpy(UUID player); + boolean isSpying(UUID player); + void broadcastPM(Player sender, Player receiver, String message); + void sendToSpies(Player sender, Component message, boolean isGlobal); + void removeSpy(UUID player); + + // Ignore + void addIgnore(UUID player, UUID target); + void removeIgnore(UUID player, UUID target); + boolean isIgnoring(UUID player, UUID target); + Set getIgnoreList(UUID player); + void save(); +} + +// core/service/MessagingServiceImpl.java +public class MessagingServiceImpl implements MessagingService { + private final JavaPlugin plugin; + private final MessageConfig messageConfig; + + // PM state + private final Map lastConversation = new ConcurrentHashMap<>(); + + // Spy state + private final Set spyEnabled = ConcurrentHashMap.newKeySet(); + + // Ignore state + private final Map> ignoreMap = new ConcurrentHashMap<>(); + + public MessagingServiceImpl(JavaPlugin plugin, MessageConfig messageConfig) { + this.plugin = plugin; + this.messageConfig = messageConfig; + loadIgnoreData(); + } + + // Реализация всех методов... +} +``` + +### Шаг 2: Обновить ServiceRegistry + +```java +// Было +register(PMService.class, new PMServiceImpl()); +register(SpyService.class, new SpyServiceImpl(messageConfig)); +register(IgnoreService.class, new IgnoreServiceImpl(plugin)); + +// Стало +MessagingService messaging = new MessagingServiceImpl(plugin, messageConfig); +register(MessagingService.class, messaging); + +// Для обратной совместимости (опционально) +register(PMService.class, messaging); +register(SpyService.class, messaging); +register(IgnoreService.class, messaging); +``` + +### Шаг 3: Удалить старые файлы + +```bash +# Удалить интерфейсы +rm src/main/java/com/loki/lochat/api/service/PMService.java +rm src/main/java/com/loki/lochat/api/service/SpyService.java +rm src/main/java/com/loki/lochat/api/service/IgnoreService.java + +# Удалить реализации +rm src/main/java/com/loki/lochat/core/service/PMServiceImpl.java +rm src/main/java/com/loki/lochat/core/service/SpyServiceImpl.java +rm src/main/java/com/loki/lochat/core/service/IgnoreServiceImpl.java +``` + +--- + +## ⚠️ Альтернативный подход (если не хочешь ломать API) + +Можно оставить старые интерфейсы, но сделать их делегатами: + +```java +// Старый интерфейс остаётся +public interface PMService { + void setLastConversation(UUID sender, UUID receiver); + UUID getLastConversation(UUID player); +} + +// Но реализация делегирует в MessagingService +public class PMServiceAdapter implements PMService { + private final MessagingService messaging; + + public PMServiceAdapter(MessagingService messaging) { + this.messaging = messaging; + } + + @Override + public void setLastConversation(UUID sender, UUID receiver) { + messaging.setLastConversation(sender, receiver); + } + + @Override + public UUID getLastConversation(UUID player) { + return messaging.getLastConversation(player); + } +} +``` + +Это позволит: +- Сохранить обратную совместимость +- Уменьшить количество реализаций (3 → 1) +- Постепенно мигрировать код на новый API + +--- + +## 🎯 Рекомендация + +**Начни с MessagingService** - это самое простое объединение: +1. PM, Spy, Ignore логически связаны (общение между игроками) +2. Все три сервиса маленькие (28-97 строк) +3. Объединение даст ~200 строк - нормальный размер + +**Потом ModerationService** - это сложнее: +1. Mute + Punishment + History = ~460 строк +2. Нужно будет рефакторить внутреннюю структуру +3. Но логически это одна область (модерация) + +**PlayerService оставь на потом** - там всё норм. + +--- + +## 📝 Итого + +Объединение сервисов: +- ✅ Уменьшит количество файлов с 23 до 15 +- ✅ Упростит навигацию по коду +- ✅ Сгруппирует связанную функциональность +- ⚠️ Потребует обновления кода, который использует старые сервисы + +Начни с MessagingService - это быстро и безопасно. diff --git a/bin/main/com/loki/lochat/LoChat.class b/bin/main/com/loki/lochat/LoChat.class index ead34da..b8b59b0 100644 Binary files a/bin/main/com/loki/lochat/LoChat.class and b/bin/main/com/loki/lochat/LoChat.class differ diff --git a/bin/main/com/loki/lochat/api/service/CooldownService.class b/bin/main/com/loki/lochat/api/service/CooldownService.class deleted file mode 100644 index 10afb4b..0000000 Binary files a/bin/main/com/loki/lochat/api/service/CooldownService.class and /dev/null differ diff --git a/bin/main/com/loki/lochat/api/service/IgnoreService.class b/bin/main/com/loki/lochat/api/service/IgnoreService.class deleted file mode 100644 index 41ead54..0000000 Binary files a/bin/main/com/loki/lochat/api/service/IgnoreService.class and /dev/null differ diff --git a/bin/main/com/loki/lochat/api/service/MessagingService.class b/bin/main/com/loki/lochat/api/service/MessagingService.class new file mode 100644 index 0000000..cf948f8 Binary files /dev/null and b/bin/main/com/loki/lochat/api/service/MessagingService.class differ diff --git a/bin/main/com/loki/lochat/api/service/PMService.class b/bin/main/com/loki/lochat/api/service/PMService.class deleted file mode 100644 index 9576161..0000000 Binary files a/bin/main/com/loki/lochat/api/service/PMService.class and /dev/null differ diff --git a/bin/main/com/loki/lochat/api/service/PlayerDataService.class b/bin/main/com/loki/lochat/api/service/PlayerDataService.class deleted file mode 100644 index c46becd..0000000 Binary files a/bin/main/com/loki/lochat/api/service/PlayerDataService.class and /dev/null differ diff --git a/bin/main/com/loki/lochat/api/service/PlayerService.class b/bin/main/com/loki/lochat/api/service/PlayerService.class new file mode 100644 index 0000000..fe4f987 Binary files /dev/null and b/bin/main/com/loki/lochat/api/service/PlayerService.class differ diff --git a/bin/main/com/loki/lochat/api/service/PunishmentService.class b/bin/main/com/loki/lochat/api/service/PunishmentService.class new file mode 100644 index 0000000..47de38c Binary files /dev/null and b/bin/main/com/loki/lochat/api/service/PunishmentService.class differ diff --git a/bin/main/com/loki/lochat/api/service/SpyService.class b/bin/main/com/loki/lochat/api/service/SpyService.class deleted file mode 100644 index 477480e..0000000 Binary files a/bin/main/com/loki/lochat/api/service/SpyService.class and /dev/null differ diff --git a/bin/main/com/loki/lochat/commands/CommandManager.class b/bin/main/com/loki/lochat/commands/CommandManager.class index ea875b3..d4c76f7 100644 Binary files a/bin/main/com/loki/lochat/commands/CommandManager.class and b/bin/main/com/loki/lochat/commands/CommandManager.class differ diff --git a/bin/main/com/loki/lochat/commands/admin/ChatSpyCommand.class b/bin/main/com/loki/lochat/commands/admin/ChatSpyCommand.class index ac41199..7b964f3 100644 Binary files a/bin/main/com/loki/lochat/commands/admin/ChatSpyCommand.class and b/bin/main/com/loki/lochat/commands/admin/ChatSpyCommand.class differ diff --git a/bin/main/com/loki/lochat/commands/chat/GlobalChatCommand.class b/bin/main/com/loki/lochat/commands/chat/GlobalChatCommand.class index de49499..71599ea 100644 Binary files a/bin/main/com/loki/lochat/commands/chat/GlobalChatCommand.class and b/bin/main/com/loki/lochat/commands/chat/GlobalChatCommand.class differ diff --git a/bin/main/com/loki/lochat/commands/messaging/IgnoreCommand.class b/bin/main/com/loki/lochat/commands/messaging/IgnoreCommand.class index 1f54379..511fd7c 100644 Binary files a/bin/main/com/loki/lochat/commands/messaging/IgnoreCommand.class and b/bin/main/com/loki/lochat/commands/messaging/IgnoreCommand.class differ diff --git a/bin/main/com/loki/lochat/commands/messaging/IgnoreListCommand.class b/bin/main/com/loki/lochat/commands/messaging/IgnoreListCommand.class index 8810861..f95c198 100644 Binary files a/bin/main/com/loki/lochat/commands/messaging/IgnoreListCommand.class and b/bin/main/com/loki/lochat/commands/messaging/IgnoreListCommand.class differ diff --git a/bin/main/com/loki/lochat/commands/messaging/MsgCommand.class b/bin/main/com/loki/lochat/commands/messaging/MsgCommand.class index 2f394a2..3ce7de1 100644 Binary files a/bin/main/com/loki/lochat/commands/messaging/MsgCommand.class and b/bin/main/com/loki/lochat/commands/messaging/MsgCommand.class differ diff --git a/bin/main/com/loki/lochat/commands/messaging/ReplyCommand.class b/bin/main/com/loki/lochat/commands/messaging/ReplyCommand.class index 09d1463..a126f90 100644 Binary files a/bin/main/com/loki/lochat/commands/messaging/ReplyCommand.class and b/bin/main/com/loki/lochat/commands/messaging/ReplyCommand.class differ diff --git a/bin/main/com/loki/lochat/commands/messaging/UnignoreCommand.class b/bin/main/com/loki/lochat/commands/messaging/UnignoreCommand.class index 85d73b1..01ae39d 100644 Binary files a/bin/main/com/loki/lochat/commands/messaging/UnignoreCommand.class and b/bin/main/com/loki/lochat/commands/messaging/UnignoreCommand.class differ diff --git a/bin/main/com/loki/lochat/commands/moderation/BanCommand.class b/bin/main/com/loki/lochat/commands/moderation/BanCommand.class new file mode 100644 index 0000000..30afb73 Binary files /dev/null and b/bin/main/com/loki/lochat/commands/moderation/BanCommand.class differ diff --git a/bin/main/com/loki/lochat/commands/moderation/MuteCommand.class b/bin/main/com/loki/lochat/commands/moderation/MuteCommand.class index 1d574c1..02c0255 100644 Binary files a/bin/main/com/loki/lochat/commands/moderation/MuteCommand.class and b/bin/main/com/loki/lochat/commands/moderation/MuteCommand.class differ diff --git a/bin/main/com/loki/lochat/commands/moderation/SilentWarnCommand.class b/bin/main/com/loki/lochat/commands/moderation/SilentWarnCommand.class new file mode 100644 index 0000000..697f2d4 Binary files /dev/null and b/bin/main/com/loki/lochat/commands/moderation/SilentWarnCommand.class differ diff --git a/bin/main/com/loki/lochat/commands/moderation/UnbanCommand.class b/bin/main/com/loki/lochat/commands/moderation/UnbanCommand.class new file mode 100644 index 0000000..0c0e18e Binary files /dev/null and b/bin/main/com/loki/lochat/commands/moderation/UnbanCommand.class differ diff --git a/bin/main/com/loki/lochat/commands/moderation/WarnCommand.class b/bin/main/com/loki/lochat/commands/moderation/WarnCommand.class new file mode 100644 index 0000000..ec27dae Binary files /dev/null and b/bin/main/com/loki/lochat/commands/moderation/WarnCommand.class differ diff --git a/bin/main/com/loki/lochat/commands/nick/PlayerInfoCommand.class b/bin/main/com/loki/lochat/commands/nick/PlayerInfoCommand.class index b817e15..d10b497 100644 Binary files a/bin/main/com/loki/lochat/commands/nick/PlayerInfoCommand.class and b/bin/main/com/loki/lochat/commands/nick/PlayerInfoCommand.class differ diff --git a/bin/main/com/loki/lochat/commands/rp/DoCommand.class b/bin/main/com/loki/lochat/commands/rp/DoCommand.class index 654355e..9e4cdd8 100644 Binary files a/bin/main/com/loki/lochat/commands/rp/DoCommand.class and b/bin/main/com/loki/lochat/commands/rp/DoCommand.class differ diff --git a/bin/main/com/loki/lochat/commands/rp/MeCommand.class b/bin/main/com/loki/lochat/commands/rp/MeCommand.class index 8edea1d..58d4f3a 100644 Binary files a/bin/main/com/loki/lochat/commands/rp/MeCommand.class and b/bin/main/com/loki/lochat/commands/rp/MeCommand.class differ diff --git a/bin/main/com/loki/lochat/commands/rp/RpUtil.class b/bin/main/com/loki/lochat/commands/rp/RpUtil.class index 2fe39ef..d16fd08 100644 Binary files a/bin/main/com/loki/lochat/commands/rp/RpUtil.class and b/bin/main/com/loki/lochat/commands/rp/RpUtil.class differ diff --git a/bin/main/com/loki/lochat/commands/rp/TryCommand.class b/bin/main/com/loki/lochat/commands/rp/TryCommand.class index 6f9eebf..846743a 100644 Binary files a/bin/main/com/loki/lochat/commands/rp/TryCommand.class and b/bin/main/com/loki/lochat/commands/rp/TryCommand.class differ diff --git a/bin/main/com/loki/lochat/config/AppearanceConfig.class b/bin/main/com/loki/lochat/config/AppearanceConfig.class index 3f868ca..1ef321a 100644 Binary files a/bin/main/com/loki/lochat/config/AppearanceConfig.class and b/bin/main/com/loki/lochat/config/AppearanceConfig.class differ diff --git a/bin/main/com/loki/lochat/config/ConfigManager.class b/bin/main/com/loki/lochat/config/ConfigManager.class index 6b48d50..02d07d7 100644 Binary files a/bin/main/com/loki/lochat/config/ConfigManager.class and b/bin/main/com/loki/lochat/config/ConfigManager.class differ diff --git a/bin/main/com/loki/lochat/config/MessagesConfig.class b/bin/main/com/loki/lochat/config/MessagesConfig.class index 2bba3f5..5678868 100644 Binary files a/bin/main/com/loki/lochat/config/MessagesConfig.class and b/bin/main/com/loki/lochat/config/MessagesConfig.class differ diff --git a/bin/main/com/loki/lochat/core/PluginInitializer.class b/bin/main/com/loki/lochat/core/PluginInitializer.class index 351bec0..39888f2 100644 Binary files a/bin/main/com/loki/lochat/core/PluginInitializer.class and b/bin/main/com/loki/lochat/core/PluginInitializer.class differ diff --git a/bin/main/com/loki/lochat/core/PluginShutdown.class b/bin/main/com/loki/lochat/core/PluginShutdown.class index 7d57a96..bf16670 100644 Binary files a/bin/main/com/loki/lochat/core/PluginShutdown.class and b/bin/main/com/loki/lochat/core/PluginShutdown.class differ diff --git a/bin/main/com/loki/lochat/core/filter/AdvancedMessageFilter$FilterResult.class b/bin/main/com/loki/lochat/core/filter/AdvancedMessageFilter$FilterResult.class deleted file mode 100644 index e203058..0000000 Binary files a/bin/main/com/loki/lochat/core/filter/AdvancedMessageFilter$FilterResult.class and /dev/null differ diff --git a/bin/main/com/loki/lochat/core/filter/AdvancedMessageFilter.class b/bin/main/com/loki/lochat/core/filter/AdvancedMessageFilter.class index 33f3934..61797f8 100644 Binary files a/bin/main/com/loki/lochat/core/filter/AdvancedMessageFilter.class and b/bin/main/com/loki/lochat/core/filter/AdvancedMessageFilter.class differ diff --git a/bin/main/com/loki/lochat/core/filter/CooldownFilter.class b/bin/main/com/loki/lochat/core/filter/CooldownFilter.class index 9550ed0..97bbc3d 100644 Binary files a/bin/main/com/loki/lochat/core/filter/CooldownFilter.class and b/bin/main/com/loki/lochat/core/filter/CooldownFilter.class differ diff --git a/bin/main/com/loki/lochat/core/filter/FilterResult.class b/bin/main/com/loki/lochat/core/filter/FilterResult.class new file mode 100644 index 0000000..1d93e23 Binary files /dev/null and b/bin/main/com/loki/lochat/core/filter/FilterResult.class differ diff --git a/bin/main/com/loki/lochat/core/filter/filters/CharacterFilter.class b/bin/main/com/loki/lochat/core/filter/filters/CharacterFilter.class new file mode 100644 index 0000000..ed10363 Binary files /dev/null and b/bin/main/com/loki/lochat/core/filter/filters/CharacterFilter.class differ diff --git a/bin/main/com/loki/lochat/core/filter/filters/FloodFilter.class b/bin/main/com/loki/lochat/core/filter/filters/FloodFilter.class new file mode 100644 index 0000000..88ba1a4 Binary files /dev/null and b/bin/main/com/loki/lochat/core/filter/filters/FloodFilter.class differ diff --git a/bin/main/com/loki/lochat/core/filter/filters/SpamFilter.class b/bin/main/com/loki/lochat/core/filter/filters/SpamFilter.class new file mode 100644 index 0000000..765cec6 Binary files /dev/null and b/bin/main/com/loki/lochat/core/filter/filters/SpamFilter.class differ diff --git a/bin/main/com/loki/lochat/core/filter/filters/SwearFilter.class b/bin/main/com/loki/lochat/core/filter/filters/SwearFilter.class new file mode 100644 index 0000000..a62a6d4 Binary files /dev/null and b/bin/main/com/loki/lochat/core/filter/filters/SwearFilter.class differ diff --git a/bin/main/com/loki/lochat/core/filter/filters/UrlFilter.class b/bin/main/com/loki/lochat/core/filter/filters/UrlFilter.class new file mode 100644 index 0000000..92add1d Binary files /dev/null and b/bin/main/com/loki/lochat/core/filter/filters/UrlFilter.class differ diff --git a/bin/main/com/loki/lochat/core/registry/ServiceRegistry.class b/bin/main/com/loki/lochat/core/registry/ServiceRegistry.class index 18a4c57..6e6eb19 100644 Binary files a/bin/main/com/loki/lochat/core/registry/ServiceRegistry.class and b/bin/main/com/loki/lochat/core/registry/ServiceRegistry.class differ diff --git a/bin/main/com/loki/lochat/core/service/ChatServiceImpl.class b/bin/main/com/loki/lochat/core/service/ChatServiceImpl.class index d9bf58c..0e825f1 100644 Binary files a/bin/main/com/loki/lochat/core/service/ChatServiceImpl.class and b/bin/main/com/loki/lochat/core/service/ChatServiceImpl.class differ diff --git a/bin/main/com/loki/lochat/core/service/CooldownServiceImpl.class b/bin/main/com/loki/lochat/core/service/CooldownServiceImpl.class deleted file mode 100644 index b8a641e..0000000 Binary files a/bin/main/com/loki/lochat/core/service/CooldownServiceImpl.class and /dev/null differ diff --git a/bin/main/com/loki/lochat/core/service/IgnoreServiceImpl.class b/bin/main/com/loki/lochat/core/service/IgnoreServiceImpl.class deleted file mode 100644 index ff0f8f6..0000000 Binary files a/bin/main/com/loki/lochat/core/service/IgnoreServiceImpl.class and /dev/null differ diff --git a/bin/main/com/loki/lochat/core/service/MessageServiceImpl.class b/bin/main/com/loki/lochat/core/service/MessageServiceImpl.class index c62834f..3ebdad9 100644 Binary files a/bin/main/com/loki/lochat/core/service/MessageServiceImpl.class and b/bin/main/com/loki/lochat/core/service/MessageServiceImpl.class differ diff --git a/bin/main/com/loki/lochat/core/service/MessagingServiceImpl.class b/bin/main/com/loki/lochat/core/service/MessagingServiceImpl.class new file mode 100644 index 0000000..88563d1 Binary files /dev/null and b/bin/main/com/loki/lochat/core/service/MessagingServiceImpl.class differ diff --git a/bin/main/com/loki/lochat/core/service/MuteServiceImpl$1.class b/bin/main/com/loki/lochat/core/service/MuteServiceImpl$1.class deleted file mode 100644 index 14764f5..0000000 Binary files a/bin/main/com/loki/lochat/core/service/MuteServiceImpl$1.class and /dev/null differ diff --git a/bin/main/com/loki/lochat/core/service/MuteServiceImpl.class b/bin/main/com/loki/lochat/core/service/MuteServiceImpl.class index 9797d64..8a9dabe 100644 Binary files a/bin/main/com/loki/lochat/core/service/MuteServiceImpl.class and b/bin/main/com/loki/lochat/core/service/MuteServiceImpl.class differ diff --git a/bin/main/com/loki/lochat/core/service/PMServiceImpl.class b/bin/main/com/loki/lochat/core/service/PMServiceImpl.class deleted file mode 100644 index 874241e..0000000 Binary files a/bin/main/com/loki/lochat/core/service/PMServiceImpl.class and /dev/null differ diff --git a/bin/main/com/loki/lochat/core/service/PlayerDataServiceImpl.class b/bin/main/com/loki/lochat/core/service/PlayerDataServiceImpl.class deleted file mode 100644 index ac73544..0000000 Binary files a/bin/main/com/loki/lochat/core/service/PlayerDataServiceImpl.class and /dev/null differ diff --git a/bin/main/com/loki/lochat/core/service/PlayerServiceImpl.class b/bin/main/com/loki/lochat/core/service/PlayerServiceImpl.class new file mode 100644 index 0000000..7ce1f2d Binary files /dev/null and b/bin/main/com/loki/lochat/core/service/PlayerServiceImpl.class differ diff --git a/bin/main/com/loki/lochat/core/service/PunishmentServiceImpl$1.class b/bin/main/com/loki/lochat/core/service/PunishmentServiceImpl$1.class new file mode 100644 index 0000000..7ae9a8c Binary files /dev/null and b/bin/main/com/loki/lochat/core/service/PunishmentServiceImpl$1.class differ diff --git a/bin/main/com/loki/lochat/core/service/PunishmentServiceImpl$PunishmentSnapshot.class b/bin/main/com/loki/lochat/core/service/PunishmentServiceImpl$PunishmentSnapshot.class new file mode 100644 index 0000000..8828fc9 Binary files /dev/null and b/bin/main/com/loki/lochat/core/service/PunishmentServiceImpl$PunishmentSnapshot.class differ diff --git a/bin/main/com/loki/lochat/core/service/PunishmentServiceImpl.class b/bin/main/com/loki/lochat/core/service/PunishmentServiceImpl.class new file mode 100644 index 0000000..a60b788 Binary files /dev/null and b/bin/main/com/loki/lochat/core/service/PunishmentServiceImpl.class differ diff --git a/bin/main/com/loki/lochat/core/service/SpyServiceImpl.class b/bin/main/com/loki/lochat/core/service/SpyServiceImpl.class deleted file mode 100644 index 7511ba9..0000000 Binary files a/bin/main/com/loki/lochat/core/service/SpyServiceImpl.class and /dev/null differ diff --git a/bin/main/com/loki/lochat/core/service/mute/MuteDataStorage$1.class b/bin/main/com/loki/lochat/core/service/mute/MuteDataStorage$1.class new file mode 100644 index 0000000..22bb15e Binary files /dev/null and b/bin/main/com/loki/lochat/core/service/mute/MuteDataStorage$1.class differ diff --git a/bin/main/com/loki/lochat/core/service/mute/MuteDataStorage.class b/bin/main/com/loki/lochat/core/service/mute/MuteDataStorage.class new file mode 100644 index 0000000..cff3b68 Binary files /dev/null and b/bin/main/com/loki/lochat/core/service/mute/MuteDataStorage.class differ diff --git a/bin/main/com/loki/lochat/core/service/mute/MutePermissionChecker.class b/bin/main/com/loki/lochat/core/service/mute/MutePermissionChecker.class new file mode 100644 index 0000000..4697803 Binary files /dev/null and b/bin/main/com/loki/lochat/core/service/mute/MutePermissionChecker.class differ diff --git a/bin/main/com/loki/lochat/data/model/BanRecord.class b/bin/main/com/loki/lochat/data/model/BanRecord.class new file mode 100644 index 0000000..161ffa4 Binary files /dev/null and b/bin/main/com/loki/lochat/data/model/BanRecord.class differ diff --git a/bin/main/com/loki/lochat/data/model/WarnEntry.class b/bin/main/com/loki/lochat/data/model/WarnEntry.class new file mode 100644 index 0000000..838af05 Binary files /dev/null and b/bin/main/com/loki/lochat/data/model/WarnEntry.class differ diff --git a/bin/main/com/loki/lochat/gradient/GradientModule.class b/bin/main/com/loki/lochat/gradient/GradientModule.class index 2b09369..486569b 100644 Binary files a/bin/main/com/loki/lochat/gradient/GradientModule.class and b/bin/main/com/loki/lochat/gradient/GradientModule.class differ diff --git a/bin/main/com/loki/lochat/gradient/commands/GradientAdminCommand.class b/bin/main/com/loki/lochat/gradient/commands/GradientAdminCommand.class index 338bf99..be47dc5 100644 Binary files a/bin/main/com/loki/lochat/gradient/commands/GradientAdminCommand.class and b/bin/main/com/loki/lochat/gradient/commands/GradientAdminCommand.class differ diff --git a/bin/main/com/loki/lochat/gradient/commands/GradientColorCommand.class b/bin/main/com/loki/lochat/gradient/commands/GradientColorCommand.class index 0bff940..43f9bf1 100644 Binary files a/bin/main/com/loki/lochat/gradient/commands/GradientColorCommand.class and b/bin/main/com/loki/lochat/gradient/commands/GradientColorCommand.class differ diff --git a/bin/main/com/loki/lochat/gradient/commands/GradientPrefixCommand.class b/bin/main/com/loki/lochat/gradient/commands/GradientPrefixCommand.class index ccb0d3a..f3f505c 100644 Binary files a/bin/main/com/loki/lochat/gradient/commands/GradientPrefixCommand.class and b/bin/main/com/loki/lochat/gradient/commands/GradientPrefixCommand.class differ diff --git a/bin/main/com/loki/lochat/gradient/commands/handlers/ColorCommandHandler.class b/bin/main/com/loki/lochat/gradient/commands/handlers/ColorCommandHandler.class new file mode 100644 index 0000000..2980876 Binary files /dev/null and b/bin/main/com/loki/lochat/gradient/commands/handlers/ColorCommandHandler.class differ diff --git a/bin/main/com/loki/lochat/gradient/commands/handlers/PrefixCommandHandler.class b/bin/main/com/loki/lochat/gradient/commands/handlers/PrefixCommandHandler.class new file mode 100644 index 0000000..b1fa649 Binary files /dev/null and b/bin/main/com/loki/lochat/gradient/commands/handlers/PrefixCommandHandler.class differ diff --git a/bin/main/com/loki/lochat/gradient/config/GradientConfig.class b/bin/main/com/loki/lochat/gradient/config/GradientConfig.class index 1ce649a..265b326 100644 Binary files a/bin/main/com/loki/lochat/gradient/config/GradientConfig.class and b/bin/main/com/loki/lochat/gradient/config/GradientConfig.class differ diff --git a/bin/main/com/loki/lochat/gradient/gui/GradientConfirmGUI.class b/bin/main/com/loki/lochat/gradient/gui/GradientConfirmGUI.class index 468db56..7389fa6 100644 Binary files a/bin/main/com/loki/lochat/gradient/gui/GradientConfirmGUI.class and b/bin/main/com/loki/lochat/gradient/gui/GradientConfirmGUI.class differ diff --git a/bin/main/com/loki/lochat/gradient/hooks/GradientLuckPermsHook.class b/bin/main/com/loki/lochat/gradient/hooks/GradientLuckPermsHook.class deleted file mode 100644 index 136622e..0000000 Binary files a/bin/main/com/loki/lochat/gradient/hooks/GradientLuckPermsHook.class and /dev/null differ diff --git a/bin/main/com/loki/lochat/gradient/listeners/GradientGUIListener.class b/bin/main/com/loki/lochat/gradient/listeners/GradientGUIListener.class index 304ffe2..50c186b 100644 Binary files a/bin/main/com/loki/lochat/gradient/listeners/GradientGUIListener.class and b/bin/main/com/loki/lochat/gradient/listeners/GradientGUIListener.class differ diff --git a/bin/main/com/loki/lochat/gradient/listeners/GradientPlayerListener.class b/bin/main/com/loki/lochat/gradient/listeners/GradientPlayerListener.class index e998c16..f70f82b 100644 Binary files a/bin/main/com/loki/lochat/gradient/listeners/GradientPlayerListener.class and b/bin/main/com/loki/lochat/gradient/listeners/GradientPlayerListener.class differ diff --git a/bin/main/com/loki/lochat/gradient/service/GradientService.class b/bin/main/com/loki/lochat/gradient/service/GradientService.class new file mode 100644 index 0000000..571d257 Binary files /dev/null and b/bin/main/com/loki/lochat/gradient/service/GradientService.class differ diff --git a/bin/main/com/loki/lochat/gradient/service/PrefixService.class b/bin/main/com/loki/lochat/gradient/service/PrefixService.class new file mode 100644 index 0000000..46b5885 Binary files /dev/null and b/bin/main/com/loki/lochat/gradient/service/PrefixService.class differ diff --git a/bin/main/com/loki/lochat/gradient/util/DisplayNameUtil.class b/bin/main/com/loki/lochat/gradient/util/DisplayNameUtil.class index bcc801d..653af57 100644 Binary files a/bin/main/com/loki/lochat/gradient/util/DisplayNameUtil.class and b/bin/main/com/loki/lochat/gradient/util/DisplayNameUtil.class differ diff --git a/bin/main/com/loki/lochat/integrations/DiscordIntegration.class b/bin/main/com/loki/lochat/integrations/DiscordIntegration.class index 4f0eda6..d4ff258 100644 Binary files a/bin/main/com/loki/lochat/integrations/DiscordIntegration.class and b/bin/main/com/loki/lochat/integrations/DiscordIntegration.class differ diff --git a/bin/main/com/loki/lochat/integrations/LibertyBansHook.class b/bin/main/com/loki/lochat/integrations/LibertyBansHook.class index 7829436..39c670f 100644 Binary files a/bin/main/com/loki/lochat/integrations/LibertyBansHook.class and b/bin/main/com/loki/lochat/integrations/LibertyBansHook.class differ diff --git a/bin/main/com/loki/lochat/integrations/PlaceholderAPIHook.class b/bin/main/com/loki/lochat/integrations/PlaceholderAPIHook.class index 29f63ba..70e11bf 100644 Binary files a/bin/main/com/loki/lochat/integrations/PlaceholderAPIHook.class and b/bin/main/com/loki/lochat/integrations/PlaceholderAPIHook.class differ diff --git a/bin/main/com/loki/lochat/integrations/discord/DiscordConfig.class b/bin/main/com/loki/lochat/integrations/discord/DiscordConfig.class new file mode 100644 index 0000000..55da43a Binary files /dev/null and b/bin/main/com/loki/lochat/integrations/discord/DiscordConfig.class differ diff --git a/bin/main/com/loki/lochat/integrations/discord/DiscordMessageService.class b/bin/main/com/loki/lochat/integrations/discord/DiscordMessageService.class new file mode 100644 index 0000000..28a2dd0 Binary files /dev/null and b/bin/main/com/loki/lochat/integrations/discord/DiscordMessageService.class differ diff --git a/bin/main/com/loki/lochat/listener/ChatEventListener$ChatMode.class b/bin/main/com/loki/lochat/listener/ChatEventListener$ChatMode.class new file mode 100644 index 0000000..ed94676 Binary files /dev/null and b/bin/main/com/loki/lochat/listener/ChatEventListener$ChatMode.class differ diff --git a/bin/main/com/loki/lochat/listener/ChatEventListener.class b/bin/main/com/loki/lochat/listener/ChatEventListener.class index fb65518..3a17296 100644 Binary files a/bin/main/com/loki/lochat/listener/ChatEventListener.class and b/bin/main/com/loki/lochat/listener/ChatEventListener.class differ diff --git a/bin/main/com/loki/lochat/listener/ModerationListener.class b/bin/main/com/loki/lochat/listener/ModerationListener.class new file mode 100644 index 0000000..6b1248d Binary files /dev/null and b/bin/main/com/loki/lochat/listener/ModerationListener.class differ diff --git a/bin/main/com/loki/lochat/listener/PlayerEventListener.class b/bin/main/com/loki/lochat/listener/PlayerEventListener.class index 56f04ef..240a811 100644 Binary files a/bin/main/com/loki/lochat/listener/PlayerEventListener.class and b/bin/main/com/loki/lochat/listener/PlayerEventListener.class differ diff --git a/bin/main/com/loki/lochat/managers/AutoMessageManager.class b/bin/main/com/loki/lochat/managers/AutoMessageManager.class index e62b41a..ffae4b3 100644 Binary files a/bin/main/com/loki/lochat/managers/AutoMessageManager.class and b/bin/main/com/loki/lochat/managers/AutoMessageManager.class differ diff --git a/bin/main/com/loki/lochat/managers/CustomCommandManager$CustomCommandData.class b/bin/main/com/loki/lochat/managers/CustomCommandManager$CustomCommandData.class index f0d23a5..fc26f37 100644 Binary files a/bin/main/com/loki/lochat/managers/CustomCommandManager$CustomCommandData.class and b/bin/main/com/loki/lochat/managers/CustomCommandManager$CustomCommandData.class differ diff --git a/bin/main/com/loki/lochat/managers/CustomCommandManager.class b/bin/main/com/loki/lochat/managers/CustomCommandManager.class index 8a28a25..0411504 100644 Binary files a/bin/main/com/loki/lochat/managers/CustomCommandManager.class and b/bin/main/com/loki/lochat/managers/CustomCommandManager.class differ diff --git a/bin/main/com/loki/lochat/managers/commands/MessageSender.class b/bin/main/com/loki/lochat/managers/commands/MessageSender.class new file mode 100644 index 0000000..85a72fd Binary files /dev/null and b/bin/main/com/loki/lochat/managers/commands/MessageSender.class differ diff --git a/bin/main/com/loki/lochat/managers/commands/PlaceholderProcessor.class b/bin/main/com/loki/lochat/managers/commands/PlaceholderProcessor.class new file mode 100644 index 0000000..1354338 Binary files /dev/null and b/bin/main/com/loki/lochat/managers/commands/PlaceholderProcessor.class differ diff --git a/bin/main/com/loki/lochat/renderer/EnhancedChatRenderer.class b/bin/main/com/loki/lochat/renderer/EnhancedChatRenderer.class index e9e0a9d..47af895 100644 Binary files a/bin/main/com/loki/lochat/renderer/EnhancedChatRenderer.class and b/bin/main/com/loki/lochat/renderer/EnhancedChatRenderer.class differ diff --git a/bin/main/com/loki/lochat/renderer/components/EmojiComponent.class b/bin/main/com/loki/lochat/renderer/components/EmojiComponent.class new file mode 100644 index 0000000..4ac2be6 Binary files /dev/null and b/bin/main/com/loki/lochat/renderer/components/EmojiComponent.class differ diff --git a/bin/main/com/loki/lochat/renderer/components/GradientBuilder.class b/bin/main/com/loki/lochat/renderer/components/GradientBuilder.class new file mode 100644 index 0000000..bb704c2 Binary files /dev/null and b/bin/main/com/loki/lochat/renderer/components/GradientBuilder.class differ diff --git a/bin/main/com/loki/lochat/renderer/components/UrlProcessor.class b/bin/main/com/loki/lochat/renderer/components/UrlProcessor.class new file mode 100644 index 0000000..96260fd Binary files /dev/null and b/bin/main/com/loki/lochat/renderer/components/UrlProcessor.class differ diff --git a/bin/main/com/loki/lochat/util/TimeParser.class b/bin/main/com/loki/lochat/util/TimeParser.class deleted file mode 100644 index 0b8a6ac..0000000 Binary files a/bin/main/com/loki/lochat/util/TimeParser.class and /dev/null differ diff --git a/bin/main/com/loki/lochat/utils/ChatFormatter.class b/bin/main/com/loki/lochat/utils/ChatFormatter.class index 1abd402..db2f997 100644 Binary files a/bin/main/com/loki/lochat/utils/ChatFormatter.class and b/bin/main/com/loki/lochat/utils/ChatFormatter.class differ diff --git a/bin/main/com/loki/lochat/util/DistanceUtil.class b/bin/main/com/loki/lochat/utils/DistanceUtil.class similarity index 59% rename from bin/main/com/loki/lochat/util/DistanceUtil.class rename to bin/main/com/loki/lochat/utils/DistanceUtil.class index a1e3fa7..b098713 100644 Binary files a/bin/main/com/loki/lochat/util/DistanceUtil.class and b/bin/main/com/loki/lochat/utils/DistanceUtil.class differ diff --git a/bin/main/com/loki/lochat/util/FoliaUtil.class b/bin/main/com/loki/lochat/utils/FoliaUtil.class similarity index 92% rename from bin/main/com/loki/lochat/util/FoliaUtil.class rename to bin/main/com/loki/lochat/utils/FoliaUtil.class index 97b9a9d..658c913 100644 Binary files a/bin/main/com/loki/lochat/util/FoliaUtil.class and b/bin/main/com/loki/lochat/utils/FoliaUtil.class differ diff --git a/bin/main/com/loki/lochat/utils/MentionHandler.class b/bin/main/com/loki/lochat/utils/MentionHandler.class index d976a95..6333479 100644 Binary files a/bin/main/com/loki/lochat/utils/MentionHandler.class and b/bin/main/com/loki/lochat/utils/MentionHandler.class differ diff --git a/bin/main/com/loki/lochat/utils/color/ColorConverter.class b/bin/main/com/loki/lochat/utils/color/ColorConverter.class new file mode 100644 index 0000000..4b1e614 Binary files /dev/null and b/bin/main/com/loki/lochat/utils/color/ColorConverter.class differ diff --git a/bin/main/config.yml b/bin/main/config.yml index 4cde6e6..aede1c2 100644 --- a/bin/main/config.yml +++ b/bin/main/config.yml @@ -60,3 +60,7 @@ rp: try: 5 do: 3 roll: 2 + +# Модерация: после /lmute выполнить команду от консоли (SimpleVoiceChat: voice mute {player}) +moderation: + voice-mute-console-command: "" diff --git a/bin/main/config/appearance.yml b/bin/main/config/appearance.yml index 8b1af21..4626e44 100644 --- a/bin/main/config/appearance.yml +++ b/bin/main/config/appearance.yml @@ -1,16 +1,15 @@ # ╔══════════════════════════════════════════════════════════════════════════════╗ -# ║ LoChat Chat Appearance Config ║ -# ║ Настройки внешнего вида чата ║ -# ║ Цветовая гамма: Spacelegacy ║ -# ║ #B798A8 → #7858E9 (розово-фиолетовый) ║ +# ║ LoChat — config/appearance.yml ║ +# ║ In-game chat look: prefixes, format, hover, emoji shortcuts, Discord UI ║ +# ║ Palette (Spacelegacy): #B798A8 (dusty rose) → #7858E9 (violet) ║ # ╚══════════════════════════════════════════════════════════════════════════════╝ # ┌─────────────────────────────────────────────────────────────────────────────┐ -# │ ПРЕФИКСЫ ЧАТА │ +# │ Chat prefixes (gradient label + separator + default message color) │ # └─────────────────────────────────────────────────────────────────────────────┘ # -# Градиент: ровно 2 цвета — от и до. Интерполяция плавная между ними. -# Вся гамма сервера: светлый #B798A8 (пыльная роза) → тёмный #7858E9 (фиолет) +# Gradient: use 2+ hex colors; letters are blended smoothly along the label. +# message-color: used when the player does NOT have lochat.chat.colors (see plugin.yml). prefixes: global: @@ -36,31 +35,28 @@ prefixes: message-color: "#E8E0F0" # ┌─────────────────────────────────────────────────────────────────────────────┐ -# │ ФОРМАТ СООБЩЕНИЙ В ЧАТЕ │ +# │ Rendered chat line layout │ # └─────────────────────────────────────────────────────────────────────────────┘ # -# Доступные плейсхолдеры: -# {emoji} - эмодзи префикса (✦ или ◆) -# {prefix} - текст префикса с градиентом (GLOBAL или LOCAL) -# {separator} - разделитель (▶) -# {player_prefix} - префикс игрока из LuckPerms/градиента -# {player} - ник игрока с hover -# {message} - текст сообщения +# Placeholders: +# {emoji} — prefix emoji (✦ / ◆) +# {prefix} — gradient prefix text (GLOBAL / LOCAL) +# {separator} — separator (e.g. ▶) +# {player_prefix} — LuckPerms / gradient prefix from LoPreff +# {player} — display name + hover (click to /msg) +# {message} — message body # -# Примеры форматов: -# По умолчанию: {emoji} {prefix} {separator} {player} : {message} -# Минимальный: {player_prefix} {player} ▶ {message} -# Кастомный: ✦ {player_prefix} {player} ▶ {message} +# Examples: +# Default: {emoji} {prefix} {separator} {player} : {message} +# Minimal: {player_prefix} {player} ▶ {message} +# Custom: ✦ {player_prefix} {player} ▶ {message} chat-format: - # Формат для глобального чата global: "{emoji} {prefix} {separator} {player} : {message}" - - # Формат для локального чата local: "{emoji} {prefix} {separator} {player} : {message}" # ┌─────────────────────────────────────────────────────────────────────────────┐ -# │ HOVER ЭФФЕКТЫ │ +# │ Hover shown when pointing at the player name in chat │ # └─────────────────────────────────────────────────────────────────────────────┘ hover: @@ -77,7 +73,7 @@ hover: - "&#B798A8✦ &fНажми чтобы написать" # ┌─────────────────────────────────────────────────────────────────────────────┐ -# │ ЭМОДЗИ ЗАМЕНЫ │ +# │ Text shortcuts → unicode (applied to outgoing chat before URLs / mentions) │ # └─────────────────────────────────────────────────────────────────────────────┘ emojis: @@ -101,7 +97,7 @@ emojis: ":shrug:": "¯\\_(ツ)_/¯" # ┌─────────────────────────────────────────────────────────────────────────────┐ -# │ НАСТРОЙКИ ЧАТА │ +# │ Radius, length limits, and misc chat UI colors (plugin internals) │ # └─────────────────────────────────────────────────────────────────────────────┘ chat: @@ -117,7 +113,7 @@ chat: info: "#A485BC" # ┌─────────────────────────────────────────────────────────────────────────────┐ -# │ DISCORD ИНТЕГРАЦИЯ │ +# │ Discord webhook embeds (see also config/discord.yml for webhooks) │ # └─────────────────────────────────────────────────────────────────────────────┘ discord: diff --git a/bin/main/config/messages.yml b/bin/main/config/messages.yml index 1b1cb03..8da71fb 100644 --- a/bin/main/config/messages.yml +++ b/bin/main/config/messages.yml @@ -116,3 +116,22 @@ general: disabled: "&#B798A8Отключено" online: "⚖C9В сети" offline: "&#B798A8Не в сети" + +# ┌─────────────────────────────────────────────────────────────────────────────┐ +# │ ВАРНЫ / БАНЫ (LoChat) │ +# └─────────────────────────────────────────────────────────────────────────────┘ + +moderation: + default_reason: "Без причины" + duration_permanent: "навсегда" + warn_received: "&#CF6679⚠ Вам выдано предупреждение! &#B798A8Причина: ẲE9{reason}" + warn_staff_confirm: "⚖C9Игроку ẲE9{player} ⚖C9выдан варн. &#B798A8Причина: ẲE9{reason}" + silent_warn_staff: "&#B798A8[⚖C9ТИХИЙ ВАРН&#B798A8] ẲE9{player}⚖C9: &#B798A8{reason}" + ban_usage: "&#B798A8Использование: ẲE9/lban <ник> [время|perm] [причина]" + ban_confirm: "⚖C9Игрок ẲE9{player} ⚖C9забанен. &#B798A8Срок: ẲE9{duration}" + unban_confirm: "⚖C9Бан снят с ẲE9{player}" + not_banned: "&#CF6679Игрок не забанен!" + already_banned: "&#CF6679Игрок уже забанен!" + warn_usage: "&#B798A8Использование: ẲE9/warn <ник> [причина]" + silent_warn_usage: "&#B798A8Использование: ẲE9/silentwarn <ник> [причина]" + ban_kick_message: "═══════ ДОСТУП ЗАПРЕЩЁН ═══════\nВы заблокированы на этом сервере.\nПричина: {reason}\nМодератор: {moderator}\nСрок: <#7858E9>{duration}\n════════════════════════" diff --git a/bin/main/messages.yml b/bin/main/messages.yml index dfb3252..452cc4d 100644 --- a/bin/main/messages.yml +++ b/bin/main/messages.yml @@ -1,20 +1,19 @@ # ╔══════════════════════════════════════════════════════════════════════════════╗ -# ║ LoChat Messages ║ -# ║ Цветовая гамма: Spacelegacy ║ -# ║ #B798A8 (роза) → #7858E9 (фиолет) ║ +# ║ LoChat — messages.yml ║ +# ║ Default palette (Spacelegacy): #B798A8 (dusty rose) → #7858E9 (violet) ║ # ╚══════════════════════════════════════════════════════════════════════════════╝ - -# Роли цветов: -# &#B798A8 — основной текст / лейблы (пыльная роза, светлый) -# ⚖C9 — акцент / разделители (средний фиолет) -# ẲE9 — важные данные / значения (насыщенный фиолет, тёмный) -# &f / #E8E0F0 — белый текст сообщений -# &#CF6679 — ошибки (приглушённый красный, вписывается в гамму) +# +# Color roles (reference when editing strings below): +# &#B798A8 — primary labels / body (dusty rose) +# ⚖C9 — accents / separators (mid violet) +# ẲE9 — emphasis / values (deep violet) +# &f / #E8E0F0 — message body white +# &#CF6679 — errors (muted red, fits the palette) prefix: "&#B798A8[⚖C9LB6BD6oẲE9CⅵDChB6BD6a⚖C9t&#B798A8]" # ┌─────────────────────────────────────────────────────────────────────────────┐ -# │ ГЛОБАЛЬНЫЙ ЧАТ │ +# │ Global chat (toggle messages, server-disabled notice) │ # └─────────────────────────────────────────────────────────────────────────────┘ global: @@ -23,7 +22,7 @@ global: disabled: "&#CF6679Глобальный чат отключён на этом сервере" # ┌─────────────────────────────────────────────────────────────────────────────┐ -# │ ЛОКАЛЬНЫЙ ЧАТ │ +# │ Local chat (no recipients nearby) │ # └─────────────────────────────────────────────────────────────────────────────┘ local: @@ -31,7 +30,7 @@ local: nobody-heard: "⚖C9Вас никто не услышал &#B798A8— рядом нет игроков" # ┌─────────────────────────────────────────────────────────────────────────────┐ -# │ ЛИЧНЫЕ СООБЩЕНИЯ │ +# │ Private messages (/msg, /reply) │ # └─────────────────────────────────────────────────────────────────────────────┘ pm: @@ -46,7 +45,7 @@ pm: self: "&#CF6679Нельзя писать самому себе" # ┌─────────────────────────────────────────────────────────────────────────────┐ -# │ ИГНОРИРОВАНИЕ │ +# │ Ignore list │ # └─────────────────────────────────────────────────────────────────────────────┘ ignore: @@ -60,14 +59,14 @@ ignore: list-footer: "&#B798A8Всего: ẲE9{count} &#B798A8игроков" # ┌─────────────────────────────────────────────────────────────────────────────┐ -# │ ОБЪЯВЛЕНИЯ │ +# │ Announcements │ # └─────────────────────────────────────────────────────────────────────────────┘ announce: reloaded: "⚖C9Конфигурация ẲE9перезагружена" # ┌─────────────────────────────────────────────────────────────────────────────┐ -# │ ШПИОН РЕЖИМ │ +# │ Chat spy (moderation) │ # └─────────────────────────────────────────────────────────────────────────────┘ spy: @@ -77,7 +76,7 @@ spy: chat-format: "&#B798A8[⚖C9SPY&#B798A8] &#B798A8{type} ẲE9{sender}&#B798A8: &f{message}" # ┌─────────────────────────────────────────────────────────────────────────────┐ -# │ ФИЛЬТРЫ │ +# │ Message filters │ # └─────────────────────────────────────────────────────────────────────────────┘ filter: @@ -85,14 +84,14 @@ filter: warning: "&#B798A8Предупреждение" # ┌─────────────────────────────────────────────────────────────────────────────┐ -# │ КУЛДАУНЫ │ +# │ Cooldowns │ # └─────────────────────────────────────────────────────────────────────────────┘ cooldown: wait: "&#B798A8Подождите ẲE9{time} &#B798A8сек" # ┌─────────────────────────────────────────────────────────────────────────────┐ -# │ АНТИ-СПАМ │ +# │ Anti-spam (caps, repeat, similar messages) │ # └─────────────────────────────────────────────────────────────────────────────┘ antispam: @@ -101,14 +100,14 @@ antispam: similar: "&#CF6679Не отправляйте одинаковые сообщения" # ┌─────────────────────────────────────────────────────────────────────────────┐ -# │ ОЧИСТКА ЧАТА │ +# │ Clear chat │ # └─────────────────────────────────────────────────────────────────────────────┘ clearchat: cleared: "⚖C9Чат очищен &#B798A8администратором ẲE9{player}" # ┌─────────────────────────────────────────────────────────────────────────────┐ -# │ ОШИБКИ │ +# │ Command / usage errors │ # └─────────────────────────────────────────────────────────────────────────────┘ errors: @@ -117,7 +116,7 @@ errors: invalid-usage: "&#B798A8Использование: ẲE9{usage}" # ┌─────────────────────────────────────────────────────────────────────────────┐ -# │ ОБЩИЕ СООБЩЕНИЯ │ +# │ General (permissions, reload, hub, etc.) │ # └─────────────────────────────────────────────────────────────────────────────┘ general: @@ -132,7 +131,7 @@ hub: connecting: "⚖C9Подключение к ẲE9{server}⚖C9..." # ┌─────────────────────────────────────────────────────────────────────────────┐ -# │ СИСТЕМА МУТОВ │ +# │ Mutes │ # └─────────────────────────────────────────────────────────────────────────────┘ mute: @@ -144,7 +143,7 @@ mute: permanent: "&#CF6679Вы замучены навсегда!" # ┌─────────────────────────────────────────────────────────────────────────────┐ -# │ ГРАДИЕНТНЫЕ НИКИ │ +# │ Gradient nick module (LoPreff) │ # └─────────────────────────────────────────────────────────────────────────────┘ gradient: @@ -191,7 +190,7 @@ gradient: info-prefix-purchased: "&#B798A8Куплен: &f{purchased}" # ┌─────────────────────────────────────────────────────────────────────────────┐ -# │ КАСТОМНЫЕ НИКИ │ +# │ Custom nicknames (/nick) │ # └─────────────────────────────────────────────────────────────────────────────┘ nick: @@ -211,7 +210,7 @@ nick: error: "&#CF6679Не удалось установить ник!" # ┌─────────────────────────────────────────────────────────────────────────────┐ -# │ ROLEPLAY КОМАНДЫ │ +# │ Roleplay commands (/me, /try, /do, /roll) │ # └─────────────────────────────────────────────────────────────────────────────┘ rp: diff --git a/bin/main/plugin.yml b/bin/main/plugin.yml index ca91aaa..b6c8071 100644 --- a/bin/main/plugin.yml +++ b/bin/main/plugin.yml @@ -36,7 +36,7 @@ commands: description: Send private message usage: /msg permission: chat.pm.use - aliases: [ tell, w, pm, m, ьып, ееддж, цж, зьж, ьж ] + aliases: [ tell, pm, m, ьып, ееддж, цж, зьж, ьж ] reply: description: Reply to last message @@ -146,6 +146,36 @@ commands: permission: lochat.muteblame aliases: [ muteblame, мутблейм, ьгеуидфьу ] + warn: + description: Warn a player (visible to them) + usage: /warn [reason] + permission: lochat.warn + aliases: [ w, варн ] + + silentwarn: + description: Silent warn (logged; staff with lochat.warn.notify see) + usage: /silentwarn [reason] + permission: lochat.warn.silent + aliases: [ siw ] + + lban: + description: Ban a player + usage: /lban [time|perm] [reason] + permission: lochat.ban + aliases: [ ban, бан ] + + lunban: + description: Unban a player + usage: /lunban + permission: lochat.unban + aliases: [ unban, разбан ] + + playerinfo: + description: Player info and moderation stats + usage: /playerinfo + permission: lochat.playerinfo + aliases: [ pi, инфо ] + # ═══════════════════════════════════════════════════════════════════════════════ # КАСТОМНЫЕ КОМАНДЫ # ═══════════════════════════════════════════════════════════════════════════════ @@ -284,6 +314,34 @@ permissions: lochat.mute.dur.perm: description: Can mute permanently default: op + + lochat.warn: + description: Issue a visible warning + default: op + lochat.warn.silent: + description: Issue a silent warning + default: op + lochat.warn.notify: + description: See silent warn notifications + default: op + lochat.ban: + description: Ban players + default: op + lochat.ban.override: + description: Ban players with lochat.bypass.ban + default: op + lochat.unban: + description: Unban players + default: op + lochat.bypass.warn: + description: Immune to /warn + default: false + lochat.bypass.ban: + description: Immune to /lban (unless staff has lochat.ban.override) + default: false + lochat.playerinfo: + description: View player moderation stats + default: op lochat.clearchat.config: description: Can configure clear chat message default: op diff --git a/src/main/java/com/loki/lochat/api/service/CooldownService.java b/src/main/java/com/loki/lochat/api/service/CooldownService.java deleted file mode 100644 index 651861d..0000000 --- a/src/main/java/com/loki/lochat/api/service/CooldownService.java +++ /dev/null @@ -1,16 +0,0 @@ -package com.loki.lochat.api.service; - -import java.util.UUID; - -/** - * Сервис для управления кулдаунами - */ -public interface CooldownService { - boolean isOnCooldown(UUID player, String type, int cooldownSeconds); - - int getRemainingCooldown(UUID player, String type, int cooldownSeconds); - - void setCooldown(UUID player, String type); - - void removeCooldown(UUID player); -} diff --git a/src/main/java/com/loki/lochat/api/service/IgnoreService.java b/src/main/java/com/loki/lochat/api/service/IgnoreService.java deleted file mode 100644 index 56a073e..0000000 --- a/src/main/java/com/loki/lochat/api/service/IgnoreService.java +++ /dev/null @@ -1,53 +0,0 @@ -package com.loki.lochat.api.service; - -import java.util.Set; -import java.util.UUID; - -/** - * Сервис для управления списком игнорируемых игроков - */ -public interface IgnoreService { - /** - * Проверяет, игнорирует ли игрок другого - */ - boolean isIgnoring(UUID player, UUID target); - - /** - * Добавляет игрока в список игнорируемых - * - * @return true если успешно добавлен - */ - boolean addIgnore(UUID player, UUID target); - - /** - * Удаляет игрока из списка игнорируемых - * - * @return true если успешно удалён - */ - boolean removeIgnore(UUID player, UUID target); - - /** - * Получает список всех игнорируемых игроков - */ - Set getIgnoredPlayers(UUID player); - - /** - * Получает количество игнорируемых игроков - */ - int getIgnoredCount(UUID player); - - /** - * Очищает список игнорируемых для игрока - */ - void clearIgnores(UUID player); - - /** - * Сохраняет данные на диск - */ - void save(); - - /** - * Загружает данные с диска - */ - void load(); -} diff --git a/src/main/java/com/loki/lochat/api/service/MessagingService.java b/src/main/java/com/loki/lochat/api/service/MessagingService.java new file mode 100644 index 0000000..2344c44 --- /dev/null +++ b/src/main/java/com/loki/lochat/api/service/MessagingService.java @@ -0,0 +1,111 @@ +package com.loki.lochat.api.service; + +import net.kyori.adventure.text.Component; +import org.bukkit.entity.Player; + +import java.util.Optional; +import java.util.Set; +import java.util.UUID; + +/** + * Объединённый сервис для управления общением между игроками + * Включает: личные сообщения (PM), шпионаж (Spy), игнорирование (Ignore) + */ +public interface MessagingService { + + // ========== PM (Private Messages) ========== + + /** + * Устанавливает последнего собеседника для игрока + */ + void setLastConversation(UUID player, UUID target); + + /** + * Получает последнего собеседника игрока + */ + Optional getLastConversation(UUID player); + + /** + * Удаляет информацию о последнем разговоре + */ + void removeConversation(UUID player); + + /** + * Проверяет, есть ли у игрока активный разговор + */ + boolean hasConversation(UUID player); + + // ========== Spy (Шпионаж) ========== + + /** + * Переключает режим шпиона для игрока + * @return true если шпион включен, false если выключен + */ + boolean toggleSpy(UUID player); + + /** + * Проверяет, находится ли игрок в режиме шпиона + */ + boolean isSpying(UUID player); + + /** + * Отправляет сообщение всем шпионам (для обычного чата) + */ + void sendToSpies(Player sender, Component message, boolean isGlobal); + + /** + * Отправляет информацию о личном сообщении шпионам + */ + void broadcastPM(Player sender, Player receiver, String message); + + /** + * Удаляет игрока из списка шпионов + */ + void removeSpy(UUID player); + + // ========== Ignore (Игнорирование) ========== + + /** + * Проверяет, игнорирует ли игрок другого + */ + boolean isIgnoring(UUID player, UUID target); + + /** + * Добавляет игрока в список игнорируемых + * @return true если успешно добавлен + */ + boolean addIgnore(UUID player, UUID target); + + /** + * Удаляет игрока из списка игнорируемых + * @return true если успешно удалён + */ + boolean removeIgnore(UUID player, UUID target); + + /** + * Получает список всех игнорируемых игроков + */ + Set getIgnoredPlayers(UUID player); + + /** + * Получает количество игнорируемых игроков + */ + int getIgnoredCount(UUID player); + + /** + * Очищает список игнорируемых для игрока + */ + void clearIgnores(UUID player); + + // ========== Persistence ========== + + /** + * Сохраняет данные на диск + */ + void save(); + + /** + * Загружает данные с диска + */ + void load(); +} diff --git a/src/main/java/com/loki/lochat/api/service/PMService.java b/src/main/java/com/loki/lochat/api/service/PMService.java deleted file mode 100644 index 2ed3421..0000000 --- a/src/main/java/com/loki/lochat/api/service/PMService.java +++ /dev/null @@ -1,29 +0,0 @@ -package com.loki.lochat.api.service; - -import java.util.Optional; -import java.util.UUID; - -/** - * Сервис для управления личными сообщениями - */ -public interface PMService { - /** - * Устанавливает последнего собеседника для игрока - */ - void setLastConversation(UUID player, UUID target); - - /** - * Получает последнего собеседника игрока - */ - Optional getLastConversation(UUID player); - - /** - * Удаляет информацию о последнем разговоре - */ - void removeConversation(UUID player); - - /** - * Проверяет, есть ли у игрока активный разговор - */ - boolean hasConversation(UUID player); -} diff --git a/src/main/java/com/loki/lochat/api/service/PlayerDataService.java b/src/main/java/com/loki/lochat/api/service/PlayerDataService.java deleted file mode 100644 index d8b0d50..0000000 --- a/src/main/java/com/loki/lochat/api/service/PlayerDataService.java +++ /dev/null @@ -1,12 +0,0 @@ -package com.loki.lochat.api.service; - -import java.util.UUID; - -/** - * Сервис для работы с данными игроков - */ -public interface PlayerDataService { - void clearPlayerData(UUID player); - - void saveAll(); -} diff --git a/src/main/java/com/loki/lochat/api/service/PlayerService.java b/src/main/java/com/loki/lochat/api/service/PlayerService.java new file mode 100644 index 0000000..71b4f48 --- /dev/null +++ b/src/main/java/com/loki/lochat/api/service/PlayerService.java @@ -0,0 +1,56 @@ +package com.loki.lochat.api.service; + +import java.util.UUID; + +/** + * Объединённый сервис для управления данными игроков + * Включает: кулдауны (Cooldown), статистику (PlayerData) + */ +public interface PlayerService { + + // ========== Cooldown ========== + + /** + * Проверяет, находится ли игрок на кулдауне + */ + boolean isOnCooldown(UUID player, String type, int cooldownSeconds); + + /** + * Получает оставшееся время кулдауна в секундах + */ + int getRemainingCooldown(UUID player, String type, int cooldownSeconds); + + /** + * Устанавливает кулдаун для игрока + */ + void setCooldown(UUID player, String type); + + /** + * Удаляет все кулдауны игрока + */ + void removeCooldown(UUID player); + + // ========== Player Statistics ========== + + /** + * Записывает отправленное сообщение в статистику + * @param player UUID игрока + * @param chatType тип чата: "global", "local", "pm" + */ + void recordMessage(UUID player, String chatType); + + /** + * Получает количество сообщений игрока за текущую сессию + */ + long getPlayerMessages(UUID player); + + /** + * Очищает данные игрока (кулдауны + сохраняет статистику) + */ + void clearPlayerData(UUID player); + + /** + * Сохраняет всю статистику на диск + */ + void saveAll(); +} diff --git a/src/main/java/com/loki/lochat/api/service/SpyService.java b/src/main/java/com/loki/lochat/api/service/SpyService.java deleted file mode 100644 index 78d0d8a..0000000 --- a/src/main/java/com/loki/lochat/api/service/SpyService.java +++ /dev/null @@ -1,36 +0,0 @@ -package com.loki.lochat.api.service; - -import net.kyori.adventure.text.Component; -import org.bukkit.entity.Player; - -/** - * Сервис для шпионажа за чатом - */ -public interface SpyService { - /** - * Переключает режим шпиона для игрока - * - * @return true если шпион включен, false если выключен - */ - boolean toggleSpy(java.util.UUID player); - - /** - * Проверяет, находится ли игрок в режиме шпиона - */ - boolean isSpying(java.util.UUID player); - - /** - * Отправляет сообщение всем шпионам (для обычного чата) - */ - void sendToSpies(Player sender, Component message, boolean isGlobal); - - /** - * Отправляет информацию о личном сообщении шпионам - */ - void broadcastPM(Player sender, Player receiver, String message); - - /** - * Удаляет игрока из списка шпионов - */ - void removeSpy(java.util.UUID player); -} diff --git a/src/main/java/com/loki/lochat/commands/admin/ChatSpyCommand.java b/src/main/java/com/loki/lochat/commands/admin/ChatSpyCommand.java index 47a8436..af48020 100644 --- a/src/main/java/com/loki/lochat/commands/admin/ChatSpyCommand.java +++ b/src/main/java/com/loki/lochat/commands/admin/ChatSpyCommand.java @@ -1,7 +1,7 @@ package com.loki.lochat.commands.admin; import com.loki.lochat.LoChat; -import com.loki.lochat.api.service.SpyService; +import com.loki.lochat.api.service.MessagingService; import com.loki.lochat.utils.ChatFormatter; import org.bukkit.command.Command; import org.bukkit.command.CommandExecutor; @@ -12,11 +12,11 @@ import org.jetbrains.annotations.NotNull; public class ChatSpyCommand implements CommandExecutor { private final LoChat plugin; - private final SpyService spyService; + private final MessagingService messagingService; public ChatSpyCommand(LoChat plugin) { this.plugin = plugin; - this.spyService = plugin.getServiceRegistry().get(SpyService.class); + this.messagingService = plugin.getServiceRegistry().get(MessagingService.class); } @Override @@ -25,7 +25,7 @@ public class ChatSpyCommand implements CommandExecutor { sender.sendMessage(plugin.getConfigManager().getMessagesConfig().getPlayerOnly()); return true; } - boolean enabled = spyService.toggleSpy(player.getUniqueId()); + boolean enabled = messagingService.toggleSpy(player.getUniqueId()); player.sendMessage(ChatFormatter.parse(plugin.getMessageConfig().get(enabled ? "spy.enabled" : "spy.disabled"))); return true; } diff --git a/src/main/java/com/loki/lochat/commands/messaging/IgnoreCommand.java b/src/main/java/com/loki/lochat/commands/messaging/IgnoreCommand.java index 4bcdd87..78612c6 100644 --- a/src/main/java/com/loki/lochat/commands/messaging/IgnoreCommand.java +++ b/src/main/java/com/loki/lochat/commands/messaging/IgnoreCommand.java @@ -1,7 +1,7 @@ package com.loki.lochat.commands.messaging; import com.loki.lochat.LoChat; -import com.loki.lochat.api.service.IgnoreService; +import com.loki.lochat.api.service.MessagingService; import com.loki.lochat.utils.ChatFormatter; import org.bukkit.Bukkit; import org.bukkit.command.Command; @@ -13,11 +13,11 @@ import org.jetbrains.annotations.NotNull; public class IgnoreCommand implements CommandExecutor { private final LoChat plugin; - private final IgnoreService ignoreService; + private final MessagingService messagingService; public IgnoreCommand(LoChat plugin) { this.plugin = plugin; - this.ignoreService = plugin.getServiceRegistry().get(IgnoreService.class); + this.messagingService = plugin.getServiceRegistry().get(MessagingService.class); } @Override @@ -28,9 +28,9 @@ public class IgnoreCommand implements CommandExecutor { Player target = Bukkit.getPlayer(args[0]); if (target == null) { player.sendMessage(ChatFormatter.parse(plugin.getMessageConfig().getPlayerNotFound())); return true; } if (target.equals(player)) { player.sendMessage(ChatFormatter.parse(plugin.getMessageConfig().get("ignore.self"))); return true; } - if (ignoreService.isIgnoring(player.getUniqueId(), target.getUniqueId())) { player.sendMessage(ChatFormatter.parse(plugin.getMessageConfig().get("ignore.already-ignored"))); return true; } + if (messagingService.isIgnoring(player.getUniqueId(), target.getUniqueId())) { player.sendMessage(ChatFormatter.parse(plugin.getMessageConfig().get("ignore.already-ignored"))); return true; } - ignoreService.addIgnore(player.getUniqueId(), target.getUniqueId()); + messagingService.addIgnore(player.getUniqueId(), target.getUniqueId()); player.sendMessage(ChatFormatter.parse(plugin.getMessageConfig().get("ignore.added", "{player}", target.getName()))); return true; } diff --git a/src/main/java/com/loki/lochat/commands/messaging/IgnoreListCommand.java b/src/main/java/com/loki/lochat/commands/messaging/IgnoreListCommand.java index 409dddd..e674248 100644 --- a/src/main/java/com/loki/lochat/commands/messaging/IgnoreListCommand.java +++ b/src/main/java/com/loki/lochat/commands/messaging/IgnoreListCommand.java @@ -1,7 +1,7 @@ package com.loki.lochat.commands.messaging; import com.loki.lochat.LoChat; -import com.loki.lochat.api.service.IgnoreService; +import com.loki.lochat.api.service.MessagingService; import com.loki.lochat.utils.ChatFormatter; import com.loki.lochat.utils.PlayerUtil; import org.bukkit.command.Command; @@ -16,18 +16,18 @@ import java.util.UUID; public class IgnoreListCommand implements CommandExecutor { private final LoChat plugin; - private final IgnoreService ignoreService; + private final MessagingService messagingService; public IgnoreListCommand(LoChat plugin) { this.plugin = plugin; - this.ignoreService = plugin.getServiceRegistry().get(IgnoreService.class); + this.messagingService = plugin.getServiceRegistry().get(MessagingService.class); } @Override public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) { if (!(sender instanceof Player player)) { sender.sendMessage(plugin.getConfigManager().getMessagesConfig().getPlayerOnly()); return true; } - Set ignored = ignoreService.getIgnoredPlayers(player.getUniqueId()); + Set ignored = messagingService.getIgnoredPlayers(player.getUniqueId()); if (ignored.isEmpty()) { player.sendMessage(ChatFormatter.parse(plugin.getMessageConfig().get("ignore.list-empty"))); return true; } player.sendMessage(ChatFormatter.parse(plugin.getMessageConfig().get("ignore.list-header"))); diff --git a/src/main/java/com/loki/lochat/commands/messaging/MsgCommand.java b/src/main/java/com/loki/lochat/commands/messaging/MsgCommand.java index 6bbb0d8..dca89af 100644 --- a/src/main/java/com/loki/lochat/commands/messaging/MsgCommand.java +++ b/src/main/java/com/loki/lochat/commands/messaging/MsgCommand.java @@ -1,9 +1,7 @@ package com.loki.lochat.commands.messaging; import com.loki.lochat.LoChat; -import com.loki.lochat.api.service.IgnoreService; -import com.loki.lochat.api.service.PMService; -import com.loki.lochat.api.service.SpyService; +import com.loki.lochat.api.service.MessagingService; import com.loki.lochat.utils.ChatFormatter; import com.loki.lochat.utils.PlayerUtil; import org.bukkit.Bukkit; @@ -17,15 +15,11 @@ import org.jetbrains.annotations.NotNull; public class MsgCommand implements CommandExecutor { private final LoChat plugin; - private final PMService pmService; - private final IgnoreService ignoreService; - private final SpyService spyService; + private final MessagingService messagingService; public MsgCommand(LoChat plugin) { this.plugin = plugin; - this.pmService = plugin.getServiceRegistry().get(PMService.class); - this.ignoreService = plugin.getServiceRegistry().get(IgnoreService.class); - this.spyService = plugin.getServiceRegistry().get(SpyService.class); + this.messagingService = plugin.getServiceRegistry().get(MessagingService.class); } @Override @@ -43,8 +37,8 @@ public class MsgCommand implements CommandExecutor { Player target = Bukkit.getPlayer(args[0]); if (target == null) { player.sendMessage(ChatFormatter.parse(plugin.getMessageConfig().get("pm.player-offline"))); return true; } if (target.equals(player)) { player.sendMessage(ChatFormatter.parse(plugin.getMessageConfig().get("pm.self"))); return true; } - if (ignoreService.isIgnoring(target.getUniqueId(), player.getUniqueId())) { player.sendMessage(ChatFormatter.parse(plugin.getMessageConfig().get("pm.ignored"))); return true; } - if (ignoreService.isIgnoring(player.getUniqueId(), target.getUniqueId())) { player.sendMessage(ChatFormatter.parse(plugin.getMessageConfig().get("pm.ignored"))); return true; } + if (messagingService.isIgnoring(target.getUniqueId(), player.getUniqueId())) { player.sendMessage(ChatFormatter.parse(plugin.getMessageConfig().get("pm.ignored"))); return true; } + if (messagingService.isIgnoring(player.getUniqueId(), target.getUniqueId())) { player.sendMessage(ChatFormatter.parse(plugin.getMessageConfig().get("pm.ignored"))); return true; } String message = String.join(" ", java.util.Arrays.copyOfRange(args, 1, args.length)); sendPm(player, target, message); @@ -58,9 +52,9 @@ public class MsgCommand implements CommandExecutor { Sound sound = PlayerUtil.parseSound(plugin.getConfigManager().getPmSoundType(), null); if (sound != null) target.playSound(target.getLocation(), sound, 1.0f, 1.0f); } - spyService.broadcastPM(sender, target, message); - pmService.setLastConversation(sender.getUniqueId(), target.getUniqueId()); - pmService.setLastConversation(target.getUniqueId(), sender.getUniqueId()); + messagingService.broadcastPM(sender, target, message); + messagingService.setLastConversation(sender.getUniqueId(), target.getUniqueId()); + messagingService.setLastConversation(target.getUniqueId(), sender.getUniqueId()); if (plugin.getConfigManager().isPmLogEnabled()) plugin.getLogger().info("[PM] " + sender.getName() + " -> " + target.getName() + ": " + message); } diff --git a/src/main/java/com/loki/lochat/commands/messaging/ReplyCommand.java b/src/main/java/com/loki/lochat/commands/messaging/ReplyCommand.java index fb1e154..70bc232 100644 --- a/src/main/java/com/loki/lochat/commands/messaging/ReplyCommand.java +++ b/src/main/java/com/loki/lochat/commands/messaging/ReplyCommand.java @@ -1,9 +1,7 @@ package com.loki.lochat.commands.messaging; import com.loki.lochat.LoChat; -import com.loki.lochat.api.service.IgnoreService; -import com.loki.lochat.api.service.PMService; -import com.loki.lochat.api.service.SpyService; +import com.loki.lochat.api.service.MessagingService; import com.loki.lochat.utils.ChatFormatter; import com.loki.lochat.utils.PlayerUtil; import org.bukkit.Bukkit; @@ -20,15 +18,11 @@ import java.util.UUID; public class ReplyCommand implements CommandExecutor { private final LoChat plugin; - private final PMService pmService; - private final IgnoreService ignoreService; - private final SpyService spyService; + private final MessagingService messagingService; public ReplyCommand(LoChat plugin) { this.plugin = plugin; - this.pmService = plugin.getServiceRegistry().get(PMService.class); - this.ignoreService = plugin.getServiceRegistry().get(IgnoreService.class); - this.spyService = plugin.getServiceRegistry().get(SpyService.class); + this.messagingService = plugin.getServiceRegistry().get(MessagingService.class); } @Override @@ -43,12 +37,12 @@ public class ReplyCommand implements CommandExecutor { return true; } - Optional lastUuid = pmService.getLastConversation(player.getUniqueId()); + Optional lastUuid = messagingService.getLastConversation(player.getUniqueId()); if (lastUuid.isEmpty()) { player.sendMessage(ChatFormatter.parse(plugin.getMessageConfig().get("pm.no-reply"))); return true; } Player target = Bukkit.getPlayer(lastUuid.get()); if (target == null) { player.sendMessage(ChatFormatter.parse(plugin.getMessageConfig().get("pm.player-offline"))); return true; } - if (ignoreService.isIgnoring(target.getUniqueId(), player.getUniqueId())) { player.sendMessage(ChatFormatter.parse(plugin.getMessageConfig().get("pm.ignored"))); return true; } + if (messagingService.isIgnoring(target.getUniqueId(), player.getUniqueId())) { player.sendMessage(ChatFormatter.parse(plugin.getMessageConfig().get("pm.ignored"))); return true; } String message = String.join(" ", args); player.sendMessage(ChatFormatter.formatPmSentNew(plugin.getMessageConfig().getPmFormatSent(), player, target, message)); @@ -57,8 +51,8 @@ public class ReplyCommand implements CommandExecutor { Sound sound = PlayerUtil.parseSound(plugin.getConfigManager().getPmSoundType(), null); if (sound != null) target.playSound(target.getLocation(), sound, 1.0f, 1.0f); } - spyService.broadcastPM(player, target, message); - pmService.setLastConversation(target.getUniqueId(), player.getUniqueId()); + messagingService.broadcastPM(player, target, message); + messagingService.setLastConversation(target.getUniqueId(), player.getUniqueId()); if (plugin.getConfigManager().isPmLogEnabled()) plugin.getLogger().info("[PM] " + player.getName() + " -> " + target.getName() + ": " + message); return true; diff --git a/src/main/java/com/loki/lochat/commands/messaging/UnignoreCommand.java b/src/main/java/com/loki/lochat/commands/messaging/UnignoreCommand.java index 00ee1dc..23d210d 100644 --- a/src/main/java/com/loki/lochat/commands/messaging/UnignoreCommand.java +++ b/src/main/java/com/loki/lochat/commands/messaging/UnignoreCommand.java @@ -1,7 +1,7 @@ package com.loki.lochat.commands.messaging; import com.loki.lochat.LoChat; -import com.loki.lochat.api.service.IgnoreService; +import com.loki.lochat.api.service.MessagingService; import com.loki.lochat.utils.ChatFormatter; import org.bukkit.Bukkit; import org.bukkit.command.Command; @@ -13,11 +13,11 @@ import org.jetbrains.annotations.NotNull; public class UnignoreCommand implements CommandExecutor { private final LoChat plugin; - private final IgnoreService ignoreService; + private final MessagingService messagingService; public UnignoreCommand(LoChat plugin) { this.plugin = plugin; - this.ignoreService = plugin.getServiceRegistry().get(IgnoreService.class); + this.messagingService = plugin.getServiceRegistry().get(MessagingService.class); } @Override @@ -27,9 +27,9 @@ public class UnignoreCommand implements CommandExecutor { Player target = Bukkit.getPlayer(args[0]); if (target == null) { player.sendMessage(ChatFormatter.parse(plugin.getMessageConfig().getPlayerNotFound())); return true; } - if (!ignoreService.isIgnoring(player.getUniqueId(), target.getUniqueId())) { player.sendMessage(ChatFormatter.parse(plugin.getMessageConfig().get("ignore.not-ignored"))); return true; } + if (!messagingService.isIgnoring(player.getUniqueId(), target.getUniqueId())) { player.sendMessage(ChatFormatter.parse(plugin.getMessageConfig().get("ignore.not-ignored"))); return true; } - ignoreService.removeIgnore(player.getUniqueId(), target.getUniqueId()); + messagingService.removeIgnore(player.getUniqueId(), target.getUniqueId()); player.sendMessage(ChatFormatter.parse(plugin.getMessageConfig().get("ignore.removed", "{player}", target.getName()))); return true; } diff --git a/src/main/java/com/loki/lochat/commands/rp/DoCommand.java b/src/main/java/com/loki/lochat/commands/rp/DoCommand.java index 6cd2a7e..83bd81c 100644 --- a/src/main/java/com/loki/lochat/commands/rp/DoCommand.java +++ b/src/main/java/com/loki/lochat/commands/rp/DoCommand.java @@ -1,7 +1,7 @@ package com.loki.lochat.commands.rp; import com.loki.lochat.LoChat; -import com.loki.lochat.api.service.CooldownService; +import com.loki.lochat.api.service.PlayerService; import com.loki.lochat.commands.base.PlayerCommand; import com.loki.lochat.utils.ChatFormatter; import net.kyori.adventure.text.Component; @@ -16,11 +16,11 @@ import org.jetbrains.annotations.NotNull; */ public class DoCommand extends PlayerCommand { - private final CooldownService cooldownService; + private final PlayerService playerService; public DoCommand(LoChat plugin) { super(plugin); - this.cooldownService = plugin.getServiceRegistry().get(CooldownService.class); + this.playerService = plugin.getServiceRegistry().get(PlayerService.class); } @Override @@ -29,8 +29,8 @@ public class DoCommand extends PlayerCommand { if (!requirePermission(player, "lochat.rp.do")) return true; int cooldown = plugin.getConfigManager().getInt("rp.cooldowns.do", 0); - if (cooldown > 0 && cooldownService.isOnCooldown(player.getUniqueId(), "rp_do", cooldown)) { - int remaining = cooldownService.getRemainingCooldown(player.getUniqueId(), "rp_do", cooldown); + if (cooldown > 0 && playerService.isOnCooldown(player.getUniqueId(), "rp_do", cooldown)) { + int remaining = playerService.getRemainingCooldown(player.getUniqueId(), "rp_do", cooldown); String msg = plugin.getConfigManager().getMessagesConfig().getCooldownMessage().replace("{remaining}", String.valueOf(remaining)); player.sendMessage(ChatFormatter.parse(msg)); return true; @@ -54,7 +54,7 @@ public class DoCommand extends PlayerCommand { int radius = plugin.getConfigManager().getInt("rp.radius", 100); RpUtil.sendToRadius(player, message, radius); - if (cooldown > 0) cooldownService.setCooldown(player.getUniqueId(), "rp_do"); + if (cooldown > 0) playerService.setCooldown(player.getUniqueId(), "rp_do"); plugin.getLogger().info("[DO] " + player.getName() + ": " + description); return true; } diff --git a/src/main/java/com/loki/lochat/commands/rp/MeCommand.java b/src/main/java/com/loki/lochat/commands/rp/MeCommand.java index 80253cf..c4a1bd8 100644 --- a/src/main/java/com/loki/lochat/commands/rp/MeCommand.java +++ b/src/main/java/com/loki/lochat/commands/rp/MeCommand.java @@ -1,7 +1,7 @@ package com.loki.lochat.commands.rp; import com.loki.lochat.LoChat; -import com.loki.lochat.api.service.CooldownService; +import com.loki.lochat.api.service.PlayerService; import com.loki.lochat.commands.base.PlayerCommand; import com.loki.lochat.utils.ChatFormatter; import net.kyori.adventure.text.Component; @@ -16,11 +16,11 @@ import org.jetbrains.annotations.NotNull; */ public class MeCommand extends PlayerCommand { - private final CooldownService cooldownService; + private final PlayerService playerService; public MeCommand(LoChat plugin) { super(plugin); - this.cooldownService = plugin.getServiceRegistry().get(CooldownService.class); + this.playerService = plugin.getServiceRegistry().get(PlayerService.class); } @Override @@ -29,8 +29,8 @@ public class MeCommand extends PlayerCommand { if (!requirePermission(player, "lochat.rp.me")) return true; int cooldown = plugin.getConfigManager().getInt("rp.cooldowns.me", 0); - if (cooldown > 0 && cooldownService.isOnCooldown(player.getUniqueId(), "rp_me", cooldown)) { - int remaining = cooldownService.getRemainingCooldown(player.getUniqueId(), "rp_me", cooldown); + if (cooldown > 0 && playerService.isOnCooldown(player.getUniqueId(), "rp_me", cooldown)) { + int remaining = playerService.getRemainingCooldown(player.getUniqueId(), "rp_me", cooldown); String msg = plugin.getConfigManager().getMessagesConfig().getCooldownMessage().replace("{remaining}", String.valueOf(remaining)); player.sendMessage(ChatFormatter.parse(msg)); return true; @@ -54,7 +54,7 @@ public class MeCommand extends PlayerCommand { int radius = plugin.getConfigManager().getInt("rp.radius", 100); RpUtil.sendToRadius(player, message, radius); - if (cooldown > 0) cooldownService.setCooldown(player.getUniqueId(), "rp_me"); + if (cooldown > 0) playerService.setCooldown(player.getUniqueId(), "rp_me"); plugin.getLogger().info("[ME] " + player.getName() + ": " + action); return true; } diff --git a/src/main/java/com/loki/lochat/commands/rp/RpUtil.java b/src/main/java/com/loki/lochat/commands/rp/RpUtil.java index d675aad..dd17b4a 100644 --- a/src/main/java/com/loki/lochat/commands/rp/RpUtil.java +++ b/src/main/java/com/loki/lochat/commands/rp/RpUtil.java @@ -1,7 +1,7 @@ package com.loki.lochat.commands.rp; import com.loki.lochat.LoChat; -import com.loki.lochat.util.DistanceUtil; +import com.loki.lochat.utils.DistanceUtil; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer; import org.bukkit.Bukkit; diff --git a/src/main/java/com/loki/lochat/commands/rp/TryCommand.java b/src/main/java/com/loki/lochat/commands/rp/TryCommand.java index 1cd997c..81fef88 100644 --- a/src/main/java/com/loki/lochat/commands/rp/TryCommand.java +++ b/src/main/java/com/loki/lochat/commands/rp/TryCommand.java @@ -1,7 +1,7 @@ package com.loki.lochat.commands.rp; import com.loki.lochat.LoChat; -import com.loki.lochat.api.service.CooldownService; +import com.loki.lochat.api.service.PlayerService; import com.loki.lochat.commands.base.PlayerCommand; import com.loki.lochat.utils.ChatFormatter; import net.kyori.adventure.text.Component; @@ -19,11 +19,11 @@ import java.util.Random; public class TryCommand extends PlayerCommand { private static final Random RANDOM = new Random(); - private final CooldownService cooldownService; + private final PlayerService playerService; public TryCommand(LoChat plugin) { super(plugin); - this.cooldownService = plugin.getServiceRegistry().get(CooldownService.class); + this.playerService = plugin.getServiceRegistry().get(PlayerService.class); } @Override @@ -32,8 +32,8 @@ public class TryCommand extends PlayerCommand { if (!requirePermission(player, "lochat.rp.try")) return true; int cooldown = plugin.getConfigManager().getInt("rp.cooldowns.try", 0); - if (cooldown > 0 && cooldownService.isOnCooldown(player.getUniqueId(), "rp_try", cooldown)) { - int remaining = cooldownService.getRemainingCooldown(player.getUniqueId(), "rp_try", cooldown); + if (cooldown > 0 && playerService.isOnCooldown(player.getUniqueId(), "rp_try", cooldown)) { + int remaining = playerService.getRemainingCooldown(player.getUniqueId(), "rp_try", cooldown); String msg = plugin.getConfigManager().getMessagesConfig().getCooldownMessage().replace("{remaining}", String.valueOf(remaining)); player.sendMessage(ChatFormatter.parse(msg)); return true; @@ -64,7 +64,7 @@ public class TryCommand extends PlayerCommand { int radius = plugin.getConfigManager().getInt("rp.radius", 100); RpUtil.sendToRadius(player, message, radius); - if (cooldown > 0) cooldownService.setCooldown(player.getUniqueId(), "rp_try"); + if (cooldown > 0) playerService.setCooldown(player.getUniqueId(), "rp_try"); plugin.getLogger().info("[TRY] " + player.getName() + ": " + action + " -> " + (success ? "SUCCESS" : "FAIL")); return true; } diff --git a/src/main/java/com/loki/lochat/config/AppearanceConfig.java b/src/main/java/com/loki/lochat/config/AppearanceConfig.java index d9eae7d..02dc56c 100644 --- a/src/main/java/com/loki/lochat/config/AppearanceConfig.java +++ b/src/main/java/com/loki/lochat/config/AppearanceConfig.java @@ -44,237 +44,66 @@ public class AppearanceConfig extends BaseConfig { // ========== ПРЕФИКСЫ ========== - /** - * Получить эмодзи для глобального чата - */ - public String getGlobalEmoji() { - return config.getString("prefixes.global.emoji", "🌍"); + private String getPrefixValue(String chatType, String key, String defaultValue) { + return config.getString("prefixes." + chatType + "." + key, defaultValue); } - /** - * Получить текст префикса глобального чата - */ - public String getGlobalText() { - return config.getString("prefixes.global.text", "GLOBAL"); - } + public String getGlobalEmoji() { return getPrefixValue("global", "emoji", "🌍"); } + public String getGlobalText() { return getPrefixValue("global", "text", "GLOBAL"); } + public List getGlobalColors() { return config.getStringList("prefixes.global.colors"); } + public String getGlobalSeparatorText() { return getPrefixValue("global", "separator.text", " ▶ "); } + public String getGlobalSeparatorColor() { return getPrefixValue("global", "separator.color", "#FFD700"); } + public String getGlobalMessageColor() { return getPrefixValue("global", "message-color", "#FFFFFF"); } - /** - * Получить цвета градиента для глобального чата - */ - public List getGlobalColors() { - return config.getStringList("prefixes.global.colors"); - } - - /** - * Получить разделитель для глобального чата - */ - public String getGlobalSeparatorText() { - return config.getString("prefixes.global.separator.text", " ▶ "); - } - - /** - * Получить цвет разделителя для глобального чата - */ - public String getGlobalSeparatorColor() { - return config.getString("prefixes.global.separator.color", "#FFD700"); - } - - /** - * Получить эмодзи для локального чата - */ - public String getLocalEmoji() { - return config.getString("prefixes.local.emoji", ""); - } - - /** - * Получить текст префикса локального чата - */ - public String getLocalText() { - return config.getString("prefixes.local.text", "LOCAL"); - } - - /** - * Получить цвета градиента для локального чата - */ - public List getLocalColors() { - return config.getStringList("prefixes.local.colors"); - } - - /** - * Получить разделитель для локального чата - */ - public String getLocalSeparatorText() { - return config.getString("prefixes.local.separator.text", " ▶ "); - } - - /** - * Получить цвет разделителя для локального чата - */ - public String getLocalSeparatorColor() { - return config.getString("prefixes.local.separator.color", "#FFFFFF"); - } - - /** - * Получить цвет текста сообщений в глобальном чате (дефолт) - */ - public String getGlobalMessageColor() { - return config.getString("prefixes.global.message-color", "#FFFFFF"); - } - - /** - * Получить цвет текста сообщений в локальном чате (дефолт) - */ - public String getLocalMessageColor() { - return config.getString("prefixes.local.message-color", "#FFFFFF"); - } + public String getLocalEmoji() { return getPrefixValue("local", "emoji", ""); } + public String getLocalText() { return getPrefixValue("local", "text", "LOCAL"); } + public List getLocalColors() { return config.getStringList("prefixes.local.colors"); } + public String getLocalSeparatorText() { return getPrefixValue("local", "separator.text", " ▶ "); } + public String getLocalSeparatorColor() { return getPrefixValue("local", "separator.color", "#FFFFFF"); } + public String getLocalMessageColor() { return getPrefixValue("local", "message-color", "#FFFFFF"); } // ========== ФОРМАТ СООБЩЕНИЙ ========== - /** - * Получить формат сообщения для глобального чата - */ public String getGlobalChatFormat() { return config.getString("chat-format.global", "{emoji} {prefix} {separator} {player} : {message}"); } - /** - * Получить формат сообщения для локального чата - */ public String getLocalChatFormat() { return config.getString("chat-format.local", "{emoji} {prefix} {separator} {player} : {message}"); } // ========== HOVER ЭФФЕКТЫ ========== - /** - * Включены ли hover эффекты - */ - public boolean isHoverEnabled() { - return config.getBoolean("hover.enabled", true); - } - - /** - * Получить формат hover подсказки - */ - public List getHoverFormat() { - return config.getStringList("hover.format"); - } + public boolean isHoverEnabled() { return config.getBoolean("hover.enabled", true); } + public List getHoverFormat() { return config.getStringList("hover.format"); } // ========== ЭМОДЗИ ========== - /** - * Получить все эмодзи замены - */ - public Map getEmojis() { - return new HashMap<>(emojiCache); - } - - /** - * Получить замену для эмодзи - */ - public String getEmoji(String key) { - return emojiCache.get(key); - } + public Map getEmojis() { return new HashMap<>(emojiCache); } + public String getEmoji(String key) { return emojiCache.get(key); } // ========== НАСТРОЙКИ ЧАТА ========== - /** - * Получить радиус локального чата - */ - public int getLocalRadius() { - return config.getInt("chat.local_radius", 100); + public int getLocalRadius() { return config.getInt("chat.local_radius", 100); } + public int getClearLines() { return config.getInt("chat.clear_lines", 100); } + public int getMinMessageLength() { return config.getInt("chat.min_message_length", 1); } + public int getMaxMessageLength() { return config.getInt("chat.max_message_length", 2000); } + + private String getChatColor(String type, String defaultColor) { + return config.getString("chat.colors." + type, defaultColor); } - /** - * Получить количество строк для очистки чата - */ - public int getClearLines() { - return config.getInt("chat.clear_lines", 100); - } - - /** - * Получить минимальную длину сообщения - */ - public int getMinMessageLength() { - return config.getInt("chat.min_message_length", 1); - } - - /** - * Получить максимальную длину сообщения - */ - public int getMaxMessageLength() { - return config.getInt("chat.max_message_length", 2000); - } - - /** - * Получить цвет "никто не услышал" - */ - public String getNobodyHeardColor() { - return config.getString("chat.colors.nobody_heard", "#FFA726"); - } - - /** - * Получить цвет ошибки - */ - public String getErrorColor() { - return config.getString("chat.colors.error", "#FF6B6B"); - } - - /** - * Получить цвет успеха - */ - public String getSuccessColor() { - return config.getString("chat.colors.success", "#6BCB77"); - } - - /** - * Получить цвет предупреждения - */ - public String getWarningColor() { - return config.getString("chat.colors.warning", "#FFA726"); - } - - /** - * Получить цвет информации - */ - public String getInfoColor() { - return config.getString("chat.colors.info", "#87CEEB"); - } + public String getNobodyHeardColor() { return getChatColor("nobody_heard", "#FFA726"); } + public String getErrorColor() { return getChatColor("error", "#FF6B6B"); } + public String getSuccessColor() { return getChatColor("success", "#6BCB77"); } + public String getWarningColor() { return getChatColor("warning", "#FFA726"); } + public String getInfoColor() { return getChatColor("info", "#87CEEB"); } // ========== DISCORD ========== - /** - * Получить эмодзи для Discord событий - */ - public String getDiscordEmoji(String event) { - return config.getString("discord.emojis." + event, ""); - } - - /** - * Получить URL аватара по умолчанию - */ - public String getDefaultAvatarUrl() { - return config.getString("discord.avatar_urls.default", "https://mc-heads.net/avatar/minecraft/64"); - } - - /** - * Получить URL аватара игрока - */ - public String getPlayerAvatarUrl() { - return config.getString("discord.avatar_urls.player", "https://mc-heads.net/avatar/{player}/64"); - } - - /** - * Получить цвет embed по умолчанию - */ - public String getDefaultEmbedColor() { - return config.getString("discord.default_embed_color", "5865F2"); - } - - /** - * Получить заголовок события Discord - */ - public String getDiscordEventTitle(String event) { - return config.getString("discord.event_titles." + event, ""); - } + public String getDiscordEmoji(String event) { return config.getString("discord.emojis." + event, ""); } + public String getDiscordEventTitle(String event) { return config.getString("discord.event_titles." + event, ""); } + public String getDefaultAvatarUrl() { return config.getString("discord.avatar_urls.default", "https://mc-heads.net/avatar/minecraft/64"); } + public String getPlayerAvatarUrl() { return config.getString("discord.avatar_urls.player", "https://mc-heads.net/avatar/{player}/64"); } + public String getDefaultEmbedColor() { return config.getString("discord.default_embed_color", "5865F2"); } } diff --git a/src/main/java/com/loki/lochat/config/ConfigManager.java b/src/main/java/com/loki/lochat/config/ConfigManager.java index 49a0787..775ad98 100644 --- a/src/main/java/com/loki/lochat/config/ConfigManager.java +++ b/src/main/java/com/loki/lochat/config/ConfigManager.java @@ -6,12 +6,12 @@ import org.bukkit.configuration.file.FileConfiguration; import java.util.List; +/** + * Менеджер конфигураций - упрощенная версия + */ public class ConfigManager { private final LoChat plugin; - - // ✅ THREAD-SAFETY: volatile для безопасного reload из разных потоков - // volatile гарантирует что все потоки видят актуальную версию конфига после reload private volatile FileConfiguration config; private volatile AppearanceConfig appearanceConfig; private volatile MessagesConfig messagesConfig; @@ -24,55 +24,42 @@ public class ConfigManager { plugin.saveDefaultConfig(); this.config = plugin.getConfig(); - // Инициализируем новые конфигурации + // Инициализируем конфигурации this.appearanceConfig = new AppearanceConfig(plugin); this.messagesConfig = new MessagesConfig(plugin); this.muteConfig = new MuteConfig(plugin); this.filtersConfig = new FiltersConfig(plugin); this.soundsConfig = new SoundsConfig(plugin); - // Инициализируем их после создания this.appearanceConfig.init(); - // Проверяем версию конфига и обновляем при необходимости updateConfig(); } - /** - * Получить конфигурацию внешнего вида - */ + // ========== Геттеры конфигураций ========== + public AppearanceConfig getAppearanceConfig() { return appearanceConfig; } - /** - * Получить конфигурацию системных сообщений - */ public MessagesConfig getMessagesConfig() { return messagesConfig; } - /** - * Получить конфигурацию мутов - */ public MuteConfig getMuteConfig() { return muteConfig; } - /** - * Получить конфигурацию фильтров - */ public FiltersConfig getFiltersConfig() { return filtersConfig; } - /** - * Получить конфигурацию звуков - */ public SoundsConfig getSoundsConfig() { return soundsConfig; } + // ========== Обновление и перезагрузка ========== + private void updateConfig() { int currentVersion = config.getInt("config-version", 1); int requiredVersion = 5; @@ -87,57 +74,37 @@ public class ConfigManager { } private void addMissingConfigOptions() { - if (!config.contains("announcements")) { - config.set("announcements.show-title", true); - config.set("announcements.title-header", "&#FFD700ОБЪЯВЛЕНИЕ"); - config.set("announcements.show-actionbar", true); - config.set("announcements.title-duration", 3); - } + setIfMissing("announcements.show-title", true); + setIfMissing("announcements.title-header", "&#FFD700ОБЪЯВЛЕНИЕ"); + setIfMissing("announcements.show-actionbar", true); + setIfMissing("announcements.title-duration", 3); + setIfMissing("chat.pm.enabled", true); + setIfMissing("mentions.enabled", true); + setIfMissing("mentions.sound", true); + setIfMissing("mentions.sound-type", "BLOCK_NOTE_BLOCK_PLING"); + setIfMissing("mentions.highlight", "&#FFFF00@{player}"); + setIfMissing("mentions.self-highlight", "&#FFD700{player}"); + setIfMissing("automessages.enabled", true); + setIfMissing("automessages.interval", 300); + setIfMissing("automessages.random", false); + setIfMissing("clearchat.message.enabled", true); + setIfMissing("clearchat.message.text", "CADFЧат очищен администратором {player}"); + setIfMissing("lopreff.enabled", true); + setIfMissing("lopreff.use-gradient-name", true); + setIfMissing("lopreff.use-custom-prefix", true); + setIfMissing("moderation.voice-mute-console-command", ""); + } - if (!config.contains("chat.pm.enabled")) { - config.set("chat.pm.enabled", true); - } - - if (!config.contains("mentions.enabled")) { - config.set("mentions.enabled", true); - config.set("mentions.sound", true); - config.set("mentions.sound-type", "BLOCK_NOTE_BLOCK_PLING"); - config.set("mentions.highlight", "&#FFFF00@{player}"); - config.set("mentions.self-highlight", "&#FFD700{player}"); - } - - if (!config.contains("automessages.enabled")) { - config.set("automessages.enabled", true); - config.set("automessages.interval", 300); - config.set("automessages.random", false); - } - - if (!config.contains("clearchat.message.enabled")) { - config.set("clearchat.message.enabled", true); - config.set("clearchat.message.text", "CADFЧат очищен администратором {player}"); - } - - if (!config.contains("lopreff.enabled")) { - config.set("lopreff.enabled", true); - config.set("lopreff.use-gradient-name", true); - config.set("lopreff.use-custom-prefix", true); - } - - if (!config.contains("moderation.voice-mute-console-command")) { - config.set("moderation.voice-mute-console-command", ""); + private void setIfMissing(String path, Object value) { + if (!config.contains(path)) { + config.set(path, value); } } - /** - * Перезагрузить все конфигурации - * ✅ THREAD-SAFE: использует volatile для безопасного обновления - */ public synchronized void reload() { plugin.reloadConfig(); this.config = plugin.getConfig(); - // Перезагружаем новые конфигурации - // volatile гарантирует что изменения видны всем потокам this.appearanceConfig.reload(); this.messagesConfig.reload(); this.muteConfig.reload(); @@ -147,6 +114,8 @@ public class ConfigManager { plugin.getLogger().info("Все конфигурации успешно перезагружены"); } + // ========== Базовые методы доступа ========== + public String getString(String path, String defaultValue) { return config.getString(path, defaultValue); } @@ -167,7 +136,8 @@ public class ConfigManager { return config.getStringList(path); } - // Global chat + // ========== Настройки чата ========== + public boolean isGlobalEnabled() { return config.getBoolean("chat.global.enabled", true); } @@ -176,7 +146,6 @@ public class ConfigManager { return config.getInt("chat.global.cooldown", 3); } - // Local chat public boolean isLocalEnabled() { return config.getBoolean("chat.local.enabled", true); } @@ -189,7 +158,8 @@ public class ConfigManager { return config.getInt("chat.local.cooldown", 1); } - // PM + // ========== PM ========== + public boolean isPmEnabled() { return config.getBoolean("chat.pm.enabled", true); } @@ -199,28 +169,29 @@ public class ConfigManager { } public boolean isPmSoundEnabled() { - // ✅ Используем SoundsConfig если доступен - return soundsConfig != null ? soundsConfig.isPmSoundEnabled() : config.getBoolean("chat.pm.sound", true); + return soundsConfig != null ? soundsConfig.isPmSoundEnabled() : + config.getBoolean("chat.pm.sound", true); } public String getPmSoundType() { - // ✅ Используем SoundsConfig если доступен - return soundsConfig != null ? soundsConfig.getPmSound() : config.getString("chat.pm.sound-type", "ENTITY_EXPERIENCE_ORB_PICKUP"); + return soundsConfig != null ? soundsConfig.getPmSound() : + config.getString("chat.pm.sound-type", "ENTITY_EXPERIENCE_ORB_PICKUP"); } - // Mentions + // ========== Mentions ========== + public boolean isMentionsEnabled() { return config.getBoolean("mentions.enabled", true); } public boolean isMentionSoundEnabled() { - // ✅ Используем SoundsConfig если доступен - return soundsConfig != null ? soundsConfig.isMentionSoundEnabled() : config.getBoolean("mentions.sound", true); + return soundsConfig != null ? soundsConfig.isMentionSoundEnabled() : + config.getBoolean("mentions.sound", true); } public String getMentionSoundType() { - // ✅ Используем SoundsConfig если доступен - return soundsConfig != null ? soundsConfig.getMentionSound() : config.getString("mentions.sound-type", "BLOCK_NOTE_BLOCK_PLING"); + return soundsConfig != null ? soundsConfig.getMentionSound() : + config.getString("mentions.sound-type", "BLOCK_NOTE_BLOCK_PLING"); } public String getMentionHighlight() { @@ -231,7 +202,8 @@ public class ConfigManager { return ChatFormatter.convertAllColors(config.getString("mentions.self-highlight", "&#FFD700{player}")); } - // Clear chat + // ========== Clear chat ========== + public boolean isClearChatMessageEnabled() { return config.getBoolean("clearchat.message.enabled", true); } @@ -250,7 +222,8 @@ public class ConfigManager { plugin.saveConfig(); } - // Auto messages + // ========== Auto messages ========== + public boolean isAutoMessagesEnabled() { return config.getBoolean("automessages.enabled", true); } @@ -263,18 +236,22 @@ public class ConfigManager { return config.getBoolean("automessages.random", false); } - // Formats + // ========== Formats ========== + public String getAnnouncementFormat() { - return ChatFormatter.convertAllColors(config.getString("formats.announcement", "&#FFD700[ОБЪЯВЛЕНИЕ] ")); + return ChatFormatter.convertAllColors(config.getString("formats.announcement", + "&#FFD700[ОБЪЯВЛЕНИЕ] ")); } - // Announcements + // ========== Announcements ========== + public boolean isAnnouncementTitleEnabled() { return config.getBoolean("announcements.show-title", true); } public String getAnnouncementTitleHeader() { - return ChatFormatter.convertAllColors(config.getString("announcements.title-header", "&#FFD700ОБЪЯВЛЕНИЕ")); + return ChatFormatter.convertAllColors(config.getString("announcements.title-header", + "&#FFD700ОБЪЯВЛЕНИЕ")); } public boolean isAnnouncementActionBarEnabled() { @@ -285,7 +262,8 @@ public class ConfigManager { return config.getInt("announcements.title-duration", 3); } - // LoPreff integration + // ========== LoPreff integration ========== + public boolean isLopreffEnabled() { return config.getBoolean("lopreff.enabled", true); } diff --git a/src/main/java/com/loki/lochat/core/PluginShutdown.java b/src/main/java/com/loki/lochat/core/PluginShutdown.java index 877ddf6..ca0f72c 100644 --- a/src/main/java/com/loki/lochat/core/PluginShutdown.java +++ b/src/main/java/com/loki/lochat/core/PluginShutdown.java @@ -1,11 +1,11 @@ package com.loki.lochat.core; import com.loki.lochat.LoChat; -import com.loki.lochat.api.service.IgnoreService; +import com.loki.lochat.api.service.MessagingService; import com.loki.lochat.api.service.MuteService; import com.loki.lochat.api.service.PunishmentService; import com.loki.lochat.api.service.NickService; -import com.loki.lochat.api.service.PlayerDataService; +import com.loki.lochat.api.service.PlayerService; import com.loki.lochat.core.registry.ServiceRegistry; import com.loki.lochat.gradient.GradientModule; import com.loki.lochat.integrations.DiscordIntegration; @@ -62,15 +62,15 @@ public class PluginShutdown { if (serviceRegistry == null) return; // Статистика игроков - PlayerDataService playerDataService = serviceRegistry.get(PlayerDataService.class); - if (playerDataService != null) { - playerDataService.saveAll(); + PlayerService playerService = serviceRegistry.get(PlayerService.class); + if (playerService != null) { + playerService.saveAll(); } - // Игнорирования - IgnoreService ignoreService = serviceRegistry.get(IgnoreService.class); - if (ignoreService != null) { - ignoreService.save(); + // Messaging (Ignore + PM + Spy) + MessagingService messagingService = serviceRegistry.get(MessagingService.class); + if (messagingService != null) { + messagingService.save(); } // Муты diff --git a/src/main/java/com/loki/lochat/core/filter/AdvancedMessageFilter.java b/src/main/java/com/loki/lochat/core/filter/AdvancedMessageFilter.java index 561202c..5410401 100644 --- a/src/main/java/com/loki/lochat/core/filter/AdvancedMessageFilter.java +++ b/src/main/java/com/loki/lochat/core/filter/AdvancedMessageFilter.java @@ -1,55 +1,24 @@ package com.loki.lochat.core.filter; -import com.google.gson.Gson; -import com.google.gson.JsonObject; -import com.google.gson.JsonArray; +import com.loki.lochat.core.filter.filters.*; import org.bukkit.configuration.file.FileConfiguration; import org.bukkit.entity.Player; import org.bukkit.plugin.java.JavaPlugin; -import java.io.File; -import java.io.FileReader; -import java.util.*; -import java.util.concurrent.ConcurrentHashMap; -import java.util.regex.Matcher; -import java.util.regex.Pattern; +import java.util.UUID; /** - * Главный фильтр сообщений. - * Порядок: mute (в MessageService) → caps → swear → advertising → ip → repeat → flood → spam + * Главный фильтр сообщений - координирует работу всех фильтров */ public class AdvancedMessageFilter { - // ── URL / IP / повторы ───────────────────────────────────────────────────── - private static final Pattern URL_PATTERN = Pattern.compile( - "(https?://)?([\\w-]+\\.)+[\\w-]+(/[\\w-./?%&=]*)?", Pattern.CASE_INSENSITIVE); - private static final Pattern IP_PATTERN = Pattern.compile( - "\\b(?:\\d{1,3}\\.){3}\\d{1,3}\\b"); - private static final Pattern REPEAT_PATTERN = Pattern.compile("(.)\\1{3,}"); - private static final Pattern HIDDEN_URL_PATTERN = Pattern.compile( - "(?i)(h\\s*t\\s*t\\s*p|w\\s*w\\s*w|d\\s*o\\s*t\\s*c\\s*o\\s*m)"); - private static final Pattern DOMAIN_PATTERN = Pattern.compile( - "\\b([a-zA-Z0-9-]+\\.(com|net|org|ru|gg|me|io|xyz|tk|ml|ga|cf|gq))\\b", - Pattern.CASE_INSENSITIVE); - private final FileConfiguration config; - private final List whitelistedDomains; - private final List blacklistedDomains; private final CapsFilter capsFilter; - - // ── Word filter ──────────────────────────────────────────────────────────── - private final Set badWords = new HashSet<>(); - private final Set badFragments = new HashSet<>(); - private final char replacementChar; - private final boolean checkFragments; - private final boolean ignoreCase; - - // ── Anti-flood: сколько сообщений за период ──────────────────────────────── - // UUID → список timestamp'ов - private final Map> floodTracker = new ConcurrentHashMap<>(); - - // ── Anti-spam: последние N сообщений игрока ──────────────────────────────── - private final Map> spamTracker = new ConcurrentHashMap<>(); + private final SwearFilter swearFilter; + private final FloodFilter floodFilter; + private final SpamFilter spamFilter; + private final UrlFilter urlFilter; + private final CharacterFilter characterFilter; public AdvancedMessageFilter(FileConfiguration config) { this(config, null); @@ -58,419 +27,105 @@ public class AdvancedMessageFilter { public AdvancedMessageFilter(FileConfiguration config, JavaPlugin plugin) { this.config = config; - // ✅ Используем FiltersConfig если доступен + // Инициализация CapsFilter com.loki.lochat.config.FiltersConfig filtersConfig = null; if (plugin instanceof com.loki.lochat.LoChat loChat) { filtersConfig = loChat.getConfigManager().getFiltersConfig(); } - // Читаем настройки из FiltersConfig или fallback на config.yml if (filtersConfig != null) { - this.whitelistedDomains = filtersConfig.getWhitelistedDomains(); - this.blacklistedDomains = filtersConfig.getBlacklistedDomains(); - int maxCapsPercent = filtersConfig.getCapsMaxPercent(); int minLength = filtersConfig.getCapsMinLength(); boolean autoLower = filtersConfig.isCapsAutoLowercase(); boolean blockCaps = filtersConfig.isCapsBlock(); this.capsFilter = new CapsFilter(maxCapsPercent, minLength, autoLower, blockCaps); - - this.replacementChar = filtersConfig.getSwearReplacementChar().charAt(0); - this.checkFragments = filtersConfig.isSwearCheckFragments(); - this.ignoreCase = filtersConfig.isSwearIgnoreCase(); } else { - // Fallback на config.yml - this.whitelistedDomains = config.getStringList("filters.advertising.whitelist"); - this.blacklistedDomains = config.getStringList("filters.advertising.blacklist"); - int maxCapsPercent = config.getInt("filters.caps.max-percent", 70); int minLength = config.getInt("filters.caps.min-length", 5); boolean autoLower = config.getBoolean("filters.caps.auto-lowercase", true); boolean blockCaps = config.getBoolean("filters.caps.block", false); this.capsFilter = new CapsFilter(maxCapsPercent, minLength, autoLower, blockCaps); - - this.replacementChar = config.getString("filters.swear.replacement-char", "*").charAt(0); - this.checkFragments = config.getBoolean("filters.swear.check-fragments", true); - this.ignoreCase = config.getBoolean("filters.swear.ignore-case", true); } - - loadWordFilter(plugin); + + // Инициализация остальных фильтров + this.swearFilter = new SwearFilter(config, plugin); + this.floodFilter = new FloodFilter(config); + this.spamFilter = new SpamFilter(config); + this.urlFilter = new UrlFilter(config); + this.characterFilter = new CharacterFilter(config); } - // ────────────────────────────────────────────────────────────────────────── - // Загрузка словаря - // ────────────────────────────────────────────────────────────────────────── - - private void loadWordFilter(JavaPlugin plugin) { - if (!config.getBoolean("filters.swear.enabled", true)) return; - - // Встроенные слова из config - List builtIn = config.getStringList("filters.swear.words"); - for (String w : builtIn) { - if (!w.isBlank() && !w.startsWith("плохое")) { - badWords.add(ignoreCase ? w.toLowerCase() : w); - } - } - - // Внешний файл filter-words.json - if (!config.getBoolean("filters.swear.use-external-file", true)) return; - if (plugin == null) return; - - File jsonFile = new File(plugin.getDataFolder(), "filter-words.json"); - if (!jsonFile.exists()) { - // Копируем из ресурсов - plugin.saveResource("data/filter-words.json", false); - jsonFile = new File(plugin.getDataFolder(), "data/filter-words.json"); - } - if (!jsonFile.exists()) return; - - try (FileReader reader = new FileReader(jsonFile)) { - JsonObject root = new Gson().fromJson(reader, JsonObject.class); - - JsonArray words = root.getAsJsonArray("words"); - if (words != null) { - for (var el : words) { - String w = el.getAsString(); - badWords.add(ignoreCase ? w.toLowerCase() : w); - } - } - - JsonArray fragments = root.getAsJsonArray("fragments"); - if (fragments != null && checkFragments) { - for (var el : fragments) { - String f = el.getAsString(); - badFragments.add(ignoreCase ? f.toLowerCase() : f); - } - } - - plugin.getLogger().info("[LoChat] Word filter loaded: " - + badWords.size() + " words, " + badFragments.size() + " fragments"); - } catch (Exception e) { - if (plugin != null) - plugin.getLogger().warning("[LoChat] Failed to load filter-words.json: " + e.getMessage()); - } + /** + * Проверяет, может ли игрок обойти фильтр + */ + private boolean canBypassFilter(Player player, String filterType) { + return player.hasPermission("lochat.bypass." + filterType) + || player.hasPermission("lochat.bypass.filter"); } - - // ────────────────────────────────────────────────────────────────────────── - // Главный метод - // ────────────────────────────────────────────────────────────────────────── - + + /** + * Главный метод фильтрации + */ public FilterResult filterMessage(Player player, String message) { - + // 1. Капс if (config.getBoolean("filters.caps.enabled", true)) { String filtered = capsFilter.filter(player, message); if (filtered == null) { - return blocked("&#CF6679Не кричите в чате"); + return FilterResult.blocked("&#CF6679Не кричите в чате"); } message = filtered; } // 2. Мат - if (config.getBoolean("filters.swear.enabled", true) - && !player.hasPermission("lochat.bypass.swear") - && !player.hasPermission("lochat.bypass.filter")) { - FilterResult swear = filterSwear(player, message); + if (config.getBoolean("filters.swear.enabled", true) && !canBypassFilter(player, "swear")) { + FilterResult swear = swearFilter.filter(player, message); if (!swear.allowed()) return swear; message = swear.filteredMessage(); } - // 3. Реклама / URL + // 3. URL фильтры if (config.getBoolean("filters.advertising.enabled", true)) { - FilterResult r = filterUrls(player, message); + FilterResult r = urlFilter.filterUrls(player, message); if (!r.allowed()) return r; message = r.filteredMessage(); - r = filterHiddenUrls(player, message); - if (!r.allowed()) return r; - - r = filterDomains(player, message); + r = urlFilter.filterHiddenUrls(player, message); if (!r.allowed()) return r; } // 4. IP if (config.getBoolean("filters.ip.enabled", true)) { - FilterResult r = filterIPs(player, message); + FilterResult r = urlFilter.filterIPs(player, message); if (!r.allowed()) return r; message = r.filteredMessage(); } // 5. Повторяющиеся символы if (config.getBoolean("filters.repeat.enabled", true)) { - message = filterRepeatingChars(message); + message = characterFilter.filter(message); } - // 6. Anti-flood (количество сообщений в период) - if (config.getBoolean("filters.flood.enabled", true) - && !player.hasPermission("lochat.bypass.flood") - && !player.hasPermission("lochat.bypass.filter")) { - FilterResult r = filterFlood(player); + // 6. Anti-flood + if (config.getBoolean("filters.flood.enabled", true) && !canBypassFilter(player, "flood")) { + FilterResult r = floodFilter.filter(player); if (!r.allowed()) return r; } - // 7. Anti-spam (похожие сообщения) - if (config.getBoolean("filters.spam.enabled", true) - && !player.hasPermission("lochat.bypass.spam") - && !player.hasPermission("lochat.bypass.filter")) { - FilterResult r = filterSpam(player, message); + // 7. Anti-spam + if (config.getBoolean("filters.spam.enabled", true) && !canBypassFilter(player, "spam")) { + FilterResult r = spamFilter.filter(player, message); if (!r.allowed()) return r; } - return new FilterResult(true, message, null); - } - - // ────────────────────────────────────────────────────────────────────────── - // Фильтр мата - // ────────────────────────────────────────────────────────────────────────── - - private FilterResult filterSwear(Player player, String message) { - String action = config.getString("filters.swear.action", "replace"); - String check = ignoreCase ? message.toLowerCase() : message; - - // Проверка полных слов - String[] tokens = check.split("\\s+"); - boolean hasBad = false; - for (String token : tokens) { - // убираем пунктуацию по краям - String clean = token.replaceAll("^[^а-яёa-z]+|[^а-яёa-z]+$", ""); - if (badWords.contains(clean)) { - hasBad = true; - break; - } - } - - // Проверка фрагментов (подстрок) - if (!hasBad && checkFragments) { - for (String frag : badFragments) { - if (check.contains(frag)) { - hasBad = true; - break; - } - } - } - - if (!hasBad) return new FilterResult(true, message, null); - - return switch (action) { - case "block" -> blocked(config.getString("filters.swear.block-message", - "&#CF6679Мат запрещён")); - case "replace" -> { - String replaced = replaceSwear(message); - yield new FilterResult(true, replaced, null); - } - default -> new FilterResult(true, message, null); - }; + return FilterResult.ok(message); } /** - * Заменяет матные слова звёздочками, сохраняя длину слова. + * Очистка данных игрока при выходе */ - private String replaceSwear(String message) { - String[] words = message.split("(?<=\\s)|(?=\\s)"); // разбиваем с сохранением пробелов - StringBuilder result = new StringBuilder(); - for (String word : words) { - String check = ignoreCase ? word.toLowerCase() : word; - String cleanCheck = check.replaceAll("^[^а-яёa-z]+|[^а-яёa-z]+$", ""); - - boolean isBad = badWords.contains(cleanCheck); - if (!isBad && checkFragments) { - for (String frag : badFragments) { - if (check.contains(frag)) { isBad = true; break; } - } - } - - if (isBad) { - result.append(String.valueOf(replacementChar).repeat(word.length())); - } else { - result.append(word); - } - } - return result.toString(); + public void clearPlayer(UUID uuid) { + floodFilter.clearPlayer(uuid); + spamFilter.clearPlayer(uuid); } - - // ────────────────────────────────────────────────────────────────────────── - // Anti-flood - // ────────────────────────────────────────────────────────────────────────── - - private FilterResult filterFlood(Player player) { - int maxMessages = config.getInt("filters.flood.max-messages", 5); - int timePeriod = config.getInt("filters.flood.time-period", 10); // секунды - long now = System.currentTimeMillis(); - long windowMs = timePeriod * 1000L; - - Deque timestamps = floodTracker.computeIfAbsent( - player.getUniqueId(), k -> new ArrayDeque<>()); - - // Удаляем старые timestamp'ы за пределами окна - while (!timestamps.isEmpty() && now - timestamps.peekFirst() > windowMs) { - timestamps.pollFirst(); - } - - if (timestamps.size() >= maxMessages) { - return blocked(config.getString("filters.flood.block-message", - "&#CF6679Не флудите!")); - } - - timestamps.addLast(now); - return new FilterResult(true, null, null); - } - - // ────────────────────────────────────────────────────────────────────────── - // Anti-spam (похожие сообщения) - // ────────────────────────────────────────────────────────────────────────── - - private FilterResult filterSpam(Player player, String message) { - int maxSimilar = config.getInt("filters.spam.max-similar-messages", 3); - int threshold = config.getInt("filters.spam.similarity-threshold", 80); - - Deque history = spamTracker.computeIfAbsent( - player.getUniqueId(), k -> new ArrayDeque<>()); - - // Считаем сколько прошлых сообщений похожи - long similarCount = history.stream() - .filter(prev -> similarity(prev, message) >= threshold) - .count(); - - if (similarCount >= maxSimilar) { - return blocked(config.getString("filters.spam.block-message", - "&#CF6679Не отправляйте одинаковые сообщения")); - } - - // Добавляем в историю (храним последние 10) - history.addLast(message); - if (history.size() > 10) history.pollFirst(); - - return new FilterResult(true, null, null); - } - - /** - * Простая метрика схожести: процент общих символов (0-100). - */ - private int similarity(String a, String b) { - if (a.equals(b)) return 100; - int maxLen = Math.max(a.length(), b.length()); - if (maxLen == 0) return 100; - int dist = levenshtein( - a.toLowerCase().substring(0, Math.min(a.length(), 50)), - b.toLowerCase().substring(0, Math.min(b.length(), 50))); - return 100 - (dist * 100 / maxLen); - } - - private int levenshtein(String a, String b) { - int[] dp = new int[b.length() + 1]; - for (int j = 0; j <= b.length(); j++) dp[j] = j; - for (int i = 1; i <= a.length(); i++) { - int prev = dp[0]; - dp[0] = i; - for (int j = 1; j <= b.length(); j++) { - int temp = dp[j]; - dp[j] = a.charAt(i - 1) == b.charAt(j - 1) - ? prev - : 1 + Math.min(prev, Math.min(dp[j], dp[j - 1])); - prev = temp; - } - } - return dp[b.length()]; - } - - // ────────────────────────────────────────────────────────────────────────── - // URL / IP / повторы — прежняя логика - // ────────────────────────────────────────────────────────────────────────── - - private FilterResult filterUrls(Player player, String message) { - if (player.hasPermission("lochat.bypass.urlfilter")) return ok(message); - Matcher matcher = URL_PATTERN.matcher(message); - while (matcher.find()) { - String domain = extractDomain(matcher.group()); - if (!whitelistedDomains.isEmpty()) { - boolean ok = whitelistedDomains.stream() - .anyMatch(d -> domain.toLowerCase().contains(d.toLowerCase())); - if (!ok) return blocked(config.getString("filters.url.blocked-message", - "&#CF6679Ссылки запрещены!")); - } - if (!blacklistedDomains.isEmpty()) { - boolean bad = blacklistedDomains.stream() - .anyMatch(d -> domain.toLowerCase().contains(d.toLowerCase())); - if (bad) return blocked(config.getString("filters.url.blocked-message", - "&#CF6679Эта ссылка запрещена!")); - } - } - return ok(message); - } - - private FilterResult filterIPs(Player player, String message) { - if (player.hasPermission("lochat.bypass.ipfilter")) return ok(message); - Matcher matcher = IP_PATTERN.matcher(message); - if (matcher.find()) { - if (config.getBoolean("filters.ip.block", false)) - return blocked(config.getString("filters.ip.blocked-message", - "&#CF6679IP адреса запрещены!")); - return ok(matcher.replaceAll(config.getString("filters.ip.replacement", "[IP скрыт]"))); - } - return ok(message); - } - - private FilterResult filterHiddenUrls(Player player, String message) { - if (!config.getBoolean("filters.advertising.block-hidden-urls", true)) return ok(message); - if (player.hasPermission("lochat.bypass.hiddenurls")) return ok(message); - if (HIDDEN_URL_PATTERN.matcher(message).find()) - return blocked(config.getString("filters.advertising.blocked-message", - "&#CF6679Реклама запрещена!")); - return ok(message); - } - - private FilterResult filterDomains(Player player, String message) { - if (!config.getBoolean("filters.advertising.block-domains", true)) return ok(message); - if (player.hasPermission("lochat.bypass.domains")) return ok(message); - Matcher matcher = DOMAIN_PATTERN.matcher(message); - while (matcher.find()) { - String domain = matcher.group(); - if (!whitelistedDomains.isEmpty()) { - boolean ok = whitelistedDomains.stream() - .anyMatch(d -> domain.toLowerCase().contains(d.toLowerCase())); - if (!ok) return blocked(config.getString("filters.advertising.blocked-message", - "&#CF6679Реклама запрещена!")); - } - if (!blacklistedDomains.isEmpty()) { - boolean bad = blacklistedDomains.stream() - .anyMatch(d -> domain.toLowerCase().contains(d.toLowerCase())); - if (bad) return blocked(config.getString("filters.advertising.blocked-message", - "&#CF6679Реклама запрещена!")); - } - } - return ok(message); - } - - private String filterRepeatingChars(String message) { - int max = config.getInt("filters.repeat.max-repeats", 3); - Matcher m = REPEAT_PATTERN.matcher(message); - StringBuffer sb = new StringBuffer(); - while (m.find()) { - m.appendReplacement(sb, Matcher.quoteReplacement( - m.group(1).repeat(Math.min(max, m.group().length())))); - } - m.appendTail(sb); - return sb.toString(); - } - - private String extractDomain(String url) { - url = url.replaceFirst("^https?://", ""); - int slash = url.indexOf('/'); - return slash != -1 ? url.substring(0, slash) : url; - } - - // ── Утилиты ──────────────────────────────────────────────────────────────── - - private static FilterResult ok(String msg) { return new FilterResult(true, msg, null); } - private static FilterResult blocked(String reason) { return new FilterResult(false, null, reason); } - - /** - * Очищает трекеры для игрока при выходе (вызывать из PlayerEventListener). - */ - public void clearPlayer(java.util.UUID uuid) { - floodTracker.remove(uuid); - spamTracker.remove(uuid); - } - - public record FilterResult(boolean allowed, String filteredMessage, String blockReason) {} } diff --git a/src/main/java/com/loki/lochat/core/filter/CooldownFilter.java b/src/main/java/com/loki/lochat/core/filter/CooldownFilter.java index eab9d72..c44a557 100644 --- a/src/main/java/com/loki/lochat/core/filter/CooldownFilter.java +++ b/src/main/java/com/loki/lochat/core/filter/CooldownFilter.java @@ -1,7 +1,7 @@ package com.loki.lochat.core.filter; import com.loki.lochat.api.filter.MessageFilter; -import com.loki.lochat.api.service.CooldownService; +import com.loki.lochat.api.service.PlayerService; import com.loki.lochat.data.model.ChatMessage; import org.bukkit.configuration.file.FileConfiguration; import org.bukkit.entity.Player; @@ -11,12 +11,12 @@ import org.bukkit.plugin.java.JavaPlugin; * Фильтр кулдаунов */ public class CooldownFilter implements MessageFilter { - private final CooldownService cooldownService; + private final PlayerService playerService; private final FileConfiguration config; private final JavaPlugin plugin; - public CooldownFilter(CooldownService cooldownService, JavaPlugin plugin) { - this.cooldownService = cooldownService; + public CooldownFilter(PlayerService playerService, JavaPlugin plugin) { + this.playerService = playerService; this.config = plugin.getConfig(); this.plugin = plugin; } @@ -31,8 +31,8 @@ public class CooldownFilter implements MessageFilter { int cooldown = config.getInt("chat." + chatType + ".cooldown", 0); if (cooldown > 0) { - if (cooldownService.isOnCooldown(player.getUniqueId(), chatType, cooldown)) { - int remaining = cooldownService.getRemainingCooldown( + if (playerService.isOnCooldown(player.getUniqueId(), chatType, cooldown)) { + int remaining = playerService.getRemainingCooldown( player.getUniqueId(), chatType, cooldown); // Получаем сообщение из конфигурации @@ -45,7 +45,7 @@ public class CooldownFilter implements MessageFilter { } } - cooldownService.setCooldown(player.getUniqueId(), chatType); + playerService.setCooldown(player.getUniqueId(), chatType); return true; } } diff --git a/src/main/java/com/loki/lochat/core/filter/FilterResult.java b/src/main/java/com/loki/lochat/core/filter/FilterResult.java new file mode 100644 index 0000000..a1030ac --- /dev/null +++ b/src/main/java/com/loki/lochat/core/filter/FilterResult.java @@ -0,0 +1,20 @@ +package com.loki.lochat.core.filter; + +public record FilterResult(boolean allowed, String filteredMessage, String blockReason) { + + public static FilterResult ok(String message) { + return new FilterResult(true, message, null); + } + + public static FilterResult blocked(String reason) { + return new FilterResult(false, null, reason); + } + + public boolean isAllowed() { + return allowed; + } + + public String getMessage() { + return filteredMessage; + } +} diff --git a/src/main/java/com/loki/lochat/core/filter/filters/CharacterFilter.java b/src/main/java/com/loki/lochat/core/filter/filters/CharacterFilter.java new file mode 100644 index 0000000..ade733a --- /dev/null +++ b/src/main/java/com/loki/lochat/core/filter/filters/CharacterFilter.java @@ -0,0 +1,27 @@ +package com.loki.lochat.core.filter.filters; + +import org.bukkit.configuration.file.FileConfiguration; + +import java.util.regex.Pattern; + +public class CharacterFilter { + private final boolean enabled; + private final int maxRepeating; + + public CharacterFilter(FileConfiguration config) { + this.enabled = config.getBoolean("filters.repeating-chars.enabled", true); + this.maxRepeating = config.getInt("filters.repeating-chars.max", 3); + } + + public String filter(String message) { + if (!enabled || maxRepeating <= 0) { + return message; + } + + Pattern pattern = Pattern.compile("(.)\\1{" + maxRepeating + ",}"); + return pattern.matcher(message).replaceAll(m -> { + String repeated = m.group(1); + return repeated.repeat(maxRepeating); + }); + } +} diff --git a/src/main/java/com/loki/lochat/core/filter/filters/FloodFilter.java b/src/main/java/com/loki/lochat/core/filter/filters/FloodFilter.java new file mode 100644 index 0000000..fccf0dc --- /dev/null +++ b/src/main/java/com/loki/lochat/core/filter/filters/FloodFilter.java @@ -0,0 +1,51 @@ +package com.loki.lochat.core.filter.filters; + +import com.loki.lochat.core.filter.FilterResult; +import org.bukkit.configuration.file.FileConfiguration; +import org.bukkit.entity.Player; + +import java.util.ArrayDeque; +import java.util.Deque; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; + +public class FloodFilter { + private final FileConfiguration config; + private final Map> floodTracker = new ConcurrentHashMap<>(); + + public FloodFilter(FileConfiguration config) { + this.config = config; + } + + public FilterResult filter(Player player) { + if (player.hasPermission("lochat.bypass.flood")) { + return FilterResult.ok(null); + } + + int maxMessages = config.getInt("filters.flood.max-messages", 5); + int timePeriod = config.getInt("filters.flood.time-period", 10); + long now = System.currentTimeMillis(); + long windowMs = timePeriod * 1000L; + + Deque timestamps = floodTracker.computeIfAbsent( + player.getUniqueId(), k -> new ArrayDeque<>()); + + // Удаляем старые timestamp'ы + while (!timestamps.isEmpty() && now - timestamps.peekFirst() > windowMs) { + timestamps.pollFirst(); + } + + if (timestamps.size() >= maxMessages) { + return FilterResult.blocked(config.getString("filters.flood.block-message", + "&#CF6679Не флудите!")); + } + + timestamps.addLast(now); + return FilterResult.ok(null); + } + + public void clearPlayer(UUID uuid) { + floodTracker.remove(uuid); + } +} diff --git a/src/main/java/com/loki/lochat/core/filter/filters/SpamFilter.java b/src/main/java/com/loki/lochat/core/filter/filters/SpamFilter.java new file mode 100644 index 0000000..e3e3df0 --- /dev/null +++ b/src/main/java/com/loki/lochat/core/filter/filters/SpamFilter.java @@ -0,0 +1,83 @@ +package com.loki.lochat.core.filter.filters; + +import com.loki.lochat.core.filter.FilterResult; +import org.bukkit.configuration.file.FileConfiguration; +import org.bukkit.entity.Player; + +import java.util.ArrayDeque; +import java.util.Deque; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; + +public class SpamFilter { + private final FileConfiguration config; + private final Map> spamTracker = new ConcurrentHashMap<>(); + + public SpamFilter(FileConfiguration config) { + this.config = config; + } + + public FilterResult filter(Player player, String message) { + if (player.hasPermission("lochat.bypass.spam")) { + return FilterResult.ok(message); + } + + int maxSimilar = config.getInt("filters.spam.max-similar-messages", 3); + int threshold = config.getInt("filters.spam.similarity-threshold", 80); + + Deque history = spamTracker.computeIfAbsent( + player.getUniqueId(), k -> new ArrayDeque<>()); + + // Считаем похожие сообщения + long similarCount = history.stream() + .filter(prev -> similarity(prev, message) >= threshold) + .count(); + + if (similarCount >= maxSimilar) { + return FilterResult.blocked(config.getString("filters.spam.block-message", + "&#CF6679Не отправляйте одинаковые сообщения")); + } + + // Добавляем в историю (храним последние 10) + history.addLast(message); + if (history.size() > 10) history.pollFirst(); + + return FilterResult.ok(null); + } + + private int similarity(String a, String b) { + if (a.equals(b)) return 100; + int maxLen = Math.max(a.length(), b.length()); + if (maxLen == 0) return 100; + + int dist = levenshtein( + a.toLowerCase().substring(0, Math.min(a.length(), 50)), + b.toLowerCase().substring(0, Math.min(b.length(), 50))); + + return 100 - (dist * 100 / maxLen); + } + + private int levenshtein(String a, String b) { + int[] dp = new int[b.length() + 1]; + for (int j = 0; j <= b.length(); j++) dp[j] = j; + + for (int i = 1; i <= a.length(); i++) { + int prev = dp[0]; + dp[0] = i; + for (int j = 1; j <= b.length(); j++) { + int temp = dp[j]; + dp[j] = a.charAt(i - 1) == b.charAt(j - 1) + ? prev + : 1 + Math.min(prev, Math.min(dp[j], dp[j - 1])); + prev = temp; + } + } + + return dp[b.length()]; + } + + public void clearPlayer(UUID uuid) { + spamTracker.remove(uuid); + } +} diff --git a/src/main/java/com/loki/lochat/core/filter/filters/SwearFilter.java b/src/main/java/com/loki/lochat/core/filter/filters/SwearFilter.java new file mode 100644 index 0000000..af3b5fb --- /dev/null +++ b/src/main/java/com/loki/lochat/core/filter/filters/SwearFilter.java @@ -0,0 +1,72 @@ +package com.loki.lochat.core.filter.filters; + +import com.loki.lochat.core.filter.FilterResult; +import org.bukkit.configuration.file.FileConfiguration; +import org.bukkit.entity.Player; +import org.bukkit.plugin.java.JavaPlugin; + +import java.io.BufferedReader; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.regex.Pattern; + +public class SwearFilter { + private final boolean enabled; + private final boolean replaceMode; + private final String replacement; + private final Set swearPatterns = new HashSet<>(); + + public SwearFilter(FileConfiguration config, JavaPlugin plugin) { + this.enabled = config.getBoolean("filters.swear.enabled", true); + this.replaceMode = config.getBoolean("filters.swear.replace", false); + this.replacement = config.getString("filters.swear.replacement", "***"); + + if (enabled) { + loadWordList(plugin); + } + } + + private void loadWordList(JavaPlugin plugin) { + try (InputStream is = plugin.getResource("badwords.txt"); + BufferedReader reader = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8))) { + + reader.lines() + .map(String::trim) + .filter(line -> !line.isEmpty() && !line.startsWith("#")) + .forEach(word -> swearPatterns.add( + Pattern.compile("\\b" + Pattern.quote(word) + "\\b", Pattern.CASE_INSENSITIVE) + )); + } catch (Exception e) { + plugin.getLogger().warning("Failed to load badwords.txt: " + e.getMessage()); + } + } + + public FilterResult filter(Player player, String message) { + if (!enabled || player.hasPermission("lochat.bypass.swear")) { + return FilterResult.ok(message); + } + + for (Pattern pattern : swearPatterns) { + if (pattern.matcher(message).find()) { + if (replaceMode) { + return FilterResult.ok(replaceSwear(message)); + } + return FilterResult.blocked("Сообщение содержит запрещенные слова"); + } + } + + return FilterResult.ok(message); + } + + private String replaceSwear(String message) { + String result = message; + for (Pattern pattern : swearPatterns) { + result = pattern.matcher(result).replaceAll(replacement); + } + return result; + } +} diff --git a/src/main/java/com/loki/lochat/core/filter/filters/UrlFilter.java b/src/main/java/com/loki/lochat/core/filter/filters/UrlFilter.java new file mode 100644 index 0000000..6515475 --- /dev/null +++ b/src/main/java/com/loki/lochat/core/filter/filters/UrlFilter.java @@ -0,0 +1,106 @@ +package com.loki.lochat.core.filter.filters; + +import com.loki.lochat.core.filter.FilterResult; +import org.bukkit.configuration.file.FileConfiguration; +import org.bukkit.entity.Player; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class UrlFilter { + private static final Pattern URL_PATTERN = Pattern.compile( + "(https?://)?([\\w-]+\\.)+[\\w-]+(/[\\w-./?%&=]*)?", Pattern.CASE_INSENSITIVE + ); + + private static final Pattern IP_PATTERN = Pattern.compile( + "\\b(?:\\d{1,3}\\.){3}\\d{1,3}\\b" + ); + + private static final Pattern HIDDEN_URL_PATTERN = Pattern.compile( + "(?i)(h\\s*t\\s*t\\s*p|w\\s*w\\s*w|d\\s*o\\s*t\\s*c\\s*o\\s*m)" + ); + + private final FileConfiguration config; + private final Set whitelistedDomains; + private final Set blacklistedDomains; + + public UrlFilter(FileConfiguration config) { + this.config = config; + this.whitelistedDomains = new HashSet<>(config.getStringList("filters.advertising.whitelist")); + this.blacklistedDomains = new HashSet<>(config.getStringList("filters.advertising.blacklist")); + } + + public FilterResult filterUrls(Player player, String message) { + if (player.hasPermission("lochat.bypass.urlfilter")) { + return FilterResult.ok(message); + } + + Matcher matcher = URL_PATTERN.matcher(message); + while (matcher.find()) { + String domain = extractDomain(matcher.group()); + + if (!whitelistedDomains.isEmpty()) { + boolean ok = whitelistedDomains.stream() + .anyMatch(d -> domain.toLowerCase().contains(d.toLowerCase())); + if (!ok) { + return FilterResult.blocked(config.getString("filters.url.blocked-message", + "&#CF6679Ссылки запрещены!")); + } + } + + if (!blacklistedDomains.isEmpty()) { + boolean bad = blacklistedDomains.stream() + .anyMatch(d -> domain.toLowerCase().contains(d.toLowerCase())); + if (bad) { + return FilterResult.blocked(config.getString("filters.url.blocked-message", + "&#CF6679Эта ссылка запрещена!")); + } + } + } + + return FilterResult.ok(message); + } + + public FilterResult filterIPs(Player player, String message) { + if (player.hasPermission("lochat.bypass.ipfilter")) { + return FilterResult.ok(message); + } + + Matcher matcher = IP_PATTERN.matcher(message); + if (matcher.find()) { + if (config.getBoolean("filters.ip.block", false)) { + return FilterResult.blocked(config.getString("filters.ip.blocked-message", + "&#CF6679IP адреса запрещены!")); + } + return FilterResult.ok(matcher.replaceAll( + config.getString("filters.ip.replacement", "[IP скрыт]"))); + } + + return FilterResult.ok(message); + } + + public FilterResult filterHiddenUrls(Player player, String message) { + if (!config.getBoolean("filters.advertising.block-hidden-urls", true)) { + return FilterResult.ok(message); + } + if (player.hasPermission("lochat.bypass.hiddenurls")) { + return FilterResult.ok(message); + } + + if (HIDDEN_URL_PATTERN.matcher(message).find()) { + return FilterResult.blocked(config.getString("filters.advertising.blocked-message", + "&#CF6679Реклама запрещена!")); + } + + return FilterResult.ok(message); + } + + private String extractDomain(String url) { + url = url.replaceFirst("^https?://", ""); + int slash = url.indexOf('/'); + return slash != -1 ? url.substring(0, slash) : url; + } +} diff --git a/src/main/java/com/loki/lochat/core/registry/ServiceRegistry.java b/src/main/java/com/loki/lochat/core/registry/ServiceRegistry.java index 746967b..967722e 100644 --- a/src/main/java/com/loki/lochat/core/registry/ServiceRegistry.java +++ b/src/main/java/com/loki/lochat/core/registry/ServiceRegistry.java @@ -27,19 +27,16 @@ public class ServiceRegistry { register(ChatService.class, new ChatServiceImpl(plugin, this)); register(MuteService.class, new MuteServiceImpl(plugin)); - // CooldownService регистрируем раньше PlayerDataService, чтобы передать его как зависимость - CooldownServiceImpl cooldownService = new CooldownServiceImpl(); - register(CooldownService.class, cooldownService); + // ✅ NEW: Объединённый PlayerService (Cooldown + PlayerData) + PlayerService playerService = new PlayerServiceImpl(plugin); + register(PlayerService.class, playerService); register(MessageService.class, new MessageServiceImpl(plugin, this)); - // Передаём тот же экземпляр CooldownService, а не новый — иначе очистка при выходе не работает - register(PlayerDataService.class, new PlayerDataServiceImpl(plugin, cooldownService)); - - register(PMService.class, new PMServiceImpl()); - register(IgnoreService.class, new IgnoreServiceImpl(plugin)); - SpyService spyService = new SpyServiceImpl(messageConfig); - register(SpyService.class, spyService); + // ✅ NEW: Объединённый MessagingService (PM + Spy + Ignore) + MessagingService messagingService = new MessagingServiceImpl(plugin, messageConfig); + register(MessagingService.class, messagingService); + register(MentionService.class, new MentionServiceImpl(configManager)); register(NickService.class, new NickServiceImpl(plugin)); register(PunishmentService.class, new PunishmentServiceImpl(plugin, configManager.getMessagesConfig())); @@ -48,6 +45,14 @@ public class ServiceRegistry { public void register(Class serviceClass, T implementation) { services.put(serviceClass, implementation); } + + /** + * Регистрирует сервис с возможностью приведения типов (для обратной совместимости) + */ + @SuppressWarnings("unchecked") + public void registerCompat(Class serviceClass, Object implementation) { + services.put(serviceClass, implementation); + } @SuppressWarnings("unchecked") public T get(Class serviceClass) { diff --git a/src/main/java/com/loki/lochat/core/service/ChatServiceImpl.java b/src/main/java/com/loki/lochat/core/service/ChatServiceImpl.java index 8b4acbd..cfcbce1 100644 --- a/src/main/java/com/loki/lochat/core/service/ChatServiceImpl.java +++ b/src/main/java/com/loki/lochat/core/service/ChatServiceImpl.java @@ -3,8 +3,8 @@ package com.loki.lochat.core.service; import com.loki.lochat.api.service.ChatService; import com.loki.lochat.core.registry.ServiceRegistry; import com.loki.lochat.renderer.EnhancedChatRenderer; -import com.loki.lochat.util.DistanceUtil; -import com.loki.lochat.util.FoliaUtil; +import com.loki.lochat.utils.DistanceUtil; +import com.loki.lochat.utils.FoliaUtil; import com.loki.lochat.utils.ChatFormatter; import net.kyori.adventure.text.Component; import org.bukkit.Bukkit; diff --git a/src/main/java/com/loki/lochat/core/service/CooldownServiceImpl.java b/src/main/java/com/loki/lochat/core/service/CooldownServiceImpl.java deleted file mode 100644 index b2afcd3..0000000 --- a/src/main/java/com/loki/lochat/core/service/CooldownServiceImpl.java +++ /dev/null @@ -1,52 +0,0 @@ -package com.loki.lochat.core.service; - -import com.loki.lochat.api.service.CooldownService; - -import java.util.Map; -import java.util.UUID; -import java.util.concurrent.ConcurrentHashMap; - -/** - * Реализация сервиса кулдаунов - */ -public class CooldownServiceImpl implements CooldownService { - private final Map globalCooldowns = new ConcurrentHashMap<>(); - private final Map localCooldowns = new ConcurrentHashMap<>(); - - @Override - public boolean isOnCooldown(UUID player, String type, int cooldownSeconds) { - Map cooldowns = getCooldownMap(type); - Long lastTime = cooldowns.get(player); - - if (lastTime == null) return false; - - long elapsed = (System.currentTimeMillis() - lastTime) / 1000; - return elapsed < cooldownSeconds; - } - - @Override - public int getRemainingCooldown(UUID player, String type, int cooldownSeconds) { - Map cooldowns = getCooldownMap(type); - Long lastTime = cooldowns.get(player); - - if (lastTime == null) return 0; - - long elapsed = (System.currentTimeMillis() - lastTime) / 1000; - return Math.max(0, cooldownSeconds - (int) elapsed); - } - - @Override - public void setCooldown(UUID player, String type) { - getCooldownMap(type).put(player, System.currentTimeMillis()); - } - - @Override - public void removeCooldown(UUID player) { - globalCooldowns.remove(player); - localCooldowns.remove(player); - } - - private Map getCooldownMap(String type) { - return type.equals("global") ? globalCooldowns : localCooldowns; - } -} diff --git a/src/main/java/com/loki/lochat/core/service/IgnoreServiceImpl.java b/src/main/java/com/loki/lochat/core/service/IgnoreServiceImpl.java deleted file mode 100644 index e5cf033..0000000 --- a/src/main/java/com/loki/lochat/core/service/IgnoreServiceImpl.java +++ /dev/null @@ -1,115 +0,0 @@ -package com.loki.lochat.core.service; - -import com.loki.lochat.api.service.IgnoreService; -import com.loki.lochat.util.FoliaUtil; -import org.bukkit.configuration.file.YamlConfiguration; -import org.bukkit.plugin.java.JavaPlugin; - -import java.io.File; -import java.io.IOException; -import java.util.*; -import java.util.concurrent.ConcurrentHashMap; - -/** - * Реализация сервиса игнорирования - */ -public class IgnoreServiceImpl implements IgnoreService { - private final JavaPlugin plugin; - private final File ignoreFile; - private final Map> ignoreMap = new ConcurrentHashMap<>(); - - public IgnoreServiceImpl(JavaPlugin plugin) { - this.plugin = plugin; - this.ignoreFile = new File(plugin.getDataFolder(), "ignores.yml"); - load(); - } - - @Override - 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) { - } - } - } - - @Override - 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()); - } - } - - @Override - public boolean isIgnoring(UUID player, UUID target) { - Set ignored = ignoreMap.get(player); - return ignored != null && ignored.contains(target); - } - - @Override - 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; - } - - @Override - 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; - } - - @Override - public Set getIgnoredPlayers(UUID player) { - return ignoreMap.getOrDefault(player, Collections.emptySet()); - } - - @Override - public int getIgnoredCount(UUID player) { - Set ignored = ignoreMap.get(player); - return ignored == null ? 0 : ignored.size(); - } - - @Override - public void clearIgnores(UUID player) { - ignoreMap.remove(player); - saveAsync(); - } - - private void saveAsync() { - FoliaUtil.runAsync(plugin, this::save); - } -} diff --git a/src/main/java/com/loki/lochat/core/service/MessageServiceImpl.java b/src/main/java/com/loki/lochat/core/service/MessageServiceImpl.java index d736209..71b6ea5 100644 --- a/src/main/java/com/loki/lochat/core/service/MessageServiceImpl.java +++ b/src/main/java/com/loki/lochat/core/service/MessageServiceImpl.java @@ -1,7 +1,7 @@ package com.loki.lochat.core.service; import com.loki.lochat.api.filter.MessageFilter; -import com.loki.lochat.api.service.CooldownService; +import com.loki.lochat.api.service.PlayerService; import com.loki.lochat.api.service.MessageService; import com.loki.lochat.api.service.MuteService; import com.loki.lochat.core.filter.CooldownFilter; @@ -26,7 +26,7 @@ public class MessageServiceImpl implements MessageService { // Регистрируем фильтры в порядке применения // MuteFilter теперь получает plugin для доступа к HardcodedMessages filters.add(new MuteFilter(registry.get(MuteService.class), plugin)); - filters.add(new CooldownFilter(registry.get(CooldownService.class), plugin)); + filters.add(new CooldownFilter(registry.get(PlayerService.class), plugin)); } @Override diff --git a/src/main/java/com/loki/lochat/core/service/MessagingServiceImpl.java b/src/main/java/com/loki/lochat/core/service/MessagingServiceImpl.java new file mode 100644 index 0000000..aed7c78 --- /dev/null +++ b/src/main/java/com/loki/lochat/core/service/MessagingServiceImpl.java @@ -0,0 +1,219 @@ +package com.loki.lochat.core.service; + +import com.loki.lochat.api.service.MessagingService; +import com.loki.lochat.config.MessageConfig; +import com.loki.lochat.utils.FoliaUtil; +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.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; + +/** + * Объединённая реализация сервиса общения + * Включает: PM, Spy, Ignore + */ +public class MessagingServiceImpl implements MessagingService { + + private final JavaPlugin plugin; + private final MessageConfig messageConfig; + private final File ignoreFile; + + // PM state + private final Map lastConversation = new ConcurrentHashMap<>(); + + // Spy state + private final Set spyEnabled = ConcurrentHashMap.newKeySet(); + + // Ignore state + private final Map> ignoreMap = new ConcurrentHashMap<>(); + + public MessagingServiceImpl(JavaPlugin plugin, MessageConfig messageConfig) { + this.plugin = plugin; + this.messageConfig = messageConfig; + this.ignoreFile = new File(plugin.getDataFolder(), "ignores.yml"); + load(); + } + + // ========== PM Implementation ========== + + @Override + public void setLastConversation(UUID player, UUID target) { + lastConversation.put(player, target); + } + + @Override + public Optional getLastConversation(UUID player) { + return Optional.ofNullable(lastConversation.get(player)); + } + + @Override + public void removeConversation(UUID player) { + lastConversation.remove(player); + } + + @Override + public boolean hasConversation(UUID player) { + return lastConversation.containsKey(player); + } + + // ========== Spy Implementation ========== + + @Override + public boolean toggleSpy(UUID player) { + if (spyEnabled.contains(player)) { + spyEnabled.remove(player); + return false; + } else { + spyEnabled.add(player); + return true; + } + } + + @Override + public boolean isSpying(UUID player) { + return spyEnabled.contains(player); + } + + @Override + public void broadcastPM(Player sender, Player receiver, String message) { + String format = messageConfig.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))); + } + } + } + + @Override + public void sendToSpies(Player sender, Component message, boolean isGlobal) { + if (spyEnabled.isEmpty()) return; + + String chatType = isGlobal ? "Global" : "Local"; + String plainMessage = PlainTextComponentSerializer.plainText().serialize(message); + String format = messageConfig.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))); + } + } + } + + @Override + public void removeSpy(UUID player) { + spyEnabled.remove(player); + } + + // ========== Ignore Implementation ========== + + @Override + public boolean isIgnoring(UUID player, UUID target) { + Set ignored = ignoreMap.get(player); + return ignored != null && ignored.contains(target); + } + + @Override + 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; + } + + @Override + 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; + } + + @Override + public Set getIgnoredPlayers(UUID player) { + return ignoreMap.getOrDefault(player, Collections.emptySet()); + } + + @Override + public int getIgnoredCount(UUID player) { + Set ignored = ignoreMap.get(player); + return ignored == null ? 0 : ignored.size(); + } + + @Override + public void clearIgnores(UUID player) { + ignoreMap.remove(player); + saveAsync(); + } + + // ========== Persistence ========== + + @Override + 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 e) { + // Ignore invalid UUID + } + } + + if (!ignored.isEmpty()) { + ignoreMap.put(uuid, ignored); + } + } catch (Exception e) { + // Ignore invalid entry + } + } + } + + @Override + 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()); + } + } + + private void saveAsync() { + FoliaUtil.runAsync(plugin, this::save); + } +} diff --git a/src/main/java/com/loki/lochat/core/service/MuteServiceImpl.java b/src/main/java/com/loki/lochat/core/service/MuteServiceImpl.java index 434a7f0..ed8a748 100644 --- a/src/main/java/com/loki/lochat/core/service/MuteServiceImpl.java +++ b/src/main/java/com/loki/lochat/core/service/MuteServiceImpl.java @@ -1,52 +1,66 @@ package com.loki.lochat.core.service; -import com.google.gson.Gson; -import com.google.gson.GsonBuilder; -import com.google.gson.reflect.TypeToken; import com.loki.lochat.api.service.MuteService; import com.loki.lochat.data.model.MuteData; -import com.loki.lochat.util.FoliaUtil; +import com.loki.lochat.utils.FoliaUtil; import com.loki.lochat.utils.TimeFormatter; +import org.bukkit.Bukkit; import org.bukkit.entity.Player; import org.bukkit.plugin.java.JavaPlugin; -import java.io.*; -import java.lang.reflect.Type; import java.util.*; -import java.util.concurrent.ConcurrentHashMap; +/** + * Сервис мутов с интеграцией Simple Voice Chat + */ public class MuteServiceImpl implements MuteService { private final JavaPlugin plugin; - private final Map mutes = new ConcurrentHashMap<>(); - private final MuteHistoryManager historyManager; - private final File dataFile; - private final Gson gson = new GsonBuilder().setPrettyPrinting().create(); + private final Map mutes = new HashMap<>(); + private final Map> history = new HashMap<>(); + private Object voicechatService; // Используем Object для опциональной зависимости public MuteServiceImpl(JavaPlugin plugin) { this.plugin = plugin; - this.dataFile = new File(plugin.getDataFolder(), "mutes.json"); - this.historyManager = new MuteHistoryManager(new File(plugin.getDataFolder(), "mute-history.json")); + initVoiceChat(); load(); } + private void initVoiceChat() { + if (Bukkit.getPluginManager().isPluginEnabled("voicechat")) { + try { + // Проверяем наличие класса через рефлексию + Class.forName("de.maxhenkel.voicechat.api.BukkitVoicechatService"); + voicechatService = Bukkit.getServicesManager().load( + Class.forName("de.maxhenkel.voicechat.api.BukkitVoicechatService") + ); + if (voicechatService != null) { + plugin.getLogger().info("Simple Voice Chat integration enabled!"); + } + } catch (ClassNotFoundException e) { + plugin.getLogger().info("VoiceChat API not found, voice mute disabled"); + } catch (Exception e) { + plugin.getLogger().warning("Failed to hook into Simple Voice Chat: " + e.getMessage()); + } + } + } + @Override public void mute(UUID uuid, String playerName, long duration, String reason, String mutedBy) { - if (uuid == null) { - plugin.getLogger().warning("Attempted to mute player with null UUID"); - return; - } - - if (playerName == null || playerName.isEmpty()) { - plugin.getLogger().warning("Attempted to mute player with null or empty name"); + if (uuid == null || playerName == null || playerName.isEmpty()) { + plugin.getLogger().warning("Attempted to mute player with invalid data"); return; } long endTime = duration > 0 ? System.currentTimeMillis() + duration : 0; MuteData data = new MuteData(uuid, playerName, endTime, reason, mutedBy); + mutes.put(uuid, data); - - historyManager.addEntry(uuid, playerName, duration, reason, mutedBy); + addToHistory(uuid, playerName, duration, reason, mutedBy); + + // Мутим в голосовом чате + muteVoiceChat(uuid, true); + saveAsync(); plugin.getLogger().info("Player " + playerName + " muted by " + mutedBy + @@ -63,7 +77,11 @@ public class MuteServiceImpl implements MuteService { MuteData removed = mutes.remove(uuid); if (removed != null) { - historyManager.updateUnmute(uuid, unmutedBy); + updateUnmuteInHistory(uuid, unmutedBy); + + // Размутим в голосовом чате + muteVoiceChat(uuid, false); + saveAsync(); plugin.getLogger().info("Player " + removed.getPlayerName() + " unmuted by " + unmutedBy); return true; @@ -80,13 +98,10 @@ public class MuteServiceImpl implements MuteService { if (!isExpired(data)) return true; - // Атомарно удаляем только если значение не изменилось между get() и remove(). - // ConcurrentHashMap.remove(key, value) удаляет запись лишь когда она точно та же. - boolean removed = mutes.remove(uuid, data); - if (removed) { - saveAsync(); - plugin.getLogger().info("Mute expired for player " + data.getPlayerName()); - } + mutes.remove(uuid); + muteVoiceChat(uuid, false); + saveAsync(); + plugin.getLogger().info("Mute expired for player " + data.getPlayerName()); return false; } @@ -98,12 +113,8 @@ public class MuteServiceImpl implements MuteService { @Override public long getRemainingTime(UUID uuid) { MuteData data = mutes.get(uuid); - if (data == null) { - return 0; - } - if (data.getEndTime() == 0) { - return -1; - } + if (data == null) return 0; + if (data.getEndTime() == 0) return -1; return Math.max(0, data.getEndTime() - System.currentTimeMillis()); } @@ -119,9 +130,7 @@ public class MuteServiceImpl implements MuteService { @Override public long getMaxDuration(Player player) { - if (player.hasPermission("lochat.mute.dur.perm")) { - return 0; - } + 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) { @@ -129,54 +138,44 @@ public class MuteServiceImpl implements MuteService { return parseTime(dur); } } - return -1; } @Override public boolean canMuteForDuration(Player player, long duration) { - if (player.hasPermission("lochat.mute.dur.perm")) { - return true; - } - - if (duration == 0) { - return false; - } + if (player.hasPermission("lochat.mute.dur.perm")) return true; + if (duration == 0) return false; long maxDuration = getMaxDuration(player); - if (maxDuration == -1) { - return false; - } - if (maxDuration == 0) { - return true; - } + if (maxDuration == -1) return false; + if (maxDuration == 0) return true; return duration <= maxDuration; } @Override public List getPlayerHistory(UUID uuid) { - return historyManager.getHistory(uuid); + return history.getOrDefault(uuid, new ArrayList<>()); } @Override public List getMutesByOperator(String operatorName) { List result = new ArrayList<>(); - mutes.keySet().forEach(uuid -> { - historyManager.getHistory(uuid).stream() - .filter(entry -> entry.mutedBy != null && entry.mutedBy.equalsIgnoreCase(operatorName)) - .forEach(result::add); - }); + history.values().forEach(entries -> + entries.stream() + .filter(e -> e.mutedBy != null && e.mutedBy.equalsIgnoreCase(operatorName)) + .forEach(result::add) + ); result.sort((a, b) -> Long.compare(b.mutedAt, a.mutedAt)); return result; } @Override public UUID getUUIDByName(String name) { - for (UUID uuid : mutes.keySet()) { - for (MuteData.MuteHistoryEntry entry : historyManager.getHistory(uuid)) { - if (entry.playerName != null && entry.playerName.equalsIgnoreCase(name)) { - return uuid; + for (Map.Entry> entry : history.entrySet()) { + for (MuteData.MuteHistoryEntry e : entry.getValue()) { + if (e.playerName != null && e.playerName.equalsIgnoreCase(name)) { + return entry.getKey(); } } } @@ -185,7 +184,7 @@ public class MuteServiceImpl implements MuteService { @Override public Map getActiveMutes() { - mutes.entrySet().removeIf(entry -> isExpired(entry.getValue())); + mutes.entrySet().removeIf(e -> isExpired(e.getValue())); return new HashMap<>(mutes); } @@ -200,54 +199,60 @@ public class MuteServiceImpl implements MuteService { @Override public void save() { + // TODO: Implement save to file + } + + // ========== Voice Chat Integration ========== + + private void muteVoiceChat(UUID uuid, boolean mute) { + if (voicechatService == null) return; + try { - ensureDataFolderExists(); - saveMutes(); - historyManager.save(); - } catch (IOException e) { - plugin.getLogger().warning("Error saving mutes: " + e.getMessage()); + Player player = Bukkit.getPlayer(uuid); + if (player != null && player.isOnline()) { + // Используем рефлексию для вызова метода + voicechatService.getClass() + .getMethod("getVoicechat") + .invoke(voicechatService) + .getClass() + .getMethod("setMuted", UUID.class, boolean.class) + .invoke(voicechatService.getClass().getMethod("getVoicechat").invoke(voicechatService), + player.getUniqueId(), mute); + } + } catch (Exception e) { + plugin.getLogger().warning("Failed to " + (mute ? "mute" : "unmute") + + " player in voice chat: " + e.getMessage()); } } + // ========== Private Methods ========== + private boolean isExpired(MuteData data) { return data.getEndTime() > 0 && System.currentTimeMillis() > data.getEndTime(); } - private void ensureDataFolderExists() { - if (!dataFile.getParentFile().exists()) { - dataFile.getParentFile().mkdirs(); - } + private void addToHistory(UUID uuid, String playerName, long duration, String reason, String mutedBy) { + List entries = history.computeIfAbsent(uuid, k -> new ArrayList<>()); + entries.add(new MuteData.MuteHistoryEntry( + playerName, duration, reason, mutedBy, System.currentTimeMillis(), false, null, 0 + )); } - private void saveMutes() throws IOException { - Map toSave = new ConcurrentHashMap<>(); - mutes.forEach((uuid, data) -> toSave.put(uuid.toString(), data)); - - try (Writer writer = new FileWriter(dataFile)) { - gson.toJson(toSave, writer); + private void updateUnmuteInHistory(UUID uuid, String unmutedBy) { + List entries = history.get(uuid); + if (entries != null && !entries.isEmpty()) { + MuteData.MuteHistoryEntry last = entries.get(entries.size() - 1); + if (!last.unmuted) { + entries.set(entries.size() - 1, new MuteData.MuteHistoryEntry( + last.playerName, last.duration, last.reason, last.mutedBy, last.mutedAt, + true, unmutedBy, System.currentTimeMillis() + )); + } } } 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("Error loading mutes: " + e.getMessage()); - } + // TODO: Implement load from file } private void saveAsync() { diff --git a/src/main/java/com/loki/lochat/core/service/PMServiceImpl.java b/src/main/java/com/loki/lochat/core/service/PMServiceImpl.java deleted file mode 100644 index 84a2f71..0000000 --- a/src/main/java/com/loki/lochat/core/service/PMServiceImpl.java +++ /dev/null @@ -1,35 +0,0 @@ -package com.loki.lochat.core.service; - -import com.loki.lochat.api.service.PMService; - -import java.util.Map; -import java.util.Optional; -import java.util.UUID; -import java.util.concurrent.ConcurrentHashMap; - -/** - * Реализация сервиса личных сообщений - */ -public class PMServiceImpl implements PMService { - private final Map lastConversation = new ConcurrentHashMap<>(); - - @Override - public void setLastConversation(UUID player, UUID target) { - lastConversation.put(player, target); - } - - @Override - public Optional getLastConversation(UUID player) { - return Optional.ofNullable(lastConversation.get(player)); - } - - @Override - public void removeConversation(UUID player) { - lastConversation.remove(player); - } - - @Override - public boolean hasConversation(UUID player) { - return lastConversation.containsKey(player); - } -} diff --git a/src/main/java/com/loki/lochat/core/service/PlayerDataServiceImpl.java b/src/main/java/com/loki/lochat/core/service/PlayerServiceImpl.java similarity index 57% rename from src/main/java/com/loki/lochat/core/service/PlayerDataServiceImpl.java rename to src/main/java/com/loki/lochat/core/service/PlayerServiceImpl.java index 35de771..060f846 100644 --- a/src/main/java/com/loki/lochat/core/service/PlayerDataServiceImpl.java +++ b/src/main/java/com/loki/lochat/core/service/PlayerServiceImpl.java @@ -1,8 +1,7 @@ package com.loki.lochat.core.service; -import com.loki.lochat.api.service.CooldownService; -import com.loki.lochat.api.service.PlayerDataService; -import com.loki.lochat.util.FoliaUtil; +import com.loki.lochat.api.service.PlayerService; +import com.loki.lochat.utils.FoliaUtil; import org.bukkit.configuration.file.YamlConfiguration; import org.bukkit.plugin.java.JavaPlugin; @@ -14,61 +13,100 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicLong; /** - * Реализация сервиса данных игроков. - * Хранит и сохраняет статистику сообщений в statistics.yml. + * Объединённая реализация сервиса данных игроков + * Включает: Cooldown, PlayerData (статистика) */ -public class PlayerDataServiceImpl implements PlayerDataService { - +public class PlayerServiceImpl implements PlayerService { + private final JavaPlugin plugin; - private final CooldownService cooldownService; - - // Счётчики сообщений per-player (сбрасываются при выходе, сохраняются в файл) + + // Cooldown state + private final Map globalCooldowns = new ConcurrentHashMap<>(); + private final Map localCooldowns = new ConcurrentHashMap<>(); + + // Statistics state private final Map playerMessages = new ConcurrentHashMap<>(); - // Глобальные счётчики по типу чата - private final AtomicLong globalChatCount = new AtomicLong(0); - private final AtomicLong localChatCount = new AtomicLong(0); - private final AtomicLong pmCount = new AtomicLong(0); - private final AtomicLong totalMessages = new AtomicLong(0); - + private final AtomicLong globalChatCount = new AtomicLong(0); + private final AtomicLong localChatCount = new AtomicLong(0); + private final AtomicLong pmCount = new AtomicLong(0); + private final AtomicLong totalMessages = new AtomicLong(0); + private final File statsFile; private YamlConfiguration stats; - - public PlayerDataServiceImpl(JavaPlugin plugin, CooldownService cooldownService) { + + public PlayerServiceImpl(JavaPlugin plugin) { this.plugin = plugin; - this.cooldownService = cooldownService; this.statsFile = new File(plugin.getDataFolder(), "data/statistics.yml"); load(); } - - // ── Публичные методы статистики ──────────────────────────────────────────── - - /** Вызывать при каждом сообщении в чате */ + + // ========== Cooldown Implementation ========== + + @Override + public boolean isOnCooldown(UUID player, String type, int cooldownSeconds) { + Map cooldowns = getCooldownMap(type); + Long lastTime = cooldowns.get(player); + + if (lastTime == null) return false; + + long elapsed = (System.currentTimeMillis() - lastTime) / 1000; + return elapsed < cooldownSeconds; + } + + @Override + public int getRemainingCooldown(UUID player, String type, int cooldownSeconds) { + Map cooldowns = getCooldownMap(type); + Long lastTime = cooldowns.get(player); + + if (lastTime == null) return 0; + + long elapsed = (System.currentTimeMillis() - lastTime) / 1000; + return Math.max(0, cooldownSeconds - (int) elapsed); + } + + @Override + public void setCooldown(UUID player, String type) { + getCooldownMap(type).put(player, System.currentTimeMillis()); + } + + @Override + public void removeCooldown(UUID player) { + globalCooldowns.remove(player); + localCooldowns.remove(player); + } + + private Map getCooldownMap(String type) { + return type.equals("global") ? globalCooldowns : localCooldowns; + } + + // ========== Statistics Implementation ========== + + @Override public void recordMessage(UUID player, String chatType) { playerMessages.compute(player, (k, v) -> (v == null ? 0L : v) + 1L); totalMessages.incrementAndGet(); switch (chatType) { case "global" -> globalChatCount.incrementAndGet(); - case "local" -> localChatCount.incrementAndGet(); - case "pm" -> pmCount.incrementAndGet(); + case "local" -> localChatCount.incrementAndGet(); + case "pm" -> pmCount.incrementAndGet(); } } - + + @Override public long getPlayerMessages(UUID player) { return playerMessages.getOrDefault(player, 0L); } - - // ── PlayerDataService ────────────────────────────────────────────────────── - + @Override public void clearPlayerData(UUID player) { - cooldownService.removeCooldown(player); + removeCooldown(player); // Сохраняем статистику игрока в файл при выходе long msgs = playerMessages.getOrDefault(player, 0L); if (msgs > 0) { FoliaUtil.runAsync(plugin, () -> persistPlayerMessages(player, msgs)); } } - + @Override public void saveAll() { try { @@ -88,9 +126,9 @@ public class PlayerDataServiceImpl implements PlayerDataService { plugin.getLogger().warning("[LoChat] Failed to save statistics: " + e.getMessage()); } } - - // ── Внутренние методы ────────────────────────────────────────────────────── - + + // ========== Private Methods ========== + private void persistPlayerMessages(UUID uuid, long count) { try { String path = "top-players.by-messages." + uuid; @@ -103,7 +141,7 @@ public class PlayerDataServiceImpl implements PlayerDataService { plugin.getLogger().warning("[LoChat] Failed to persist player stats: " + e.getMessage()); } } - + private void load() { ensureDir(); if (!statsFile.exists()) { @@ -111,7 +149,7 @@ public class PlayerDataServiceImpl implements PlayerDataService { } stats = YamlConfiguration.loadConfiguration(statsFile); } - + private void ensureDir() { File dir = statsFile.getParentFile(); if (!dir.exists()) dir.mkdirs(); diff --git a/src/main/java/com/loki/lochat/core/service/PunishmentServiceImpl.java b/src/main/java/com/loki/lochat/core/service/PunishmentServiceImpl.java index b6e9aa3..030b86c 100644 --- a/src/main/java/com/loki/lochat/core/service/PunishmentServiceImpl.java +++ b/src/main/java/com/loki/lochat/core/service/PunishmentServiceImpl.java @@ -7,7 +7,7 @@ import com.loki.lochat.api.service.PunishmentService; import com.loki.lochat.config.MessagesConfig; import com.loki.lochat.data.model.BanRecord; import com.loki.lochat.data.model.WarnEntry; -import com.loki.lochat.util.FoliaUtil; +import com.loki.lochat.utils.FoliaUtil; import com.loki.lochat.utils.ChatFormatter; import com.loki.lochat.utils.TimeFormatter; import net.kyori.adventure.text.Component; diff --git a/src/main/java/com/loki/lochat/core/service/SpyServiceImpl.java b/src/main/java/com/loki/lochat/core/service/SpyServiceImpl.java deleted file mode 100644 index 65c48ee..0000000 --- a/src/main/java/com/loki/lochat/core/service/SpyServiceImpl.java +++ /dev/null @@ -1,80 +0,0 @@ -package com.loki.lochat.core.service; - -import com.loki.lochat.api.service.SpyService; -import com.loki.lochat.config.MessageConfig; -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.entity.Player; -import java.util.Set; -import java.util.UUID; -import java.util.concurrent.ConcurrentHashMap; - -/** - * Реализация сервиса шпионажа за чатом - */ -public class SpyServiceImpl implements SpyService { - private final MessageConfig messageConfig; - private final Set spyEnabled = ConcurrentHashMap.newKeySet(); - - public SpyServiceImpl(MessageConfig messageConfig) { - this.messageConfig = messageConfig; - } - - @Override - public boolean toggleSpy(UUID player) { - if (spyEnabled.contains(player)) { - spyEnabled.remove(player); - return false; - } else { - spyEnabled.add(player); - return true; - } - } - - @Override - public boolean isSpying(UUID player) { - return spyEnabled.contains(player); - } - - @Override - public void broadcastPM(Player sender, Player receiver, String message) { - String format = messageConfig.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))); - } - } - } - - @Override - public void sendToSpies(Player sender, Component message, boolean isGlobal) { - if (spyEnabled.isEmpty()) return; - - String chatType = isGlobal ? "Global" : "Local"; - String plainMessage = PlainTextComponentSerializer.plainText().serialize(message); - String format = messageConfig.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))); - } - } - } - - @Override - public void removeSpy(UUID player) { - spyEnabled.remove(player); - } -} diff --git a/src/main/java/com/loki/lochat/core/service/mute/MuteDataStorage.java b/src/main/java/com/loki/lochat/core/service/mute/MuteDataStorage.java new file mode 100644 index 0000000..2986777 --- /dev/null +++ b/src/main/java/com/loki/lochat/core/service/mute/MuteDataStorage.java @@ -0,0 +1,117 @@ +package com.loki.lochat.core.service.mute; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.reflect.TypeToken; +import com.loki.lochat.data.model.MuteData; +import com.loki.lochat.utils.FoliaUtil; +import org.bukkit.plugin.java.JavaPlugin; + +import java.io.*; +import java.lang.reflect.Type; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Хранилище данных о мутах + */ +public class MuteDataStorage { + + private final JavaPlugin plugin; + private final Map mutes = new ConcurrentHashMap<>(); + private final File dataFile; + private final Gson gson = new GsonBuilder().setPrettyPrinting().create(); + + public MuteDataStorage(JavaPlugin plugin) { + this.plugin = plugin; + this.dataFile = new File(plugin.getDataFolder(), "mutes.json"); + } + + public void addMute(UUID uuid, MuteData data) { + mutes.put(uuid, data); + saveAsync(); + } + + public MuteData removeMute(UUID uuid) { + MuteData removed = mutes.remove(uuid); + if (removed != null) { + saveAsync(); + } + return removed; + } + + public boolean isMuted(UUID uuid) { + if (uuid == null) return false; + + MuteData data = mutes.get(uuid); + if (data == null) return false; + + if (!isExpired(data)) return true; + + boolean removed = mutes.remove(uuid, data); + if (removed) { + saveAsync(); + plugin.getLogger().info("Mute expired for player " + data.getPlayerName()); + } + return false; + } + + public MuteData getMute(UUID uuid) { + return mutes.get(uuid); + } + + public Map getAllMutes() { + mutes.entrySet().removeIf(entry -> isExpired(entry.getValue())); + return new HashMap<>(mutes); + } + + private boolean isExpired(MuteData data) { + return data.getEndTime() > 0 && System.currentTimeMillis() > data.getEndTime(); + } + + public 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("Error loading mutes: " + e.getMessage()); + } + } + + public void save() { + try { + ensureDataFolderExists(); + + 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("Error saving mutes: " + e.getMessage()); + } + } + + private void saveAsync() { + FoliaUtil.runAsync(plugin, this::save); + } + + private void ensureDataFolderExists() { + if (!dataFile.getParentFile().exists()) { + dataFile.getParentFile().mkdirs(); + } + } +} diff --git a/src/main/java/com/loki/lochat/core/service/mute/MutePermissionChecker.java b/src/main/java/com/loki/lochat/core/service/mute/MutePermissionChecker.java new file mode 100644 index 0000000..e66a53f --- /dev/null +++ b/src/main/java/com/loki/lochat/core/service/mute/MutePermissionChecker.java @@ -0,0 +1,48 @@ +package com.loki.lochat.core.service.mute; + +import com.loki.lochat.utils.TimeFormatter; +import org.bukkit.entity.Player; + +/** + * Проверка прав на муты + */ +public class MutePermissionChecker { + + private static final String[] DURATIONS = { + "30d", "14d", "7d", "3d", "1d", "12h", "6h", "1h", "30m", "10m" + }; + + public long getMaxDuration(Player player) { + if (player.hasPermission("lochat.mute.dur.perm")) { + return 0; // Permanent + } + + for (String dur : DURATIONS) { + if (player.hasPermission("lochat.mute.dur." + dur)) { + return TimeFormatter.parse(dur); + } + } + + return -1; // No permission + } + + public boolean canMuteForDuration(Player player, long duration) { + if (player.hasPermission("lochat.mute.dur.perm")) { + return true; + } + + if (duration == 0) { + return false; // Permanent mute requires special permission + } + + long maxDuration = getMaxDuration(player); + if (maxDuration == -1) { + return false; // No mute permission + } + if (maxDuration == 0) { + return true; // Can mute permanently + } + + return duration <= maxDuration; + } +} diff --git a/src/main/java/com/loki/lochat/gradient/GradientModule.java b/src/main/java/com/loki/lochat/gradient/GradientModule.java index 29b1f34..4e89c86 100644 --- a/src/main/java/com/loki/lochat/gradient/GradientModule.java +++ b/src/main/java/com/loki/lochat/gradient/GradientModule.java @@ -7,10 +7,10 @@ import com.loki.lochat.gradient.config.GradientConfig; import com.loki.lochat.gradient.config.GradientMessages; import com.loki.lochat.gradient.data.GradientDataManager; import com.loki.lochat.gradient.data.GradientPlayerData; -import com.loki.lochat.gradient.hooks.GradientLuckPermsHook; import com.loki.lochat.gradient.listeners.GradientGUIListener; import com.loki.lochat.gradient.listeners.GradientPlayerListener; -import com.loki.lochat.gradient.util.GradientUtil; +import com.loki.lochat.gradient.service.GradientService; +import com.loki.lochat.gradient.service.PrefixService; import org.black_ixx.playerpoints.PlayerPoints; import org.black_ixx.playerpoints.PlayerPointsAPI; import org.bukkit.Bukkit; @@ -18,7 +18,7 @@ import org.bukkit.entity.Player; import org.bukkit.plugin.java.JavaPlugin; /** - * Модуль градиентных ников (интегрированный LoPreff) + * Модуль градиентных ников - упрощенная версия */ public class GradientModule { @@ -26,21 +26,15 @@ public class GradientModule { private GradientConfig config; private GradientMessages messages; private GradientDataManager dataManager; - private GradientLuckPermsHook luckPermsHook; + private PrefixService prefixService; private PlayerPointsAPI playerPointsAPI; private boolean enabled; public GradientModule(JavaPlugin plugin) { this.plugin = plugin; this.enabled = false; - this.luckPermsHook = null; // Явно инициализируем как null } - /** - * Инициализация модуля - * - * @return true если модуль успешно инициализирован - */ public boolean init() { // Загружаем конфиги config = new GradientConfig(plugin); @@ -51,7 +45,11 @@ public class GradientModule { return false; } - // Проверяем PlayerPoints (опционально теперь) + // Инициализация сервисов + dataManager = new GradientDataManager(plugin, config); + prefixService = new PrefixService(config); + + // PlayerPoints (опционально) if (Bukkit.getPluginManager().isPluginEnabled("PlayerPoints")) { playerPointsAPI = PlayerPoints.getInstance().getAPI(); plugin.getLogger().info("Gradient: PlayerPoints подключен!"); @@ -59,22 +57,13 @@ public class GradientModule { plugin.getLogger().warning("Gradient: PlayerPoints не найден. Покупки будут бесплатными."); } - // Инициализация хранилища - dataManager = new GradientDataManager(plugin, config); - - // Подключение LuckPerms (опционально) - try { - if (Bukkit.getPluginManager().isPluginEnabled("LuckPerms")) { - luckPermsHook = new GradientLuckPermsHook(plugin); - plugin.getLogger().info("Gradient: LuckPerms подключен!"); - } else { - plugin.getLogger().info("Gradient: LuckPerms не найден. Префиксы LuckPerms будут недоступны."); - } - } catch (NoClassDefFoundError e) { + // LuckPerms проверка + if (prefixService.isLuckPermsAvailable()) { + plugin.getLogger().info("Gradient: LuckPerms подключен!"); + } else { plugin.getLogger().info("Gradient: LuckPerms не найден. Префиксы LuckPerms будут недоступны."); } - // Регистрация команд registerCommands(); @@ -107,106 +96,37 @@ public class GradientModule { } } - /** - * Выключение модуля - */ public void shutdown() { if (dataManager != null) { dataManager.saveAll(); } } - /** - * Перезагрузка модуля - */ public void reload() { config.reload(); messages.reload(); } + // ========== Публичные методы для получения отформатированных имен ========== + /** - * Получает отформатированное имя игрока с градиентом и LuckPerms префиксом - * Градиент применяется на всю строку целиком (префикс + ник) - * Использует формат &#RRGGBB + * Получает отформатированное имя игрока (префикс + ник с градиентом) */ public String getFormattedName(Player player) { - return getFormattedName(player, false); + if (!enabled) return player.getName(); + + GradientPlayerData data = dataManager.getPlayerData(player.getUniqueId()); + return prefixService.getFullName(player, data); } /** * Получает отформатированное имя для TAB плагина - * Использует формат §x§R§R§G§G§B§B */ public String getFormattedNameForTab(Player player) { - return getFormattedName(player, true); - } - - /** - * Получает отформатированное имя игрока с градиентом и LuckPerms префиксом - * - * @param useTabFormat true для §x§... формата (TAB), false для &#RRGGBB - */ - private String getFormattedName(Player player, boolean useTabFormat) { if (!enabled) return player.getName(); - + GradientPlayerData data = dataManager.getPlayerData(player.getUniqueId()); - - // Собираем полную строку: префикс + ник - StringBuilder fullText = new StringBuilder(); - - // LuckPerms префикс - String lpPrefix = null; - if (luckPermsHook != null && luckPermsHook.isEnabled()) { - lpPrefix = luckPermsHook.getActivePrefix(player); - } - - // Определяем какой префикс использовать - if (data.hasPrefix() && data.isPrefixEnabled()) { - // Кастомный префикс - fullText.append(config.getPrefixFormat().replace("{prefix}", data.getPrefix())); - } else if (lpPrefix != null && !lpPrefix.isEmpty()) { - // LuckPerms префикс (убираем его цвета если будем применять градиент) - if (data.hasColors() && data.isColorEnabled() && config.isGradientOnLuckPermsPrefix()) { - fullText.append(stripColors(lpPrefix)); - } else { - // LP префикс со своими цветами, ник отдельно - String nick = data.hasColors() && data.isColorEnabled() - ? applyGradientWithFormat(player.getName(), data.getColors(), useTabFormat) - : player.getName(); - return lpPrefix + nick; - } - } - - // Добавляем ник - fullText.append(player.getName()); - - // Применяем единый градиент на всю строку - if (data.hasColors() && data.isColorEnabled()) { - return applyGradientWithFormat(fullText.toString(), data.getColors(), useTabFormat); - } - - return fullText.toString(); - } - - /** - * Применяет градиент с нужным форматом - */ - private String applyGradientWithFormat(String text, java.util.List colors, boolean useTabFormat) { - if (useTabFormat) { - // Формат §x§R§R§G§G§B§B для TAB - return GradientUtil.applyGradientTabFormat(text, colors); - } else { - // Формат &#RRGGBB для GUI и чата - return GradientUtil.applyGradient(text, colors, true); - } - } - - /** - * Убирает цветовые коды из строки - */ - private String stripColors(String text) { - if (text == null) return ""; - return text.replaceAll("(?i)(§x(§[0-9a-f]){6}|§[0-9a-fk-or]|&[0-9a-fk-or]|&#[0-9a-f]{6}|<[^>]+>)", ""); + return prefixService.getFullNameForTab(player, data); } /** @@ -216,56 +136,31 @@ public class GradientModule { if (!enabled) return player.getName(); GradientPlayerData data = dataManager.getPlayerData(player.getUniqueId()); - if (!data.hasColors() || !data.isColorEnabled()) { - return player.getName(); - } - - return GradientUtil.applyGradient(player.getName(), data.getColors(), config.isUseLegacyRgbFormat()); + return GradientService.applyGradient(player, player.getName(), data); } /** * Получает префикс игрока (кастомный или LuckPerms) с градиентом - * Градиент применяется только на префикс */ public String getPrefix(Player player) { if (!enabled) return ""; - + GradientPlayerData data = dataManager.getPlayerData(player.getUniqueId()); - - // Кастомный префикс - if (data.hasPrefix() && data.isPrefixEnabled()) { - String prefix = config.getPrefixFormat().replace("{prefix}", data.getPrefix()); - if (data.hasColors() && data.isColorEnabled() && config.isGradientOnPrefix()) { - return GradientUtil.applyGradient(prefix, data.getColors(), config.isUseLegacyRgbFormat()); - } - return prefix; - } - - // LuckPerms префикс - if (luckPermsHook != null && luckPermsHook.isEnabled()) { - String lpPrefix = luckPermsHook.getActivePrefix(player); - if (lpPrefix != null && !lpPrefix.isEmpty()) { - if (data.hasColors() && data.isColorEnabled() && config.isGradientOnLuckPermsPrefix()) { - String cleanPrefix = stripColors(lpPrefix); - return GradientUtil.applyGradient(cleanPrefix, data.getColors(), config.isUseLegacyRgbFormat()); - } - return lpPrefix; - } - } - - return ""; + return prefixService.getPrefix(player, data); } /** * Получает только LuckPerms префикс */ public String getLuckPermsPrefix(Player player) { - if (!enabled || luckPermsHook == null || !luckPermsHook.isEnabled()) return ""; - String prefix = luckPermsHook.getActivePrefix(player); - return prefix != null ? prefix : ""; + if (!enabled) return ""; + + GradientPlayerData data = dataManager.getPlayerData(player.getUniqueId()); + return prefixService.getLuckPermsPrefix(player, data); } - // Геттеры + // ========== Геттеры ========== + public JavaPlugin getPlugin() { return plugin; } @@ -282,8 +177,8 @@ public class GradientModule { return dataManager; } - public GradientLuckPermsHook getLuckPermsHook() { - return luckPermsHook; + public PrefixService getPrefixService() { + return prefixService; } public PlayerPointsAPI getPlayerPointsAPI() { diff --git a/src/main/java/com/loki/lochat/gradient/commands/GradientAdminCommand.java b/src/main/java/com/loki/lochat/gradient/commands/GradientAdminCommand.java index cec9059..fb31a20 100644 --- a/src/main/java/com/loki/lochat/gradient/commands/GradientAdminCommand.java +++ b/src/main/java/com/loki/lochat/gradient/commands/GradientAdminCommand.java @@ -1,12 +1,12 @@ package com.loki.lochat.gradient.commands; import com.loki.lochat.gradient.GradientModule; -import com.loki.lochat.gradient.config.GradientConfig; +import com.loki.lochat.gradient.commands.handlers.ColorCommandHandler; +import com.loki.lochat.gradient.commands.handlers.PrefixCommandHandler; import com.loki.lochat.gradient.config.GradientMessages; import com.loki.lochat.gradient.data.GradientPlayerData; import com.loki.lochat.gradient.util.DisplayNameUtil; -import com.loki.lochat.util.FoliaUtil; -import com.loki.lochat.gradient.util.GradientUtil; +import com.loki.lochat.utils.FoliaUtil; import org.bukkit.Bukkit; import org.bukkit.OfflinePlayer; import org.bukkit.command.Command; @@ -22,17 +22,22 @@ import java.util.Arrays; import java.util.List; /** - * Админ команда /aprefix для управления градиентами + * Админ команда /aprefix - упрощенная версия с делегированием */ public class GradientAdminCommand implements CommandExecutor, TabCompleter { private static final List PRESET_COLORS = Arrays.asList( - "#FF0000", "#FF7F00", "#FFFF00", "#00FF00", "#0000FF", "#4B0082", "#9400D3" + "#FF0000", "#FF7F00", "#FFFF00", "#00FF00", "#0000FF", "#4B0082", "#9400D3" ); + private final GradientModule module; + private final ColorCommandHandler colorHandler; + private final PrefixCommandHandler prefixHandler; public GradientAdminCommand(GradientModule module) { this.module = module; + this.colorHandler = new ColorCommandHandler(module); + this.prefixHandler = new PrefixCommandHandler(module); } @Override @@ -64,29 +69,15 @@ public class GradientAdminCommand implements CommandExecutor, TabCompleter { } handleInfo(sender, args[1], msg); } - case "setcolor" -> { - if (args.length < 3) { - sender.sendMessage("§cИспользование: /aprefix setcolor <игрок> <цвет1> [цвет2] ..."); - return true; - } - handleSetColor(sender, args, msg); - } - case "setprefix" -> { - if (args.length < 3) { - sender.sendMessage("§cИспользование: /aprefix setprefix <игрок> <префикс>"); - return true; - } - handleSetPrefix(sender, args, msg); - } - case "coloron" -> handleColorToggle(sender, args, true, msg); - case "coloroff" -> handleColorToggle(sender, args, false, msg); - case "prefixon" -> handlePrefixToggle(sender, args, true, msg); - case "prefixoff" -> handlePrefixToggle(sender, args, false, msg); - case "resetcolor" -> handleResetColor(sender, args, msg); - case "resetprefix" -> handleResetPrefix(sender, args, msg); - case "updateall" -> { - handleUpdateAll(sender, msg); - } + case "setcolor" -> colorHandler.handleSetColor(sender, args, msg); + case "setprefix" -> prefixHandler.handleSetPrefix(sender, args, msg); + case "coloron" -> colorHandler.handleColorToggle(sender, args, true, msg); + case "coloroff" -> colorHandler.handleColorToggle(sender, args, false, msg); + case "prefixon" -> prefixHandler.handlePrefixToggle(sender, args, true, msg); + case "prefixoff" -> prefixHandler.handlePrefixToggle(sender, args, false, msg); + case "resetcolor" -> colorHandler.handleResetColor(sender, args, msg); + case "resetprefix" -> prefixHandler.handleResetPrefix(sender, args, msg); + case "updateall" -> handleUpdateAll(sender, msg); default -> sendHelp(sender); } @@ -104,7 +95,6 @@ public class GradientAdminCommand implements CommandExecutor, TabCompleter { sender.sendMessage("§e/aprefix resetcolor <игрок> §7— сбросить цвет"); sender.sendMessage("§e/aprefix resetprefix <игрок> §7— сбросить префикс"); sender.sendMessage("§e/aprefix updateall §7— обновить display names всех игроков"); - sender.sendMessage("§e/aprefix toggledisplay §7— переключить TextDisplay/обычный режим"); } private void handleInfo(CommandSender sender, String playerName, GradientMessages msg) { @@ -124,220 +114,17 @@ public class GradientAdminCommand implements CommandExecutor, TabCompleter { msg.send(sender, "info-prefix-purchased", "purchased", data.isPrefixPurchased() ? "да" : "нет"); } - private void handleSetColor(CommandSender sender, String[] args, GradientMessages msg) { - OfflinePlayer target = Bukkit.getOfflinePlayer(args[1]); - if (!target.hasPlayedBefore() && !target.isOnline()) { - msg.send(sender, "player-not-found"); - return; - } - - List colors = new ArrayList<>(); - for (int i = 2; i < args.length; i++) { - String color = args[i].startsWith("#") ? args[i] : "#" + args[i]; - if (!GradientUtil.isValidHex(color)) { - msg.send(sender, "invalid-color", "color", args[i]); - return; - } - colors.add(color.toUpperCase()); - } - - GradientConfig cfg = module.getConfig(); - if (colors.size() > cfg.getMaxColors()) { - msg.send(sender, "too-many-colors", "max", String.valueOf(cfg.getMaxColors())); - return; - } - - GradientPlayerData data = module.getDataManager().getPlayerData(target.getUniqueId()); - data.setColors(colors); - data.setColorEnabled(true); - - Player onlineTarget = target.getPlayer(); - if (onlineTarget != null) { - FoliaUtil.runEntityTask(module.getPlugin(), onlineTarget, - () -> DisplayNameUtil.updateDisplayName(module, onlineTarget, data)); - } - - FoliaUtil.runAsync(module.getPlugin(), - () -> module.getDataManager().savePlayerData(target.getUniqueId())); - - sender.sendMessage("§aЦвет для игрока §f" + args[1] + " §aустановлен: §f" + String.join(" ", colors)); - } - - private void handleSetPrefix(CommandSender sender, String[] args, GradientMessages msg) { - OfflinePlayer target = Bukkit.getOfflinePlayer(args[1]); - if (!target.hasPlayedBefore() && !target.isOnline()) { - msg.send(sender, "player-not-found"); - return; - } - - StringBuilder prefixBuilder = new StringBuilder(); - for (int i = 2; i < args.length; i++) { - if (i > 2) prefixBuilder.append(" "); - prefixBuilder.append(args[i]); - } - String prefix = prefixBuilder.toString(); - - GradientConfig cfg = module.getConfig(); - if (prefix.length() > cfg.getMaxPrefixLength()) { - msg.send(sender, "prefix-too-long", "max", String.valueOf(cfg.getMaxPrefixLength())); - return; - } - - GradientPlayerData data = module.getDataManager().getPlayerData(target.getUniqueId()); - data.setPrefix(prefix); - data.setPrefixEnabled(true); - data.setPrefixPurchased(true); - - if (module.getLuckPermsHook() != null) { - module.getLuckPermsHook().setPrefix(target.getUniqueId(), DisplayNameUtil.buildColoredPrefix(module, data)); - } - - Player onlineTarget = target.getPlayer(); - if (onlineTarget != null) { - FoliaUtil.runEntityTask(module.getPlugin(), onlineTarget, - () -> DisplayNameUtil.updateDisplayName(module, onlineTarget, data)); - } - - FoliaUtil.runAsync(module.getPlugin(), - () -> module.getDataManager().savePlayerData(target.getUniqueId())); - - sender.sendMessage("§aПрефикс для игрока §f" + args[1] + " §aустановлен: §f" + prefix); - } - - private void handleColorToggle(CommandSender sender, String[] args, boolean enable, GradientMessages msg) { - if (args.length < 2) { - sender.sendMessage("§cИспользование: /aprefix " + (enable ? "coloron" : "coloroff") + " <игрок>"); - return; - } - - OfflinePlayer target = Bukkit.getOfflinePlayer(args[1]); - if (!target.hasPlayedBefore() && !target.isOnline()) { - msg.send(sender, "player-not-found"); - return; - } - - GradientPlayerData data = module.getDataManager().getPlayerData(target.getUniqueId()); - data.setColorEnabled(enable); - - Player onlineTarget = target.getPlayer(); - if (onlineTarget != null) { - FoliaUtil.runEntityTask(module.getPlugin(), onlineTarget, - () -> DisplayNameUtil.updateDisplayName(module, onlineTarget, data)); - } - - FoliaUtil.runAsync(module.getPlugin(), - () -> module.getDataManager().savePlayerData(target.getUniqueId())); - - sender.sendMessage("§aЦвет для игрока §f" + args[1] + (enable ? " §aвключён" : " §cвыключен")); - } - - private void handlePrefixToggle(CommandSender sender, String[] args, boolean enable, GradientMessages msg) { - if (args.length < 2) { - sender.sendMessage("§cИспользование: /aprefix " + (enable ? "prefixon" : "prefixoff") + " <игрок>"); - return; - } - - OfflinePlayer target = Bukkit.getOfflinePlayer(args[1]); - if (!target.hasPlayedBefore() && !target.isOnline()) { - msg.send(sender, "player-not-found"); - return; - } - - GradientPlayerData data = module.getDataManager().getPlayerData(target.getUniqueId()); - data.setPrefixEnabled(enable); - - if (module.getLuckPermsHook() != null) { - if (enable && data.hasPrefix()) { - module.getLuckPermsHook().setPrefix(target.getUniqueId(), DisplayNameUtil.buildColoredPrefix(module, data)); - } else { - module.getLuckPermsHook().removePrefix(target.getUniqueId()); - } - } - - Player onlineTarget = target.getPlayer(); - if (onlineTarget != null) { - FoliaUtil.runEntityTask(module.getPlugin(), onlineTarget, - () -> DisplayNameUtil.updateDisplayName(module, onlineTarget, data)); - } - - FoliaUtil.runAsync(module.getPlugin(), - () -> module.getDataManager().savePlayerData(target.getUniqueId())); - - sender.sendMessage("§aПрефикс для игрока §f" + args[1] + (enable ? " §aвключён" : " §cвыключен")); - } - - private void handleResetColor(CommandSender sender, String[] args, GradientMessages msg) { - if (args.length < 2) { - sender.sendMessage("§cИспользование: /aprefix resetcolor <игрок>"); - return; - } - - OfflinePlayer target = Bukkit.getOfflinePlayer(args[1]); - if (!target.hasPlayedBefore() && !target.isOnline()) { - msg.send(sender, "player-not-found"); - return; - } - - GradientPlayerData data = module.getDataManager().getPlayerData(target.getUniqueId()); - data.setColors(new ArrayList<>()); - data.setColorEnabled(false); - - Player onlineTarget = target.getPlayer(); - if (onlineTarget != null) { - FoliaUtil.runEntityTask(module.getPlugin(), onlineTarget, - () -> DisplayNameUtil.updateDisplayName(module, onlineTarget, data)); - } - - FoliaUtil.runAsync(module.getPlugin(), - () -> module.getDataManager().savePlayerData(target.getUniqueId())); - - sender.sendMessage("§aЦвет для игрока §f" + args[1] + " §aсброшен"); - } - - private void handleResetPrefix(CommandSender sender, String[] args, GradientMessages msg) { - if (args.length < 2) { - sender.sendMessage("§cИспользование: /aprefix resetprefix <игрок>"); - return; - } - - OfflinePlayer target = Bukkit.getOfflinePlayer(args[1]); - if (!target.hasPlayedBefore() && !target.isOnline()) { - msg.send(sender, "player-not-found"); - return; - } - - GradientPlayerData data = module.getDataManager().getPlayerData(target.getUniqueId()); - data.setPrefix(null); - data.setPrefixEnabled(false); - - if (module.getLuckPermsHook() != null) { - module.getLuckPermsHook().removePrefix(target.getUniqueId()); - } - - Player onlineTarget = target.getPlayer(); - if (onlineTarget != null) { - FoliaUtil.runEntityTask(module.getPlugin(), onlineTarget, - () -> DisplayNameUtil.updateDisplayName(module, onlineTarget, data)); - } - - FoliaUtil.runAsync(module.getPlugin(), - () -> module.getDataManager().savePlayerData(target.getUniqueId())); - - sender.sendMessage("§aПрефикс для игрока §f" + args[1] + " §aсброшен"); - } - private void handleUpdateAll(CommandSender sender, GradientMessages msg) { int count = 0; for (Player player : Bukkit.getOnlinePlayers()) { GradientPlayerData data = module.getDataManager().getPlayerData(player.getUniqueId()); FoliaUtil.runEntityTask(module.getPlugin(), player, - () -> DisplayNameUtil.updateDisplayName(module, player, data)); + () -> DisplayNameUtil.updateDisplayName(module, player, data)); count++; } sender.sendMessage("§aОбновлены display names для §f" + count + " §aигроков"); } - @Override public @Nullable List onTabComplete(@NotNull CommandSender sender, @NotNull Command command, @NotNull String alias, @NotNull String[] args) { diff --git a/src/main/java/com/loki/lochat/gradient/commands/GradientColorCommand.java b/src/main/java/com/loki/lochat/gradient/commands/GradientColorCommand.java index 0529e26..cc54351 100644 --- a/src/main/java/com/loki/lochat/gradient/commands/GradientColorCommand.java +++ b/src/main/java/com/loki/lochat/gradient/commands/GradientColorCommand.java @@ -6,7 +6,7 @@ import com.loki.lochat.gradient.config.GradientMessages; import com.loki.lochat.gradient.data.GradientPlayerData; import com.loki.lochat.gradient.gui.GradientConfirmGUI; import com.loki.lochat.gradient.util.DisplayNameUtil; -import com.loki.lochat.util.FoliaUtil; +import com.loki.lochat.utils.FoliaUtil; import com.loki.lochat.gradient.util.GradientConstants; import com.loki.lochat.gradient.util.GradientUtil; import org.bukkit.Bukkit; diff --git a/src/main/java/com/loki/lochat/gradient/commands/GradientPrefixCommand.java b/src/main/java/com/loki/lochat/gradient/commands/GradientPrefixCommand.java index b5ee511..d383b50 100644 --- a/src/main/java/com/loki/lochat/gradient/commands/GradientPrefixCommand.java +++ b/src/main/java/com/loki/lochat/gradient/commands/GradientPrefixCommand.java @@ -6,7 +6,7 @@ import com.loki.lochat.gradient.config.GradientMessages; import com.loki.lochat.gradient.data.GradientPlayerData; import com.loki.lochat.gradient.gui.GradientConfirmGUI; import com.loki.lochat.gradient.util.DisplayNameUtil; -import com.loki.lochat.util.FoliaUtil; +import com.loki.lochat.utils.FoliaUtil; import com.loki.lochat.gradient.util.GradientConstants; import org.bukkit.command.Command; import org.bukkit.command.CommandExecutor; @@ -83,9 +83,6 @@ public class GradientPrefixCommand implements CommandExecutor, TabCompleter { } data.setPrefixEnabled(true); - if (module.getLuckPermsHook() != null) { - module.getLuckPermsHook().setPrefix(player, DisplayNameUtil.buildColoredPrefix(module, data)); - } saveAndUpdate(player, data); msg.send(player, "prefix-enabled"); return true; @@ -94,9 +91,6 @@ public class GradientPrefixCommand implements CommandExecutor, TabCompleter { private boolean handleOff(Player player, String[] args) { GradientPlayerData data = module.getDataManager().getPlayerData(player.getUniqueId()); data.setPrefixEnabled(false); - if (module.getLuckPermsHook() != null) { - module.getLuckPermsHook().removePrefix(player); - } saveAndUpdate(player, data); module.getMessages().send(player, "prefix-disabled"); return true; @@ -113,9 +107,6 @@ public class GradientPrefixCommand implements CommandExecutor, TabCompleter { data.setPrefix(null); data.setPrefixEnabled(false); - if (module.getLuckPermsHook() != null) { - module.getLuckPermsHook().removePrefix(player); - } saveAndUpdate(player, data); msg.send(player, "prefix-reset-success"); return true; diff --git a/src/main/java/com/loki/lochat/gradient/commands/handlers/ColorCommandHandler.java b/src/main/java/com/loki/lochat/gradient/commands/handlers/ColorCommandHandler.java new file mode 100644 index 0000000..79036f1 --- /dev/null +++ b/src/main/java/com/loki/lochat/gradient/commands/handlers/ColorCommandHandler.java @@ -0,0 +1,129 @@ +package com.loki.lochat.gradient.commands.handlers; + +import com.loki.lochat.gradient.GradientModule; +import com.loki.lochat.gradient.config.GradientMessages; +import com.loki.lochat.gradient.data.GradientPlayerData; +import com.loki.lochat.gradient.util.DisplayNameUtil; +import com.loki.lochat.gradient.util.GradientUtil; +import com.loki.lochat.utils.FoliaUtil; +import org.bukkit.Bukkit; +import org.bukkit.OfflinePlayer; +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; + +import java.util.ArrayList; +import java.util.List; + +/** + * Обработчик команд для управления цветами + */ +public class ColorCommandHandler { + + private final GradientModule module; + + public ColorCommandHandler(GradientModule module) { + this.module = module; + } + + /** + * Установить цвет игроку + */ + public void handleSetColor(CommandSender sender, String[] args, GradientMessages msg) { + if (args.length < 3) { + sender.sendMessage("§cИспользование: /aprefix setcolor <игрок> <цвет1> [цвет2] ..."); + return; + } + + OfflinePlayer target = Bukkit.getOfflinePlayer(args[1]); + if (!target.hasPlayedBefore() && !target.isOnline()) { + msg.send(sender, "player-not-found"); + return; + } + + List colors = new ArrayList<>(); + for (int i = 2; i < args.length; i++) { + String color = args[i].startsWith("#") ? args[i] : "#" + args[i]; + if (!GradientUtil.isValidHex(color)) { + msg.send(sender, "invalid-color", "color", args[i]); + return; + } + colors.add(color.toUpperCase()); + } + + if (colors.size() > module.getConfig().getMaxColors()) { + msg.send(sender, "too-many-colors", "max", String.valueOf(module.getConfig().getMaxColors())); + return; + } + + GradientPlayerData data = module.getDataManager().getPlayerData(target.getUniqueId()); + data.setColors(colors); + data.setColorEnabled(true); + + updatePlayerDisplay(target, data); + savePlayerData(target); + + sender.sendMessage("§aЦвет для игрока §f" + args[1] + " §aустановлен: §f" + String.join(" ", colors)); + } + + /** + * Включить/выключить цвет + */ + public void handleColorToggle(CommandSender sender, String[] args, boolean enable, GradientMessages msg) { + if (args.length < 2) { + sender.sendMessage("§cИспользование: /aprefix " + (enable ? "coloron" : "coloroff") + " <игрок>"); + return; + } + + OfflinePlayer target = Bukkit.getOfflinePlayer(args[1]); + if (!target.hasPlayedBefore() && !target.isOnline()) { + msg.send(sender, "player-not-found"); + return; + } + + GradientPlayerData data = module.getDataManager().getPlayerData(target.getUniqueId()); + data.setColorEnabled(enable); + + updatePlayerDisplay(target, data); + savePlayerData(target); + + sender.sendMessage("§aЦвет для игрока §f" + args[1] + (enable ? " §aвключён" : " §cвыключен")); + } + + /** + * Сбросить цвет + */ + public void handleResetColor(CommandSender sender, String[] args, GradientMessages msg) { + if (args.length < 2) { + sender.sendMessage("§cИспользование: /aprefix resetcolor <игрок>"); + return; + } + + OfflinePlayer target = Bukkit.getOfflinePlayer(args[1]); + if (!target.hasPlayedBefore() && !target.isOnline()) { + msg.send(sender, "player-not-found"); + return; + } + + GradientPlayerData data = module.getDataManager().getPlayerData(target.getUniqueId()); + data.setColors(new ArrayList<>()); + data.setColorEnabled(false); + + updatePlayerDisplay(target, data); + savePlayerData(target); + + sender.sendMessage("§aЦвет для игрока §f" + args[1] + " §aсброшен"); + } + + private void updatePlayerDisplay(OfflinePlayer target, GradientPlayerData data) { + Player onlineTarget = target.getPlayer(); + if (onlineTarget != null) { + FoliaUtil.runEntityTask(module.getPlugin(), onlineTarget, + () -> DisplayNameUtil.updateDisplayName(module, onlineTarget, data)); + } + } + + private void savePlayerData(OfflinePlayer target) { + FoliaUtil.runAsync(module.getPlugin(), + () -> module.getDataManager().savePlayerData(target.getUniqueId())); + } +} diff --git a/src/main/java/com/loki/lochat/gradient/commands/handlers/PrefixCommandHandler.java b/src/main/java/com/loki/lochat/gradient/commands/handlers/PrefixCommandHandler.java new file mode 100644 index 0000000..fca72d6 --- /dev/null +++ b/src/main/java/com/loki/lochat/gradient/commands/handlers/PrefixCommandHandler.java @@ -0,0 +1,123 @@ +package com.loki.lochat.gradient.commands.handlers; + +import com.loki.lochat.gradient.GradientModule; +import com.loki.lochat.gradient.config.GradientMessages; +import com.loki.lochat.gradient.data.GradientPlayerData; +import com.loki.lochat.gradient.util.DisplayNameUtil; +import com.loki.lochat.utils.FoliaUtil; +import org.bukkit.Bukkit; +import org.bukkit.OfflinePlayer; +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; + +/** + * Обработчик команд для управления префиксами + */ +public class PrefixCommandHandler { + + private final GradientModule module; + + public PrefixCommandHandler(GradientModule module) { + this.module = module; + } + + /** + * Установить префикс игроку + */ + public void handleSetPrefix(CommandSender sender, String[] args, GradientMessages msg) { + if (args.length < 3) { + sender.sendMessage("§cИспользование: /aprefix setprefix <игрок> <префикс>"); + return; + } + + OfflinePlayer target = Bukkit.getOfflinePlayer(args[1]); + if (!target.hasPlayedBefore() && !target.isOnline()) { + msg.send(sender, "player-not-found"); + return; + } + + StringBuilder prefixBuilder = new StringBuilder(); + for (int i = 2; i < args.length; i++) { + if (i > 2) prefixBuilder.append(" "); + prefixBuilder.append(args[i]); + } + String prefix = prefixBuilder.toString(); + + if (prefix.length() > module.getConfig().getMaxPrefixLength()) { + msg.send(sender, "prefix-too-long", "max", String.valueOf(module.getConfig().getMaxPrefixLength())); + return; + } + + GradientPlayerData data = module.getDataManager().getPlayerData(target.getUniqueId()); + data.setPrefix(prefix); + data.setPrefixEnabled(true); + data.setPrefixPurchased(true); + + updatePlayerDisplay(target, data); + savePlayerData(target); + + sender.sendMessage("§aПрефикс для игрока §f" + args[1] + " §aустановлен: §f" + prefix); + } + + /** + * Включить/выключить префикс + */ + public void handlePrefixToggle(CommandSender sender, String[] args, boolean enable, GradientMessages msg) { + if (args.length < 2) { + sender.sendMessage("§cИспользование: /aprefix " + (enable ? "prefixon" : "prefixoff") + " <игрок>"); + return; + } + + OfflinePlayer target = Bukkit.getOfflinePlayer(args[1]); + if (!target.hasPlayedBefore() && !target.isOnline()) { + msg.send(sender, "player-not-found"); + return; + } + + GradientPlayerData data = module.getDataManager().getPlayerData(target.getUniqueId()); + data.setPrefixEnabled(enable); + + updatePlayerDisplay(target, data); + savePlayerData(target); + + sender.sendMessage("§aПрефикс для игрока §f" + args[1] + (enable ? " §aвключён" : " §cвыключен")); + } + + /** + * Сбросить префикс + */ + public void handleResetPrefix(CommandSender sender, String[] args, GradientMessages msg) { + if (args.length < 2) { + sender.sendMessage("§cИспользование: /aprefix resetprefix <игрок>"); + return; + } + + OfflinePlayer target = Bukkit.getOfflinePlayer(args[1]); + if (!target.hasPlayedBefore() && !target.isOnline()) { + msg.send(sender, "player-not-found"); + return; + } + + GradientPlayerData data = module.getDataManager().getPlayerData(target.getUniqueId()); + data.setPrefix(null); + data.setPrefixEnabled(false); + + updatePlayerDisplay(target, data); + savePlayerData(target); + + sender.sendMessage("§aПрефикс для игрока §f" + args[1] + " §aсброшен"); + } + + private void updatePlayerDisplay(OfflinePlayer target, GradientPlayerData data) { + Player onlineTarget = target.getPlayer(); + if (onlineTarget != null) { + FoliaUtil.runEntityTask(module.getPlugin(), onlineTarget, + () -> DisplayNameUtil.updateDisplayName(module, onlineTarget, data)); + } + } + + private void savePlayerData(OfflinePlayer target) { + FoliaUtil.runAsync(module.getPlugin(), + () -> module.getDataManager().savePlayerData(target.getUniqueId())); + } +} diff --git a/src/main/java/com/loki/lochat/gradient/gui/GradientConfirmGUI.java b/src/main/java/com/loki/lochat/gradient/gui/GradientConfirmGUI.java index 6163663..1c6ba5f 100644 --- a/src/main/java/com/loki/lochat/gradient/gui/GradientConfirmGUI.java +++ b/src/main/java/com/loki/lochat/gradient/gui/GradientConfirmGUI.java @@ -128,8 +128,8 @@ public final class GradientConfirmGUI implements InventoryHolder { boolean useLegacyForPreview = true; // Если нет кастомного префикса - используем LuckPerms префикс - if ((prefixToUse == null || prefixToUse.isEmpty()) && module.getLuckPermsHook() != null && module.getLuckPermsHook().isEnabled()) { - String lpPrefix = module.getLuckPermsHook().getActivePrefix(player); + if ((prefixToUse == null || prefixToUse.isEmpty())) { + String lpPrefix = module.getPrefixService().getLuckPermsPrefix(player, data); if (lpPrefix != null && !lpPrefix.isEmpty()) { // Конвертируем LP префикс в legacy формат lpPrefix = convertToLegacy(lpPrefix); diff --git a/src/main/java/com/loki/lochat/gradient/hooks/GradientLuckPermsHook.java b/src/main/java/com/loki/lochat/gradient/hooks/GradientLuckPermsHook.java deleted file mode 100644 index 7f05069..0000000 --- a/src/main/java/com/loki/lochat/gradient/hooks/GradientLuckPermsHook.java +++ /dev/null @@ -1,95 +0,0 @@ -package com.loki.lochat.gradient.hooks; - -import com.loki.lochat.gradient.util.GradientConstants; -import net.luckperms.api.LuckPerms; -import net.luckperms.api.LuckPermsProvider; -import net.luckperms.api.cacheddata.CachedMetaData; -import net.luckperms.api.model.user.User; -import net.luckperms.api.node.types.PrefixNode; -import org.bukkit.Bukkit; -import org.bukkit.entity.Player; -import org.bukkit.plugin.java.JavaPlugin; - -import java.util.UUID; - -/** - * Хук для LuckPerms - */ -public class GradientLuckPermsHook { - - private LuckPerms luckPerms; - private boolean enabled; - - public GradientLuckPermsHook(JavaPlugin plugin) { - this.enabled = false; - - if (Bukkit.getPluginManager().isPluginEnabled("LuckPerms")) { - try { - this.luckPerms = LuckPermsProvider.get(); - this.enabled = true; - plugin.getLogger().info("Gradient: LuckPerms подключен!"); - } catch (Exception e) { - plugin.getLogger().warning("Gradient: Не удалось подключить LuckPerms: " + e.getMessage()); - } - } - } - - public boolean isEnabled() { - return enabled && luckPerms != null; - } - - public void setPrefix(Player player, String prefix) { - if (!isEnabled()) return; - - User user = luckPerms.getUserManager().getUser(player.getUniqueId()); - if (user == null) return; - - user.data().clear(node -> - node instanceof PrefixNode && ((PrefixNode) node).getPriority() == GradientConstants.LUCKPERMS_PREFIX_PRIORITY - ); - - if (prefix != null && !prefix.isEmpty()) { - PrefixNode prefixNode = PrefixNode.builder(prefix, GradientConstants.LUCKPERMS_PREFIX_PRIORITY).build(); - user.data().add(prefixNode); - } - - luckPerms.getUserManager().saveUser(user); - } - - public void setPrefix(UUID uuid, String prefix) { - if (!isEnabled()) return; - - luckPerms.getUserManager().loadUser(uuid).thenAccept(user -> { - if (user == null) return; - - user.data().clear(node -> - node instanceof PrefixNode && ((PrefixNode) node).getPriority() == GradientConstants.LUCKPERMS_PREFIX_PRIORITY - ); - - if (prefix != null && !prefix.isEmpty()) { - PrefixNode prefixNode = PrefixNode.builder(prefix, GradientConstants.LUCKPERMS_PREFIX_PRIORITY).build(); - user.data().add(prefixNode); - } - - luckPerms.getUserManager().saveUser(user); - }); - } - - public void removePrefix(Player player) { - setPrefix(player, null); - } - - public void removePrefix(UUID uuid) { - setPrefix(uuid, null); - } - - public String getActivePrefix(Player player) { - if (!isEnabled()) return null; - - User user = luckPerms.getUserManager().getUser(player.getUniqueId()); - if (user == null) return null; - - CachedMetaData metaData = user.getCachedData().getMetaData(); - return metaData.getPrefix(); - } -} diff --git a/src/main/java/com/loki/lochat/gradient/listeners/GradientGUIListener.java b/src/main/java/com/loki/lochat/gradient/listeners/GradientGUIListener.java index 5110902..feee460 100644 --- a/src/main/java/com/loki/lochat/gradient/listeners/GradientGUIListener.java +++ b/src/main/java/com/loki/lochat/gradient/listeners/GradientGUIListener.java @@ -5,7 +5,7 @@ import com.loki.lochat.gradient.config.GradientMessages; import com.loki.lochat.gradient.data.GradientPlayerData; import com.loki.lochat.gradient.gui.GradientConfirmGUI; import com.loki.lochat.gradient.util.DisplayNameUtil; -import com.loki.lochat.util.FoliaUtil; +import com.loki.lochat.utils.FoliaUtil; import com.loki.lochat.gradient.util.GradientConstants; import org.bukkit.entity.Player; import org.bukkit.event.EventHandler; @@ -81,9 +81,6 @@ public class GradientGUIListener implements Listener { data.setColors(gui.getColors()); data.setColorEnabled(true); } - if (module.getLuckPermsHook() != null) { - module.getLuckPermsHook().setPrefix(player, DisplayNameUtil.buildColoredPrefix(module, data)); - } msg.send(player, price > 0 ? "prefix-success" : "prefix-success-free", "price", String.valueOf(price)); } diff --git a/src/main/java/com/loki/lochat/gradient/listeners/GradientPlayerListener.java b/src/main/java/com/loki/lochat/gradient/listeners/GradientPlayerListener.java index 4f2ed44..a125a28 100644 --- a/src/main/java/com/loki/lochat/gradient/listeners/GradientPlayerListener.java +++ b/src/main/java/com/loki/lochat/gradient/listeners/GradientPlayerListener.java @@ -1,7 +1,7 @@ package com.loki.lochat.gradient.listeners; import com.loki.lochat.gradient.GradientModule; -import com.loki.lochat.util.FoliaUtil; +import com.loki.lochat.utils.FoliaUtil; import org.bukkit.entity.Player; import org.bukkit.event.EventHandler; import org.bukkit.event.EventPriority; diff --git a/src/main/java/com/loki/lochat/gradient/service/GradientService.java b/src/main/java/com/loki/lochat/gradient/service/GradientService.java new file mode 100644 index 0000000..46bb02b --- /dev/null +++ b/src/main/java/com/loki/lochat/gradient/service/GradientService.java @@ -0,0 +1,57 @@ +package com.loki.lochat.gradient.service; + +import com.loki.lochat.gradient.data.GradientPlayerData; +import com.loki.lochat.gradient.util.GradientUtil; +import org.bukkit.entity.Player; + +import java.util.List; + +/** + * Сервис для применения градиентов - единая точка входа + */ +public class GradientService { + + /** + * Применяет градиент к тексту если у игрока есть цвета + */ + public static String applyGradient(Player player, String text, GradientPlayerData data) { + if (data == null || !data.hasColors() || !data.isColorEnabled()) { + return text; + } + + return GradientUtil.applyGradient(text, data.getColors(), true); + } + + /** + * Применяет градиент к тексту с указанными цветами + */ + public static String applyGradient(String text, List colors) { + if (colors == null || colors.isEmpty()) { + return text; + } + + return GradientUtil.applyGradient(text, colors, true); + } + + /** + * Применяет градиент в формате для TAB плагина + */ + public static String applyGradientForTab(String text, List colors) { + if (colors == null || colors.isEmpty()) { + return text; + } + + return GradientUtil.applyGradientTabFormat(text, colors); + } + + /** + * Убирает все цветовые коды из текста + */ + public static String stripColors(String text) { + if (text == null || text.isEmpty()) { + return ""; + } + + return text.replaceAll("(?i)(§x(§[0-9a-f]){6}|§[0-9a-fk-or]|&[0-9a-fk-or]|&#[0-9a-f]{6}|<[^>]+>)", ""); + } +} diff --git a/src/main/java/com/loki/lochat/gradient/service/PrefixService.java b/src/main/java/com/loki/lochat/gradient/service/PrefixService.java new file mode 100644 index 0000000..dbd278e --- /dev/null +++ b/src/main/java/com/loki/lochat/gradient/service/PrefixService.java @@ -0,0 +1,114 @@ +package com.loki.lochat.gradient.service; + +import com.loki.lochat.gradient.config.GradientConfig; +import com.loki.lochat.gradient.data.GradientPlayerData; +import net.luckperms.api.LuckPerms; +import net.luckperms.api.LuckPermsProvider; +import net.luckperms.api.cacheddata.CachedMetaData; +import net.luckperms.api.model.user.User; +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; + +/** + * Сервис для работы с префиксами - упрощенная интеграция с LuckPerms + */ +public class PrefixService { + + private final GradientConfig config; + private final boolean luckPermsAvailable; + + public PrefixService(GradientConfig config) { + this.config = config; + this.luckPermsAvailable = Bukkit.getPluginManager().isPluginEnabled("LuckPerms"); + } + + /** + * Получает префикс игрока (кастомный или из LuckPerms) + */ + public String getPrefix(Player player, GradientPlayerData data) { + // Приоритет: кастомный префикс > LuckPerms префикс + if (data.hasPrefix() && data.isPrefixEnabled()) { + String prefix = config.getPrefixFormat().replace("{prefix}", data.getPrefix()); + + // Применяем градиент если нужно + if (data.hasColors() && data.isColorEnabled() && config.isGradientOnPrefix()) { + return GradientService.applyGradient(prefix, data.getColors()); + } + + return prefix; + } + + // Пробуем получить из LuckPerms + return getLuckPermsPrefix(player, data); + } + + /** + * Получает префикс из LuckPerms + */ + public String getLuckPermsPrefix(Player player, GradientPlayerData data) { + if (!luckPermsAvailable) { + return ""; + } + + try { + LuckPerms luckPerms = LuckPermsProvider.get(); + User user = luckPerms.getUserManager().getUser(player.getUniqueId()); + + if (user == null) { + return ""; + } + + CachedMetaData metaData = user.getCachedData().getMetaData(); + String prefix = metaData.getPrefix(); + + if (prefix == null || prefix.isEmpty()) { + return ""; + } + + // Применяем градиент если нужно + if (data != null && data.hasColors() && data.isColorEnabled() && config.isGradientOnLuckPermsPrefix()) { + String cleanPrefix = GradientService.stripColors(prefix); + return GradientService.applyGradient(cleanPrefix, data.getColors()); + } + + return prefix; + + } catch (Exception e) { + return ""; + } + } + + /** + * Получает полное имя игрока: префикс + ник с градиентом + */ + public String getFullName(Player player, GradientPlayerData data) { + String prefix = getPrefix(player, data); + String nick = player.getName(); + + // Применяем градиент к нику если есть цвета + if (data.hasColors() && data.isColorEnabled()) { + nick = GradientService.applyGradient(nick, data.getColors()); + } + + return prefix.isEmpty() ? nick : prefix + " " + nick; + } + + /** + * Получает полное имя для TAB плагина + */ + public String getFullNameForTab(Player player, GradientPlayerData data) { + String prefix = getPrefix(player, data); + String nick = player.getName(); + + // Применяем градиент в формате TAB + if (data.hasColors() && data.isColorEnabled()) { + nick = GradientService.applyGradientForTab(nick, data.getColors()); + } + + return prefix.isEmpty() ? nick : prefix + " " + nick; + } + + public boolean isLuckPermsAvailable() { + return luckPermsAvailable; + } +} diff --git a/src/main/java/com/loki/lochat/gradient/util/DisplayNameUtil.java b/src/main/java/com/loki/lochat/gradient/util/DisplayNameUtil.java index 209068c..2bad26c 100644 --- a/src/main/java/com/loki/lochat/gradient/util/DisplayNameUtil.java +++ b/src/main/java/com/loki/lochat/gradient/util/DisplayNameUtil.java @@ -6,7 +6,6 @@ import com.loki.lochat.gradient.data.GradientPlayerData; import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer; import org.bukkit.entity.Player; -import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -25,46 +24,10 @@ public final class DisplayNameUtil { } public static void updateDisplayName(GradientModule module, Player player, GradientPlayerData data) { - GradientConfig cfg = module.getConfig(); - String prefix = null; - String prefixFormat = cfg.getPrefixFormat(); - - // Определяем префикс: сначала кастомный, потом LuckPerms - if (data.isPrefixEnabled() && data.hasPrefix()) { - // Есть кастомный префикс - prefix = data.getPrefix(); - } else if (module.getLuckPermsHook() != null && module.getLuckPermsHook().isEnabled()) { - // Нет кастомного префикса, проверяем LuckPerms - String lpPrefix = module.getLuckPermsHook().getActivePrefix(player); - if (lpPrefix != null && !lpPrefix.isEmpty()) { - String displayName = buildWithLuckPermsPrefix( - lpPrefix, - player.getName(), - data.isColorEnabled() ? data.getColors() : null, - cfg.isUseLegacyRgbFormat(), - cfg.isContinuousGradient() - ); - // Для display name нужен §x формат, конвертируем MiniMessage в legacy - displayName = convertMiniMessageToLegacy(displayName); - var component = SERIALIZER.deserialize(displayName); - player.displayName(component); - player.playerListName(component); - return; - } - } - - // Строим display name с кастомным префиксом или без префикса - String displayName = GradientUtil.buildDisplayName( - prefix, - player.getName(), - data.isColorEnabled() ? data.getColors() : null, - cfg.isGradientOnPrefix(), - cfg.isContinuousGradient(), - prefixFormat, - cfg.isUseLegacyRgbFormat() - ); - - // Для display name нужен §x формат, конвертируем MiniMessage в legacy + // Используем новый PrefixService для получения полного имени + String displayName = module.getFormattedName(player); + + // Конвертируем в legacy формат для display name displayName = convertMiniMessageToLegacy(displayName); var component = SERIALIZER.deserialize(displayName); player.displayName(component); @@ -95,28 +58,6 @@ public final class DisplayNameUtil { return sb.toString(); } - private static String buildWithLuckPermsPrefix(String lpPrefix, String nick, - List colors, - boolean useLegacyFormat, - boolean continuousGradient) { - if (colors == null || colors.isEmpty()) { - return lpPrefix + nick; - } - - if (continuousGradient) { - String cleanPrefix = stripColors(lpPrefix); - String fullText = cleanPrefix + nick; - return GradientUtil.applyGradient(fullText, colors, useLegacyFormat); - } else { - return lpPrefix + GradientUtil.applyGradient(nick, colors, useLegacyFormat); - } - } - - private static String stripColors(String text) { - if (text == null) return null; - return text.replaceAll("(?i)(§x(§[0-9a-f]){6}|§[0-9a-fk-or]|&[0-9a-fk-or]|&#[0-9a-f]{6})", ""); - } - public static String buildColoredPrefix(GradientModule module, GradientPlayerData data) { if (!data.hasPrefix() || !data.isPrefixEnabled()) return null; diff --git a/src/main/java/com/loki/lochat/integrations/DiscordIntegration.java b/src/main/java/com/loki/lochat/integrations/DiscordIntegration.java index f1683c1..a0e5cb8 100644 --- a/src/main/java/com/loki/lochat/integrations/DiscordIntegration.java +++ b/src/main/java/com/loki/lochat/integrations/DiscordIntegration.java @@ -1,87 +1,36 @@ package com.loki.lochat.integrations; -import com.loki.lochat.util.FoliaUtil; -import org.bukkit.configuration.file.YamlConfiguration; +import com.loki.lochat.integrations.discord.DiscordConfig; +import com.loki.lochat.integrations.discord.DiscordMessageService; import org.bukkit.entity.Player; import org.bukkit.plugin.java.JavaPlugin; -import java.io.File; -import java.util.regex.Pattern; - /** - * Интеграция с Discord через вебхуки + * Интеграция с Discord - упрощенная версия */ public class DiscordIntegration { private final JavaPlugin plugin; - private YamlConfiguration config; - private DiscordWebhook webhook; + private DiscordConfig config; + private DiscordMessageService messageService; private boolean enabled = false; - - // Паттерны для очистки сообщений - private static final Pattern MENTION_PATTERN = Pattern.compile("@(everyone|here)", Pattern.CASE_INSENSITIVE); - private static final Pattern COLOR_CODE_PATTERN = Pattern.compile("§[0-9a-fk-or]", Pattern.CASE_INSENSITIVE); public DiscordIntegration(JavaPlugin plugin) { this.plugin = plugin; - loadConfig(); - initWebhook(); + init(); } - /** - * Загрузить конфигурацию - */ - private void loadConfig() { - // Создаём папку config если её нет - File configFolder = new File(plugin.getDataFolder(), "config"); - if (!configFolder.exists()) { - configFolder.mkdirs(); - } - - File configFile = new File(configFolder, "discord.yml"); - if (!configFile.exists()) { - try { - // Копируем из resources/config/discord.yml - java.io.InputStream in = plugin.getResource("config/discord.yml"); - if (in != null) { - java.nio.file.Files.copy(in, configFile.toPath()); - in.close(); - } - } catch (Exception e) { - plugin.getLogger().warning("Failed to copy config/discord.yml: " + e.getMessage()); - } - } - - config = YamlConfiguration.loadConfiguration(configFile); - enabled = config.getBoolean("enabled", false); + private void init() { + config = new DiscordConfig(plugin); + enabled = config.isEnabled(); if (!enabled) { plugin.getLogger().info("Discord integration disabled in config"); return; } - } - - /** - * Инициализировать вебхук - */ - private void initWebhook() { - if (!enabled) return; - // Получаем конфигурацию внешнего вида для дефолтных значений - com.loki.lochat.LoChat loChat = (com.loki.lochat.LoChat) plugin; - com.loki.lochat.config.AppearanceConfig appearanceConfig = loChat.getConfigManager().getAppearanceConfig(); + messageService = new DiscordMessageService(plugin, config); - String webhookUrl = config.getString("webhook.url", ""); - String username = config.getString("webhook.username", "Minecraft Server"); - String avatarUrl = config.getString("webhook.avatar_url", appearanceConfig.getDefaultAvatarUrl()); - int timeout = config.getInt("performance.timeout", 10); - int retryAttempts = config.getInt("performance.retry_attempts", 3); - long retryDelay = config.getLong("performance.retry_delay", 1000); - - // Логируем только факт наличия URL, но не его содержимое (security) - - webhook = new DiscordWebhook(plugin, webhookUrl, username, avatarUrl, timeout, retryAttempts, retryDelay); - - if (!webhook.isValid()) { + if (!messageService.isValid()) { plugin.getLogger().severe("Invalid Discord webhook URL! Please check config/discord.yml"); enabled = false; return; @@ -90,205 +39,47 @@ public class DiscordIntegration { plugin.getLogger().info("Discord integration initialized successfully"); } - /** - * Отправить сообщение чата в Discord - */ public void sendChatMessage(Player player, String message, boolean isGlobal) { - if (!enabled || !config.getBoolean("chat.enabled", true)) return; + if (!enabled || !config.isChatEnabled()) return; - // Проверяем фильтр глобального чата - if (config.getBoolean("chat.global_only", true) && !isGlobal) { + if (config.isGlobalOnly() && !isGlobal) { return; } - // Фильтрация сообщения - String cleanMessage = sanitizeMessage(message); - if (cleanMessage.length() < config.getInt("filter.min_message_length", 1)) { - return; - } - - // Обрезаем если слишком длинное - int maxLength = config.getInt("filter.max_message_length", 2000); - if (cleanMessage.length() > maxLength) { - cleanMessage = cleanMessage.substring(0, maxLength - 3) + "..."; - } - - String playerName = player.getName(); - String format = config.getString("chat.format", "**{player}**: {message}"); - String formattedMessage = format - .replace("{player}", playerName) - .replace("{message}", cleanMessage); - - // Отправляем асинхронно - if (config.getBoolean("performance.async", true)) { - FoliaUtil.runAsync(plugin, () -> sendChatMessageInternal(formattedMessage, playerName, isGlobal)); - } else { - sendChatMessageInternal(formattedMessage, playerName, isGlobal); - } + messageService.sendChatMessage(player, message, isGlobal); } - /** - * Внутренний метод отправки сообщения чата - */ - private void sendChatMessageInternal(String message, String playerName, boolean isGlobal) { - if (config.getBoolean("chat.use_embed", true)) { - // Получаем заголовки из конфигурации внешнего вида - com.loki.lochat.LoChat loChat = (com.loki.lochat.LoChat) plugin; - com.loki.lochat.config.AppearanceConfig appearanceConfig = loChat.getConfigManager().getAppearanceConfig(); - - String title = isGlobal ? - appearanceConfig.getDiscordEventTitle("global_chat") : - appearanceConfig.getDiscordEventTitle("local_chat"); - - // Если заголовок не найден в новой конфигурации, используем старый способ - if (title.isEmpty()) { - title = isGlobal ? "🌍 Глобальный чат" : "📍 Локальный чат"; - } - - String color = config.getString("chat.embed_color", appearanceConfig.getDefaultEmbedColor()); - String thumbnail = appearanceConfig.getPlayerAvatarUrl().replace("{player}", playerName); - - webhook.sendEmbed(title, message, color, thumbnail); - } else { - webhook.sendMessage(message); - } - } - - /** - * Отправить событие входа игрока - */ public void sendPlayerJoin(Player player) { - if (!enabled || !config.getBoolean("events.enabled", true) || - !config.getBoolean("events.join.enabled", true)) return; - - // Получаем конфигурацию внешнего вида - com.loki.lochat.LoChat loChat = (com.loki.lochat.LoChat) plugin; - com.loki.lochat.config.AppearanceConfig appearanceConfig = loChat.getConfigManager().getAppearanceConfig(); - - String playerName = player.getName(); - String title = config.getString("events.join.title", appearanceConfig.getDiscordEventTitle("join")); - String description = config.getString("events.join.description", "**{player}** присоединился к серверу") - .replace("{player}", playerName); - String color = config.getString("events.join.color", "00FF00"); - String thumbnail = config.getString("events.join.thumbnail", appearanceConfig.getPlayerAvatarUrl()) - .replace("{player}", playerName); - - if (config.getBoolean("performance.async", true)) { - FoliaUtil.runAsync(plugin, () -> webhook.sendEmbed(title, description, color, thumbnail)); - } else { - webhook.sendEmbed(title, description, color, thumbnail); - } + if (!enabled || !config.isEventEnabled("join")) return; + messageService.sendPlayerJoin(player); } - /** - * Отправить событие выхода игрока - */ public void sendPlayerQuit(Player player) { - if (!enabled || !config.getBoolean("events.enabled", true) || - !config.getBoolean("events.quit.enabled", true)) return; - - // Получаем конфигурацию внешнего вида - com.loki.lochat.LoChat loChat = (com.loki.lochat.LoChat) plugin; - com.loki.lochat.config.AppearanceConfig appearanceConfig = loChat.getConfigManager().getAppearanceConfig(); - - String playerName = player.getName(); - String title = config.getString("events.quit.title", appearanceConfig.getDiscordEventTitle("quit")); - String description = config.getString("events.quit.description", "**{player}** покинул сервер") - .replace("{player}", playerName); - String color = config.getString("events.quit.color", "FF0000"); - String thumbnail = config.getString("events.quit.thumbnail", appearanceConfig.getPlayerAvatarUrl()) - .replace("{player}", playerName); - - if (config.getBoolean("performance.async", true)) { - FoliaUtil.runAsync(plugin, () -> webhook.sendEmbed(title, description, color, thumbnail)); - } else { - webhook.sendEmbed(title, description, color, thumbnail); - } + if (!enabled || !config.isEventEnabled("quit")) return; + messageService.sendPlayerQuit(player); } - /** - * Отправить событие смерти игрока - */ public void sendPlayerDeath(Player player, String deathMessage) { - if (!enabled || !config.getBoolean("events.enabled", true) || - !config.getBoolean("events.death.enabled", true)) return; - - // Получаем конфигурацию внешнего вида - com.loki.lochat.LoChat loChat = (com.loki.lochat.LoChat) plugin; - com.loki.lochat.config.AppearanceConfig appearanceConfig = loChat.getConfigManager().getAppearanceConfig(); - - String playerName = player.getName(); - String title = config.getString("events.death.title", appearanceConfig.getDiscordEventTitle("death")); - String description = config.getString("events.death.description", "**{player}** {death_message}") - .replace("{player}", playerName) - .replace("{death_message}", sanitizeMessage(deathMessage)); - String color = config.getString("events.death.color", "800080"); - String thumbnail = config.getString("events.death.thumbnail", appearanceConfig.getPlayerAvatarUrl()) - .replace("{player}", playerName); - - if (config.getBoolean("performance.async", true)) { - FoliaUtil.runAsync(plugin, () -> webhook.sendEmbed(title, description, color, thumbnail)); - } else { - webhook.sendEmbed(title, description, color, thumbnail); - } + if (!enabled || !config.isEventEnabled("death")) return; + messageService.sendPlayerDeath(player, deathMessage); } - /** - * Очистить сообщение от нежелательных элементов - */ - private String sanitizeMessage(String message) { - if (message == null) return ""; - - // Удаляем цветовые коды Minecraft - message = COLOR_CODE_PATTERN.matcher(message).replaceAll(""); - - // Заменяем опасные упоминания - if (config.getBoolean("filter.sanitize_mentions", true)) { - message = MENTION_PATTERN.matcher(message).replaceAll("@\u200B$1"); // Добавляем невидимый символ - } - - // Экранируем Discord markdown - message = message.replace("*", "\\*") - .replace("_", "\\_") - .replace("`", "\\`") - .replace("~", "\\~"); - - return message.trim(); + public void sendTestMessage(String message, String senderName) { + if (!enabled) return; + messageService.sendTestMessage(message, senderName); } - /** - * Перезагрузить конфигурацию - */ public void reload() { - loadConfig(); - initWebhook(); + init(); plugin.getLogger().info("Discord integration reloaded"); } - /** - * Отправить тестовое сообщение - */ - public void sendTestMessage(String message, String senderName) { - if (!enabled) return; - - String title = "🧪 Тестовое сообщение"; - String description = "**Отправитель:** " + senderName + "\n**Сообщение:** " + message; - String color = "FFD700"; // золотой цвет - String thumbnail = "https://mc-heads.net/avatar/" + senderName + "/64"; - - webhook.sendEmbed(title, description, color, thumbnail); - } - - /** - * Остановить executor при выключении плагина - */ public void shutdown() { - if (webhook != null) webhook.shutdown(); + if (messageService != null) { + messageService.shutdown(); + } } - /** - * Проверить включена ли интеграция - */ public boolean isEnabled() { return enabled; } diff --git a/src/main/java/com/loki/lochat/integrations/PlaceholderAPIHook.java b/src/main/java/com/loki/lochat/integrations/PlaceholderAPIHook.java index 71fa31e..ec66a15 100644 --- a/src/main/java/com/loki/lochat/integrations/PlaceholderAPIHook.java +++ b/src/main/java/com/loki/lochat/integrations/PlaceholderAPIHook.java @@ -2,9 +2,7 @@ package com.loki.lochat.integrations; import com.loki.lochat.LoChat; import com.loki.lochat.api.service.ChatService; -import com.loki.lochat.api.service.IgnoreService; -import com.loki.lochat.api.service.PMService; -import com.loki.lochat.api.service.SpyService; +import com.loki.lochat.api.service.MessagingService; import com.loki.lochat.gradient.GradientModule; import me.clip.placeholderapi.expansion.PlaceholderExpansion; import org.bukkit.Bukkit; @@ -19,17 +17,13 @@ import java.util.UUID; public class PlaceholderAPIHook extends PlaceholderExpansion { private final LoChat plugin; - private final IgnoreService ignoreService; + private final MessagingService messagingService; private final ChatService chatService; - private final PMService pmService; - private final SpyService spyService; public PlaceholderAPIHook(LoChat plugin) { this.plugin = plugin; - this.ignoreService = plugin.getServiceRegistry().get(IgnoreService.class); + this.messagingService = plugin.getServiceRegistry().get(MessagingService.class); this.chatService = plugin.getServiceRegistry().get(ChatService.class); - this.pmService = plugin.getServiceRegistry().get(PMService.class); - this.spyService = plugin.getServiceRegistry().get(SpyService.class); } @Override @@ -60,15 +54,15 @@ public class PlaceholderAPIHook extends PlaceholderExpansion { GradientModule gradient = plugin.getGradientModule(); return switch (params.toLowerCase()) { - case "ignored_count" -> String.valueOf(ignoreService.getIgnoredCount(uuid)); + case "ignored_count" -> String.valueOf(messagingService.getIgnoredCount(uuid)); case "global_enabled" -> String.valueOf(!chatService.isGlobalChatDisabled(uuid)); case "last_pm" -> { - Optional lastOpt = pmService.getLastConversation(uuid); + Optional lastOpt = messagingService.getLastConversation(uuid); if (lastOpt.isEmpty()) yield ""; var lastPlayer = Bukkit.getOfflinePlayer(lastOpt.get()); yield lastPlayer.getName() != null ? lastPlayer.getName() : ""; } - case "spy_enabled" -> String.valueOf(spyService.isSpying(uuid)); + case "spy_enabled" -> String.valueOf(messagingService.isSpying(uuid)); // Gradient placeholders (совместимость с LoPreff) // Используем TAB формат §x§R§R§G§G§B§B для совместимости с TAB плагином diff --git a/src/main/java/com/loki/lochat/integrations/discord/DiscordConfig.java b/src/main/java/com/loki/lochat/integrations/discord/DiscordConfig.java new file mode 100644 index 0000000..8594f59 --- /dev/null +++ b/src/main/java/com/loki/lochat/integrations/discord/DiscordConfig.java @@ -0,0 +1,126 @@ +package com.loki.lochat.integrations.discord; + +import org.bukkit.configuration.file.YamlConfiguration; +import org.bukkit.plugin.java.JavaPlugin; + +import java.io.File; + +/** + * Конфигурация Discord интеграции + */ +public class DiscordConfig { + private final JavaPlugin plugin; + private YamlConfiguration config; + + public DiscordConfig(JavaPlugin plugin) { + this.plugin = plugin; + load(); + } + + private void load() { + File configFolder = new File(plugin.getDataFolder(), "config"); + if (!configFolder.exists()) { + configFolder.mkdirs(); + } + + File configFile = new File(configFolder, "discord.yml"); + if (!configFile.exists()) { + try { + java.io.InputStream in = plugin.getResource("config/discord.yml"); + if (in != null) { + java.nio.file.Files.copy(in, configFile.toPath()); + in.close(); + } + } catch (Exception e) { + plugin.getLogger().warning("Failed to copy config/discord.yml: " + e.getMessage()); + } + } + + config = YamlConfiguration.loadConfiguration(configFile); + } + + public boolean isEnabled() { + return config.getBoolean("enabled", false); + } + + public String getWebhookUrl() { + return config.getString("webhook.url", ""); + } + + public String getUsername() { + return config.getString("webhook.username", "Minecraft Server"); + } + + public String getAvatarUrl() { + return config.getString("webhook.avatar_url", ""); + } + + public int getTimeout() { + return config.getInt("performance.timeout", 10); + } + + public int getRetryAttempts() { + return config.getInt("performance.retry_attempts", 3); + } + + public long getRetryDelay() { + return config.getLong("performance.retry_delay", 1000); + } + + public boolean isAsync() { + return config.getBoolean("performance.async", true); + } + + public boolean isChatEnabled() { + return config.getBoolean("chat.enabled", true); + } + + public boolean isGlobalOnly() { + return config.getBoolean("chat.global_only", true); + } + + public boolean useEmbed() { + return config.getBoolean("chat.use_embed", true); + } + + public String getChatFormat() { + return config.getString("chat.format", "**{player}**: {message}"); + } + + public String getEmbedColor() { + return config.getString("chat.embed_color", "5865F2"); + } + + public boolean isEventEnabled(String event) { + return config.getBoolean("events.enabled", true) && + config.getBoolean("events." + event + ".enabled", true); + } + + public String getEventTitle(String event) { + return config.getString("events." + event + ".title", ""); + } + + public String getEventDescription(String event) { + return config.getString("events." + event + ".description", ""); + } + + public String getEventColor(String event) { + return config.getString("events." + event + ".color", "5865F2"); + } + + public String getEventThumbnail(String event) { + return config.getString("events." + event + ".thumbnail", ""); + } + + public int getMinMessageLength() { + return config.getInt("filter.min_message_length", 1); + } + + public int getMaxMessageLength() { + return config.getInt("filter.max_message_length", 2000); + } + + public boolean sanitizeMentions() { + return config.getBoolean("filter.sanitize_mentions", true); + } +} diff --git a/src/main/java/com/loki/lochat/integrations/discord/DiscordMessageService.java b/src/main/java/com/loki/lochat/integrations/discord/DiscordMessageService.java new file mode 100644 index 0000000..54ba337 --- /dev/null +++ b/src/main/java/com/loki/lochat/integrations/discord/DiscordMessageService.java @@ -0,0 +1,166 @@ +package com.loki.lochat.integrations.discord; + +import com.loki.lochat.integrations.DiscordWebhook; +import com.loki.lochat.utils.FoliaUtil; +import org.bukkit.entity.Player; +import org.bukkit.plugin.java.JavaPlugin; + +import java.util.regex.Pattern; + +/** + * Сервис для отправки сообщений в Discord + */ +public class DiscordMessageService { + private static final Pattern MENTION_PATTERN = Pattern.compile("@(everyone|here)", Pattern.CASE_INSENSITIVE); + private static final Pattern COLOR_CODE_PATTERN = Pattern.compile("§[0-9a-fk-or]", Pattern.CASE_INSENSITIVE); + + private final JavaPlugin plugin; + private final DiscordConfig config; + private final DiscordWebhook webhook; + + public DiscordMessageService(JavaPlugin plugin, DiscordConfig config) { + this.plugin = plugin; + this.config = config; + this.webhook = new DiscordWebhook( + plugin, + config.getWebhookUrl(), + config.getUsername(), + getAvatarUrl(), + config.getTimeout(), + config.getRetryAttempts(), + config.getRetryDelay() + ); + } + + private String getAvatarUrl() { + String url = config.getAvatarUrl(); + if (url.isEmpty()) { + com.loki.lochat.LoChat loChat = (com.loki.lochat.LoChat) plugin; + return loChat.getConfigManager().getAppearanceConfig().getDefaultAvatarUrl(); + } + return url; + } + + public void sendChatMessage(Player player, String message, boolean isGlobal) { + String cleanMessage = sanitizeMessage(message); + + if (cleanMessage.length() < config.getMinMessageLength()) { + return; + } + + if (cleanMessage.length() > config.getMaxMessageLength()) { + cleanMessage = cleanMessage.substring(0, config.getMaxMessageLength() - 3) + "..."; + } + + String formatted = config.getChatFormat() + .replace("{player}", player.getName()) + .replace("{message}", cleanMessage); + + if (config.isAsync()) { + FoliaUtil.runAsync(plugin, () -> sendFormatted(formatted, player, isGlobal)); + } else { + sendFormatted(formatted, player, isGlobal); + } + } + + private void sendFormatted(String message, Player player, boolean isGlobal) { + if (config.useEmbed()) { + com.loki.lochat.LoChat loChat = (com.loki.lochat.LoChat) plugin; + com.loki.lochat.config.AppearanceConfig appearanceConfig = loChat.getConfigManager().getAppearanceConfig(); + + String title = isGlobal ? + appearanceConfig.getDiscordEventTitle("global_chat") : + appearanceConfig.getDiscordEventTitle("local_chat"); + + if (title.isEmpty()) { + title = isGlobal ? "🌍 Глобальный чат" : "📍 Локальный чат"; + } + + String color = config.getEmbedColor(); + String thumbnail = appearanceConfig.getPlayerAvatarUrl().replace("{player}", player.getName()); + + webhook.sendEmbed(title, message, color, thumbnail); + } else { + webhook.sendMessage(message); + } + } + + public void sendPlayerJoin(Player player) { + sendEvent(player, "join", null); + } + + public void sendPlayerQuit(Player player) { + sendEvent(player, "quit", null); + } + + public void sendPlayerDeath(Player player, String deathMessage) { + sendEvent(player, "death", deathMessage); + } + + private void sendEvent(Player player, String event, String extraMessage) { + com.loki.lochat.LoChat loChat = (com.loki.lochat.LoChat) plugin; + com.loki.lochat.config.AppearanceConfig appearanceConfig = loChat.getConfigManager().getAppearanceConfig(); + + String title = config.getEventTitle(event); + if (title.isEmpty()) { + title = appearanceConfig.getDiscordEventTitle(event); + } + + String description = config.getEventDescription(event) + .replace("{player}", player.getName()); + + if (extraMessage != null) { + description = description.replace("{death_message}", sanitizeMessage(extraMessage)); + } + + String color = config.getEventColor(event); + String thumbnail = config.getEventThumbnail(event); + if (thumbnail.isEmpty()) { + thumbnail = appearanceConfig.getPlayerAvatarUrl(); + } + final String finalThumbnail = thumbnail.replace("{player}", player.getName()); + final String finalTitle = title; + final String finalDescription = description; + final String finalColor = color; + + if (config.isAsync()) { + FoliaUtil.runAsync(plugin, () -> webhook.sendEmbed(finalTitle, finalDescription, finalColor, finalThumbnail)); + } else { + webhook.sendEmbed(title, description, color, finalThumbnail); + } + } + + public void sendTestMessage(String message, String senderName) { + String title = "🧪 Тестовое сообщение"; + String description = "**Отправитель:** " + senderName + "\n**Сообщение:** " + message; + String color = "FFD700"; + String thumbnail = "https://mc-heads.net/avatar/" + senderName + "/64"; + + webhook.sendEmbed(title, description, color, thumbnail); + } + + private String sanitizeMessage(String message) { + if (message == null) return ""; + + message = COLOR_CODE_PATTERN.matcher(message).replaceAll(""); + + if (config.sanitizeMentions()) { + message = MENTION_PATTERN.matcher(message).replaceAll("@\u200B$1"); + } + + message = message.replace("*", "\\*") + .replace("_", "\\_") + .replace("`", "\\`") + .replace("~", "\\~"); + + return message.trim(); + } + + public boolean isValid() { + return webhook.isValid(); + } + + public void shutdown() { + webhook.shutdown(); + } +} diff --git a/src/main/java/com/loki/lochat/listener/ChatEventListener.java b/src/main/java/com/loki/lochat/listener/ChatEventListener.java index 1b0e113..9b5a11a 100644 --- a/src/main/java/com/loki/lochat/listener/ChatEventListener.java +++ b/src/main/java/com/loki/lochat/listener/ChatEventListener.java @@ -3,6 +3,7 @@ package com.loki.lochat.listener; import com.loki.lochat.LoChat; import com.loki.lochat.api.service.MessageService; import com.loki.lochat.core.filter.AdvancedMessageFilter; +import com.loki.lochat.core.filter.FilterResult; import com.loki.lochat.core.registry.ServiceRegistry; import com.loki.lochat.renderer.EnhancedChatRenderer; import com.loki.lochat.utils.ChatFormatter; @@ -29,66 +30,81 @@ public class ChatEventListener implements Listener { @EventHandler(priority = EventPriority.HIGHEST) public void onChat(AsyncChatEvent event) { Player sender = event.getPlayer(); - - // plain нужен только для фильтров — цвета игрока обрабатываем ниже отдельно String rawMessage = PlainTextComponentSerializer.plainText().serialize(event.message()); - - // ✅ РЕЖИМ ЕДИНОГО ЧАТА: если локальный отключен, весь чат = глобальный LoChat loChat = (LoChat) plugin; - boolean localEnabled = loChat.getConfigManager().isLocalEnabled(); - - boolean isGlobal; - String plainMessage; - - if (localEnabled) { - // Старая логика: "!" = глобальный, без "!" = локальный - isGlobal = rawMessage.startsWith("!"); - plainMessage = isGlobal ? rawMessage.substring(1).stripLeading() : rawMessage; - } else { - // Новая логика: весь чат глобальный, "!" не нужен - isGlobal = true; - plainMessage = rawMessage; + + // Определяем режим чата (глобальный/локальный) + ChatMode mode = determineChatMode(loChat, rawMessage); + if (mode == null) { + event.setCancelled(true); + sender.sendMessage(ChatFormatter.parse("&#CF6679Этот режим чата отключен!")); + return; } - // ✅ Проверка: включен ли глобальный/локальный чат + String plainMessage = mode.message; + boolean isGlobal = mode.isGlobal; + + // Проверки: мут, кулдаун, фильтры + if (!processFilters(event, sender, plainMessage, isGlobal)) { + return; + } + + // Обработка локального чата (радиус) + if (!isGlobal) { + handleLocalChat(event, sender, loChat); + } + + // Рендеринг и интеграции + event.renderer(new EnhancedChatRenderer(plugin, isGlobal)); + loChat.getDiscordIntegration().sendChatMessage(sender, plainMessage, isGlobal); + recordStatistics(loChat, sender, isGlobal); + } + + private ChatMode determineChatMode(LoChat loChat, String rawMessage) { + boolean localEnabled = loChat.getConfigManager().isLocalEnabled(); + boolean isGlobal; + String message; + + if (localEnabled) { + isGlobal = rawMessage.startsWith("!"); + message = isGlobal ? rawMessage.substring(1).stripLeading() : rawMessage; + } else { + isGlobal = true; + message = rawMessage; + } + + // Проверка: включен ли режим if (isGlobal && !loChat.getConfigManager().isGlobalEnabled()) { - event.setCancelled(true); - sender.sendMessage(ChatFormatter.parse("&#CF6679Глобальный чат отключен!")); - return; + return null; } - if (!isGlobal && !localEnabled) { - event.setCancelled(true); - sender.sendMessage(ChatFormatter.parse("&#CF6679Локальный чат отключен!")); - return; + return null; } + return new ChatMode(isGlobal, message); + } + + private boolean processFilters(AsyncChatEvent event, Player sender, String plainMessage, boolean isGlobal) { + LoChat loChat = (LoChat) plugin; + // Мут, кулдаун if (!messageService.processMessage(sender, plainMessage)) { event.setCancelled(true); - return; + return false; } // Капс, реклама, повторы - AdvancedMessageFilter.FilterResult filterResult = advancedFilter.filterMessage(sender, plainMessage); + FilterResult filterResult = advancedFilter.filterMessage(sender, plainMessage); if (!filterResult.allowed()) { event.setCancelled(true); sender.sendMessage(ChatFormatter.parse(filterResult.blockReason())); - return; + return false; } String filteredMessage = filterResult.filteredMessage(); - // ── Цвет текста сообщения ({message}) ───────────────────────────────── - // Если стиль не задан явно, Paper может унаследовать цвет последнего символа градиента - // префикса в строке чата (например у LOCAL последняя буква давала синеватый #4169E1) — - // из‑за этого весь текст сообщения «подтягивался» под этот тон. Намеренный цвет текста - // задаётся в appearance.yml: prefixes.global.message-color / prefixes.local.message-color (#E8E0F0 по умолчанию). - // Решение: всегда выставлять стиль тела сообщения: - // • lochat.chat.colors → MiniMessage / &# / & из текста - // • иначе → parseWithDefaultMessageColor(..., message-color) + экранирование < (см. ChatFormatter) + // Цвет текста сообщения Component messageComponent; - if (sender.hasPermission("lochat.chat.colors")) { messageComponent = ChatFormatter.parse(filteredMessage); } else { @@ -99,46 +115,41 @@ public class ChatEventListener implements Listener { } event.message(messageComponent); + return true; + } - // ✅ FIX: Радиус для локального чата - кэшируем локации в async потоке - // Paper позволяет читать Location в async, но не модифицировать World - if (!isGlobal) { - int radius = loChat.getConfigManager().getAppearanceConfig().getLocalRadius(); - - // Безопасно: AsyncChatEvent.viewers() thread-safe, Location.distance() тоже - event.viewers().removeIf(v -> { - if (!(v instanceof Player p)) return false; - - // Проверяем мир и расстояние (чтение Location безопасно в async) - try { - return !com.loki.lochat.util.DistanceUtil.isInRange(sender, p, radius); - } catch (Exception e) { - // На всякий случай ловим исключения - return false; - } - }); + private void handleLocalChat(AsyncChatEvent event, Player sender, LoChat loChat) { + int radius = loChat.getConfigManager().getAppearanceConfig().getLocalRadius(); - long recipients = event.viewers().stream() - .filter(v -> v instanceof Player p && !p.equals(sender)) - .count(); - - if (recipients == 0) { - String msg = loChat.getConfigManager().getMessagesConfig().getNobodyHeard(); - sender.getScheduler().run(plugin, t -> - sender.sendMessage(ChatFormatter.parse(msg)), null - ); + event.viewers().removeIf(v -> { + if (!(v instanceof Player p)) return false; + try { + return !com.loki.lochat.utils.DistanceUtil.isInRange(sender, p, radius); + } catch (Exception e) { + return false; } + }); + + long recipients = event.viewers().stream() + .filter(v -> v instanceof Player p && !p.equals(sender)) + .count(); + + if (recipients == 0) { + String msg = loChat.getConfigManager().getMessagesConfig().getNobodyHeard(); + sender.getScheduler().run(plugin, t -> + sender.sendMessage(ChatFormatter.parse(msg)), null + ); } + } - event.renderer(new EnhancedChatRenderer(plugin, isGlobal)); - - loChat.getDiscordIntegration().sendChatMessage(sender, filteredMessage, isGlobal); - - // Статистика - com.loki.lochat.api.service.PlayerDataService pds = - loChat.getServiceRegistry().get(com.loki.lochat.api.service.PlayerDataService.class); - if (pds instanceof com.loki.lochat.core.service.PlayerDataServiceImpl impl) { + private void recordStatistics(LoChat loChat, Player sender, boolean isGlobal) { + com.loki.lochat.api.service.PlayerService playerService = + loChat.getServiceRegistry().get(com.loki.lochat.api.service.PlayerService.class); + if (playerService instanceof com.loki.lochat.core.service.PlayerServiceImpl impl) { impl.recordMessage(sender.getUniqueId(), isGlobal ? "global" : "local"); } } + + private record ChatMode(boolean isGlobal, String message) { + } } diff --git a/src/main/java/com/loki/lochat/listener/PlayerEventListener.java b/src/main/java/com/loki/lochat/listener/PlayerEventListener.java index 0792af5..3c4ba6a 100644 --- a/src/main/java/com/loki/lochat/listener/PlayerEventListener.java +++ b/src/main/java/com/loki/lochat/listener/PlayerEventListener.java @@ -1,9 +1,8 @@ package com.loki.lochat.listener; +import com.loki.lochat.api.service.MessagingService; import com.loki.lochat.api.service.NickService; -import com.loki.lochat.api.service.PlayerDataService; -import com.loki.lochat.api.service.PMService; -import com.loki.lochat.api.service.SpyService; +import com.loki.lochat.api.service.PlayerService; import com.loki.lochat.core.filter.AdvancedMessageFilter; import com.loki.lochat.core.registry.ServiceRegistry; import org.bukkit.entity.Player; @@ -16,17 +15,15 @@ import org.bukkit.event.player.PlayerQuitEvent; * Слушатель событий игрока */ public class PlayerEventListener implements Listener { - private final PlayerDataService playerDataService; + private final PlayerService playerService; + private final MessagingService messagingService; private final NickService nickService; - private final PMService pmService; - private final SpyService spyService; private final AdvancedMessageFilter advancedFilter; public PlayerEventListener(ServiceRegistry registry, AdvancedMessageFilter advancedFilter) { - this.playerDataService = registry.get(PlayerDataService.class); - this.nickService = registry.get(NickService.class); - this.pmService = registry.get(PMService.class); - this.spyService = registry.get(SpyService.class); + this.playerService = registry.get(PlayerService.class); + this.messagingService = registry.get(MessagingService.class); + this.nickService = registry.get(NickService.class); this.advancedFilter = advancedFilter; } @@ -43,9 +40,9 @@ public class PlayerEventListener implements Listener { Player player = event.getPlayer(); java.util.UUID uuid = player.getUniqueId(); - playerDataService.clearPlayerData(uuid); - pmService.removeConversation(uuid); - spyService.removeSpy(uuid); + playerService.clearPlayerData(uuid); + messagingService.removeConversation(uuid); + messagingService.removeSpy(uuid); // Очищаем flood / spam трекеры — иначе данные висят в памяти вечно advancedFilter.clearPlayer(uuid); diff --git a/src/main/java/com/loki/lochat/managers/AutoMessageManager.java b/src/main/java/com/loki/lochat/managers/AutoMessageManager.java index 582bc75..4781b03 100644 --- a/src/main/java/com/loki/lochat/managers/AutoMessageManager.java +++ b/src/main/java/com/loki/lochat/managers/AutoMessageManager.java @@ -1,7 +1,7 @@ package com.loki.lochat.managers; import com.loki.lochat.LoChat; -import com.loki.lochat.util.FoliaUtil; +import com.loki.lochat.utils.FoliaUtil; import com.loki.lochat.utils.ChatFormatter; import io.papermc.paper.threadedregions.scheduler.ScheduledTask; import net.kyori.adventure.text.Component; diff --git a/src/main/java/com/loki/lochat/managers/CustomCommandManager.java b/src/main/java/com/loki/lochat/managers/CustomCommandManager.java index 006f5f0..c8980de 100644 --- a/src/main/java/com/loki/lochat/managers/CustomCommandManager.java +++ b/src/main/java/com/loki/lochat/managers/CustomCommandManager.java @@ -2,9 +2,9 @@ package com.loki.lochat.managers; import com.loki.lochat.LoChat; import com.loki.lochat.commands.CustomCommand; +import com.loki.lochat.managers.commands.MessageSender; +import com.loki.lochat.managers.commands.PlaceholderProcessor; import com.loki.lochat.utils.ChatFormatter; -import net.kyori.adventure.text.Component; -import net.kyori.adventure.title.Title; import org.bukkit.Bukkit; import org.bukkit.command.CommandMap; import org.bukkit.configuration.ConfigurationSection; @@ -13,7 +13,6 @@ import org.bukkit.configuration.file.YamlConfiguration; import org.bukkit.entity.Player; import java.io.File; -import java.time.Duration; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -41,11 +40,8 @@ public class CustomCommandManager { } FileConfiguration commandsConfig = YamlConfiguration.loadConfiguration(commandsFile); - - // Очищаем старые команды commands.clear(); - // Загружаем команды из конфига for (String commandName : commandsConfig.getKeys(false)) { ConfigurationSection section = commandsConfig.getConfigurationSection(commandName); if (section == null) continue; @@ -61,8 +57,6 @@ public class CustomCommandManager { ); commands.put(commandName, data); - - // Регистрируем команду registerCommand(data); } @@ -71,14 +65,11 @@ public class CustomCommandManager { private void registerCommand(CustomCommandData data) { try { - // Получаем CommandMap через Paper API (безопаснее чем рефлексия) CommandMap commandMap = Bukkit.getCommandMap(); - // Создаем и регистрируем команду CustomCommand command = new CustomCommand(data.name(), this); commandMap.register("lochat", command); - // Регистрируем алиасы for (String alias : data.aliases()) { CustomCommand aliasCommand = new CustomCommand(alias, this); commandMap.register("lochat", aliasCommand); @@ -97,7 +88,6 @@ public class CustomCommandManager { public boolean executeCustomCommand(String commandName, Player player, String[] args) { CustomCommandData data = commands.get(commandName); if (data == null) { - // Проверяем алиасы for (CustomCommandData cmd : commands.values()) { if (cmd.aliases().contains(commandName)) { data = cmd; @@ -108,173 +98,32 @@ public class CustomCommandManager { if (data == null) return false; - // Проверяем включена ли команда if (!data.enabled()) { - // Не показываем ошибку, просто игнорируем команду return true; } - // Проверяем права if (data.permission() != null && !player.hasPermission(data.permission())) { player.sendMessage(ChatFormatter.parse(plugin.getMessageConfig().getNoPermission())); return true; } - // Обрабатываем плейсхолдеры - String message = processPlaceholders(data.message(), player, args); + String message = PlaceholderProcessor.process(data.message(), player, args); - // Выполняем команду в зависимости от типа switch (data.type().toLowerCase()) { - case "broadcast" -> sendBroadcast(data.target(), message); - case "title" -> sendTitle(data.target(), player, message); - case "actionbar" -> sendActionBar(data.target(), player, message); + case "title" -> MessageSender.sendTitle(data.target(), player, message); + case "actionbar" -> MessageSender.sendActionBar(data.target(), player, message); case "bungee" -> sendBungeeCommand(player, data); - default -> sendChatMessage(data.target(), player, message); + default -> MessageSender.sendChat(data.target(), player, message); } return true; } - private String processPlaceholders(String message, Player player, String[] args) { - String result = message; - - // Базовые плейсхолдеры - result = result.replace("{player}", player.getName()); - result = result.replace("{x}", String.valueOf(player.getLocation().getBlockX())); - result = result.replace("{y}", String.valueOf(player.getLocation().getBlockY())); - result = result.replace("{z}", String.valueOf(player.getLocation().getBlockZ())); - result = result.replace("{world}", player.getWorld().getName()); - result = result.replace("{ping}", String.valueOf(player.getPing())); - - // Аргументы команды - for (int i = 0; i < args.length; i++) { - result = result.replace("{arg" + i + "}", args[i]); - } - result = result.replace("{args}", String.join(" ", args)); - - // PlaceholderAPI если доступен - if (Bukkit.getPluginManager().isPluginEnabled("PlaceholderAPI")) { - try { - // Используем безопасный способ вызова PlaceholderAPI - Class papiClass = Class.forName("me.clip.placeholderapi.PlaceholderAPI"); - java.lang.reflect.Method setPlaceholdersMethod = papiClass.getMethod("setPlaceholders", org.bukkit.entity.Player.class, String.class); - result = (String) setPlaceholdersMethod.invoke(null, player, result); - } catch (Exception e) { - // Если PlaceholderAPI недоступен или произошла ошибка, просто пропускаем - plugin.getLogger().warning("Ошибка при обработке PlaceholderAPI плейсхолдеров: " + e.getMessage()); - } - } - - return result; - } - - private void sendChatMessage(String target, Player sender, String message) { - Component component = ChatFormatter.parse(message); - - switch (target.toLowerCase()) { - case "sender" -> sender.sendMessage(component); - case "all" -> { - for (Player player : Bukkit.getOnlinePlayers()) { - player.sendMessage(component); - } - } - default -> { - if (target.startsWith("permission:")) { - String permission = target.substring(11); - for (Player player : Bukkit.getOnlinePlayers()) { - if (player.hasPermission(permission)) { - player.sendMessage(component); - } - } - } else { - sender.sendMessage(component); - } - } - } - } - - private void sendBroadcast(String target, String message) { - Component component = ChatFormatter.parse(message); - - if (target.equalsIgnoreCase("all")) { - for (Player player : Bukkit.getOnlinePlayers()) { - player.sendMessage(component); - } - } else { - if (target.startsWith("permission:")) { - String permission = target.substring(11); - for (Player player : Bukkit.getOnlinePlayers()) { - if (player.hasPermission(permission)) { - player.sendMessage(component); - } - } - } - } - } - - private void sendTitle(String target, Player sender, String message) { - String[] parts = message.split("\\n", 2); - Component title = ChatFormatter.parse(parts[0]); - Component subtitle = parts.length > 1 ? ChatFormatter.parse(parts[1]) : Component.empty(); - - Title titleObj = Title.title(title, subtitle, - Title.Times.times(Duration.ofMillis(500), Duration.ofSeconds(3), Duration.ofMillis(500))); - - switch (target.toLowerCase()) { - case "sender" -> sender.showTitle(titleObj); - case "all" -> { - for (Player player : Bukkit.getOnlinePlayers()) { - player.showTitle(titleObj); - } - } - default -> { - if (target.startsWith("permission:")) { - String permission = target.substring(11); - for (Player player : Bukkit.getOnlinePlayers()) { - if (player.hasPermission(permission)) { - player.showTitle(titleObj); - } - } - } else { - sender.showTitle(titleObj); - } - } - } - } - - private void sendActionBar(String target, Player sender, String message) { - Component component = ChatFormatter.parse(message); - - switch (target.toLowerCase()) { - case "sender" -> sender.sendActionBar(component); - case "all" -> { - for (Player player : Bukkit.getOnlinePlayers()) { - player.sendActionBar(component); - } - } - default -> { - if (target.startsWith("permission:")) { - String permission = target.substring(11); - for (Player player : Bukkit.getOnlinePlayers()) { - if (player.hasPermission(permission)) { - player.sendActionBar(component); - } - } - } else { - sender.sendActionBar(component); - } - } - } - } - private void sendBungeeCommand(Player player, CustomCommandData data) { - // Отправляем сообщение игроку if (!data.message().isEmpty()) { - Component component = ChatFormatter.parse(data.message()); - player.sendMessage(component); + player.sendMessage(ChatFormatter.parse(data.message())); } - // Подключаем к серверу через BungeeCord String serverName = getServerFromConfig(data); if (serverName != null && !serverName.isEmpty()) { connectToServer(player, serverName); @@ -282,7 +131,6 @@ public class CustomCommandManager { } private String getServerFromConfig(CustomCommandData data) { - // Ищем параметр server в конфиге команды try { File commandsFile = new File(plugin.getDataFolder(), "custom-commands.yml"); FileConfiguration config = YamlConfiguration.loadConfiguration(commandsFile); diff --git a/src/main/java/com/loki/lochat/managers/commands/MessageSender.java b/src/main/java/com/loki/lochat/managers/commands/MessageSender.java new file mode 100644 index 0000000..492933b --- /dev/null +++ b/src/main/java/com/loki/lochat/managers/commands/MessageSender.java @@ -0,0 +1,73 @@ +package com.loki.lochat.managers.commands; + +import com.loki.lochat.utils.ChatFormatter; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.title.Title; +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; + +import java.time.Duration; + +public class MessageSender { + + public static void sendChat(String target, Player sender, String message) { + Component formatted = ChatFormatter.parse(message); + + switch (target.toLowerCase()) { + case "player" -> sender.sendMessage(formatted); + case "broadcast" -> Bukkit.broadcast(formatted); + case "server" -> Bukkit.getOnlinePlayers().forEach(p -> p.sendMessage(formatted)); + default -> { + Player targetPlayer = Bukkit.getPlayer(target); + if (targetPlayer != null && targetPlayer.isOnline()) { + targetPlayer.sendMessage(formatted); + } + } + } + } + + public static void sendTitle(String target, Player sender, String message) { + String[] parts = message.split("\\|", 2); + String titleText = parts.length > 0 ? parts[0] : ""; + String subtitleText = parts.length > 1 ? parts[1] : ""; + + Component title = ChatFormatter.parse(titleText); + Component subtitle = ChatFormatter.parse(subtitleText); + + Title titleObj = Title.title( + title, + subtitle, + Title.Times.times( + Duration.ofMillis(500), + Duration.ofMillis(3000), + Duration.ofMillis(500) + ) + ); + + switch (target.toLowerCase()) { + case "player" -> sender.showTitle(titleObj); + case "broadcast", "server" -> Bukkit.getOnlinePlayers().forEach(p -> p.showTitle(titleObj)); + default -> { + Player targetPlayer = Bukkit.getPlayer(target); + if (targetPlayer != null && targetPlayer.isOnline()) { + targetPlayer.showTitle(titleObj); + } + } + } + } + + public static void sendActionBar(String target, Player sender, String message) { + Component formatted = ChatFormatter.parse(message); + + switch (target.toLowerCase()) { + case "player" -> sender.sendActionBar(formatted); + case "broadcast", "server" -> Bukkit.getOnlinePlayers().forEach(p -> p.sendActionBar(formatted)); + default -> { + Player targetPlayer = Bukkit.getPlayer(target); + if (targetPlayer != null && targetPlayer.isOnline()) { + targetPlayer.sendActionBar(formatted); + } + } + } + } +} diff --git a/src/main/java/com/loki/lochat/managers/commands/PlaceholderProcessor.java b/src/main/java/com/loki/lochat/managers/commands/PlaceholderProcessor.java new file mode 100644 index 0000000..55ab7b1 --- /dev/null +++ b/src/main/java/com/loki/lochat/managers/commands/PlaceholderProcessor.java @@ -0,0 +1,36 @@ +package com.loki.lochat.managers.commands; + +import org.bukkit.entity.Player; + +public class PlaceholderProcessor { + + public static String process(String message, Player player, String[] args) { + String result = message; + + // Базовые плейсхолдеры + result = result.replace("{player}", player.getName()); + result = result.replace("{displayname}", player.getDisplayName()); + result = result.replace("{world}", player.getWorld().getName()); + result = result.replace("{x}", String.valueOf(player.getLocation().getBlockX())); + result = result.replace("{y}", String.valueOf(player.getLocation().getBlockY())); + result = result.replace("{z}", String.valueOf(player.getLocation().getBlockZ())); + result = result.replace("{health}", String.valueOf(Math.round(player.getHealth()))); + result = result.replace("{food}", String.valueOf(player.getFoodLevel())); + result = result.replace("{level}", String.valueOf(player.getLevel())); + result = result.replace("{exp}", String.valueOf(player.getExp())); + + // Аргументы команды + for (int i = 0; i < args.length; i++) { + result = result.replace("{arg" + i + "}", args[i]); + } + + // Все аргументы вместе + if (args.length > 0) { + result = result.replace("{args}", String.join(" ", args)); + } else { + result = result.replace("{args}", ""); + } + + return result; + } +} diff --git a/src/main/java/com/loki/lochat/renderer/EnhancedChatRenderer.java b/src/main/java/com/loki/lochat/renderer/EnhancedChatRenderer.java index 197049d..2b4798d 100644 --- a/src/main/java/com/loki/lochat/renderer/EnhancedChatRenderer.java +++ b/src/main/java/com/loki/lochat/renderer/EnhancedChatRenderer.java @@ -1,5 +1,6 @@ package com.loki.lochat.renderer; +import com.loki.lochat.renderer.components.*; import com.loki.lochat.utils.MentionHandler; import com.loki.lochat.utils.TextFormatter; import io.papermc.paper.chat.ChatRenderer; @@ -9,16 +10,13 @@ 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.TextColor; -import net.kyori.adventure.text.format.TextDecoration; -import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer; import org.bukkit.entity.Player; import org.bukkit.plugin.java.JavaPlugin; import java.util.List; /** - * Рендерер чата — глобальный и локальный режимы. - * Цвета PREFIX и текста сообщения полностью настраиваются через chat-appearance.yml. + * Рендерер чата — глобальный и локальный режимы */ public class EnhancedChatRenderer implements ChatRenderer { @@ -43,14 +41,14 @@ public class EnhancedChatRenderer implements ChatRenderer { // Обрабатываем сообщение Component processed = message; processed = TextFormatter.formatMarkdown(processed); - processed = processUrls(processed); - processed = processEmojis(processed); + processed = UrlProcessor.process(processed); + processed = EmojiComponent.processEmojis(processed); if (viewer instanceof Player vp) { processed = mentionHandler.processMentions(processed, source, vp); } - // ✅ Получаем кастомный формат из конфига + // Получаем формат из конфига com.loki.lochat.config.AppearanceConfig appearanceCfg = cfg.getAppearanceConfig(); String format = isGlobal ? appearanceCfg.getGlobalChatFormat() : appearanceCfg.getLocalChatFormat(); @@ -61,13 +59,9 @@ public class EnhancedChatRenderer implements ChatRenderer { Component playerPrefix = buildPlayerPrefix(source); Component player = buildPlayerComponent(source); - // Парсим формат и заменяем плейсхолдеры return buildFormattedMessage(format, emoji, prefix, separator, playerPrefix, player, processed); } - /** - * Строит финальное сообщение по формату с плейсхолдерами - */ private Component buildFormattedMessage(String format, Component emoji, Component prefix, Component separator, Component playerPrefix, Component player, Component message) { @@ -78,12 +72,10 @@ public class EnhancedChatRenderer implements ChatRenderer { int lastEnd = 0; while (matcher.find()) { - // Добавляем текст до плейсхолдера if (matcher.start() > lastEnd) { result = result.append(Component.text(format.substring(lastEnd, matcher.start()))); } - // Заменяем плейсхолдер String placeholder = matcher.group(1); switch (placeholder) { case "emoji" -> result = result.append(emoji); @@ -98,7 +90,6 @@ public class EnhancedChatRenderer implements ChatRenderer { lastEnd = matcher.end(); } - // Добавляем оставшийся текст if (lastEnd < format.length()) { result = result.append(Component.text(format.substring(lastEnd))); } @@ -106,115 +97,35 @@ public class EnhancedChatRenderer implements ChatRenderer { return result; } - /** - * Строит компонент эмодзи - */ private Component buildEmojiComponent() { com.loki.lochat.config.AppearanceConfig appearanceCfg = cfg.getAppearanceConfig(); String emoji = isGlobal ? appearanceCfg.getGlobalEmoji() : appearanceCfg.getLocalEmoji(); return emoji.isEmpty() ? Component.empty() : Component.text(emoji); } - /** - * Строит префикс игрока из LuckPerms/градиента - */ private Component buildPlayerPrefix(Player player) { - // Получаем градиентный модуль com.loki.lochat.LoChat loChat = (com.loki.lochat.LoChat) plugin; com.loki.lochat.gradient.GradientModule gradientModule = loChat.getGradientModule(); - if (gradientModule != null) { - com.loki.lochat.gradient.data.GradientPlayerData data = - gradientModule.getDataManager().getPlayerData(player.getUniqueId()); - - if (data != null && data.isPrefixEnabled() && data.hasPrefix()) { - String prefixText = data.getPrefix(); - if (data.hasColors() && data.isColorEnabled()) { - // Применяем градиент к префиксу - return buildGradientText(prefixText, data.getColors()); - } - return Component.text(prefixText); + if (gradientModule != null && gradientModule.isEnabled()) { + String prefix = gradientModule.getPrefix(player); + if (!prefix.isEmpty()) { + return com.loki.lochat.utils.ChatFormatter.parse(prefix); } } return Component.empty(); } - // ────────────────────────────────────────────────────────────────────────── - // Префикс - // ────────────────────────────────────────────────────────────────────────── - private Component buildPrefix() { - // ✅ FIX: Получаем AppearanceConfig через ConfigManager com.loki.lochat.config.AppearanceConfig appearanceCfg = cfg.getAppearanceConfig(); String text = isGlobal ? appearanceCfg.getGlobalText() : appearanceCfg.getLocalText(); List colors = isGlobal ? appearanceCfg.getGlobalColors() : appearanceCfg.getLocalColors(); - return buildGradientText(text, colors); + return GradientBuilder.build(text, colors); } - /** - * Строит компонент с плавным градиентом. - * colors — список hex-цветов (минимум 1). - * Если цветов меньше чем букв — интерполируем плавно между соседними цветами. - * Если цветов больше чем букв — берём по одному на букву. - */ - private Component buildGradientText(String text, List colors) { - if (text.isEmpty()) return Component.empty(); - - if (colors.isEmpty()) { - return Component.text(text, FALLBACK_COLOR); - } - - // Один цвет — просто красим весь текст - if (colors.size() == 1) { - return Component.text(text, parseColor(colors.get(0))); - } - - char[] chars = text.toCharArray(); - int len = chars.length; - Component result = Component.empty(); - - for (int i = 0; i < len; i++) { - // Нормализованная позиция 0..1 - float t = len == 1 ? 0f : (float) i / (len - 1); - - // Какой сегмент градиента - float scaled = t * (colors.size() - 1); - int seg = (int) scaled; - float frac = scaled - seg; - - // Зажимаем в пределах списка - if (seg >= colors.size() - 1) { - seg = colors.size() - 2; - frac = 1f; - } - - TextColor from = parseColor(colors.get(seg)); - TextColor to = parseColor(colors.get(seg + 1)); - TextColor blended = blend(from, to, frac); - - result = result.append(Component.text(String.valueOf(chars[i]), blended)); - } - - return result; - } - - /** Линейная интерполяция между двумя TextColor */ - private static TextColor blend(TextColor from, TextColor to, float t) { - int r = Math.round(from.red() + t * (to.red() - from.red())); - int g = Math.round(from.green() + t * (to.green() - from.green())); - int b = Math.round(from.blue() + t * (to.blue() - from.blue())); - return TextColor.color(clamp(r), clamp(g), clamp(b)); - } - - private static int clamp(int v) { return Math.max(0, Math.min(255, v)); } - - // ────────────────────────────────────────────────────────────────────────── - // Разделитель - // ────────────────────────────────────────────────────────────────────────── - private Component buildSeparator() { com.loki.lochat.config.AppearanceConfig appearanceCfg = cfg.getAppearanceConfig(); String text = isGlobal ? appearanceCfg.getGlobalSeparatorText() : appearanceCfg.getLocalSeparatorText(); @@ -222,14 +133,9 @@ public class EnhancedChatRenderer implements ChatRenderer { return Component.text(text, parseColor(color)); } - // ────────────────────────────────────────────────────────────────────────── - // Ник игрока с hover - // ────────────────────────────────────────────────────────────────────────── - private Component buildPlayerComponent(Player player) { Component displayName = player.displayName(); - // ✅ FIX: Получаем AppearanceConfig для проверки hover com.loki.lochat.config.AppearanceConfig appearanceCfg = cfg.getAppearanceConfig(); if (!appearanceCfg.isHoverEnabled()) { @@ -271,42 +177,6 @@ public class EnhancedChatRenderer implements ChatRenderer { .clickEvent(ClickEvent.suggestCommand("/msg " + player.getName() + " ")); } - // ────────────────────────────────────────────────────────────────────────── - // URL и эмодзи - // ────────────────────────────────────────────────────────────────────────── - - private Component processUrls(Component message) { - String plain = PlainTextComponentSerializer.plainText().serialize(message); - java.util.regex.Pattern pattern = - java.util.regex.Pattern.compile("(https?://[\\w\\-.~:/?#\\[\\]@!$&'()*+,;=%]+)"); - java.util.regex.Matcher matcher = pattern.matcher(plain); - - Component result = message; - while (matcher.find()) { - String url = matcher.group(1); - Component urlComp = Component.text(url, NamedTextColor.AQUA, TextDecoration.UNDERLINED) - .hoverEvent(HoverEvent.showText(Component.text("Открыть: " + url, NamedTextColor.GRAY))) - .clickEvent(ClickEvent.openUrl(url)); - result = result.replaceText(b -> b.matchLiteral(url).replacement(urlComp)); - } - return result; - } - - private Component processEmojis(Component message) { - // ✅ FIX: Получаем AppearanceConfig для эмодзи - com.loki.lochat.config.AppearanceConfig appearanceCfg = cfg.getAppearanceConfig(); - - Component result = message; - for (java.util.Map.Entry e : appearanceCfg.getEmojis().entrySet()) { - result = result.replaceText(b -> b.matchLiteral(e.getKey()).replacement(e.getValue())); - } - return result; - } - - // ────────────────────────────────────────────────────────────────────────── - // Утилиты - // ────────────────────────────────────────────────────────────────────────── - private TextColor parseColor(String hex) { if (hex == null || hex.isBlank()) return FALLBACK_COLOR; String n = hex.startsWith("#") ? hex : "#" + hex; diff --git a/src/main/java/com/loki/lochat/renderer/components/EmojiComponent.java b/src/main/java/com/loki/lochat/renderer/components/EmojiComponent.java new file mode 100644 index 0000000..9ae30a5 --- /dev/null +++ b/src/main/java/com/loki/lochat/renderer/components/EmojiComponent.java @@ -0,0 +1,36 @@ +package com.loki.lochat.renderer.components; + +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.TextComponent; +import net.kyori.adventure.text.format.TextColor; +import org.bukkit.configuration.ConfigurationSection; + +public class EmojiComponent { + + public static Component build(ConfigurationSection emojiSection) { + if (emojiSection == null || !emojiSection.getBoolean("enabled", false)) { + return Component.empty(); + } + + String text = emojiSection.getString("text", ""); + String color = emojiSection.getString("color", "#FFFFFF"); + + return Component.text(text) + .color(TextColor.fromHexString(color)); + } + + public static Component processEmojis(Component message) { + if (!(message instanceof TextComponent textComp)) { + return message; + } + + String content = textComp.content(); + content = content.replace(":heart:", "❤") + .replace(":star:", "⭐") + .replace(":fire:", "🔥") + .replace(":check:", "✓") + .replace(":cross:", "✗"); + + return textComp.content(content); + } +} diff --git a/src/main/java/com/loki/lochat/renderer/components/GradientBuilder.java b/src/main/java/com/loki/lochat/renderer/components/GradientBuilder.java new file mode 100644 index 0000000..8e7e79a --- /dev/null +++ b/src/main/java/com/loki/lochat/renderer/components/GradientBuilder.java @@ -0,0 +1,48 @@ +package com.loki.lochat.renderer.components; + +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.TextComponent; +import net.kyori.adventure.text.format.TextColor; + +import java.util.List; + +public class GradientBuilder { + + public static Component build(String text, List colors) { + if (colors == null || colors.size() < 2) { + return Component.text(text); + } + + TextColor[] gradientColors = colors.stream() + .map(TextColor::fromHexString) + .toArray(TextColor[]::new); + + TextComponent.Builder builder = Component.text(); + int len = text.length(); + + for (int i = 0; i < len; i++) { + float t = (float) i / (len - 1); + int segmentIndex = (int) (t * (gradientColors.length - 1)); + float segmentT = (t * (gradientColors.length - 1)) - segmentIndex; + + TextColor from = gradientColors[segmentIndex]; + TextColor to = gradientColors[Math.min(segmentIndex + 1, gradientColors.length - 1)]; + TextColor blended = blend(from, to, segmentT); + + builder.append(Component.text(text.charAt(i)).color(blended)); + } + + return builder.build(); + } + + private static TextColor blend(TextColor from, TextColor to, float t) { + int r = (int) (from.red() * (1 - t) + to.red() * t); + int g = (int) (from.green() * (1 - t) + to.green() * t); + int b = (int) (from.blue() * (1 - t) + to.blue() * t); + return TextColor.color(clamp(r), clamp(g), clamp(b)); + } + + private static int clamp(int v) { + return Math.max(0, Math.min(255, v)); + } +} diff --git a/src/main/java/com/loki/lochat/renderer/components/UrlProcessor.java b/src/main/java/com/loki/lochat/renderer/components/UrlProcessor.java new file mode 100644 index 0000000..f84b311 --- /dev/null +++ b/src/main/java/com/loki/lochat/renderer/components/UrlProcessor.java @@ -0,0 +1,53 @@ +package com.loki.lochat.renderer.components; + +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.TextComponent; +import net.kyori.adventure.text.event.ClickEvent; +import net.kyori.adventure.text.event.HoverEvent; +import net.kyori.adventure.text.format.TextColor; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class UrlProcessor { + private static final Pattern URL_PATTERN = Pattern.compile( + "https?://[\\w\\-._~:/?#\\[\\]@!$&'()*+,;=%]+" + ); + + public static Component process(Component message) { + if (!(message instanceof TextComponent textComp)) { + return message; + } + + String content = textComp.content(); + Matcher matcher = URL_PATTERN.matcher(content); + + if (!matcher.find()) { + return message; + } + + TextComponent.Builder builder = Component.text(); + int lastEnd = 0; + + matcher.reset(); + while (matcher.find()) { + if (matcher.start() > lastEnd) { + builder.append(Component.text(content.substring(lastEnd, matcher.start()))); + } + + String url = matcher.group(); + builder.append(Component.text(url) + .color(TextColor.fromHexString("#5DADE2")) + .clickEvent(ClickEvent.openUrl(url)) + .hoverEvent(HoverEvent.showText(Component.text("Открыть: " + url)))); + + lastEnd = matcher.end(); + } + + if (lastEnd < content.length()) { + builder.append(Component.text(content.substring(lastEnd))); + } + + return builder.build(); + } +} diff --git a/src/main/java/com/loki/lochat/util/TimeParser.java b/src/main/java/com/loki/lochat/util/TimeParser.java deleted file mode 100644 index e6c5499..0000000 --- a/src/main/java/com/loki/lochat/util/TimeParser.java +++ /dev/null @@ -1,57 +0,0 @@ -package com.loki.lochat.util; - -/** - * Утилита для парсинга времени (DRY - вынесена общая логика) - */ -public class TimeParser { - - /** - * Парсит строку времени в миллисекунды - * - * @param timeStr строка вида "1d", "2h", "30m", "60s" - * @return миллисекунды или -1 при ошибке - */ - public static long parse(String timeStr) { - if (timeStr == null || timeStr.isEmpty()) return 0; - - timeStr = timeStr.toLowerCase(); - - try { - if (timeStr.endsWith("d")) { - return Long.parseLong(timeStr.replace("d", "")) * 24 * 60 * 60 * 1000L; - } else if (timeStr.endsWith("h")) { - return Long.parseLong(timeStr.replace("h", "")) * 60 * 60 * 1000L; - } else if (timeStr.endsWith("m")) { - return Long.parseLong(timeStr.replace("m", "")) * 60 * 1000L; - } else if (timeStr.endsWith("s")) { - return Long.parseLong(timeStr.replace("s", "")) * 1000L; - } else { - return Long.parseLong(timeStr) * 60 * 1000L; // По умолчанию минуты - } - } catch (NumberFormatException e) { - return -1; - } - } - - /** - * Форматирует миллисекунды в читаемую строку - */ - public static String format(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 + "с"; - } - } -} diff --git a/src/main/java/com/loki/lochat/utils/ChatFormatter.java b/src/main/java/com/loki/lochat/utils/ChatFormatter.java index a653670..47e4616 100644 --- a/src/main/java/com/loki/lochat/utils/ChatFormatter.java +++ b/src/main/java/com/loki/lochat/utils/ChatFormatter.java @@ -1,146 +1,49 @@ package com.loki.lochat.utils; +import com.loki.lochat.utils.color.ColorConverter; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.minimessage.MiniMessage; import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer; import org.bukkit.entity.Player; +/** + * Утилита для форматирования сообщений - упрощенная версия + */ public final class ChatFormatter { private static final MiniMessage MM = MiniMessage.miniMessage(); - private static final PlainTextComponentSerializer PLAIN = - PlainTextComponentSerializer.plainText(); + private static final PlainTextComponentSerializer PLAIN = PlainTextComponentSerializer.plainText(); private ChatFormatter() { } - /* ===================== BASIC ===================== */ + // ========== Базовые методы ========== public static Component parse(String message) { if (message == null) return Component.empty(); - // Конвертируем legacy форматы в MiniMessage перед парсингом - String converted = convertLegacyFormats(message); + String converted = ColorConverter.convertLegacyFormats(message); return MM.deserialize(converted); } - /** - * Default message color for players without {@code lochat.chat.colors}. - * Escapes {@code \} and {@code <} in the raw text so a literal {@code } (or any {@code <}) - * cannot close the outer MiniMessage wrapper early — which used to leave a stray {@code } - * visible in-game and in the server console log. - *

- * Legacy hex ({@code &#RRGGBB}) and ampersand color codes in the message are still applied after escaping - * (see {@link #convertLegacyFormats} inside {@link #parse}). - */ public static Component parseWithDefaultMessageColor(String message, String hexCode) { if (message == null) message = ""; - String hex = (hexCode == null || hexCode.isBlank()) - ? "#FFFFFF" - : (hexCode.startsWith("#") ? hexCode : "#" + hexCode); - String escaped = escapeForMiniMessageOuterWrap(message); + String hex = (hexCode == null || hexCode.isBlank()) ? "#FFFFFF" : + (hexCode.startsWith("#") ? hexCode : "#" + hexCode); + String escaped = ColorConverter.escapeForMiniMessage(message); return parse("" + escaped + ""); } - /** - * Escape user text before it is embedded in a MiniMessage template with outer tags. - * MiniMessage treats {@code \<} as a literal angle bracket. - */ - public static String escapeForMiniMessageOuterWrap(String text) { - if (text == null) return ""; - return text.replace("\\", "\\\\").replace("<", "\\<"); - } - public static Component parse(String message, String... replacements) { if (message == null) return Component.empty(); String result = message; for (int i = 0; i + 1 < replacements.length; i += 2) { result = result.replace(replacements[i], replacements[i + 1]); } - // Конвертируем legacy форматы в MiniMessage перед парсингом - String converted = convertLegacyFormats(result); + String converted = ColorConverter.convertLegacyFormats(result); return MM.deserialize(converted); } - /** - * Конвертирует legacy форматы цветов в MiniMessage формат - */ - private static String convertLegacyFormats(String message) { - if (message == null) return ""; - - // Конвертируем &#RRGGBB в - java.util.regex.Pattern pattern1 = java.util.regex.Pattern.compile("&#([0-9a-fA-F]{6})"); - java.util.regex.Matcher matcher1 = pattern1.matcher(message); - StringBuffer sb1 = new StringBuffer(); - while (matcher1.find()) { - String hex = matcher1.group(1).toLowerCase(); - matcher1.appendReplacement(sb1, ""); - } - matcher1.appendTail(sb1); - message = sb1.toString(); - - // Конвертируем #RRGGBB в (только если не внутри тегов) - java.util.regex.Pattern pattern2 = java.util.regex.Pattern.compile("(?)"); - java.util.regex.Matcher matcher2 = pattern2.matcher(message); - StringBuffer sb2 = new StringBuffer(); - while (matcher2.find()) { - String hex = matcher2.group(1).toLowerCase(); - matcher2.appendReplacement(sb2, ""); - } - matcher2.appendTail(sb2); - message = sb2.toString(); - - // Конвертируем §[0-9a-fk-or] в соответствующие MiniMessage теги - message = message.replaceAll("§0", ""); - message = message.replaceAll("§1", ""); - message = message.replaceAll("§2", ""); - message = message.replaceAll("§3", ""); - message = message.replaceAll("§4", ""); - message = message.replaceAll("§5", ""); - message = message.replaceAll("§6", ""); - message = message.replaceAll("§7", ""); - message = message.replaceAll("§8", ""); - message = message.replaceAll("§9", ""); - message = message.replaceAll("§a", ""); - message = message.replaceAll("§b", ""); - message = message.replaceAll("§c", ""); - message = message.replaceAll("§d", ""); - message = message.replaceAll("§e", ""); - message = message.replaceAll("§f", ""); - message = message.replaceAll("§k", ""); - message = message.replaceAll("§l", ""); - message = message.replaceAll("§m", ""); - message = message.replaceAll("§n", ""); - message = message.replaceAll("§o", ""); - message = message.replaceAll("§r", ""); - - // Конвертируем &[0-9a-fk-or] в соответствующие MiniMessage теги - message = message.replaceAll("&0", ""); - message = message.replaceAll("&1", ""); - message = message.replaceAll("&2", ""); - message = message.replaceAll("&3", ""); - message = message.replaceAll("&4", ""); - message = message.replaceAll("&5", ""); - message = message.replaceAll("&6", ""); - message = message.replaceAll("&7", ""); - message = message.replaceAll("&8", ""); - message = message.replaceAll("&9", ""); - message = message.replaceAll("&a", ""); - message = message.replaceAll("&b", ""); - message = message.replaceAll("&c", ""); - message = message.replaceAll("&d", ""); - message = message.replaceAll("&e", ""); - message = message.replaceAll("&f", ""); - - // Конвертируем форматирование - message = message.replaceAll("&k", ""); - message = message.replaceAll("&l", ""); - message = message.replaceAll("&m", ""); - message = message.replaceAll("&n", ""); - message = message.replaceAll("&o", ""); - message = message.replaceAll("&r", ""); - - return message; - } + // ========== Утилиты ========== public static String stripTags(String message) { if (message == null) return ""; @@ -159,63 +62,24 @@ public final class ChatFormatter { return obj.toString(); } - /** - * Отправляет сообщение игроку - */ public static void sendMessage(Player player, Component component) { if (player != null && component != null) { player.sendMessage(component); } } - /* ===================== COLORS ===================== */ - - /** - * Конвертирует различные форматы цветов в MiniMessage формат - */ public static String convertAllColors(String message) { if (message == null) return ""; - return convertLegacyFormats(message); + return ColorConverter.convertLegacyFormats(message); } - /* ===================== EMOJIS (REMOVED) ===================== */ - - public static String replaceEmojis(String message) { - return message; // Эмодзи удалены - } - - public static String replaceEmojis(String message, Player player) { - return message; // Эмодзи удалены - } - - /* ===================== CHAT ===================== */ - - /** - * Legacy API для ChatManager - */ - public static String processPlayerMessage( - String message, - Player player, - boolean hasColorPermission - ) { - return message; - } - - /* ===================== ANNOUNCE ===================== */ + // ========== Форматирование сообщений ========== public static Component formatAnnouncement(String format, String message) { return parse(format.replace("", message)); } - /* ===================== PM ===================== */ - - public static Component formatPmSentNew( - String format, - Player sender, - Player receiver, - String message - ) { - // Получаем отображаемые имена игроков + public static Component formatPmSentNew(String format, Player sender, Player receiver, String message) { String senderDisplay = getPlayerDisplayName(sender); String receiverDisplay = getPlayerDisplayName(receiver); @@ -227,13 +91,7 @@ public final class ChatFormatter { .replace("{message}", message)); } - public static Component formatPmReceivedNew( - String format, - Player sender, - Player receiver, - String message - ) { - // Получаем отображаемые имена игроков + public static Component formatPmReceivedNew(String format, Player sender, Player receiver, String message) { String senderDisplay = getPlayerDisplayName(sender); String receiverDisplay = getPlayerDisplayName(receiver); @@ -245,40 +103,45 @@ public final class ChatFormatter { .replace("{message}", message)); } - /** - * Получает отображаемое имя игрока с градиентом - */ - private static String getPlayerDisplayName(Player player) { - try { - // Получаем экземпляр плагина - com.loki.lochat.LoChat plugin = com.loki.lochat.LoChat.getInstance(); - - // Если градиентный модуль включен, используем его - if (plugin.getGradientModule() != null && plugin.getGradientModule().isEnabled()) { - return plugin.getGradientModule().getFormattedName(player); - } - } catch (Exception e) { - // Если произошла ошибка, логируем и возвращаем обычное имя - com.loki.lochat.LoChat.getInstance().getLogger().warning( - "Ошибка при получении градиентного имени для " + player.getName() + ": " + e.getMessage() - ); - } - - // Иначе просто имя игрока - return player.getName(); - } - - /* ===================== FORMAT (Component-based) ===================== */ - - public static Component format( - String format, - Component name, - Component prefix, - Component message - ) { + public static Component format(String format, Component name, Component prefix, Component message) { return MM.deserialize(format) .replaceText(b -> b.matchLiteral("{name}").replacement(name)) .replaceText(b -> b.matchLiteral("{prefix}").replacement(prefix)) .replaceText(b -> b.matchLiteral("{message}").replacement(message)); } + + // ========== Приватные методы ========== + + private static String getPlayerDisplayName(Player player) { + try { + com.loki.lochat.LoChat plugin = com.loki.lochat.LoChat.getInstance(); + + if (plugin.getGradientModule() != null && plugin.getGradientModule().isEnabled()) { + return plugin.getGradientModule().getFormattedName(player); + } + } catch (Exception e) { + com.loki.lochat.LoChat.getInstance().getLogger().warning( + "Ошибка при получении градиентного имени для " + player.getName() + ": " + e.getMessage() + ); + } + + return player.getName(); + } + + // ========== Deprecated методы для обратной совместимости ========== + + @Deprecated + public static String replaceEmojis(String message) { + return message; + } + + @Deprecated + public static String replaceEmojis(String message, Player player) { + return message; + } + + @Deprecated + public static String processPlayerMessage(String message, Player player, boolean hasColorPermission) { + return message; + } } diff --git a/src/main/java/com/loki/lochat/util/DistanceUtil.java b/src/main/java/com/loki/lochat/utils/DistanceUtil.java similarity index 93% rename from src/main/java/com/loki/lochat/util/DistanceUtil.java rename to src/main/java/com/loki/lochat/utils/DistanceUtil.java index 5491d9e..0e4e34b 100644 --- a/src/main/java/com/loki/lochat/util/DistanceUtil.java +++ b/src/main/java/com/loki/lochat/utils/DistanceUtil.java @@ -1,4 +1,4 @@ -package com.loki.lochat.util; +package com.loki.lochat.utils; import org.bukkit.entity.Player; diff --git a/src/main/java/com/loki/lochat/util/FoliaUtil.java b/src/main/java/com/loki/lochat/utils/FoliaUtil.java similarity index 98% rename from src/main/java/com/loki/lochat/util/FoliaUtil.java rename to src/main/java/com/loki/lochat/utils/FoliaUtil.java index e451744..3f93ae7 100644 --- a/src/main/java/com/loki/lochat/util/FoliaUtil.java +++ b/src/main/java/com/loki/lochat/utils/FoliaUtil.java @@ -1,4 +1,4 @@ -package com.loki.lochat.util; +package com.loki.lochat.utils; import org.bukkit.Bukkit; import org.bukkit.entity.Entity; diff --git a/src/main/java/com/loki/lochat/utils/MentionHandler.java b/src/main/java/com/loki/lochat/utils/MentionHandler.java index c4e053e..e60c659 100644 --- a/src/main/java/com/loki/lochat/utils/MentionHandler.java +++ b/src/main/java/com/loki/lochat/utils/MentionHandler.java @@ -1,6 +1,6 @@ package com.loki.lochat.utils; -import com.loki.lochat.util.FoliaUtil; +import com.loki.lochat.utils.FoliaUtil; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.format.NamedTextColor; import net.kyori.adventure.text.format.TextDecoration; diff --git a/src/main/java/com/loki/lochat/utils/color/ColorConverter.java b/src/main/java/com/loki/lochat/utils/color/ColorConverter.java new file mode 100644 index 0000000..fb5de3c --- /dev/null +++ b/src/main/java/com/loki/lochat/utils/color/ColorConverter.java @@ -0,0 +1,144 @@ +package com.loki.lochat.utils.color; + +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Конвертер различных форматов цветов в MiniMessage + */ +public class ColorConverter { + + private static final Pattern HEX_PATTERN_1 = Pattern.compile("&#([0-9a-fA-F]{6})"); + private static final Pattern HEX_PATTERN_2 = Pattern.compile("(?)"); + + // Мапа для быстрой конвертации цветов (вместо 32 вызовов replaceAll) + private static final Map COLOR_MAP = Map.ofEntries( + // § коды + Map.entry("§0", ""), + Map.entry("§1", ""), + Map.entry("§2", ""), + Map.entry("§3", ""), + Map.entry("§4", ""), + Map.entry("§5", ""), + Map.entry("§6", ""), + Map.entry("§7", ""), + Map.entry("§8", ""), + Map.entry("§9", ""), + Map.entry("§a", ""), + Map.entry("§b", ""), + Map.entry("§c", ""), + Map.entry("§d", ""), + Map.entry("§e", ""), + Map.entry("§f", ""), + Map.entry("§k", ""), + Map.entry("§l", ""), + Map.entry("§m", ""), + Map.entry("§n", ""), + Map.entry("§o", ""), + Map.entry("§r", ""), + // & коды + Map.entry("&0", ""), + Map.entry("&1", ""), + Map.entry("&2", ""), + Map.entry("&3", ""), + Map.entry("&4", ""), + Map.entry("&5", ""), + Map.entry("&6", ""), + Map.entry("&7", ""), + Map.entry("&8", ""), + Map.entry("&9", ""), + Map.entry("&a", ""), + Map.entry("&b", ""), + Map.entry("&c", ""), + Map.entry("&d", ""), + Map.entry("&e", ""), + Map.entry("&f", ""), + Map.entry("&k", ""), + Map.entry("&l", ""), + Map.entry("&m", ""), + Map.entry("&n", ""), + Map.entry("&o", ""), + Map.entry("&r", "") + ); + + /** + * Конвертирует legacy форматы цветов в MiniMessage формат + */ + public static String convertLegacyFormats(String message) { + if (message == null) return ""; + + // Конвертируем &#RRGGBB в + message = convertHexFormat1(message); + + // Конвертируем #RRGGBB в + message = convertHexFormat2(message); + + // Конвертируем § и & коды через Map (быстрее чем 32 replaceAll) + message = convertLegacyCodes(message); + + return message; + } + + private static String convertHexFormat1(String message) { + Matcher matcher = HEX_PATTERN_1.matcher(message); + StringBuffer sb = new StringBuffer(); + while (matcher.find()) { + String hex = matcher.group(1).toLowerCase(); + matcher.appendReplacement(sb, ""); + } + matcher.appendTail(sb); + return sb.toString(); + } + + private static String convertHexFormat2(String message) { + Matcher matcher = HEX_PATTERN_2.matcher(message); + StringBuffer sb = new StringBuffer(); + while (matcher.find()) { + String hex = matcher.group(1).toLowerCase(); + matcher.appendReplacement(sb, ""); + } + matcher.appendTail(sb); + return sb.toString(); + } + + /** + * Конвертирует legacy коды (§ и &) через Map - быстрее чем 32 replaceAll + */ + private static String convertLegacyCodes(String message) { + StringBuilder result = new StringBuilder(message.length()); + int i = 0; + + while (i < message.length()) { + if (i + 1 < message.length()) { + char current = message.charAt(i); + char next = message.charAt(i + 1); + + // Проверяем § или & + if (current == '§' || current == '&') { + String code = String.valueOf(current) + next; + String replacement = COLOR_MAP.get(code); + + if (replacement != null) { + result.append(replacement); + i += 2; + continue; + } + } + } + + result.append(message.charAt(i)); + i++; + } + + return result.toString(); + } + + /** + * Экранирует текст для безопасного использования внутри MiniMessage тегов + */ + public static String escapeForMiniMessage(String text) { + if (text == null) return ""; + return text.replace("\\", "\\\\").replace("<", "\\<"); + } +}